Repository: saleor/saleor

Issue:
Difficulties customizing shop for cash on delivery
(Please take the time and describe the problem you're having before you propose a solution.)

### What I'm trying to achieve

I have a shop with the following methods of payment: card, cash on delivery and bank transfer. All use the same delivery service provider, so there's a fixed delivery cost, but when using the "cash on delivery" option, there's an additional cost. To make it easy for me, I have created a new product "Cash on delivery", so that it gets included in the bill and receipts and is transparent to the users what happens. This is implemented as a new "Payment Provider", that looks up the CashOnDelivery product and adds it to the order lines, when processing.

The problems:

* This product initially had no variant (variants didn't make sense). I can't add it from the "Add variant to order" option in the Dashboard.
* I have added a variant, but now I needed to customize 2 views, because this product is not published. Publishing it will just place its category in the main storefront navigation and that's not what I want.

Some other stuff I've noticed: 

* there's no easy "upload your logo" page, I need to customize at least the base.html and invoice.html.
* empty categories will show up on the navigation, even if they don't have published products inside. Solved by other customizations.
* category_index is not tested with subcategories. At some point at the beginning of this year, the categories have been refactored and I can assign only one category to a product. Main category, fine. But now all top categories are empty, because all products have moved to the sub-categories. The proper code should be something like this in category_index():

```python
    categories = category.get_descendants(include_self=True)
    ids = [c.id for c in categories]
    products = products_with_details(user=request.user).filter(
        category__id__in=ids).order_by('name')
    product_filter = ProductCategoryFilter(request.GET, queryset=products,
                                           category=category)
```


Discussion:
Hey @tiberiuichim, thanks a lot for your input! Generally, when designing Saleor we try to come up with possibly most universal solutions, but that'll obviously not work for everyone, so that's where such customizations as yours should come in. We also never had many options to customize the storefront from the dashboard (like the logo), but that's something that we want to improve in the future releases.

To address your "cash on delivery" problem - the solution you came up is smart, but of course, products were not designed for this purpose, that's why can't use "Add variant to order option". If you're going to keep using this solution I would advise adding the ability to hide categories from navigation and create a hidden category just for your "cash on delivery" option.

You're right about the category_index view not displaying products from subcategories properly. I'll open an issue for that.
For me it's fine if we close the ticket, just wanted to express my ideas. I'm not a Django developer, but for me, at least, Saleor has some of the cleanest Django codebases that I have encountered so far. I am of the opinion that large scale open source Django projects need to incorporate some form of a component architecture to facilitate the job of integrators in the customization process, but this is probably something that needs to be tackled across the entire Django community.

Resolving patch:
diff --git a/saleor/product/views.py b/saleor/product/views.py
--- a/saleor/product/views.py
+++ b/saleor/product/views.py
@@ -113,8 +113,10 @@ def category_index(request, path, category_id):
     if actual_path != path:
         return redirect('product:category', permanent=True, path=actual_path,
                         category_id=category_id)
+    # Check for subcategories
+    categories = category.get_descendants(include_self=True)
     products = products_with_details(user=request.user).filter(
-        category__id=category.id).order_by('name')
+        category__in=categories).order_by('name')
     product_filter = ProductCategoryFilter(
         request.GET, queryset=products, category=category)
     ctx = get_product_list_context(request, product_filter)


Test patch:
diff --git a/tests/test_product.py b/tests/test_product.py
--- a/tests/test_product.py
+++ b/tests/test_product.py
@@ -11,6 +11,7 @@
 from saleor.cart.models import Cart
 from saleor.product import (
     ProductAvailabilityStatus, VariantAvailabilityStatus, models)
+from saleor.product.models import Category
 from saleor.product.utils import (
     allocate_stock, deallocate_stock, decrease_stock,
     get_attributes_display_map, get_availability,
@@ -515,3 +516,19 @@ def test_render_product_page_with_no_variant(
         kwargs={'product_id': product.pk, 'slug': product.get_slug()})
     response = admin_client.get(url)
     assert response.status_code == 200
+
+
+def test_include_products_from_subcategories_in_main_view(
+        default_category, product_in_stock, authorized_client):
+    subcategory = Category.objects.create(
+        name='sub', slug='test', parent=default_category)
+    product = product_in_stock
+    product.category = subcategory
+    product.save()
+    path = default_category.get_full_path()
+    # URL to parent category view
+    url = reverse(
+        'product:category', kwargs={
+            'path': path, 'category_id': default_category.pk})
+    response = authorized_client.get(url)
+    assert product in response.context_data['products'][0]