Skip to content

Commit

Permalink
Merge pull request saleor#4442 from mirumee/4318/voucher_once_per_user
Browse files Browse the repository at this point in the history
Add voucher once per user
  • Loading branch information
maarcingebala authored Jul 17, 2019
2 parents 084d4cc + 2473d85 commit 3f2f86e
Show file tree
Hide file tree
Showing 26 changed files with 291 additions and 332 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Fix form reloading - #4467 by @dominik-zeglen
- Fix time zone based tests - #4468 by @fowczarek
- Move Django Debug Toolbar requirement to the "dev" one (also downgrade it 2.0 -> 1.11, see PR) - #4454 by @derenio
- Add voucher once per customer - #4442 by @fowczarek


## 2.8.0
Expand Down
3 changes: 3 additions & 0 deletions saleor/checkout/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ def __iter__(self):
def __len__(self):
return self.lines.count()

def get_customer_email(self):
return self.user.email if self.user else self.email

def is_shipping_required(self):
"""Return `True` if any of the lines requires shipping."""
return any(line.is_shipping_required() for line in self)
Expand Down
28 changes: 15 additions & 13 deletions saleor/checkout/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@
from ..discount import VoucherType
from ..discount.models import NotApplicable, Voucher
from ..discount.utils import (
add_voucher_usage_by_customer,
decrease_voucher_usage,
get_products_voucher_discount,
get_shipping_voucher_discount,
get_value_voucher_discount,
increase_voucher_usage,
remove_voucher_usage_by_customer,
validate_voucher_for_checkout,
)
from ..giftcard.utils import (
add_gift_card_code_to_checkout,
Expand Down Expand Up @@ -767,12 +768,8 @@ def _get_shipping_voucher_discount_for_checkout(voucher, checkout, discounts=Non
)
raise NotApplicable(msg)

return get_shipping_voucher_discount(
voucher,
calculate_checkout_subtotal(checkout, discounts).gross,
calculate_checkout_shipping(checkout, discounts).gross,
checkout.quantity,
)
shipping_price = calculate_checkout_shipping(checkout, discounts).gross
return voucher.get_discount_amount_for(shipping_price)


def _get_products_voucher_discount(checkout, voucher, discounts=None):
Expand All @@ -798,18 +795,18 @@ def _get_products_voucher_discount(checkout, voucher, discounts=None):
"Voucher not applicable", "This offer is only valid for selected items."
)
raise NotApplicable(msg)
subtotal = calculate_checkout_subtotal(checkout, discounts).gross
return get_products_voucher_discount(voucher, prices, subtotal, checkout.quantity)
return get_products_voucher_discount(voucher, prices)


