Skip to content

Commit

Permalink
Merge pull request #2158 from tulimaki/few-admin-suggestions
Browse files Browse the repository at this point in the history
Admin: show product orderability errors as list and allow sorting categories by name
  • Loading branch information
chessbr committed Apr 6, 2020
2 parents c7cf850 + 3240e50 commit d37fb9e
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 17 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

List all changes after the last release here (newer on top). Each change on a separate bullet point line.


### Fixed

- Admin: Notification name when deleteing it

### Changed

- Front: Add supplier choice to best selling product context function
- Admin: allow sorting categories by name
- Admin: show product orderability errors as list


## [1.10.9] - 2020-03-24

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion shuup/admin/modules/categories/views/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class CategoryListView(PicotableListView):
default_columns = [
Column("image", _("Image"), sortable=False, linked=True, raw=True),
Column(
"name", _(u"Name"), sortable=False, display="format_name", linked=True, allow_highlight=False,
"name", _(u"Name"), sort_field="translations__name", display="format_name",
linked=True, allow_highlight=False,
filter_config=MPTTFilter(
choices="get_name_filter_choices",
filter_field="id"
Expand Down
9 changes: 7 additions & 2 deletions shuup/admin/templates/shuup/admin/products/edit.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
<form method="post" id="product_form" novalidate>
{% if orderability_errors %}
{# TODO: FIX THIS FOR MOBILE SCREEN SIZES #}
<p class="alert alert-info"
<div class="alert alert-info"
data-toggle="tooltip"
data-title="{% for error in orderability_errors %}{{ error }} {% endfor %}"
data-placement="bottom">
<i class="fa fa-info-circle text-info"></i> {% trans %}This product is currently not orderable.{% endtrans %}
</p>
<ul>
{% for error in orderability_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% csrf_token %}
{% call content_block(_("Basic Information"), "fa-info-circle", id="basic-information-section") %}
Expand Down
19 changes: 11 additions & 8 deletions shuup/core/utils/product_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@
from shuup.utils.dates import to_aware


def get_best_selling_product_info(shop_ids, cutoff_days=30):
def get_best_selling_product_info(shop_ids, cutoff_days=30, supplier=None):
shop_ids = sorted(map(int, shop_ids))
cutoff_date = datetime.date.today() - datetime.timedelta(days=cutoff_days)
cache_key = "best_sellers:%r_%s" % (shop_ids, cutoff_date)
cache_key = "best_sellers:%r_%s_%s" % (shop_ids, cutoff_date, (supplier.pk if supplier else ""))
sales_data = cache.get(cache_key)
if sales_data is None:
queryset = OrderLine.objects.filter(
order__shop_id__in=shop_ids,
order__order_date__gte=to_aware(cutoff_date),
type=OrderLineType.PRODUCT
)
if supplier:
queryset = queryset.filter(supplier=supplier)

sales_data = (
OrderLine.objects
.filter(
order__shop_id__in=shop_ids,
order__order_date__gte=to_aware(cutoff_date),
type=OrderLineType.PRODUCT
)
queryset
.values("product")
.annotate(n=Sum("quantity"))
.order_by("-n")[:100]
Expand Down
16 changes: 10 additions & 6 deletions shuup/front/template_helpers/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ def get_listed_products(context, n_products, ordering=None, filter_dict=None, or


@contextfunction
def get_best_selling_products(context, n_products=12, cutoff_days=30, orderable_only=True):
def get_best_selling_products(context, n_products=12, cutoff_days=30, orderable_only=True, supplier=None):
request = context["request"]

key, products = context_cache.get_cached_value(
identifier="best_selling_products",
identifier="best_selling_products_%s" % (supplier.pk if supplier else ""),
item=cache_utils.get_best_selling_products_cache_item(request.shop),
context=request,
n_products=n_products, cutoff_days=cutoff_days,
Expand All @@ -154,15 +154,16 @@ def get_best_selling_products(context, n_products=12, cutoff_days=30, orderable_
if products is not None:
return products

products = _get_best_selling_products(cutoff_days, n_products, orderable_only, request)
products = _get_best_selling_products(cutoff_days, n_products, orderable_only, request, supplier=supplier)
context_cache.set_cached_value(key, products, settings.SHUUP_TEMPLATE_HELPERS_CACHE_DURATION)
return products


def _get_best_selling_products(cutoff_days, n_products, orderable_only, request): # noqa (C901)
def _get_best_selling_products(cutoff_days, n_products, orderable_only, request, supplier=None): # noqa (C901)
data = get_best_selling_product_info(
shop_ids=[request.shop.pk],
cutoff_days=cutoff_days
cutoff_days=cutoff_days,
supplier=supplier
)
combined_variation_products = defaultdict(int)
for product_id, parent_id, qty in data:
Expand Down Expand Up @@ -200,7 +201,10 @@ def _get_best_selling_products(cutoff_days, n_products, orderable_only, request)

if orderable_only:
valid_products = []
suppliers = Supplier.objects.enabled().filter(shops=request.shop)
if supplier:
suppliers = [supplier]
else:
suppliers = Supplier.objects.enabled().filter(shops=request.shop)

for product in products:
# this instance should always exist as the listed() queryset uses the current shop as a filter
Expand Down
53 changes: 53 additions & 0 deletions shuup_tests/front/test_general_template_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,59 @@ def test_get_best_selling_products():
assert product3 in best_selling_products


@pytest.mark.django_db
def test_get_best_selling_products_per_supplier():
from shuup.front.template_helpers import general
context = get_jinja_context()

# No products sold
assert len(list(general.get_best_selling_products(context, n_products=3))) == 0
shop = get_default_shop()

supplier = get_default_supplier()
supplier2 = Supplier.objects.create(name="supplier2", enabled=True, is_approved=True)
supplier2.shops.add(shop)

product1 = create_product("product1", shop, supplier, 10)
product2 = create_product("product2", shop, supplier2, 20)
create_order_with_product(product1, supplier, quantity=1, taxless_base_unit_price=10, shop=shop)
create_order_with_product(product2, supplier2, quantity=2, taxless_base_unit_price=20, shop=shop)

cache.clear()
# Two products sold, but only one supplier
for cache_test in range(2):
best_selling_products = list(general.get_best_selling_products(context, n_products=3, supplier=supplier))
assert len(best_selling_products) == 1
assert product1 in best_selling_products
assert product2 not in best_selling_products

# Two products sold, but only one supplier
for cache_test in range(2):
best_selling_products = list(general.get_best_selling_products(context, n_products=3, supplier=supplier2))
assert len(best_selling_products) == 1
assert product1 not in best_selling_products
assert product2 in best_selling_products


# Make product 1 also sold by supplier2
shop_product = product1.get_shop_instance(shop)
shop_product.suppliers.add(supplier2)

cache.clear()
for cache_test in range(2):
best_selling_products = list(general.get_best_selling_products(context, n_products=3, supplier=supplier2))
assert len(best_selling_products) == 1 # Since there isn't any orders yet for supplier 2
assert product2 in best_selling_products

create_order_with_product(product1, supplier2, quantity=2, taxless_base_unit_price=20, shop=shop)
cache.clear()
for cache_test in range(2):
best_selling_products = list(general.get_best_selling_products(context, n_products=3, supplier=supplier2))
assert len(best_selling_products) == 2
assert product1 in best_selling_products
assert product2 in best_selling_products


@pytest.mark.django_db
def test_get_best_selling_products_cache_bump():
supplier = get_default_supplier()
Expand Down

0 comments on commit d37fb9e

Please sign in to comment.