Skip to content

Commit

Permalink
Merge d21f282 into cdaf960
Browse files Browse the repository at this point in the history
  • Loading branch information
Pikkupomo committed Jun 14, 2016
2 parents cdaf960 + d21f282 commit 942e2d3
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 30 deletions.
1 change: 1 addition & 0 deletions shoop/campaigns/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class CampaignAppConfig(AppConfig):
"basket_campaign_effect": [
"shoop.campaigns.models.basket_effects:BasketDiscountAmount",
"shoop.campaigns.models.basket_effects:BasketDiscountPercentage",
"shoop.campaigns.models.basket_line_effects:FreeProductLine",
],
"campaign_context_condition": [
"shoop.campaigns.models.context_conditions:ContactGroupCondition",
Expand Down
14 changes: 10 additions & 4 deletions shoop/campaigns/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-14 00:17+0000\n"
"POT-Creation-Date: 2016-06-14 21:14+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -216,6 +216,15 @@ msgstr ""
msgid "Give percentage discount."
msgstr ""

msgid "Free Product(s)"
msgstr ""

msgid "product"
msgstr ""

msgid "Select product(s) to give free."
msgstr ""

msgid "shop"
msgstr ""

Expand Down Expand Up @@ -301,9 +310,6 @@ msgstr ""
msgid "Product"
msgstr ""

msgid "product"
msgstr ""

msgid "Limit the campaign to selected products."
msgstr ""

Expand Down
26 changes: 26 additions & 0 deletions shoop/campaigns/migrations/0007_freeproductline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('shoop', '0025_add_codes_for_order'),
('campaigns', '0006_effects'),
]

operations = [
migrations.CreateModel(
name='FreeProductLine',
fields=[
('basketdiscounteffect_ptr', models.OneToOneField(primary_key=True, serialize=False, auto_created=True, to='campaigns.BasketDiscountEffect', parent_link=True)),
('products', models.ManyToManyField(verbose_name='product', to='shoop.Product')),
],
options={
'abstract': False,
},
bases=('campaigns.basketdiscounteffect',),
),
]
2 changes: 2 additions & 0 deletions shoop/campaigns/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# LICENSE file in the root directory of this source tree.
from .basket_conditions import BasketCondition
from .basket_effects import BasketDiscountEffect
from .basket_line_effects import BasketLineEffect
from .campaigns import BasketCampaign, Campaign, CatalogCampaign, Coupon
from .catalog_filters import CatalogFilter
from .contact_group_sales_ranges import ContactGroupSalesRange
Expand All @@ -16,6 +17,7 @@
'BasketCampaign',
'BasketDiscountEffect',
'BasketCondition',
'BasketLineEffect',
'Campaign',
'ProductDiscountEffect',
'CatalogCampaign',
Expand Down
67 changes: 67 additions & 0 deletions shoop/campaigns/models/basket_line_effects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.
import random

from django.db import models
from django.utils.translation import ugettext_lazy as _

from shoop.campaigns.models import BasketDiscountEffect
from shoop.core.models import OrderLineType, Product


class BasketLineEffect(BasketDiscountEffect):

class Meta:
abstract = True

def get_discount_lines(self, order_source, original_lines):
"""
Applies the effect based on given `order_source`
:return: amount of discount to accumulate for the product
:rtype: Iterable[shoop.core.order_creator.SourceLine]
"""
raise NotImplementedError("Not implemented!")


class FreeProductLine(BasketLineEffect):
identifier = "free_product_line_effect"
model = Product
yields = True
name = _("Free Product(s)")

products = models.ManyToManyField(Product, verbose_name=_("product"))

@property
def description(self):
return _("Select product(s) to give free.")

@property
def values(self):
return self.products

@values.setter
def values(self, values):
self.products = values

def get_discount_lines(self, order_source, original_lines):
lines = []
shop = order_source.shop
for product in self.products.all():
line_data = dict(
line_id="free_product_%s" % str(random.randint(0, 0x7FFFFFFF)),
type=OrderLineType.PRODUCT,
quantity=1,
shop=shop,
text=("%s (%s)" % (product.name, self.campaign.public_name)),
base_unit_price=shop.create_price(0),
product=product,
sku=product.sku,
supplier=product.get_shop_instance(shop).suppliers.first()
)
lines.append(order_source.create_line(**line_data))
return lines
25 changes: 22 additions & 3 deletions shoop/campaigns/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ class BasketCampaignModule(OrderSourceModifierModule):

def get_new_lines(self, order_source, lines):
matching_campaigns = BasketCampaign.get_matching(order_source, lines)
for line in self._handle_total_discount_campaigns(matching_campaigns, order_source, lines):
for line in self._handle_total_discount_effects(matching_campaigns, order_source, lines):
yield line

for line in self._handle_line_effects(matching_campaigns, order_source, lines):
yield line

def _get_campaign_line(self, campaign, highest_discount, order_source):
Expand Down Expand Up @@ -94,8 +97,8 @@ def use_code(self, order, code):
def clear_codes(self, order):
CouponUsage.objects.filter(order=order).delete()

def _handle_total_discount_campaigns(self, matching_campaigns, order_source, lines):
price_so_far = sum((x.price for x in lines), order_source.zero_price)
def _handle_total_discount_effects(self, matching_campaigns, order_source, original_lines):
price_so_far = sum((x.price for x in original_lines), order_source.zero_price)

def get_discount_line(campaign, amount, price_so_far):
new_amount = min(amount, price_so_far)
Expand All @@ -110,6 +113,9 @@ def get_discount_line(campaign, amount, price_so_far):
if not effect:
continue

if hasattr(effect, "get_discount_lines"):
continue

