diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 60c614f6f59..39d9a27615e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.1' +__version__ = '13.5.2' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index dd346bc2408..2f86c6c1de2 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): amount, base_amount = calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency) + if not amount: + return + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 0a5ebef5236..8aa073402ac 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -41,6 +41,30 @@ class TestProductConfigurator(unittest.TestCase): "show_variant_in_website": 1 }).insert() + def create_regular_web_item(self, name, item_group=None): + if not frappe.db.exists('Item', name): + doc = frappe.get_doc({ + "description": name, + "item_code": name, + "item_name": name, + "doctype": "Item", + "is_stock_item": 1, + "item_group": item_group or "_Test Item Group", + "stock_uom": "_Test UOM", + "item_defaults": [{ + "company": "_Test Company", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + "income_account": "Sales - _TC" + }], + "show_in_website": 1 + }).insert() + else: + doc = frappe.get_doc("Item", name) + return doc + def test_product_list(self): template_items = frappe.get_all('Item', {'show_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) @@ -77,3 +101,42 @@ class TestProductConfigurator(unittest.TestCase): 'Test Size': ['2XL'] }) self.assertEqual(len(items), 1) + + def test_products_in_multiple_item_groups(self): + """Check if product is visible on multiple item group pages barring its own.""" + from erpnext.shopping_cart.product_query import ProductQuery + + if not frappe.db.exists("Item Group", {"name": "Tech Items"}): + item_group_doc = frappe.get_doc({ + "doctype": "Item Group", + "item_group_name": "Tech Items", + "parent_item_group": "All Item Groups", + "show_in_website": 1 + }).insert() + else: + item_group_doc = frappe.get_doc("Item Group", "Tech Items") + + doc = self.create_regular_web_item("Portal Item", item_group="Tech Items") + if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}): + doc.append("website_item_groups", { + "item_group": "_Test Item Group Desktops" + }) + doc.save() + + # check if item is visible in its own Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + self.assertEqual(len(items), 1) + self.assertEqual(items[0].item_code, "Portal Item") + + # check if item is visible in configured foreign Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + item_codes = [row.item_code for row in items] + + self.assertIn(len(items), [2, 3]) + self.assertIn("Portal Item", item_codes) + + # teardown + doc.delete() + item_group_doc.delete() \ No newline at end of file diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 9402cf9ea48..5962859be5a 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -467,11 +467,15 @@ body.product-page { .btn-change-address { color: var(--blue-500); - box-shadow: none; - border: 1px solid var(--blue-500); } } +.btn-new-address:hover, .btn-change-address:hover { + box-shadow: none; + color: var(--blue-500) !important; + border: 1px solid var(--blue-500); +} + .modal .address-card { .card-body { padding: var(--padding-sm); diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index db480e05e2a..1a83cb62dd3 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): field_filters['item_group'] = self.name engine = ProductQuery() - context.items = engine.query(attribute_filters, field_filters, search, start) + context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) filter_engine = ProductFiltersBuilder(self.name) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 6c63d8759b4..7dfa09e2d62 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -22,12 +22,15 @@ class ProductFiltersBuilder: filter_data = [] for df in fields: - filters = {} + filters, or_filters = {}, [] if df.fieldtype == "Link": if self.item_group: - filters['item_group'] = self.item_group + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname) else: doctype = df.get_link_doctype() @@ -44,7 +47,9 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - values = values.remove(None) if None in values else values + if None in values: + values.remove(None) + if values: filter_data.append([df, values]) @@ -61,14 +66,18 @@ class ProductFiltersBuilder: for attr_doc in attribute_docs: selected_attributes = [] for attr in attr_doc.item_attribute_values: + or_filters = [] filters= [ ["Item Variant Attribute", "attribute", "=", attr.parent], ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] ] if self.item_group: - filters.append(["item_group", "=", self.item_group]) + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - if frappe.db.get_all("Item", filters, limit=1): + if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): selected_attributes.append(attr) if selected_attributes: diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index dd94c26bc60..3eab4ffbcc9 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -22,13 +22,14 @@ class ProductQuery: self.settings = frappe.get_doc("Products Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route'] + self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', + 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] self.filters = [] self.or_filters = [['show_in_website', '=', 1]] if not self.settings.get('hide_variants'): self.or_filters.append(['show_variant_in_website', '=', 1]) - def query(self, attributes=None, fields=None, search_term=None, start=0): + def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary Args: @@ -44,6 +45,15 @@ class ProductQuery: if search_term: self.build_search_filters(search_term) result = [] + website_item_groups = [] + + # if from item group page consider website item group table + if item_group: + website_item_groups = frappe.db.get_all( + "Item", + fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], + filters=[["Website Item Group", "item_group", "=", item_group]] + ) if attributes: all_items = [] @@ -66,7 +76,6 @@ class ProductQuery: ) items_dict = {item.name: item for item in items} - # TODO: Replace Variants by their parent templates all_items.append(set(items_dict.keys())) @@ -78,14 +87,22 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, start=start, - limit=self.page_length, - order_by="weightage desc" + limit=self.page_length ) + # Combine results having context of website item groups into item results + if item_group and website_item_groups: + items_list = {row.name for row in result} + for row in website_item_groups: + if row.wig_parent not in items_list: + result.append(row) + + result = sorted(result, key=lambda x: x.get("weightage"), reverse=True) + for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None + item.formatted_price = (product_info.get('price') or {}).get('formatted_price') return result @@ -99,7 +116,16 @@ class ProductQuery: if not values: continue - if isinstance(values, list): + # handle multiselect fields in filter addition + meta = frappe.get_meta('Item', cached=True) + df = meta.get_field(field) + if df.fieldtype == 'Table MultiSelect': + child_doctype = df.options + child_meta = frappe.get_meta(child_doctype, cached=True) + fields = child_meta.get("fields") + if fields: + self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) + elif isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) else: diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 84a9430956e..4482bc10cf7 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -99,6 +99,7 @@ frappe.ready(() => { fieldname: 'country', fieldtype: 'Link', options: 'Country', + only_select: true, reqd: 1 }, {