def test_admin_form(rf, admin_user): supplier = get_simple_supplier() shop = get_default_shop() product = create_product("simple-test-product", shop, supplier) request = rf.get("/") request.user = admin_user frm = SimpleSupplierForm(product=product, request=request) # Form contains 1 product even if the product is not stocked assert len(frm.products) == 1 assert not frm.products[0].is_stocked() product.stock_behavior = StockBehavior.STOCKED # Make product stocked product.save() # Now since product is stocked it should be in the form frm = SimpleSupplierForm(product=product, request=request) assert len(frm.products) == 1 # Add stocked children for product child_product = create_product("child-test-product", shop, supplier) child_product.stock_behavior = StockBehavior.STOCKED child_product.save() child_product.link_to_parent(product) # Admin form should now contain only child products for product frm = SimpleSupplierForm(product=product, request=request) assert len(frm.products) == 1 assert frm.products[0] == child_product
def test_product_cross_sells(admin_user): shop1 = get_default_shop() supplier1 = create_simple_supplier("supplier1") product1 = create_product("product1", shop=shop1, supplier=supplier1) product2 = create_product("product2", shop=shop1, supplier=supplier1) # assign cross sell of product1 and product2 ProductCrossSell.objects.create(product1=product1, product2=product2, type=ProductCrossSellType.RECOMMENDED) ProductCrossSell.objects.create(product1=product1, product2=product2, type=ProductCrossSellType.RELATED) ProductCrossSell.objects.create(product1=product1, product2=product2, type=ProductCrossSellType.COMPUTED) ProductCrossSell.objects.create(product1=product1, product2=product2, type=ProductCrossSellType.BOUGHT_WITH) # product1 must have cross shell of product2 request = get_request("/api/shuup/front/shop_products/", admin_user) response = FrontShopProductViewSet.as_view({"get": "list"})(request) response.render() products_data = json.loads(response.content.decode("utf-8")) products = sorted(products_data, key=lambda p: p["id"]) assert products[0]["product_id"] == product1.id assert products[0]["cross_sell"]["recommended"][0]["product_id"] == product2.id assert products[0]["cross_sell"]["related"][0]["product_id"] == product2.id assert products[0]["cross_sell"]["computed"][0]["product_id"] == product2.id assert products[0]["cross_sell"]["bought_with"][0]["product_id"] == product2.id assert products[1]["product_id"] == product2.id assert products[1]["cross_sell"]["recommended"] == [] assert products[1]["cross_sell"]["related"] == [] assert products[1]["cross_sell"]["computed"] == [] assert products[1]["cross_sell"]["bought_with"] == []
def test_get_listed_products_filter(): context = get_jinja_context() shop = get_default_shop() supplier = get_default_supplier() product_1 = create_product( "test-sku-1", supplier=supplier, shop=shop, ) product_2 = create_product( "test-sku-2", supplier=supplier, shop=shop, ) cache.clear() from shuup.front.template_helpers import general filter_dict = {"id": product_1.id} for cache_test in range(2): product_list = general.get_listed_products(context, n_products=2, filter_dict=filter_dict) assert product_1 in product_list assert product_2 not in product_list for cache_test in range(2): product_list = general.get_listed_products(context, n_products=2, filter_dict=filter_dict, orderable_only=False) assert product_1 in product_list assert product_2 not in product_list
def test_variation_product_price_simple(client): shop = get_default_shop() supplier = get_default_supplier() product = create_product("Parent", supplier=supplier, shop=shop, default_price="10") child = create_product("SimpleVarChild", supplier=supplier, shop=shop, default_price="5") child.link_to_parent(product, variables={"size": "S"}) response = client.get( reverse('shuup:xtheme_extra_view', kwargs={ 'view': 'product_price' } ) + "?id=%s&quantity=%s&var_1=1" % (product.pk, 1) ) assert response.context_data["product"] == child assert b"form" in response.content sp = child.get_shop_instance(shop) sp.suppliers.remove(supplier) response = client.get( reverse('shuup:xtheme_extra_view', kwargs={ 'view': 'product_price' } ) + "?id=%s&quantity=%s&var_1=1" % (product.pk, 1) ) assert response.context_data["product"] == child # product isn't orderable since no supplier assert b"no-price" in response.content
def test_get_newest_products(): from shuup.front.template_helpers import general supplier = get_default_supplier() shop = get_default_shop() products = [create_product("sku-%d" % x, supplier=supplier, shop=shop) for x in range(2)] children = [create_product("SimpleVarChild-%d" % x, supplier=supplier, shop=shop) for x in range(2)] for child in children: child.link_to_parent(products[0]) context = get_jinja_context() for cache_test in range(2): newest_products = list(general.get_newest_products(context, n_products=10)) # only 2 products exist assert len(newest_products) == 2 assert products[0] in newest_products assert products[1] in newest_products # Delete one product products[0].soft_delete() for cache_test in range(2): newest_products = list(general.get_newest_products(context, n_products=10)) # only 2 products exist assert len(newest_products) == 1 assert products[0] not in newest_products assert products[1] in newest_products
def test_get_random_products_cache_bump(): from shuup.front.template_helpers import general supplier = get_default_supplier() shop = get_default_shop() products = [create_product("sku-%d" % x, supplier=supplier, shop=shop) for x in range(2)] children = [create_product("SimpleVarChild-%d" % x, supplier=supplier, shop=shop) for x in range(2)] for child in children: child.link_to_parent(products[0]) context = get_jinja_context() cache.clear() set_cached_value_mock = mock.Mock(wraps=context_cache.set_cached_value) def set_cache_value(key, value, timeout=None): if "random_products" in key: return set_cached_value_mock(key, value, timeout) with mock.patch.object(context_cache, "set_cached_value", new=set_cache_value): assert set_cached_value_mock.call_count == 0 assert general.get_random_products(context, n_products=10) assert set_cached_value_mock.call_count == 1 # call again, the cache should be returned instead and the set_cached_value shouldn't be called again assert general.get_random_products(context, n_products=10) assert set_cached_value_mock.call_count == 1 # change a shop product, the cache should be bumped ShopProduct.objects.filter(shop=shop).first().save() assert general.get_random_products(context, n_products=10) assert set_cached_value_mock.call_count == 2
def test_extract_products_shortdescription(): activate("en") out = StringIO() html_description1 = "<b>a HTML description</b>" product1 = create_product("p1", description=html_description1) html_description2 = '<p class="what">another HTML description</p>' product2 = create_product("p2", description=html_description2) faker = Faker() long_description = faker.sentence(nb_words=150, variable_nb_words=True) product3 = create_product("p3", description=long_description) for lang, _ in settings.LANGUAGES: product2.set_current_language(lang) product2.description = html_description2 product2.save() call_command("shuup_extract_products_shortdescription", stdout=out) product1 = Product.objects.get(pk=product1.pk) product2 = Product.objects.get(pk=product2.pk) product3 = Product.objects.get(pk=product3.pk) assert product1.short_description == do_striptags(html_description1) for lang, _ in settings.LANGUAGES: product2.set_current_language(lang) assert product2.short_description == do_striptags(html_description2) assert product3.short_description == long_description[:150] assert "Done." in out.getvalue()
def test_add_invalid_product(): shop = get_default_shop() supplier = get_default_supplier() request = get_request_with_basket() basket = request.basket # cannot add simple/variable variation parent to the basket parent = create_product("parent", shop=shop, supplier=supplier) child = create_product("child", shop=shop, supplier=supplier) child.link_to_parent(parent) parent.refresh_from_db() assert parent.mode == ProductMode.SIMPLE_VARIATION_PARENT with pytest.raises(ValidationError) as excinfo: basket_commands.handle_add(request, basket, product_id=parent.pk, quantity=2) assert excinfo.value.code == 'invalid_product' child.unlink_from_parent() child.link_to_parent(parent, variables={"size": "XXL"}) parent.refresh_from_db() assert parent.mode == ProductMode.VARIABLE_VARIATION_PARENT with pytest.raises(ValidationError) as excinfo: basket_commands.handle_add(request, basket, product_id=parent.pk, quantity=3) assert excinfo.value.code == 'invalid_product'
def test_make_product_package(admin_user): shop = get_default_shop() client = _get_client(admin_user) product1 = create_product("product1") product2 = create_product("product2") product3 = create_product("product3") product4 = create_product("product4") assert product1.mode == product2.mode == product3.mode == product4.mode == ProductMode.NORMAL package_data = [ {"product": product2.pk, "quantity":1}, {"product": product3.pk, "quantity":2}, {"product": product4.pk, "quantity":3.5} ] response = client.post("/api/shuup/product/%d/make_package/" % product1.pk, content_type="application/json", data=json.dumps(package_data)) assert response.status_code == status.HTTP_201_CREATED product1.refresh_from_db() product2.refresh_from_db() product3.refresh_from_db() product4.refresh_from_db() assert product1.mode == ProductMode.PACKAGE_PARENT child1 = ProductPackageLink.objects.get(parent=product1, child=product2) assert child1.quantity == 1 child2 = ProductPackageLink.objects.get(parent=product1, child=product3) assert child2.quantity == 2 child3 = ProductPackageLink.objects.get(parent=product1, child=product4) assert child3.quantity == 3.5
def test_mass_edit_products2(rf, admin_user): shop = get_default_shop() supplier = get_default_supplier() product1 = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price="50") product2 = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price="501") shop_product1 = product1.get_shop_instance(shop) shop_product2 = product2.get_shop_instance(shop) # ensure no categories set assert shop_product1.primary_category is None assert shop_product2.primary_category is None payload = { "action": InvisibleMassAction().identifier, "values": [product1.pk, product2.pk] } request = apply_request_middleware(rf.post( "/", user=admin_user, )) request._body = json.dumps(payload).encode("UTF-8") view = ProductListView.as_view() response = view(request=request) assert response.status_code == 200 for product in Product.objects.all(): assert product.get_shop_instance(shop).visibility == ShopProductVisibility.NOT_VISIBLE
def test_best_selling_products_with_multiple_orders(): context = get_jinja_context() supplier = get_default_supplier() shop = get_default_shop() n_products = 2 price = 10 product_1 = create_product("test-sku-1", supplier=supplier, shop=shop) product_2 = create_product("test-sku-2", supplier=supplier, shop=shop) create_order_with_product(product_1, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) create_order_with_product(product_2, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) cache.clear() # Two initial products sold assert product_1 in general.get_best_selling_products(context, n_products=n_products) assert product_2 in general.get_best_selling_products(context, n_products=n_products) product_3 = create_product("test-sku-3", supplier=supplier, shop=shop) create_order_with_product(product_3, supplier, quantity=2, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product sold in greater quantity assert product_3 in general.get_best_selling_products(context, n_products=n_products) create_order_with_product(product_1, supplier, quantity=4, taxless_base_unit_price=price, shop=shop) create_order_with_product(product_2, supplier, quantity=4, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product outsold by first two products assert product_3 not in general.get_best_selling_products(context, n_products=n_products)
def test_simple_variation(): shop = get_default_shop() parent = create_product("SimpleVarParent") children = [create_product("SimpleVarChild-%d" % x) for x in range(10)] for child in children: child.link_to_parent(parent) sp = ShopProduct.objects.create(shop=shop, product=child, listed=True) assert child.is_variation_child() assert not sp.is_list_visible() # Variation children are not list visible assert parent.mode == ProductMode.SIMPLE_VARIATION_PARENT assert not list(parent.get_all_available_combinations()) # Simple variations can't have these. # Validation tests dummy = create_product("InvalidSimpleVarChild") with pytest.raises(ValueError): dummy.link_to_parent(parent, variables={"size": "XL"}) with pytest.raises(ValueError): parent.link_to_parent(dummy) with pytest.raises(ValueError): dummy.link_to_parent(children[0]) # Unlinkage for child in children: child.unlink_from_parent() assert not child.is_variation_child() assert child.mode == ProductMode.NORMAL assert not parent.is_variation_parent() assert parent.variation_children.count() == 0
def get_frontend_order_state(contact, valid_lines=True): """ Get a dict structure mirroring what the frontend JavaScript would submit. :type contact: Contact|None """ translation.activate("en") shop = get_default_shop() tax = Tax.objects.create(code="test_code", rate=decimal.Decimal("0.20"), name="Default") tax_class = TaxClass.objects.create(identifier="test_tax_class", name="Default") rule = TaxRule.objects.create(tax=tax) rule.tax_classes.add(tax_class) rule.save() product = create_product( sku=printable_gibberish(), supplier=get_default_supplier(), shop=shop ) product.tax_class = tax_class product.save() if valid_lines: lines = [ {"id": "x", "type": "product", "product": {"id": product.id}, "quantity": "32", "baseUnitPrice": 50}, {"id": "y", "type": "other", "sku": "hello", "text": "A greeting", "quantity": 1, "unitPrice": "5.5"}, {"id": "z", "type": "text", "text": "This was an order!", "quantity": 0}, ] else: unshopped_product = create_product(sku=printable_gibberish(), supplier=get_default_supplier()) not_visible_product = create_product( sku=printable_gibberish(), supplier=get_default_supplier(), shop=shop ) not_visible_shop_product = not_visible_product.get_shop_instance(shop) not_visible_shop_product.visible = False not_visible_shop_product.save() lines = [ {"id": "x", "type": "product"}, # no product? {"id": "x", "type": "product", "product": {"id": unshopped_product.id}}, # not in this shop? {"id": "y", "type": "product", "product": {"id": -product.id}}, # invalid product? {"id": "z", "type": "other", "quantity": 1, "unitPrice": "q"}, # what's that price? {"id": "rr", "type": "product", "quantity": 1, "product": {"id": not_visible_product.id}} # not visible ] state = { "customer": {"id": contact.id if contact else None}, "lines": lines, "methods": { "shippingMethod": {"id": get_default_shipping_method().id}, "paymentMethod": {"id": get_default_payment_method().id}, }, "shop": { "selected": { "id": shop.id, "name": shop.name, "currency": shop.currency, "priceIncludeTaxes": shop.prices_include_tax } } } return state
def test_get_front_products(admin_user): shop1 = get_default_shop() shop2 = Shop.objects.create(status=ShopStatus.ENABLED) supplier = get_default_supplier() p1 = create_product("product1", shop=shop1, supplier=supplier) p2 = create_product("product2", shop=shop1, supplier=supplier) sp1 = ShopProduct.objects.get(shop=shop1, product=p1) sp2 = ShopProduct.objects.get(shop=shop1, product=p2) # add products to other shop sp3 = ShopProduct.objects.create(shop=shop2, product=p1) sp4 = ShopProduct.objects.create(shop=shop2, product=p2) request = get_request("/api/shuup/front/products/", admin_user) response = FrontProductViewSet.as_view({"get": "list"})(request) response.render() assert response.status_code == status.HTTP_200_OK products_data = json.loads(response.content.decode("utf-8")) # should return only 2 procuts with 2 shops each assert len(products_data) == 2 products = sorted(products_data, key=lambda p: p["id"]) assert products[0]["id"] == p1.id assert products[0]["shop_products"] == [sp1.id, sp3.id] assert products[1]["id"] == p2.id assert products[1]["shop_products"] == [sp2.id, sp4.id]
def test_price_info_cache_bump(rf): request, shop, group = initialize_test(rf, True) product_one = create_product("Product_1", shop, default_price=150) product_two = create_product("Product_2", shop, default_price=250) contact = create_customer() group2 = ContactGroup.objects.create(name="Group 2", shop=shop) cgp_price = CgpPrice.objects.create(product=product_one, shop=shop, group=group, price_value=100) cgp_discount = CgpDiscount.objects.create(product=product_two, shop=shop, group=group, discount_amount_value=200) spm = get_pricing_module() assert isinstance(spm, CustomerGroupPricingModule) pricing_context = spm.get_context_from_request(request) for function in [ lambda: cgp_price.save(), lambda: cgp_discount.save(), lambda: group2.members.add(contact), lambda: cgp_price.delete(), lambda: cgp_discount.delete() ]: cache_price_info(pricing_context, product_one, 1, product_one.get_price_info(pricing_context)) cache_price_info(pricing_context, product_two, 1, product_two.get_price_info(pricing_context)) # prices are cached assert get_cached_price_info(pricing_context, product_one) assert get_cached_price_info(pricing_context, product_two) # caches should be bumped function() assert get_cached_price_info(pricing_context, product_one) is None assert get_cached_price_info(pricing_context, product_two) is None
def test_complex_variation(): request = get_request_with_basket() basket = request.basket shop = get_default_shop() supplier = get_default_supplier() parent = create_product("SuperComplexVarParent", shop=shop, supplier=supplier) color_var = ProductVariationVariable.objects.create(product=parent, identifier="color") size_var = ProductVariationVariable.objects.create(product=parent, identifier="size") ProductVariationVariableValue.objects.create(variable=color_var, identifier="yellow") ProductVariationVariableValue.objects.create(variable=size_var, identifier="small") combinations = list(parent.get_all_available_combinations()) for combo in combinations: child = create_product("xyz-%s" % combo["sku_part"], shop=shop, supplier=supplier) child.link_to_parent(parent, combo["variable_to_value"]) # Elided product should not yield a result yellow_color_value = ProductVariationVariableValue.objects.get(variable=color_var, identifier="yellow") small_size_value = ProductVariationVariableValue.objects.get(variable=size_var, identifier="small") # add to basket yellow + small kwargs = {"var_%d" % color_var.pk: yellow_color_value.pk, "var_%d" % size_var.pk: small_size_value.pk} basket_commands.handle_add_var(request, basket, parent.id, **kwargs) assert basket.get_product_ids_and_quantities()[child.pk] == 1 with pytest.raises(ValidationError): kwargs = {"var_%d" % color_var.pk: yellow_color_value.pk, "var_%d" % size_var.pk: small_size_value.pk + 1} basket_commands.handle_add_var(request, basket, parent.id, **kwargs)
def test_basket_shipping_error(rf): StoredBasket.objects.all().delete() shop = get_default_shop() supplier = get_default_supplier() shipped_product = create_product( printable_gibberish(), shop=shop, supplier=supplier, default_price=50, shipping_mode=ShippingMode.SHIPPED ) unshipped_product = create_product( printable_gibberish(), shop=shop, supplier=supplier, default_price=50, shipping_mode=ShippingMode.NOT_SHIPPED ) request = rf.get("/") request.session = {} request.shop = shop apply_request_middleware(request) basket = get_basket(request) # With a shipped product but no shipping methods, we oughta get an error basket.add_product(supplier=supplier, shop=shop, product=shipped_product, quantity=1) assert any(ve.code == "no_common_shipping" for ve in basket.get_validation_errors()) basket.clear_all() # But with an unshipped product, we should not basket.add_product(supplier=supplier, shop=shop, product=unshipped_product, quantity=1) assert not any(ve.code == "no_common_shipping" for ve in basket.get_validation_errors())
def test_product_in_basket_condition_with_variation_parent(rf): request, shop, group = initialize_test(rf, False) basket = get_basket(request) supplier = get_default_supplier() product = create_product( "test-product", shop, default_price="200", supplier=supplier, mode=ProductMode.SIMPLE_VARIATION_PARENT) child_products = [] for x in range(0, 3): child_product = create_product("test-product-%s" % x, shop, default_price="10", supplier=supplier) child_product.link_to_parent(product) child_products.append(child_product) condition = ProductsInBasketCondition.objects.create() condition.values = [product] condition.operator = ComparisonOperator.EQUALS condition.quantity = 3 condition.save() assert not condition.matches(basket, []) for child_product in child_products: basket.add_product(supplier=supplier, shop=shop, product=child_product, quantity=1) assert condition.matches(basket, [])
def test_mass_edit_orders3(rf, admin_user): shop = get_default_shop() supplier = get_default_supplier() contact1 = create_random_person() product1 = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price="50") product2 = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price="501") order1 = create_random_order(customer=contact1, products=[product1, product2], completion_probability=0) order2 = create_random_order(customer=contact1, products=[product1, product2], completion_probability=0) assert order1.status.role != OrderStatusRole.CANCELED assert order2.status.role != OrderStatusRole.CANCELED payload = { "action": OrderConfirmationPdfAction().identifier, "values": [order1.pk, order2.pk] } request = apply_request_middleware(rf.post( "/", user=admin_user, )) request._body = json.dumps(payload).encode("UTF-8") view = OrderListView.as_view() response = view(request=request) assert response.status_code == 200 if weasyprint: assert response['Content-Disposition'] == 'attachment; filename=order_confirmation_pdf.zip' else: assert response["content-type"] == "application/json"
def init_test(request, shop, prices): apply_request_middleware(request) parent = create_product("parent_product", shop=shop) children = [create_product("child-%d" % price, shop=shop, default_price=price) for price in prices] for child in children: child.link_to_parent(parent) return parent
def test_best_selling_products_with_multiple_orders(): context = get_jinja_context() supplier = get_default_supplier() shop = get_default_shop() n_products = 2 price = 10 product_1 = create_product("test-sku-1", supplier=supplier, shop=shop) product_2 = create_product("test-sku-2", supplier=supplier, shop=shop) create_order_with_product(product_1, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) create_order_with_product(product_2, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) cache.clear() # Two initial products sold assert product_1 in general.get_best_selling_products(context, n_products=n_products) assert product_2 in general.get_best_selling_products(context, n_products=n_products) product_3 = create_product("test-sku-3", supplier=supplier, shop=shop) create_order_with_product(product_3, supplier, quantity=2, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product sold in greater quantity assert product_3 in general.get_best_selling_products(context, n_products=n_products) create_order_with_product(product_1, supplier, quantity=4, taxless_base_unit_price=price, shop=shop) create_order_with_product(product_2, supplier, quantity=4, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product outsold by first two products assert product_3 not in general.get_best_selling_products(context, n_products=n_products) children = [create_product("SimpleVarChild-%d" % x, supplier=supplier, shop=shop) for x in range(5)] for child in children: child.link_to_parent(product_3) create_order_with_product(child, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product now sold in greatest quantity assert product_3 == general.get_best_selling_products(context, n_products=n_products)[0]
def test_products_form_add_multiple_products(): shop = get_default_shop() category = get_default_category() category.shops.add(shop) product_ids = [] for x in range(0, 15): product = create_product("%s" % x, shop=shop) product_ids.append(product.id) for x in range(0, 5): product = create_product("parent_%s" % x, shop=shop, mode=ProductMode.SIMPLE_VARIATION_PARENT) for y in range(0, 3): child_product = create_product("child_%s_%s" % (x, y), shop=shop) child_product.link_to_parent(product) product_ids.append(product.id) assert (category.shop_products.count() == 0) data = { "additional_products": ["%s" % product_id for product_id in product_ids] } form = CategoryProductForm(shop=shop, category=category, data=data) form.full_clean() form.save() category.refresh_from_db() assert (category.shop_products.count() == 35) # 15 normal products and 5 parents with 3 children each
def test_mass_edit_orders(rf, admin_user): shop = get_default_shop() supplier = get_default_supplier() contact1 = create_random_person() product1 = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price="50") product2 = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price="501") order = create_random_order(customer=contact1, products=[product1, product2], completion_probability=0) assert order.status.role != OrderStatusRole.CANCELED payload = { "action": CancelOrderAction().identifier, "values": [order.pk] } request = apply_request_middleware(rf.post( "/", user=admin_user, )) request._body = json.dumps(payload).encode("UTF-8") view = OrderListView.as_view() response = view(request=request) assert response.status_code == 200 for order in Order.objects.all(): assert order.status.role == OrderStatusRole.CANCELED
def test_multivariable_variation(): parent = create_product("SuperComplexVarParent") color_var = ProductVariationVariable.objects.create(product=parent, identifier="color") size_var = ProductVariationVariable.objects.create(product=parent, identifier="size") for color in ("yellow", "blue", "brown"): ProductVariationVariableValue.objects.create(variable=color_var, identifier=color) for size in ("small", "medium", "large", "huge"): ProductVariationVariableValue.objects.create(variable=size_var, identifier=size) combinations = list(parent.get_all_available_combinations()) assert len(combinations) == (3 * 4) for combo in combinations: assert not combo["result_product_pk"] # Elide a combination (yellow/small) for testing: if combo["variable_to_value"][color_var].identifier == "yellow" and combo["variable_to_value"][size_var].identifier == "small": continue child = create_product("xyz-%s" % combo["sku_part"]) child.link_to_parent(parent, combo["variable_to_value"]) assert parent.mode == ProductMode.VARIABLE_VARIATION_PARENT # Elided product should not yield a result yellow_color_value = ProductVariationVariableValue.objects.get(variable=color_var, identifier="yellow") small_size_value = ProductVariationVariableValue.objects.get(variable=size_var, identifier="small") assert not ProductVariationResult.resolve(parent, {color_var: yellow_color_value, size_var: small_size_value}) # Anything else should brown_color_value = ProductVariationVariableValue.objects.get(variable=color_var, identifier="brown") result1 = ProductVariationResult.resolve(parent, {color_var: brown_color_value, size_var: small_size_value}) result2 = ProductVariationResult.resolve(parent, {color_var.pk: brown_color_value.pk, size_var.pk: small_size_value.pk}) assert result1 and result2 assert result1.pk == result2.pk assert len(parent.get_available_variation_results()) == (3 * 4 - 1)
def test_price_infos(rf): request, shop, group = initialize_test(rf, True) price = shop.create_price product_one = create_product("Product_1", shop, default_price=150) product_two = create_product("Product_2", shop, default_price=250) spp = CgpPrice(product=product_one, shop=shop, group=group, price_value=100) spp.save() spp = CgpPrice(product=product_two, shop=shop, group=group, price_value=200) spp.save() product_ids = [product_one.pk, product_two.pk] spm = get_pricing_module() assert isinstance(spm, CustomerGroupPricingModule) pricing_context = spm.get_context_from_request(request) price_infos = spm.get_price_infos(pricing_context, product_ids) assert len(price_infos) == 2 assert product_one.pk in price_infos assert product_two.pk in price_infos assert price_infos[product_one.pk].price == price(100) assert price_infos[product_two.pk].price == price(200) assert price_infos[product_one.pk].base_price == price(100) assert price_infos[product_two.pk].base_price == price(200)
def test_product_hightlight_plugin(rf, highlight_type, orderable): shop = get_default_shop() p1 = create_product("p1", shop, get_default_supplier(), "10") p2 = create_product("p2", shop, get_default_supplier(), "20") p3 = create_product("p3", shop, get_default_supplier(), "30") p4 = create_product("p4", shop, get_default_supplier(), "40") sp4 = p4.get_shop_instance(shop) sp4.purchasable = False sp4.save() context = get_context(rf) plugin = ProductHighlightPlugin({ "type": highlight_type, "count": 4, "orderable_only": orderable }) context_products = plugin.get_context_data(context)["products"] assert p1 in context_products assert p2 in context_products assert p3 in context_products if orderable: assert p4 not in context_products else: assert p4 in context_products
def test_limited_methods(): """ Test that products can declare that they limit available shipping methods. """ unique_shipping_method = get_shipping_method(name="unique", price=0) shop = get_default_shop() common_product = create_product(sku="SH_COMMON", shop=shop) # A product that does not limit shipping methods unique_product = create_product(sku="SH_UNIQUE", shop=shop) # A product that only supports unique_shipping_method unique_shop_product = unique_product.get_shop_instance(shop) unique_shop_product.limit_shipping_methods = True unique_shop_product.shipping_methods.add(unique_shipping_method) unique_shop_product.save() impossible_product = create_product(sku="SH_IMP", shop=shop) # A product that can't be shipped at all imp_shop_product = impossible_product.get_shop_instance(shop) imp_shop_product.limit_shipping_methods = True imp_shop_product.save() for product_ids, method_ids in [ ((common_product.pk, unique_product.pk), (unique_shipping_method.pk,)), ((common_product.pk,), ShippingMethod.objects.values_list("pk", flat=True)), ((unique_product.pk,), (unique_shipping_method.pk,)), ((unique_product.pk, impossible_product.pk,), ()), ((common_product.pk, impossible_product.pk,), ()), ]: product_ids = set(product_ids) assert ShippingMethod.objects.available_ids(shop=shop, products=product_ids) == set(method_ids)
def test_variable_variation(): parent = create_product("ComplexVarParent") sizes_and_children = [("%sL" % ("X" * x), create_product("ComplexVarChild-%d" % x)) for x in range(4)] for size, child in sizes_and_children: child.link_to_parent(parent, variables={"size": size}) assert parent.mode == ProductMode.VARIABLE_VARIATION_PARENT assert all(child.is_variation_child() for (size, child) in sizes_and_children) # Validation tests dummy = create_product("InvalidComplexVarChild") with pytest.raises(ValueError): dummy.link_to_parent(parent) with pytest.raises(ValueError): parent.link_to_parent(dummy) with pytest.raises(ValueError): dummy.link_to_parent(sizes_and_children[0][1]) # Variable tests size_attr = parent.variation_variables.get(identifier="size") for size, child in sizes_and_children: size_val = size_attr.values.get(identifier=size) result_product = ProductVariationResult.resolve(parent, {size_attr: size_val}) assert result_product == child
def test_protected_fields(): activate("en") shop = Shop.objects.create( name="testshop", identifier="testshop", status=ShopStatus.ENABLED, public_name="test shop", domain="derp", currency="EUR" ) get_currency("EUR") get_currency("USD") assert shop.name == "testshop" assert shop.currency == "EUR" assert not ConfigurationItem.objects.filter(shop=shop, key="languages").exists() shop_form = ShopBaseForm(instance=shop, languages=settings.LANGUAGES) assert not shop_form._get_protected_fields() # No protected fields just yet, right? data = get_form_data(shop_form, prepared=True) shop_form = ShopBaseForm(data=data, instance=shop, languages=settings.LANGUAGES) _test_cleanliness(shop_form) shop_form.save() # Now let's make it protected! create_product(printable_gibberish(), shop=shop, supplier=get_default_supplier()) order = create_random_order(customer=create_random_person(), shop=shop) assert order.shop == shop # And try again... data["currency"] = "USD" shop_form = ShopBaseForm(data=data, instance=shop, languages=settings.LANGUAGES) assert shop_form._get_protected_fields() # So protected! _test_cleanliness(shop_form) shop = shop_form.save() assert shop.currency == "EUR" # But the shop form ignored the change . . .
def test_get_orderable_variation_children(rf): supplier = get_default_supplier() shop = get_default_shop() variable_name = "Color" parent = create_product("test-sku-1", shop=shop) variation_variable = ProductVariationVariable.objects.create(product=parent, identifier="color", name=variable_name) red_value = ProductVariationVariableValue.objects.create(variable=variation_variable, identifier="red", value="Red") blue_value =ProductVariationVariableValue.objects.create(variable=variation_variable, identifier="blue", value="Blue") combinations = list(parent.get_all_available_combinations()) assert len(combinations) == 2 for combo in combinations: assert not combo["result_product_pk"] child = create_product("xyz-%s" % combo["sku_part"], shop=shop, supplier=get_default_supplier(), default_price=20) child.link_to_parent(parent, combination_hash=combo["hash"]) combinations = list(parent.get_all_available_combinations()) assert len(combinations) == 2 parent.refresh_from_db() assert parent.is_variation_parent() request = apply_request_middleware(rf.get("/")) cache.clear() for time in range(2): orderable_children, is_orderable = get_orderable_variation_children(parent, request, None) assert len(orderable_children) for var_variable, var_values in dict(orderable_children).items(): assert var_variable == variation_variable assert red_value in var_values assert blue_value in var_values
def test_bump_caches_signal(rf): """ Test that prices are bumped when discount objects changes """ initial_price = 10 discounted_price = 5 shop1 = factories.get_default_shop() shop2 = factories.get_shop(identifier="shop2", domain="shop2") product1 = factories.create_product( "product", shop=shop1, supplier=factories.get_default_supplier(), default_price=initial_price) product2 = factories.create_product( "product2", shop=shop2, supplier=factories.get_default_supplier(), default_price=20) now = datetime(2018, 1, 1, 9, 0, tzinfo=pytz.UTC) # 01/01/2018 09:00 AM with patch("django.utils.timezone.now", new=lambda: now): discount = Discount.objects.create( name="discount", active=True, start_datetime=now - timedelta(days=10), end_datetime=now + timedelta(days=10), discounted_price_value=discounted_price, shop=shop1, ) request = apply_request_middleware(rf.get("/")) request_shop2 = apply_request_middleware( rf.get("/", HTTP_HOST=shop2.domain)) def assert_cache_product1(discounted=False): cache_price_info(request, product1, 1, product1.get_price_info(request)) if discounted: assert get_cached_price_info( request, product1, 1).price == shop1.create_price(discounted_price) else: assert get_cached_price_info( request, product1, 1).price == shop1.create_price(initial_price) def assert_product1_is_not_cached(): assert get_cached_price_info(request, product1) is None def assert_product2_is_cached(): assert get_cached_price_info(request_shop2, product2) is not None assert_product1_is_not_cached() assert_cache_product1(True) # cache bumped - the cache should be dropped - then, cache again discount.save() assert_product1_is_not_cached() assert_cache_product1(True) # cache product 2.. from now on, shop2 cache should never be bumped cache_price_info(request_shop2, product2, 1, product2.get_price_info(request_shop2)) assert_product2_is_cached() discount.product = product1 discount.save() assert_product1_is_not_cached() assert_cache_product1(True) assert_product2_is_cached() happy_hour = HappyHour.objects.create(name="hh 1", shop=shop1) happy_hour.discounts.add(discount) assert_product1_is_not_cached() assert_cache_product1(True) assert_product2_is_cached() happy_hour.save() assert_product1_is_not_cached() assert_cache_product1(True) assert_product2_is_cached() time_range = TimeRange.objects.create( happy_hour=happy_hour, from_hour=(now - timedelta(hours=1)).time(), to_hour=(now + timedelta(hours=1)).time(), weekday=now.weekday(), ) assert_product1_is_not_cached() assert_cache_product1(True) assert_product2_is_cached() time_range.save() assert_product1_is_not_cached() assert_cache_product1(True) assert_product2_is_cached() time_range.delete() assert_product1_is_not_cached() assert_cache_product1(True) assert_product2_is_cached() with pytest.raises(DiscountM2MChangeError): handle_generic_m2m_changed("test", time_range)
def test_many_price_info_cache_bump(rf): initial_price = 10 shop = factories.get_default_shop() tax = factories.get_default_tax() tax_class = factories.get_default_tax_class() product = factories.create_product( "product", shop=shop, supplier=factories.get_default_supplier(), default_price=initial_price) child1 = factories.create_product( "child1", shop=shop, supplier=factories.get_default_supplier(), default_price=5) child2 = factories.create_product( "child2", shop=shop, supplier=factories.get_default_supplier(), default_price=9) child1.link_to_parent(product, variables={"color": "red"}) child2.link_to_parent(product, variables={"color": "blue"}) request = apply_request_middleware(rf.get("/")) child1_pi = child1.get_price_info(request) child2_pi = child1.get_price_info(request) def assert_cache_products(): cache_many_price_info(request, product, 1, [child1_pi, child2_pi]) assert get_many_cached_price_info(request, product, 1)[0].price == child1_pi.price assert get_many_cached_price_info(request, product, 1)[1].price == child2_pi.price def assert_nothing_is_cached(): # nothing is cached assert get_many_cached_price_info(request, product, 1) is None # cache the item assert_nothing_is_cached() assert_cache_products() # cache bumped - the cache should be dropped - then, cache again tax.save() assert_nothing_is_cached() assert_cache_products() # cache bumped - the cache should be dropped - then, cache again tax_class.save() assert_nothing_is_cached() assert_cache_products() # cache bumped - the cache should be dropped - then, cache again product.save() assert_nothing_is_cached() assert_cache_products() shop_product = product.get_shop_instance(shop) # cache bumped - the cache should be dropped - then, cache again shop_product.save() assert_nothing_is_cached() assert_cache_products() category = factories.get_default_category() # cache bumped - the cache should be dropped - then, cache again shop_product.categories.add(category) assert_nothing_is_cached() assert_cache_products() # cache bumped - the cache should be dropped - then, cache again supplier = shop_product.suppliers.first() supplier.enabled = False supplier.save() assert_nothing_is_cached() assert_cache_products()
def test_best_selling_products_with_multiple_orders(): context = get_jinja_context() supplier = get_default_supplier() shop = get_default_shop() n_products = 2 price = 10 product_1 = create_product("test-sku-1", supplier=supplier, shop=shop) product_2 = create_product("test-sku-2", supplier=supplier, shop=shop) create_order_with_product(product_1, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) create_order_with_product(product_2, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) cache.clear() # Two initial products sold assert product_1 in general.get_best_selling_products( context, n_products=n_products) assert product_2 in general.get_best_selling_products( context, n_products=n_products) product_3 = create_product("test-sku-3", supplier=supplier, shop=shop) create_order_with_product(product_3, supplier, quantity=2, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product sold in greater quantity assert product_3 in general.get_best_selling_products( context, n_products=n_products) create_order_with_product(product_1, supplier, quantity=4, taxless_base_unit_price=price, shop=shop) create_order_with_product(product_2, supplier, quantity=4, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product outsold by first two products assert product_3 not in general.get_best_selling_products( context, n_products=n_products) children = [ create_product("SimpleVarChild-%d" % x, supplier=supplier, shop=shop) for x in range(5) ] for child in children: child.link_to_parent(product_3) create_order_with_product(child, supplier, quantity=1, taxless_base_unit_price=price, shop=shop) cache.clear() # Third product now sold in greatest quantity assert product_3 == general.get_best_selling_products( context, n_products=n_products)[0]
def test_basket_partial_quantity_update_all_product_counts(): shop = get_default_shop() supplier = get_default_supplier() request = get_request_with_basket() basket = request.basket pieces = SalesUnit.objects.create(identifier="pieces", decimals=0, name="Pieces", symbol='pc.') kilograms = SalesUnit.objects.create(identifier="kilograms", decimals=3, name="Kilograms", symbol='kg') cup = create_product(sku="COFFEE-CUP-123", sales_unit=pieces, shop=shop, supplier=supplier) beans = create_product(sku="COFFEEBEANS3", sales_unit=kilograms, shop=shop, supplier=supplier) beans_shop_product = beans.get_shop_instance(shop) beans_shop_product.minimum_purchase_quantity = Decimal('0.1') beans_shop_product.save() pears = create_product(sku="PEARS-27", sales_unit=kilograms, shop=shop, supplier=supplier) add = basket_commands.handle_add update = basket_commands.handle_update # Empty basket assert basket.product_count == 0 assert basket.smart_product_count == 0 assert basket.product_line_count == 0 # 1 cup add(request, basket, product_id=cup.pk, quantity=1) assert basket.product_count == 1 assert basket.smart_product_count == 1 assert basket.product_line_count == 1 # Basket update operations work by prefixing line id with operation qty_update_cup = 'q_' + basket.get_lines()[0].line_id delete_cup = 'delete_' + basket.get_lines()[0].line_id # 3 cups update(request, basket, **{qty_update_cup: "3"}) assert basket.product_count == 3 assert basket.smart_product_count == 3 assert basket.product_line_count == 1 # 3 cups + 0.5 kg beans add(request, basket, product_id=beans.pk, quantity='0.5') assert basket.product_count == Decimal('3.5') assert basket.smart_product_count == 4 assert basket.product_line_count == 2 qty_update_beans = 'q_' + basket.get_lines()[1].line_id delete_beans1 = 'delete_' + basket.get_lines()[1].line_id # 1 cup + 2.520 kg beans update(request, basket, **{qty_update_cup: "1.0"}) update(request, basket, **{qty_update_beans: "2.520"}) assert basket.product_count == Decimal('3.520') assert basket.smart_product_count == 2 assert basket.product_line_count == 2 # 42 cups + 2.520 kg beans update(request, basket, **{qty_update_cup: "42"}) assert basket.product_count == Decimal('44.520') assert basket.smart_product_count == 43 assert basket.product_line_count == 2 # 42 cups + 2.520 kg beans + 3.5 kg pears add(request, basket, product_id=pears.pk, quantity='3.5') assert basket.product_count == Decimal('48.020') assert basket.smart_product_count == 44 assert basket.product_line_count == 3 # 42 cups + 3.5 kg pears update(request, basket, **{delete_beans1: "1"}) assert basket.product_count == Decimal('45.5') assert basket.smart_product_count == 43 assert basket.product_line_count == 2 # 3.5 kg pears update(request, basket, **{delete_cup: "1"}) assert basket.product_count == Decimal('3.5') assert basket.smart_product_count == 1 assert basket.product_line_count == 1
def test_refunds_for_discounted_order_lines(): shop = get_default_shop() supplier = get_default_supplier() product = create_product("test-sku", shop=get_default_shop(), default_price=10, stock_behavior=StockBehavior.STOCKED) order = create_order_with_product(product, supplier, 2, 200, shop=shop) discount_line = OrderLine(order_id=order.id, type=OrderLineType.DISCOUNT, quantity=1, discount_amount_value=Decimal("0.54321")) discount_line.save() order.lines.add(discount_line) product_line = order.lines.filter(type=OrderLineType.PRODUCT).first() product_line.discount_amount = TaxfulPrice(100, order.currency) product_line.save() taxful_price_with_discount = product_line.taxful_price order.cache_prices() order.save() assert product_line.base_price == TaxfulPrice(400, order.currency) assert taxful_price_with_discount == TaxfulPrice(300, order.currency) # try to refund only the product line - should fail since this would result in a negative total with pytest.raises(RefundExceedsAmountException): order.create_refund([{ "line": product_line, "quantity": 2, "amount": taxful_price_with_discount.amount }]) # try to refund the product line with a negative amount with pytest.raises(InvalidRefundAmountException): order.create_refund([{ "line": product_line, "quantity": 1, "amount": -taxful_price_with_discount.amount }]) # try to refund the discount line with a positive amount with pytest.raises(InvalidRefundAmountException): order.create_refund([{ "line": discount_line, "quantity": 1, "amount": -discount_line.taxful_price.amount }]) order.create_refund([{ "line": discount_line, "quantity": 1, "amount": discount_line.taxful_price.amount }, { "line": product_line, "quantity": 2, "amount": taxful_price_with_discount.amount }]) assert product_line.max_refundable_amount.value == 0 assert discount_line.max_refundable_amount.value == 0 assert order.taxful_total_price.value == 0 order = create_order_with_product(product, supplier, 2, 200, shop=shop) discount_line = OrderLine(order_id=order.id, type=OrderLineType.DISCOUNT, quantity=1, discount_amount_value=Decimal("0.54321")) discount_line.save() order.lines.add(discount_line) product_line = order.lines.filter(type=OrderLineType.PRODUCT).first() product_line.discount_amount = TaxfulPrice(100, order.currency) product_line.save() order.cache_prices() order.save() order.create_full_refund(restock_products=False) assert order.taxful_total_price.value == 0
def test_order_full_refund_with_taxes(include_tax): tax_rate = Decimal(0.2) # 20% product_price = 100 discount_amount = 30 random_line_price = 5 shop = factories.get_shop(include_tax) source = OrderSource(shop) source.status = factories.get_initial_order_status() supplier = factories.get_default_supplier() create_default_order_statuses() tax = factories.get_tax("sales-tax", "Sales Tax", tax_rate) factories.create_default_tax_rule(tax) product = factories.create_product("sku", shop=shop, supplier=supplier, default_price=product_price) line = source.add_line( line_id="product-line", type=OrderLineType.PRODUCT, product=product, supplier=supplier, quantity=1, shop=shop, base_unit_price=source.create_price(product_price), ) discount_line = source.add_line( line_id="discount-line", type=OrderLineType.DISCOUNT, supplier=supplier, quantity=1, base_unit_price=source.create_price(0), discount_amount=source.create_price(discount_amount), parent_line_id=line.line_id) raw_total_price = Decimal(product_price - discount_amount) total_taxful = bround(source.taxful_total_price.value) total_taxless = bround(source.taxless_total_price.value) if include_tax: assert total_taxful == bround(raw_total_price) assert total_taxless == bround(raw_total_price / (1 + tax_rate)) else: assert total_taxful == bround(raw_total_price * (1 + tax_rate)) assert total_taxless == bround(raw_total_price) # Lines without quantity shouldn't affect refunds other_line = source.add_line( text="This random line for textual information", line_id="other-line", type=OrderLineType.OTHER, quantity=0) # Lines with quantity again should be able to be refunded normally. other_line_with_quantity = source.add_line( line_id="other_line_with_quantity", type=OrderLineType.OTHER, text="Special service $5/h", quantity=1, base_unit_price=source.create_price(random_line_price)) raw_total_price = Decimal(product_price - discount_amount + random_line_price) total_taxful = bround(source.taxful_total_price.value) total_taxless = bround(source.taxless_total_price.value) if include_tax: assert total_taxful == bround(raw_total_price) assert total_taxless == bround(raw_total_price / (1 + tax_rate)) else: assert total_taxful == bround(raw_total_price * (1 + tax_rate)) assert total_taxless == bround(raw_total_price) creator = OrderCreator() order = creator.create_order(source) assert order.taxful_total_price.value == total_taxful assert order.taxless_total_price.value == total_taxless order.create_payment(order.taxful_total_price) assert order.is_paid() order.create_full_refund() assert order.taxful_total_price_value == 0 for parent_order_line in order.lines.filter(parent_line__isnull=True): if parent_order_line.quantity == 0: assert not parent_order_line.child_lines.exists() else: refund_line = parent_order_line.child_lines.filter( type=OrderLineType.REFUND).first() assert refund_line assert parent_order_line.taxful_price.value == -refund_line.taxful_price.value assert parent_order_line.taxless_price.value == -refund_line.taxless_price.value assert parent_order_line.price.value == -refund_line.price.value
def test_get_best_selling_products(admin_user): shop1 = get_default_shop() shop2 = get_shop(True) person1 = create_random_person() person1.user = admin_user person1.save() supplier = create_simple_supplier("supplier1") client = _get_client(admin_user) # list best selling products response = client.get("/api/shuup/front/shop_products/best_selling/", { "shop": shop2.pk, "limit": 20 }) assert response.status_code == status.HTTP_200_OK products = json.loads(response.content.decode("utf-8")) assert len(products["results"]) == 0 # THIS IS IMPORTANT! cache.clear() products = [ create_product("Standard-%d" % x, supplier=supplier, shop=shop2) for x in range(10) ] # create 1 product with 4 variations parent_product = create_product("ParentProduct1", supplier=supplier, shop=shop2) children = [ create_product("SimpleVarChild-%d" % x, supplier=supplier, shop=shop2) for x in range(4) ] for child in children: child.link_to_parent(parent_product) best_selling = defaultdict(int) # create orders with standard products for p_index in range(len(products)): order = create_empty_order(shop=shop2) order.save() qty = (len(products) - p_index) add_product_to_order(order, supplier, products[p_index], qty, Decimal(1.0)) order.create_shipment_of_all_products() order.status = OrderStatus.objects.get_default_complete() order.save(update_fields=("status", )) best_selling[products[p_index].id] = qty # create orders with variation products - the parent product is counted instead of its children for p_index in range(2): variation = random.choice(children) qty = 5 order = create_empty_order(shop=shop2) order.save() add_product_to_order(order, supplier, variation, qty, Decimal(1.0)) order.create_shipment_of_all_products() order.status = OrderStatus.objects.get_default_complete() order.save(update_fields=("status", )) best_selling[parent_product.id] = best_selling[parent_product.id] + qty # get the top 100 best selling products response = client.get("/api/shuup/front/shop_products/best_selling/", { "shop": shop2.pk, "limit": 100 }) assert response.status_code == status.HTTP_200_OK products = json.loads(response.content.decode("utf-8")) assert len(products["results"]) == len( best_selling) # as we added less then 100, this must be true assert products["next"] is None # check the if all IDS are part of best selling for ix in range(len(products)): assert products["results"][ix]["product_id"] in best_selling.keys() # get the top 5 best selling products (we should get paginated results) response = client.get("/api/shuup/front/shop_products/best_selling/", { "shop": shop2.pk, "limit": 5 }) assert response.status_code == status.HTTP_200_OK products = json.loads(response.content.decode("utf-8")) assert len(products["results"]) == 5 assert products["count"] == len(best_selling) assert products["next"] is not None sorted_best_selling_ids = [ prod[0] for prod in sorted(best_selling.items(), key=lambda prod: -prod[1]) ][:5] # check the if all the 5 best sellers are part of best selling for ix in range(len(products)): assert products["results"][ix]["product_id"] in sorted_best_selling_ids
def test_basket_campaign_case2(rf): request, shop, group = initialize_test(rf, False) price = shop.create_price basket = get_basket(request) supplier = get_default_supplier() # create a basket rule that requires at least value of 200 rule = BasketTotalAmountCondition.objects.create(value="200") single_product_price = "50" discount_amount_value = "10" unique_shipping_method = get_shipping_method(shop, price=50) for x in range(3): product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=single_product_price) basket.add_product(supplier=supplier, shop=shop, product=product, quantity=1) assert basket.product_count == 3 campaign = BasketCampaign.objects.create(shop=shop, public_name="test", name="test", active=True) campaign.conditions.add(rule) campaign.save() BasketDiscountAmount.objects.create(discount_amount=discount_amount_value, campaign=campaign) assert len(basket.get_final_lines()) == 3 assert basket.total_price == price( single_product_price) * basket.product_count # check that shipping method affects campaign basket.shipping_method = unique_shipping_method basket.save() basket.uncache() assert len(basket.get_final_lines() ) == 4 # Shipping should not affect the rule being triggered line_types = [l.type for l in basket.get_final_lines()] assert OrderLineType.DISCOUNT not in line_types product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=single_product_price) basket.add_product(supplier=supplier, shop=shop, product=product, quantity=1) assert len(basket.get_final_lines()) == 6 # Discount included assert OrderLineType.DISCOUNT in [l.type for l in basket.get_final_lines()]
def test_order_partial_refund_with_taxes(include_tax): tax_rate = Decimal(0.2) # 20% product_price = 100 discount_amount = 30 random_line_price = 5 refunded_amount = 15 shop = factories.get_shop(include_tax) source = OrderSource(shop) source.status = factories.get_initial_order_status() supplier = factories.get_default_supplier() create_default_order_statuses() tax = factories.get_tax("sales-tax", "Sales Tax", tax_rate) factories.create_default_tax_rule(tax) product = factories.create_product("sku", shop=shop, supplier=supplier, default_price=product_price) line = source.add_line( line_id="product-line", type=OrderLineType.PRODUCT, product=product, supplier=supplier, quantity=1, shop=shop, base_unit_price=source.create_price(product_price), ) discount_line = source.add_line( line_id="discount-line", type=OrderLineType.DISCOUNT, supplier=supplier, quantity=1, base_unit_price=source.create_price(0), discount_amount=source.create_price(discount_amount), parent_line_id=line.line_id) raw_total_price = Decimal(product_price - discount_amount) total_taxful = bround(source.taxful_total_price.value) total_taxless = bround(source.taxless_total_price.value) if include_tax: assert total_taxful == bround(raw_total_price) assert total_taxless == bround(raw_total_price / (1 + tax_rate)) else: assert total_taxful == bround(raw_total_price * (1 + tax_rate)) assert total_taxless == bround(raw_total_price) creator = OrderCreator() order = creator.create_order(source) assert order.taxful_total_price.value == total_taxful assert order.taxless_total_price.value == total_taxless order.create_payment(order.taxful_total_price) assert order.is_paid() refund_data = [ dict( amount=Money(refunded_amount, shop.currency), quantity=1, line=order.lines.products().first(), ) ] order.create_refund(refund_data) total_taxful = bround(order.taxful_total_price.value) total_taxless = bround(order.taxless_total_price.value) taxless_refunded_amount = (refunded_amount / (1 + tax_rate)) if include_tax: raw_total_price = Decimal(product_price - discount_amount - refunded_amount) assert total_taxful == bround(raw_total_price) assert total_taxless == bround(raw_total_price / (1 + tax_rate)) else: # the refunded amount it considered a taxful price internally raw_total_price = Decimal(product_price - discount_amount) assert total_taxful == bround((raw_total_price * (1 + tax_rate)) - refunded_amount) assert total_taxless == bround(raw_total_price - taxless_refunded_amount) refund_line = order.lines.refunds().filter( type=OrderLineType.REFUND).first() if include_tax: assert refund_line.taxful_price.value == -bround(refunded_amount) assert refund_line.taxless_price.value == -bround( taxless_refunded_amount) else: assert refund_line.taxful_price.value == -bround(refunded_amount) assert refund_line.taxless_price.value == -bround( taxless_refunded_amount)
def test_product_media_api(admin_user): shop = get_default_shop() client = _get_client(admin_user) product = create_product("product", shop=shop) # create 2 images for product, 1 with contents, other with external url image = get_random_filer_image() media = ProductMedia.objects.create( product=product, kind=ProductMediaKind.IMAGE, file=image, enabled=True, public=True ) media_external = ProductMedia.objects.create( product=product, kind=ProductMediaKind.IMAGE, external_url="http://www.myimage.com/img.gif", enabled=True, public=True, ) product.primary_image = media product.save() # get product media response = client.get("/api/shuup/product_media/%d/" % media.pk) media_data = json.loads(response.content.decode("utf-8")) assert media_data["kind"] == media.kind.value assert media_data["product"] == product.pk assert media_data["public"] == media.public assert media_data["id"] == media.pk # get external media response = client.get("/api/shuup/product_media/%d/" % media_external.pk) media_data = json.loads(response.content.decode("utf-8")) assert media_data["kind"] == media_external.kind.value assert media_data["product"] == product.pk assert media_data["public"] == media_external.public assert media_data["id"] == media_external.pk assert media_data["external_url"] == media_external.external_url # update product media image = get_random_filer_image() with open(image.path, "rb") as f: img_base64 = base64.b64encode(os.urandom(50)).decode() uri = "data:application/octet-stream;base64,{}".format(img_base64) media_data = { "translations": {"en": {"title": "title 2", "description": "desc2"}}, "shops": [shop.pk], "kind": ProductMediaKind.IMAGE.value, "file": uri, "path": "/what/the/zzzz", } response = client.put( "/api/shuup/product_media/%d/" % media.pk, content_type="application/json", data=json.dumps(media_data) ) assert response.status_code == status.HTTP_200_OK media.refresh_from_db() assert media.title == media_data["translations"]["en"]["title"] assert media.description == media_data["translations"]["en"]["description"] assert media.shops.count() == len(media_data["shops"]) assert set(media.shops.all().values_list("pk", flat=True)) >= set(media_data["shops"]) assert media.kind.value == media_data["kind"] assert media.file.folder.pretty_logical_path == media_data["path"] with open(media.file.path, "rb") as f: assert img_base64 == base64.b64encode(f.read()).decode() media_count = product.media.count() # deletes an image response = client.delete("/api/shuup/product_media/%d/" % media.pk) assert response.status_code == status.HTTP_204_NO_CONTENT assert media_count - 1 == product.media.count()
def test_multiple_campaigns_match_with_coupon(rf): request, shop, group = initialize_test(rf, False) price = shop.create_price basket = get_basket(request) supplier = get_default_supplier() # create a basket rule that requires atleast value of 200 rule = BasketTotalAmountCondition.objects.create(value="200") product_price = "200" discount1 = "10" discount2 = "20" product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=product_price) basket.add_product(supplier=supplier, shop=shop, product=product, quantity=1) basket.shipping_method = get_shipping_method(shop=shop) campaign = BasketCampaign.objects.create(shop=shop, public_name="test", name="test", active=True) campaign.conditions.add(rule) campaign.save() BasketDiscountAmount.objects.create(discount_amount=discount1, campaign=campaign) dc = Coupon.objects.create(code="TEST", active=True) campaign2 = BasketCampaign.objects.create(shop=shop, public_name="test", name="test", coupon=dc, active=True) BasketDiscountAmount.objects.create(discount_amount=discount2, campaign=campaign2) basket.add_product(supplier=supplier, shop=shop, product=product, quantity=1) resp = handle_add_campaign_code(request, basket, dc.code) assert resp.get("ok") discount_lines_values = [ line.discount_amount for line in basket.get_final_lines() if line.type == OrderLineType.DISCOUNT ] assert price(discount1) in discount_lines_values assert price(discount2) in discount_lines_values assert basket.total_price == (price(product_price) * basket.product_count - price(discount1) - price(discount2)) assert basket.codes == [dc.code] # test code removal resp = handle_remove_campaign_code(request, basket, dc.code) assert resp.get("ok") assert basket.codes == [] discount_lines_values = [ line.discount_amount for line in basket.get_final_lines() if line.type == OrderLineType.DISCOUNT ] assert price(discount1) in discount_lines_values assert not price(discount2) in discount_lines_values
def create_orderable_product(name, sku, price): supplier = get_default_supplier() shop = get_default_shop() product = create_product(sku=sku, shop=shop, supplier=supplier, default_price=price, name=name) return product
def _create_product_for_day(shop, day): product = create_product("test_product") product.created_on = day product.save()
def get_frontend_order_state(contact, payment_method, product_price, valid_lines=True): """ Get a dict structure mirroring what the frontend JavaScript would submit. :type contact: Contact|None """ activate("en") shop = get_default_shop() tax = Tax.objects.create(code="test_code", rate=decimal.Decimal("0.20"), name="Default") tax_class = TaxClass.objects.create(identifier="test_tax_class", name="Default") rule = TaxRule.objects.create(tax=tax) rule.tax_classes.add(tax_class) rule.save() supplier = get_default_supplier() product = create_product(sku=printable_gibberish(), supplier=supplier, shop=shop) product.tax_class = tax_class product.save() if valid_lines: lines = [ { "id": "x", "type": "product", "product": { "id": product.id }, "quantity": "1", "baseUnitPrice": product_price, 'supplier': { 'name': supplier.name, 'id': supplier.id } }, { "id": "z", "type": "text", "text": "This was an order!", "quantity": 0 }, ] else: unshopped_product = create_product(sku=printable_gibberish(), supplier=supplier) not_visible_product = create_product(sku=printable_gibberish(), supplier=supplier, shop=shop) not_visible_shop_product = not_visible_product.get_shop_instance(shop) not_visible_shop_product.visibility = ShopProductVisibility.NOT_VISIBLE not_visible_shop_product.save() lines = [ { "id": "x", "type": "product", 'supplier': { 'name': supplier.name, 'id': supplier.id } }, # no product? { "id": "x", "type": "product", "product": { "id": unshopped_product.id }, 'supplier': { 'name': supplier.name, 'id': supplier.id } }, # not in this shop? { "id": "y", "type": "product", "product": { "id": -product.id }, 'supplier': { 'name': supplier.name, 'id': supplier.id } }, # invalid product? { "id": "z", "type": "other", "quantity": 1, "unitPrice": "q", 'supplier': { 'name': supplier.name, 'id': supplier.id } }, # what's that price? { "id": "rr", "type": "product", "quantity": 1, "product": { "id": not_visible_product.id }, 'supplier': { 'name': supplier.name, 'id': supplier.id } }, # not visible { "id": "y", "type": "product", "product": { "id": product.id } } # no supplier ] state = { "customer": { "id": contact.id if contact else None, "billingAddress": encode_address(contact.default_billing_address) if contact else {}, "shippingAddress": encode_address(contact.default_shipping_address) if contact else {}, }, "lines": lines, "methods": { "shippingMethod": { "id": get_default_shipping_method().id }, "paymentMethod": { "id": payment_method.id }, }, "shop": { "selected": { "id": shop.id, "name": shop.name, "currency": shop.currency, "priceIncludeTaxes": shop.prices_include_tax } } } return state
def _create_total_sales(shop, day): product = create_product("test", shop=shop) supplier = get_default_supplier() order = create_order_with_product(product, supplier, 1, 10, shop=shop) order.order_date = day order.save()
def test_catalog_campaign_sync(): shop = factories.get_default_shop() supplier = factories.get_default_supplier() default_price = 100 product1 = factories.create_product("test1", shop=shop, supplier=supplier, default_price=default_price) product2 = factories.create_product("test2", shop=shop, supplier=supplier, default_price=default_price) product3 = factories.create_product("test3", shop=shop, supplier=supplier, default_price=default_price) category = factories.get_default_category() shop_product = product1.get_shop_instance(shop) shop_product.primary_category = category shop_product.save() shop_product.categories.add(category) contact1 = factories.create_random_person() contact2 = factories.create_random_person() contact_group = factories.get_default_customer_group() contact2.groups.add(contact_group) happy_hour1_weekdays = "0,1" # Mon, Tue happy_hour1_start = datetime.time(21) happy_hour1_end = datetime.time(3) happy_hour1_condition = HourCondition.objects.create( days=happy_hour1_weekdays, hour_start=happy_hour1_start, hour_end=happy_hour1_end) happy_hour2_weekdays = "2,6" # Wed, Sun happy_hour2_start = datetime.time(14) happy_hour2_end = datetime.time(16) happy_hour2_condition = HourCondition.objects.create( days=happy_hour2_weekdays, hour_start=happy_hour2_start, hour_end=happy_hour2_end) discount_amount_value = 50 discount_percentage = decimal.Decimal("0.35") _create_catalog_campaign_for_products(shop, [product1], discount_amount_value, happy_hour1_condition) _create_catalog_campaign_for_products(shop, [product2, product3], discount_amount_value) _create_catalog_campaign_for_category(shop, category, discount_percentage) _create_catalog_campaign_for_contact(shop, product1, contact1, discount_amount_value) _create_catalog_campaign_for_contact_group(shop, [product1, product2, product3], contact_group, discount_percentage, happy_hour2_condition) call_command("import_catalog_campaigns", *[], **{}) # From first campaign we should get 1 discount with happy hour # From second campaign we should get 2 discounts # From third campaign we should get 1 discount # From fourth campaign we should get also 1 discount # From last campaign we should get 3 discounts with happy hour assert Discount.objects.count() == 8 # There should be 2 happy hours in total assert HappyHour.objects.count() == 2 # From first happy hour there should be 4 ranges # Mon 21-23, Tue 0-3, Tue 21-23, Wed 0-3 # From second happy hour there should be 2 ranges # Wed 14-16 and Sun 14-16 assert TimeRange.objects.count() == 6 # Let's go through all our 8 discounts to make sure all is good first_discount = Discount.objects.filter( product=product1, category__isnull=True, contact__isnull=True, contact_group__isnull=True).first() assert first_discount.happy_hours.count() == 1 assert first_discount.discount_amount_value == discount_amount_value second_discount = Discount.objects.filter( product=product2, category__isnull=True, contact__isnull=True, contact_group__isnull=True).first() assert second_discount.happy_hours.count() == 0 assert second_discount.discount_amount_value == discount_amount_value third_discount = Discount.objects.filter( product=product3, category__isnull=True, contact__isnull=True, contact_group__isnull=True).first() assert third_discount.happy_hours.count() == 0 assert third_discount.discount_amount_value == discount_amount_value category_discount = Discount.objects.filter( product__isnull=True, category=category, contact__isnull=True, contact_group__isnull=True).first() assert category_discount.happy_hours.count() == 0 assert category_discount.discount_percentage == discount_percentage contact_discount = Discount.objects.filter( product=product1, category__isnull=True, contact=contact1, contact_group__isnull=True).first() assert contact_discount.discount_amount_value == discount_amount_value product1_contact_group_discount = Discount.objects.filter( product=product1, category__isnull=True, contact__isnull=True, contact_group=contact_group).first() assert product1_contact_group_discount.happy_hours.count() == 1 assert product1_contact_group_discount.discount_percentage == discount_percentage product2_contact_group_discount = Discount.objects.filter( product=product2, category__isnull=True, contact__isnull=True, contact_group=contact_group).first() assert product2_contact_group_discount.happy_hours.count() == 1 assert product2_contact_group_discount.discount_percentage == discount_percentage product3_contact_group_discount = Discount.objects.filter( product=product3, category__isnull=True, contact__isnull=True, contact_group=contact_group).first() assert product3_contact_group_discount.happy_hours.count() == 1 assert product3_contact_group_discount.discount_percentage == discount_percentage
def test_nearby_products(admin_user): get_default_shop() supplier = create_simple_supplier("supplier1") # create Apple and its products shop1 = Shop.objects.create(status=ShopStatus.ENABLED) shop1.contact_address = MutableAddress.objects.create( name="Apple Infinite Loop", street="1 Infinite Loop", country="US", city="Cupertino", latitude=37.331667, longitude=-122.030146) shop1.save() product1 = create_product("macbook", shop1, supplier=supplier) product2 = create_product("imac", shop1, supplier=supplier) # create Google and its products shop2 = Shop.objects.create(status=ShopStatus.ENABLED) shop2.contact_address = MutableAddress.objects.create( name="Google", street="1600 Amphitheatre Pkwy", country="US", city="Mountain View", latitude=37.422000, longitude=-122.084024) shop2.save() product3 = create_product("nexus 1", shop2, supplier=supplier) product4 = create_product("nexux 7", shop2, supplier=supplier) my_position_to_apple = 2.982 my_position_to_google = 10.57 # YMCA my_position = (37.328330, -122.063612) # fetch products and their closest shops params = { "lat": my_position[0], "lng": my_position[1], "ordering": "-distance" } request = get_request("/api/shuup/front/products/", admin_user, data=params) response = FrontProductViewSet.as_view({"get": "list"})(request) response.render() assert response.status_code == status.HTTP_200_OK products = json.loads(response.content.decode("utf-8")) assert len(products) == 4 assert products[0]["id"] == product3.id assert products[0]["name"] == product3.name assert products[0][ "closest_shop_distance"] - my_position_to_google < 0.05 # 5 meters of error margin assert products[1]["id"] == product4.id assert products[1]["name"] == product4.name assert products[1][ "closest_shop_distance"] - my_position_to_google < 0.05 # 5 meters of error margin assert products[2]["id"] == product1.id assert products[2]["name"] == product1.name assert products[2][ "closest_shop_distance"] - my_position_to_apple < 0.05 # 5 meters of error margin assert products[3]["id"] == product2.id assert products[3]["name"] == product2.name assert products[3][ "closest_shop_distance"] - my_position_to_apple < 0.05 # 5 meters of error margin
def test_discount_no_limits(rf, include_tax): # check whether is it possible to earn money buying from us # adding lots of effects request, shop, _ = initialize_test(rf, include_tax) basket = get_basket(request) supplier = get_default_supplier() single_product_price = Decimal(50) quantity = 4 discount_amount = (single_product_price * quantity * 2) discount_percentage = Decimal(1.9) # 190% second_product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=single_product_price) # the expected discount amount should not be greater than the products expected_discount_amount = basket.create_price(single_product_price) * quantity category = CategoryFactory() # create basket rule that requires 2 products in basket product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=single_product_price) ShopProduct.objects.get(shop=shop, product=product).categories.add(category) basket.add_product(supplier=supplier, shop=shop, product=product, quantity=quantity) basket.shipping_method = get_shipping_method(shop=shop) basket.save() rule = ProductsInBasketCondition.objects.create(quantity=2) rule.products.add(product) rule.save() # BasketCampaign campaign = BasketCampaign.objects.create(active=True, shop=shop, name="test", public_name="test") campaign.conditions.add(rule) # effect 1 - categories from products DiscountFromCategoryProducts.objects.create(campaign=campaign, discount_percentage=discount_percentage, category=category) # effect 2 - discount from products effect2 = DiscountFromProduct.objects.create(campaign=campaign, discount_amount=discount_amount) effect2.products.add(product) # effect 3 - basket discount BasketDiscountAmount.objects.create(campaign=campaign, discount_amount=discount_amount) # effect 4 - basket discount percetage BasketDiscountPercentage.objects.create(campaign=campaign, discount_percentage=discount_percentage) basket.uncache() final_lines = basket.get_final_lines() assert len(final_lines) == 3 original_price = basket.create_price(single_product_price) * quantity line = final_lines[0] assert line.discount_amount == expected_discount_amount assert basket.total_price == original_price - expected_discount_amount # effect free - aaaww yes, it's free effect_free = FreeProductLine.objects.create(campaign=campaign, quantity=1) effect_free.products.add(second_product) basket.uncache() final_lines = basket.get_final_lines() assert len(final_lines) == 4 line = final_lines[0] assert line.discount_amount == expected_discount_amount assert basket.total_price == original_price - expected_discount_amount # CatalogCampaign catalog_campaign = CatalogCampaign.objects.create(active=True, shop=shop, name="test2", public_name="test") product_filter = ProductFilter.objects.create() product_filter.products.add(product) catalog_campaign.filters.add(product_filter) # effect 5 - ProductDiscountAmount ProductDiscountAmount.objects.create(campaign=catalog_campaign, discount_amount=discount_amount) # effct 6 - ProductDiscountPercentage ProductDiscountPercentage.objects.create(campaign=catalog_campaign, discount_percentage=discount_percentage) basket.uncache() final_lines = basket.get_final_lines() assert len(final_lines) == 4 line = final_lines[0] assert line.discount_amount == expected_discount_amount assert basket.total_price == original_price - expected_discount_amount
def get_product(): shop = get_default_shop() product = create_product("tmp", shop, default_price=200) return product
def test_complex_orderability(admin_user): shop = get_default_shop() fake_supplier = Supplier.objects.create(identifier="fake") admin_contact = get_person_contact(admin_user) parent = create_product("SuperComplexVarParent") shop_product = ShopProduct.objects.create(product=parent, shop=shop, visibility=ShopProductVisibility.ALWAYS_VISIBLE) shop_product.suppliers.add(fake_supplier) shop_product.visibility = ShopProductVisibility.ALWAYS_VISIBLE shop_product.save() color_var = ProductVariationVariable.objects.create(product=parent, identifier="color") size_var = ProductVariationVariable.objects.create(product=parent, identifier="size") for color in ("yellow", "blue", "brown"): ProductVariationVariableValue.objects.create(variable=color_var, identifier=color) for size in ("small", "medium", "large", "huge"): ProductVariationVariableValue.objects.create(variable=size_var, identifier=size) combinations = list(parent.get_all_available_combinations()) assert len(combinations) == (3 * 4) for combo in combinations: assert not combo["result_product_pk"] child = create_product("xyz-%s" % combo["sku_part"], shop=shop, supplier=fake_supplier) child.link_to_parent(parent, combo["variable_to_value"]) result_product = ProductVariationResult.resolve(parent, combo["variable_to_value"]) assert result_product == child assert parent.mode == ProductMode.VARIABLE_VARIATION_PARENT small_size_value = ProductVariationVariableValue.objects.get(variable=size_var, identifier="small") brown_color_value = ProductVariationVariableValue.objects.get(variable=color_var, identifier="brown") result1 = ProductVariationResult.resolve(parent, {color_var: brown_color_value, size_var: small_size_value}) result2 = ProductVariationResult.resolve(parent, {color_var.pk: brown_color_value.pk, size_var.pk: small_size_value.pk}) assert result1 and result2 assert result1.pk == result2.pk assert error_does_not_exist( shop_product.get_orderability_errors(supplier=fake_supplier, customer=admin_contact, quantity=1), code="no_sellable_children") # result 1 is no longer sellable sp = result1.get_shop_instance(shop) sp.visibility = ShopProductVisibility.NOT_VISIBLE sp.save() assert error_does_not_exist( shop_product.get_orderability_errors(supplier=fake_supplier, customer=admin_contact, quantity=1), code="no_sellable_children") # no sellable children for combo in combinations: result_product = ProductVariationResult.resolve(parent, combo["variable_to_value"]) sp = result_product.get_shop_instance(shop) sp.visibility = ShopProductVisibility.NOT_VISIBLE sp.save() assert error_exists( shop_product.get_orderability_errors(supplier=fake_supplier, customer=admin_contact, quantity=1), code="no_sellable_children")
def test_product_catalog_campaigns(): shop = get_default_shop() product = create_product("test", shop, default_price=20) parent_product = create_product("parent", shop, default_price=40) no_shop_child = create_product("child-no-shop") shop_child = create_product("child-shop", shop, default_price=60) shop_child.link_to_parent(parent_product) no_shop_child.link_to_parent(parent_product) shop_product = product.get_shop_instance(shop) parent_shop_product = parent_product.get_shop_instance(shop) child_shop_product = shop_child.get_shop_instance(shop) cat = Category.objects.create(name="test") campaign = CatalogCampaign.objects.create(shop=shop, name="test", active=True) # no rules assert CatalogCampaign.get_for_product(shop_product).count() == 0 assert CatalogCampaign.get_for_product(parent_shop_product).count() == 0 assert CatalogCampaign.get_for_product(child_shop_product).count() == 0 # category filter that doesn't match cat_filter = CategoryFilter.objects.create() cat_filter.categories.add(cat) campaign.filters.add(cat_filter) assert CatalogCampaign.get_for_product(shop_product).count() == 0 assert CatalogCampaign.get_for_product(parent_shop_product).count() == 0 assert CatalogCampaign.get_for_product(child_shop_product).count() == 0 for sp in [shop_product, parent_shop_product, child_shop_product]: sp.primary_category = cat sp.save() assert CatalogCampaign.get_for_product(sp).count() == 1 sp.categories.remove(cat) sp.primary_category = None sp.save() assert CatalogCampaign.get_for_product(sp).count() == 0 # category filter that matches sp.categories.add(cat) assert CatalogCampaign.get_for_product(sp).count() == 1 # create other shop shop1 = Shop.objects.create(name="testshop", identifier="testshop", status=ShopStatus.ENABLED, public_name="testshop") sp = ShopProduct.objects.create(product=product, shop=shop1, default_price=shop1.create_price(200)) assert product.get_shop_instance(shop1) == sp campaign2 = CatalogCampaign.objects.create(shop=shop1, name="test1", active=True) cat_filter2 = CategoryFilter.objects.create() cat_filter2.categories.add(cat) campaign2.filters.add(cat_filter2) assert CatalogCampaign.get_for_product(sp).count() == 0 # add product to this category sp.primary_category = cat sp.save() assert CatalogCampaign.get_for_product(sp).count() == 1 # matches now sp.categories.remove(cat) sp.primary_category = None sp.save() assert CatalogCampaign.get_for_product(sp).count() == 0 # no match sp.categories.add(cat) assert CatalogCampaign.get_for_product(sp).count() == 1 # matches now campaign3 = CatalogCampaign.objects.create(shop=shop1, name="test1", active=True) cat_filter3 = CategoryFilter.objects.create() cat_filter3.categories.add(cat) campaign3.filters.add(cat_filter3) assert CatalogCampaign.get_for_product( sp).count() == 2 # there are now two matching campaigns in same shop assert CatalogCampaign.get_for_product( shop_product).count() == 1 # another campaign matches only once
def new_product(i, shop, category): product = create_product(sku="test%s" % i, shop=shop, name="test%s" % i) sp = product.get_shop_instance(shop) sp.primary_category = category sp.save() return product
def test_get_suppliers(admin_user): client = _get_client(admin_user) shop1 = Shop.objects.create() shop2 = Shop.objects.create() supplier1 = create_simple_supplier("supplier1") supplier2 = create_simple_supplier("supplier2") product1 = create_product("product 1") product1.stock_behavior = StockBehavior.STOCKED sp = ShopProduct.objects.create(product=product1, shop=shop1) sp = ShopProduct.objects.create(product=product1, shop=shop2) sp.suppliers.add(supplier1) sp.suppliers.add(supplier2) product2 = create_product("product 2") product2.stock_behavior = StockBehavior.STOCKED sp = ShopProduct.objects.create(product=product2, shop=shop1) sp = ShopProduct.objects.create(product=product2, shop=shop2) sp.suppliers.add(supplier1) product3 = create_product("product 3") product3.stock_behavior = StockBehavior.STOCKED sp = ShopProduct.objects.create(product=product3, shop=shop1) sp.suppliers.add(supplier2) # put some stock supplier1.adjust_stock(product1.pk, 100) supplier1.adjust_stock(product2.pk, 300) supplier2.adjust_stock(product1.pk, 110) supplier2.adjust_stock(product3.pk, 300) # list suppliers response = client.get("/api/shuup/supplier/") assert response.status_code == status.HTTP_200_OK supplier_data = json.loads(response.content.decode("utf-8")) assert len(supplier_data) == 2 assert supplier_data[0]["id"] == supplier1.pk assert supplier_data[1]["id"] == supplier2.pk # get supplier by id response = client.get("/api/shuup/supplier/%s/" % supplier2.pk) assert response.status_code == status.HTTP_200_OK supplier_data = json.loads(response.content.decode("utf-8")) assert supplier_data["id"] == supplier2.pk assert supplier_data["type"] == supplier2.type.value # get stocks by supplier response = client.get("/api/shuup/supplier/%s/stock_statuses/" % supplier1.pk) assert response.status_code == status.HTTP_200_OK supplier_data = sorted(json.loads(response.content.decode("utf-8")), key=lambda sup: sup["product"]) assert supplier_data[0]["sku"] == product1.sku assert supplier_data[0]["product"] == product1.pk assert supplier_data[0]["physical_count"] == supplier1.get_stock_status( product1.pk).physical_count assert supplier_data[0]["logical_count"] == supplier1.get_stock_status( product1.pk).logical_count assert supplier_data[1]["sku"] == product2.sku assert supplier_data[1]["product"] == product2.pk assert supplier_data[1]["physical_count"] == supplier1.get_stock_status( product2.pk).physical_count assert supplier_data[1]["logical_count"] == supplier1.get_stock_status( product2.pk).logical_count # get stocks by supplier - filter by product id response = client.get("/api/shuup/supplier/%s/stock_statuses/" % supplier1.pk, format="json", data={"products": [product1.id]}) assert response.status_code == status.HTTP_200_OK supplier_data = json.loads(response.content.decode("utf-8")) assert len(supplier_data) == 1 assert supplier_data[0]["sku"] == product1.sku assert supplier_data[0]["product"] == product1.pk assert supplier_data[0]["physical_count"] == supplier1.get_stock_status( product1.pk).physical_count assert supplier_data[0]["logical_count"] == supplier1.get_stock_status( product1.pk).logical_count # get stocks by supplier - filter by sku response = client.get("/api/shuup/supplier/%s/stock_statuses/" % supplier1.pk, format="json", data={"skus": [product2.sku]}) assert response.status_code == status.HTTP_200_OK supplier_data = json.loads(response.content.decode("utf-8")) assert len(supplier_data) == 1 assert supplier_data[0]["sku"] == product2.sku assert supplier_data[0]["product"] == product2.pk assert supplier_data[0]["physical_count"] == supplier1.get_stock_status( product2.pk).physical_count assert supplier_data[0]["logical_count"] == supplier1.get_stock_status( product2.pk).logical_count
def test_start_end_dates(): rf = RequestFactory() activate("en") original_price = "180" discounted_price = "160" request, shop, group = initialize_test(rf, False) cat = Category.objects.create(name="test") rule1, rule2 = create_condition_and_filter(cat, request) discount_amount = 20 campaign = CatalogCampaign.objects.create(shop=shop, name="test", active=True) campaign.conditions.add(rule1) campaign.save() ProductDiscountAmount.objects.create(discount_amount=discount_amount, campaign=campaign) price = shop.create_price product = create_product("Just-A-Product-Too", shop, default_price=original_price) today = now() # starts in future campaign.start_datetime = (today + datetime.timedelta(days=2)) campaign.save() assert not campaign.is_available() assert product.get_price_info(request, quantity=1).price == price(original_price) # has already started campaign.start_datetime = (today - datetime.timedelta(days=2)) campaign.save() assert product.get_price_info(request, quantity=1).price == price(discounted_price) # already ended campaign.end_datetime = (today - datetime.timedelta(days=1)) campaign.save() assert not campaign.is_available() assert product.get_price_info(request, quantity=1).price == price(original_price) # not ended yet campaign.end_datetime = (today + datetime.timedelta(days=1)) campaign.save() assert product.get_price_info(request, quantity=1).price == price(discounted_price) # no start datetime campaign.start_datetime = None campaign.save() assert product.get_price_info(request, quantity=1).price == price(discounted_price) # no start datetime but ended campaign.end_datetime = (today - datetime.timedelta(days=1)) campaign.save() assert not campaign.is_available() assert product.get_price_info(request, quantity=1).price == price(original_price)
def test_xtheme_edit_product(admin_user, browser, live_server, settings): shop = factories.get_default_shop() supplier = factories.get_default_supplier() products = [] for x in range(3): products.append( factories.create_product("test%s" % x, shop=shop, supplier=supplier, default_price=10)) browser = initialize_admin_browser_test( browser, live_server, settings) # Login to admin as admin user browser.visit(live_server + "/") wait_until_condition(browser, lambda x: x.is_text_present("Welcome to Default!")) # Start edit click_element(browser, ".xt-edit-toggle button[type='submit']") # Visit first product and edit the layout with custom text first_product = products.pop() first_product_url = "%s%s" % (live_server, reverse("shuup:product", kwargs={ "pk": first_product.pk, "slug": first_product.slug })) browser.visit(first_product_url) first_product_text_content = "This text is only visible for product %s." % first_product.name _edit_layout( browser, "product_extra_1", "#xt-ph-product_extra_1-xtheme-product-layout-%s" % first_product.pk, first_product_text_content) # Visit second product and edit the layout with custom text second_product = products.pop() second_product_url = "%s%s" % (live_server, reverse("shuup:product", kwargs={ "pk": second_product.pk, "slug": second_product.slug })) browser.visit(second_product_url) second_product_text_content = "This text is only visible for product %s." % second_product.name _edit_layout( browser, "product_extra_1", "#xt-ph-product_extra_1-xtheme-product-layout-%s" % second_product.pk, second_product_text_content) # Visit third product and edit common layout with text third_product = products.pop() third_product_url = "%s%s" % (live_server, reverse("shuup:product", kwargs={ "pk": third_product.pk, "slug": third_product.slug })) browser.visit(third_product_url) common_text_content = "This text is visible for all products." _edit_layout(browser, "product_extra_1", "#xt-ph-product_extra_1", common_text_content) # Close edit click_element(browser, ".xt-edit-toggle button[type='submit']") # Logout click_element(browser, "div.top-nav i.menu-icon.fa.fa-user") click_element(browser, "a[href='/logout/']") # Let's revisit the product details as anonymous and check the placeholder content browser.visit(first_product_url) wait_until_condition(browser, lambda x: x.is_text_present(common_text_content)) wait_until_condition( browser, lambda x: x.is_text_present(first_product_text_content)) wait_until_condition( browser, lambda x: not x.is_text_present(second_product_text_content)) browser.visit(second_product_url) wait_until_condition(browser, lambda x: x.is_text_present(common_text_content)) wait_until_condition( browser, lambda x: not x.is_text_present(first_product_text_content)) wait_until_condition( browser, lambda x: x.is_text_present(second_product_text_content)) browser.visit(third_product_url) wait_until_condition(browser, lambda x: x.is_text_present(common_text_content)) wait_until_condition( browser, lambda x: not x.is_text_present(first_product_text_content)) wait_until_condition( browser, lambda x: not x.is_text_present(second_product_text_content))
def test_product_summary(): shop = get_default_shop() supplier = get_simple_supplier() product = create_product("test-sku", shop=get_default_shop(), default_price=10, stock_behavior=StockBehavior.STOCKED) supplier.adjust_stock(product.id, 5) # Order with 2 unshipped, non-refunded items and a shipping cost order = create_order_with_product(product, supplier, 2, 200, shop=shop) order.cache_prices() product_line = order.lines.first() shipping_line = order.lines.create(type=OrderLineType.SHIPPING, base_unit_price_value=5, quantity=1) # Make sure no invalid entries and check product quantities product_summary = order.get_product_summary() assert all(product_summary.keys()) summary = product_summary[product.id] assert_defaultdict_values(summary, ordered=2, shipped=0, refunded=0, unshipped=2) # Create a shipment for the other item, make sure status changes assert order.shipping_status == ShippingStatus.NOT_SHIPPED assert order.can_create_shipment() order.create_shipment(supplier=supplier, product_quantities={product: 1}) assert order.shipping_status == ShippingStatus.PARTIALLY_SHIPPED order.create_refund([{ "line": shipping_line, "quantity": 1, "amount": Money(5, order.currency), "restock": False }]) product_summary = order.get_product_summary() assert all(product_summary.keys()) summary = product_summary[product.id] assert_defaultdict_values(summary, ordered=2, shipped=1, refunded=0, unshipped=1) # Create a refund for 2 items, we should get no negative values order.create_refund([{ "line": product_line, "quantity": 2, "amount": Money(200, order.currency), "restock": False }]) product_summary = order.get_product_summary() assert all(product_summary.keys()) summary = product_summary[product.id] assert_defaultdict_values(summary, ordered=2, shipped=1, refunded=2, unshipped=0)
def test_product_selection_plugin(rf, reindex_catalog): shop = factories.get_default_shop() shop2 = factories.get_shop(identifier="shop2") category1 = factories.CategoryFactory(status=CategoryStatus.VISIBLE) category2 = factories.CategoryFactory(status=CategoryStatus.VISIBLE) p1 = factories.create_product("p1", shop, factories.get_default_supplier(), "10") p2 = factories.create_product("p2", shop, factories.get_default_supplier(), "20") p3 = factories.create_product("p3", shop, factories.get_default_supplier(), "30") p4 = factories.create_product("p4", shop, factories.get_default_supplier(), "40") p5 = factories.create_product("p5", shop, factories.get_default_supplier(), "50") sp1 = p1.get_shop_instance(shop) sp2 = p2.get_shop_instance(shop) sp3 = p3.get_shop_instance(shop) sp4 = p4.get_shop_instance(shop) sp1.categories.add(category1, category2) sp2.categories.add(category1) sp3.categories.add(category2) sp4.categories.add(category2) # this discount should show products: p1, p2 and p5 discount1 = Discount.objects.create( shop=shop, name="discount1", active=True, start_datetime=now() - timedelta(days=10), end_datetime=now() + timedelta(days=1), product=p5, category=category1, ) # this discount should show products: p1, p3 and p4 discount2 = Discount.objects.create( shop=shop, name="discount2", active=True, start_datetime=now() - timedelta(days=10), end_datetime=now() + timedelta(days=1), category=category2, ) # this discount shouldn't be available for this shop discount3 = Discount.objects.create( shop=shop2, name="discount3", active=True, start_datetime=now() - timedelta(days=10), end_datetime=now() + timedelta(days=1), category=category2, ) reindex_catalog() context = get_context(rf) # test only discount1 plugin = DiscountedProductsPlugin({ "discounts": [discount1.pk], "count": 10 }) context_products = plugin.get_context_data(context)["products"] assert p1 in context_products assert p2 in context_products assert p3 not in context_products assert p4 not in context_products assert p5 in context_products for status in range(2): if status == 1: discount2.active = False discount2.save() reindex_catalog() # test only discount2 plugin = DiscountedProductsPlugin({ "discounts": [discount2.pk], "count": 10 }) context_products = plugin.get_context_data(context)["products"] if status == 1: assert list(context_products) == [] else: assert p1 in context_products assert p2 not in context_products assert p3 in context_products assert p4 in context_products assert p5 not in context_products # test discount3 plugin = DiscountedProductsPlugin({ "discounts": [discount3.pk], "count": 10 }) assert list(plugin.get_context_data(context)["products"]) == [] discount2.active = True discount2.save() reindex_catalog() # test both discount1 and discount2 plugin = DiscountedProductsPlugin({ "discounts": [discount1.pk, discount2.pk], "count": 10 }) context_products = plugin.get_context_data(context)["products"] assert p1 in context_products assert p2 in context_products assert p3 in context_products assert p4 in context_products assert p5 in context_products # test the plugin form with override_current_theme_class(None): theme = get_current_theme(shop) cell = LayoutCell(theme, DiscountedProductsPlugin.identifier, sizes={"md": 8}) lcfg = LayoutCellFormGroup(layout_cell=cell, theme=theme, request=apply_request_middleware( rf.get("/"))) # not valid, products are required assert not lcfg.is_valid() lcfg = LayoutCellFormGroup( data={ "general-cell_width": "8", "general-cell_align": "pull-right", "plugin-discounts": [discount1.pk, discount2.pk], "plugin-count": 6, }, layout_cell=cell, theme=theme, request=apply_request_middleware(rf.get("/")), ) assert lcfg.is_valid() lcfg.save() assert cell.config["discounts"] == [discount1.pk, discount2.pk]
def test_broken_order(admin_user): """ """ quantities = [44, 23, 65] expected = sum(quantities) * 50 expected_based_on = expected / 1.5 # Shuup is calculating taxes per line so there will be some "errors" expected_based_on = ensure_decimal_places( Decimal("%s" % (expected_based_on + 0.01))) shop = get_default_shop() supplier = get_default_supplier() product1 = create_product("simple-test-product1", shop, supplier, 50) product2 = create_product("simple-test-product2", shop, supplier, 50) product3 = create_product("simple-test-product3", shop, supplier, 50) tax = get_default_tax() source = BasketishOrderSource(get_default_shop()) billing_address = get_address(country="US") shipping_address = get_address(name="Test street", country="US") source.status = get_initial_order_status() source.billing_address = billing_address source.shipping_address = shipping_address source.customer = create_random_person() source.payment_method = get_default_payment_method() source.shipping_method = get_default_shipping_method() source.add_line( type=OrderLineType.PRODUCT, product=product1, supplier=get_default_supplier(), quantity=quantities[0], base_unit_price=source.create_price(50), ) source.add_line( type=OrderLineType.PRODUCT, product=product2, supplier=get_default_supplier(), quantity=quantities[1], base_unit_price=source.create_price(50), ) source.add_line( type=OrderLineType.PRODUCT, product=product3, supplier=get_default_supplier(), quantity=quantities[2], base_unit_price=source.create_price(50), ) currency = "EUR" summary = source.get_tax_summary() assert len(summary) == 1 summary = summary[0] assert summary.taxful == Money(expected, "EUR") assert summary.based_on == Money(expected_based_on, "EUR") # originally non-rounded value assert bankers_round(source.get_total_tax_amount()) == summary.tax_amount assert source.taxless_total_price.value == expected_based_on assert summary.taxful.value == source.taxful_total_price.value assert summary.tax_amount == Money( bankers_round(source.taxful_total_price.value - source.taxless_total_price.value), currency) assert summary.taxful == summary.raw_based_on + summary.tax_amount assert summary.tax_rate == tax.rate assert summary.taxful.value == ( summary.based_on + summary.tax_amount).value - Decimal("%s" % 0.01) # create order from basket creator = OrderCreator() order = creator.create_order(source) assert order.taxless_total_price.value == expected_based_on # originally non-rounded value assert bankers_round(order.get_total_tax_amount()) == summary.tax_amount
def create_products(shop): supplier = get_default_supplier() for i in range(0, 200): sku = "sku-%d" % i create_product(sku, shop, supplier, default_price=i)
def test_refunds(): shop = get_default_shop() supplier = get_default_supplier() product = create_product( "test-sku", shop=get_default_shop(), default_price=10, ) tax_rate = Decimal("0.1") taxless_base_unit_price = shop.create_price(200) order = create_order_with_product(product, supplier, 3, taxless_base_unit_price, tax_rate, shop=shop) order.payment_status = PaymentStatus.DEFERRED order.cache_prices() order.save() assert order.get_total_refunded_amount().value == 0 assert order.get_total_unrefunded_amount( ).value == order.taxful_total_price.value assert not order.can_edit() assert len(order.lines.all()) == 1 product_line = order.lines.first() assert product_line.ordering == 0 assert order.can_create_refund() assert not order.has_refunds() order.create_refund([{ "line": product_line, "quantity": 1, "amount": (product_line.taxful_price.amount / 3) }]) assert len(order.lines.all()) == 2 assert order.lines.last().ordering == 1 assert order.has_refunds() # Confirm the value of the refund assert order.lines.last().taxful_price == -product_line.base_unit_price assert order.lines.last().tax_amount == -( product_line.taxless_base_unit_price * tax_rate).amount # Create a refund with a parent line and amount order.create_refund([{ "line": product_line, "quantity": 1, "amount": product_line.taxful_price.amount / 3 }]) assert len(order.lines.all()) == 3 assert order.lines.last().ordering == 2 assert order.lines.last( ).taxful_price.amount == -taxless_base_unit_price.amount * (1 + tax_rate) assert order.lines.last( ).tax_amount == -taxless_base_unit_price.amount * tax_rate assert order.taxless_total_price.amount == taxless_base_unit_price.amount assert order.taxful_total_price.amount == taxless_base_unit_price.amount * ( 1 + tax_rate) assert order.can_create_refund() assert order.get_total_tax_amount() == Money( (order.taxful_total_price_value - order.taxless_total_price_value), order.currency) # Try to refunding remaining amount without a parent line with pytest.raises(AssertionError): order.create_refund([{"amount": taxless_base_unit_price}]) # refund remaining amount order.create_refund([{ "line": product_line, "quantity": 1, "amount": product_line.taxful_price.amount / 3 }]) assert len(order.lines.all()) == 4 assert order.lines.last().ordering == 3 assert order.lines.last( ).taxful_price.amount == -taxless_base_unit_price.amount * (1 + tax_rate) assert not order.taxful_total_price.amount assert not order.can_create_refund() assert order.get_total_tax_amount() == Money( (order.taxful_total_price_value - order.taxless_total_price_value), order.currency) with pytest.raises(RefundExceedsAmountException): order.create_refund([{ "line": product_line, "quantity": 1, "amount": taxless_base_unit_price.amount }])