def get_voucher_discount_for_checkout(voucher, checkout, discounts=None):
"""Calculate discount value depending on voucher and discount types.
Raise NotApplicable if voucher of given type cannot be applied.
"""
validate_voucher_for_checkout(voucher, checkout, discounts)
if voucher.type == VoucherType.ENTIRE_ORDER:
subtotal = calculate_checkout_subtotal(checkout, discounts).gross
return get_value_voucher_discount(voucher, subtotal, checkout.quantity)
return voucher.get_discount_amount_for(subtotal)
if voucher.type == VoucherType.SHIPPING:
return _get_shipping_voucher_discount_for_checkout(voucher, checkout, discounts)
if voucher.type in (
Expand Down Expand Up @@ -994,6 +991,8 @@ def _get_voucher_data_for_order(checkout):
return {}

increase_voucher_usage(voucher)
if voucher.apply_once_per_customer:
add_voucher_usage_by_customer(voucher, checkout.get_customer_email())
return {
"voucher": voucher,
"discount_amount": checkout.discount_amount,
Expand Down Expand Up @@ -1034,7 +1033,7 @@ def _process_user_data_for_order(checkout):

return {
"user": checkout.user,
"user_email": checkout.user.email if checkout.user else checkout.email,
"user_email": checkout.get_customer_email(),
"billing_address": billing_address,
"customer_note": checkout.note,
}
Expand Down Expand Up @@ -1135,7 +1134,10 @@ def prepare_order_data(*, checkout: Checkout, tracking_code: str, discounts) ->

def abort_order_data(order_data: dict):
if "voucher" in order_data:
decrease_voucher_usage(order_data["voucher"])
voucher = order_data["voucher"]
decrease_voucher_usage(voucher)
if "user_email" in order_data:
remove_voucher_usage_by_customer(voucher, order_data["user_email"])


@transaction.atomic
Expand Down
6 changes: 3 additions & 3 deletions saleor/dashboard/order/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Meta:

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.get_user_current_email():
if not self.instance.get_customer_email():
self.fields.pop("notify_customer")

def clean(self):
Expand Down Expand Up @@ -548,7 +548,7 @@ class Meta:

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.order.get_user_current_email():
if not self.instance.order.get_customer_email():
self.fields.pop("send_mail")


Expand Down Expand Up @@ -675,7 +675,7 @@ def __init__(self, *args, **kwargs):
order = kwargs.pop("order")
super().__init__(*args, **kwargs)
self.instance.order = order
if not order.get_user_current_email():
if not order.get_customer_email():
self.fields.pop("send_mail")


Expand Down
22 changes: 5 additions & 17 deletions saleor/dashboard/order/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
from ...core.taxes import zero_money
from ...discount import VoucherType
from ...discount.models import NotApplicable
from ...discount.utils import (
get_products_voucher_discount,
get_shipping_voucher_discount,
get_value_voucher_discount,
)
from ...discount.utils import get_products_voucher_discount, validate_voucher_in_order

INVOICE_TEMPLATE = "dashboard/order/pdf/invoice.html"
PACKING_SLIP_TEMPLATE = "dashboard/order/pdf/packing_slip.html"
Expand Down Expand Up @@ -140,9 +136,7 @@ def get_products_voucher_discount_for_order(order, voucher):
"Voucher not applicable", "This offer is only valid for selected items."
)
raise NotApplicable(msg)
return get_products_voucher_discount(
voucher, prices, order.get_subtotal().gross, order.get_total_quantity()
)
return get_products_voucher_discount(voucher, prices)


def get_voucher_discount_for_order(order):
Expand All @@ -152,18 +146,12 @@ def get_voucher_discount_for_order(order):
"""
if not order.voucher:
return zero_money()
validate_voucher_in_order(order)
subtotal = order.get_subtotal()
if order.voucher.type == VoucherType.ENTIRE_ORDER:
return get_value_voucher_discount(
order.voucher, subtotal.gross, order.get_total_quantity()
)
return order.voucher.get_discount_amount_for(subtotal.gross)
if order.voucher.type == VoucherType.SHIPPING:
return get_shipping_voucher_discount(
order.voucher,
subtotal.gross,
order.shipping_price,
order.get_total_quantity(),
)
return order.voucher.get_discount_amount_for(order.shipping_price)
if order.voucher.type in (
VoucherType.PRODUCT,
VoucherType.COLLECTION,
Expand Down
41 changes: 41 additions & 0 deletions saleor/discount/migrations/0016_auto_20190716_0330.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions saleor/discount/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Voucher(models.Model):
# this field indicates if discount should be applied per order or
# individually to every item
apply_once_per_order = models.BooleanField(default=False)
apply_once_per_customer = models.BooleanField(default=False)
discount_value_type = models.CharField(
max_length=10,
choices=DiscountValueType.CHOICES,
Expand Down Expand Up @@ -165,6 +166,26 @@ def validate_min_checkout_items_quantity(self, quantity):
min_checkout_items_quantity=min_checkout_items_quantity,
)

def validate_once_per_customer(self, customer_email):
voucher_customer = VoucherCustomer.objects.filter(
voucher=self, customer_email=customer_email
)
if voucher_customer:
msg = pgettext(
"Voucher not applicable", "This offer is valid only once per customer."
)
raise NotApplicable(msg)


class VoucherCustomer(models.Model):
voucher = models.ForeignKey(
Voucher, related_name="customers", on_delete=models.CASCADE
)
customer_email = models.EmailField()

class Meta:
unique_together = (("voucher", "customer_email"),)


class SaleQueryset(models.QuerySet):
def active(self, date):
Expand Down
51 changes: 39 additions & 12 deletions saleor/discount/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from django.utils.translation import pgettext

from ..core.taxes import zero_money
from ..core.taxes.interface import calculate_checkout_subtotal
from . import DiscountInfo
from .models import NotApplicable, Sale
from .models import NotApplicable, Sale, VoucherCustomer


def increase_voucher_usage(voucher):
Expand All @@ -22,6 +23,28 @@ def decrease_voucher_usage(voucher):
voucher.save(update_fields=["used"])


def add_voucher_usage_by_customer(voucher, customer_email):
voucher_customer = VoucherCustomer.objects.filter(
voucher=voucher, customer_email=customer_email
)
if voucher_customer:
raise NotApplicable(
pgettext(
"Voucher not applicable",
("This offer is only valid once per customer."),
)
)
VoucherCustomer.objects.create(voucher=voucher, customer_email=customer_email)


def remove_voucher_usage_by_customer(voucher, customer_email):
voucher_customer = VoucherCustomer.objects.filter(
voucher=voucher, customer_email=customer_email
)
if voucher_customer:
voucher_customer.delete()


def are_product_collections_on_sale(product, discount: DiscountInfo):
"""Check if any collection is on sale."""
discounted_collections = discount.collection_ids
Expand Down Expand Up @@ -61,24 +84,28 @@ def calculate_discounted_price(product, price, discounts: Iterable[DiscountInfo]
return price


def get_value_voucher_discount(voucher, total_price, quantity):
"""Calculate discount value for a voucher of value type."""
voucher.validate_min_amount_spent(total_price)
voucher.validate_min_checkout_items_quantity(quantity)
return voucher.get_discount_amount_for(total_price)
def validate_voucher_for_checkout(voucher, checkout, discounts):
subtotal = calculate_checkout_subtotal(checkout, discounts)
customer_email = checkout.get_customer_email()
validate_voucher(voucher, subtotal.gross, checkout.quantity, customer_email)


def validate_voucher_in_order(order):
subtotal = order.get_subtotal()
quantity = order.get_total_quantity()
customer_email = order.get_customer_email()
validate_voucher(order.voucher, subtotal.gross, quantity, customer_email)

def get_shipping_voucher_discount(voucher, total_price, shipping_price, quantity):
"""Calculate discount value for a voucher of shipping type."""

def validate_voucher(voucher, total_price, quantity, customer_email):
voucher.validate_min_amount_spent(total_price)
voucher.validate_min_checkout_items_quantity(quantity)
return voucher.get_discount_amount_for(shipping_price)
if voucher.apply_once_per_customer:
voucher.validate_once_per_customer(customer_email)


def get_products_voucher_discount(voucher, prices, total_price, quantity):
def get_products_voucher_discount(voucher, prices):
"""Calculate discount value for a voucher of product or category type."""
voucher.validate_min_amount_spent(total_price)
voucher.validate_min_checkout_items_quantity(quantity)
if voucher.apply_once_per_order:
return voucher.get_discount_amount_for(min(prices))
discounts = (voucher.get_discount_amount_for(price) for price in prices)
Expand Down
4 changes: 4 additions & 0 deletions saleor/graphql/checkout/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ class Meta:
interfaces = [graphene.relay.Node]
filter_fields = ["token"]

@staticmethod
def resolve_email(root: models.Checkout, info):
return root.get_customer_email()

@staticmethod
def resolve_total_price(root: models.Checkout, info):
taxed_total = (
Expand Down
3 changes: 3 additions & 0 deletions saleor/graphql/discount/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ class VoucherInput(graphene.InputObjectType):
apply_once_per_order = graphene.Boolean(
description="Voucher should be applied to the cheapest item or entire order."
)
apply_once_per_customer = graphene.Boolean(
description="Voucher should be applied once per customer."
)
usage_limit = graphene.Int(
description="Limit number of times this voucher can be used in total"
)
Expand Down
1 change: 1 addition & 0 deletions saleor/graphql/discount/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class Meta:
providing valid voucher codes."""
only_fields = [
"apply_once_per_order",
"apply_once_per_customer",
"code",
"discount_value",
"discount_value_type",
Expand Down
6 changes: 1 addition & 5 deletions saleor/graphql/order/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,7 @@ def resolve_can_finalize(root: models.Order, _info):
@staticmethod
@gql_optimizer.resolver_hints(select_related="user")
def resolve_user_email(root: models.Order, _info):
if root.user_email:
return root.user_email
if root.user_id:
return root.user.email
return None
return root.get_customer_email()

@staticmethod
def resolve_available_shipping_methods(root: models.Order, _info):
Expand Down
2 changes: 2 additions & 0 deletions saleor/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3135,6 +3135,7 @@ type Voucher implements Node {
startDate: DateTime!
endDate: DateTime
applyOncePerOrder: Boolean!
applyOncePerCustomer: Boolean!
discountValueType: DiscountValueTypeEnum!
discountValue: Float!
minAmountSpent: Money
Expand Down Expand Up @@ -3206,6 +3207,7 @@ input VoucherInput {
minCheckoutItemsQuantity: Int
countries: [String]
applyOncePerOrder: Boolean
applyOncePerCustomer: Boolean
usageLimit: Int
}

Expand Down
2 changes: 1 addition & 1 deletion saleor/order/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def collect_data_for_email(order_pk, template):
template (str): email template path
"""
order = Order.objects.get(pk=order_pk)
recipient_email = order.get_user_current_email()
recipient_email = order.get_customer_email()
email_context = get_email_base_context()
email_context["order_details_url"] = build_absolute_uri(
reverse("order:details", kwargs={"token": order.token})
Expand Down
Loading

0 comments on commit 3f2f86e

Please sign in to comment.