discount_amount = effect.apply_for_basket(order_source=order_source)
# if campaign has coupon, match it to order_source.codes
if campaign.coupon:
Expand All @@ -122,3 +128,16 @@ def get_discount_line(campaign, amount, price_so_far):
if best_discount is not None:
lines.append(get_discount_line(best_discount_campaign, best_discount, price_so_far))
return lines

def _handle_line_effects(self, matching_campaigns, order_source, original_lines):
lines = []
for campaign in matching_campaigns:
effect = campaign.effects.first()
if not effect:
continue

if not hasattr(effect, "get_discount_lines"):
continue

lines += effect.get_discount_lines(order_source=order_source, original_lines=original_lines)
return lines
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,27 @@
{% if product and product.is_package_parent() %}
{{ macros.render_package_children(product) }}
{% endif %}
<div class="quantity">
{% if product and line.can_change_quantity %}
<div class="input-group">
<div class="input-group-addon">
{% trans %}Qty{% endtrans %}

{% if not line.line_id.startswith("free_") %}
<div class="quantity">
{% if product and line.can_change_quantity %}
<div class="input-group">
<div class="input-group-addon">
{% trans %}Qty{% endtrans %}
</div>
<input
id="qty_{{ line.line_id }}"
type="number"
name="q_{{ line.line_id }}"
size="2"
class="line_quantity form-control"
step="{{ shop_product.quantity_step }}"
value="{{ line.quantity }}"
min="{{ shop_product.rounded_minimum_purchase_quantity }}">
</div>
<input
id="qty_{{ line.line_id }}"
type="number"
name="q_{{ line.line_id }}"
size="2"
class="line_quantity form-control"
step="{{ shop_product.quantity_step }}"
value="{{ line.quantity }}"
min="{{ shop_product.rounded_minimum_purchase_quantity }}">
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% if show_prices() %}
<div class="product-sum">
Expand All @@ -61,13 +64,16 @@
</h4>
</div>
{% endif %}
<div class="delete">
{% if line.can_delete %}
<button type="submit" class="btn btn-sm" name="delete_{{ line.line_id }}" title="{% trans %}Remove product from basket{% endtrans %}">
<i class="fa fa-times"></i>
</button>
{% endif %}
</div>

{% if not line.line_id.startswith("free_") %}
<div class="delete">
{% if line.can_delete %}
<button type="submit" class="btn btn-sm" name="delete_{{ line.line_id }}" title="{% trans %}Remove product from basket{% endtrans %}">
<i class="fa fa-times"></i>
</button>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="basket-summary">
Expand Down
87 changes: 87 additions & 0 deletions shoop_tests/campaigns/test_basket_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
BasketTotalProductAmountCondition, BasketTotalAmountCondition
)
from shoop.campaigns.models.basket_effects import BasketDiscountAmount, BasketDiscountPercentage
from shoop.campaigns.models.basket_line_effects import FreeProductLine
from shoop.campaigns.models.campaigns import BasketCampaign, Coupon, CouponUsage
from shoop.core.models import OrderLineType
from shoop.core.order_creator import OrderCreator
Expand Down Expand Up @@ -308,3 +309,89 @@ def test_coupon_uniqueness(rf):
with pytest.raises(IntegrityError):
second_campaign.coupon = coupon
second_campaign.save()


@pytest.mark.django_db
def test_basket_free_product(rf):
request, shop, group = initialize_test(rf, False)
price = shop.create_price

basket = get_basket(request)
supplier = get_default_supplier()

single_product_price = "50"
discount_amount_value = "10"

# create basket rule that requires 2 products in basket
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)
basket.add_product(supplier=supplier, shop=shop, product=product, quantity=1)
basket.save()

second_product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=single_product_price)

rule = BasketTotalProductAmountCondition.objects.create(value="2")

campaign = BasketCampaign.objects.create(active=True, shop=shop, name="test", public_name="test")
campaign.conditions.add(rule)

effect = FreeProductLine.objects.create(campaign=campaign)
effect.products.add(second_product)


basket.uncache()
final_lines = basket.get_final_lines()

assert len(final_lines) == 2

line_types = [l.type for l in final_lines]
assert OrderLineType.DISCOUNT not in line_types

for line in basket.get_final_lines():
assert line.type == OrderLineType.PRODUCT
if line.product != product:
assert line.product == second_product


@pytest.mark.django_db
def test_basket_free_product_coupon(rf):
request, shop, group = initialize_test(rf, False)
price = shop.create_price

basket = get_basket(request)
supplier = get_default_supplier()

single_product_price = "50"
discount_amount_value = "10"

# create basket rule that requires 2 products in basket
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)
basket.add_product(supplier=supplier, shop=shop, product=product, quantity=1)
basket.save()

second_product = create_product(printable_gibberish(), shop=shop, supplier=supplier, default_price=single_product_price)

rule = BasketTotalProductAmountCondition.objects.create(value="2")
coupon = Coupon.objects.create(code="TEST", active=True)

campaign = BasketCampaign.objects.create(active=True, shop=shop, name="test", public_name="test", coupon=coupon)
campaign.conditions.add(rule)

effect = FreeProductLine.objects.create(campaign=campaign)
effect.products.add(second_product)

basket.add_code(coupon.code)

basket.uncache()
final_lines = basket.get_final_lines()

assert len(final_lines) == 2

line_types = [l.type for l in final_lines]
assert OrderLineType.DISCOUNT not in line_types

for line in basket.get_final_lines():
assert line.type == OrderLineType.PRODUCT
if line.product != product:
assert line.product == second_product

0 comments on commit 942e2d3

Please sign in to comment.