Commit f86e1ca3 by Marko Jevtic

(SOL-1327) Implement voucher creation

parent c5c88d94
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from oscar.core.loading import get_model
Category = get_model("catalogue", "Category")
ProductAttribute = get_model("catalogue", "ProductAttribute")
ProductClass = get_model("catalogue", "ProductClass")
def create_product_class(apps, schema_editor):
"""Create a Coupon product class."""
coupon = ProductClass.objects.create(
track_stock=False,
requires_shipping=False,
name='Coupon',
slug='coupon',
)
ProductAttribute.objects.create(
product_class=coupon,
name='Coupon vouchers',
code='coupon_vouchers',
type='entity',
required=False
)
# Create a category for course seats
Category.objects.create(
description='All Coupons',
slug='coupons',
depth=1,
path='0002',
image='',
name='Coupons'
)
def remove_product_class(apps, schema_editor):
""" Reverse function. """
Category.objects.filter(slug='coupon').delete()
ProductClass.objects.filter(slug='coupon').delete()
def remove_enrollment_code(apps, schema_editor):
""" Removes the enrollment code product and it's attributes. """
Category.objects.filter(slug='enrollment_codes').delete()
ProductClass.objects.filter(slug='enrollment_code').delete()
class Migration(migrations.Migration):
dependencies = [
('catalogue', '0001_initial'),
('catalogue', '0012_enrollment_code_product_class')
]
operations = [
migrations.RunPython(remove_enrollment_code),
migrations.RunPython(create_product_class, remove_product_class)
]
from oscar.core.loading import get_model
from oscar.test.factories import ProductFactory, VoucherFactory
from ecommerce.extensions.voucher.models import CouponVouchers
from ecommerce.tests.testcases import TestCase
Product = get_model('catalogue', 'Product')
ProductClass = get_model('catalogue', 'ProductClass')
class CouponProductTest(TestCase):
""" Test coupon products."""
def test_coupon_product(self):
"""Test if a coupon product is properly created."""
coupon_product_class, _ = ProductClass.objects.get_or_create(name='coupon')
coupon_product = ProductFactory(
product_class=coupon_product_class,
title='Test product'
)
voucher = VoucherFactory(code='MYVOUCHER')
voucherList = CouponVouchers.objects.create(coupon=coupon_product)
voucherList.vouchers.add(voucher)
coupon_product.attr.coupon_voucher = voucherList
# clean() is an Oscar validation method for products
self.assertIsNone(coupon_product.clean())
self.assertIsInstance(coupon_product, Product)
self.assertEqual(coupon_product.title, 'Test product')
self.assertEqual(coupon_product.attr.coupon_voucher.vouchers.count(), 1)
self.assertEqual(coupon_product.attr.coupon_voucher.vouchers.first().code, 'MYVOUCHER')
import datetime
from oscar.core.loading import get_model
from oscar.test.factories import ProductFactory
from ecommerce.tests.testcases import TestCase
AttributeOption = get_model('catalogue', 'AttributeOption')
Catalog = get_model('catalogue', 'Catalog')
Course = get_model('courses', 'Course')
Product = get_model('catalogue', 'Product')
ProductClass = get_model('catalogue', 'ProductClass')
class EnrollmentCodeProductTest(TestCase):
""" Testing the creation of an enrollment code product."""
def test_enrollment_code_product(self):
"""
Test if an enrollment code is properly created
and has a course associated with it.
"""
catalog = Catalog.objects.create(
partner_id=self.partner.id
)
ec_product_class = ProductClass.objects.get(slug='enrollment_code')
enrollment_code_product = ProductFactory(
product_class=ec_product_class,
title='Test product'
)
enrollment_code_type = AttributeOption.objects.get(option='single_use')
enrollment_code_product.attr.catalog = catalog
enrollment_code_product.attr.start_date = datetime.date(2016, 11, 30)
enrollment_code_product.attr.end_date = datetime.date(2017, 11, 30)
enrollment_code_product.attr.type = enrollment_code_type
# clean() is an Oscar validation method for products
self.assertIsNone(enrollment_code_product.clean())
"""Tests of the Fulfillment API's fulfillment modules."""
import datetime
import json
import ddt
from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import override_settings
import httpretty
import mock
from oscar.core.loading import get_model
from django.conf import settings
from django.test import override_settings
from oscar.core.loading import get_class, get_model
from oscar.test import factories
from oscar.test.newfactories import UserFactory, BasketFactory
from requests.exceptions import ConnectionError, Timeout
......@@ -19,13 +19,19 @@ from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.fulfillment.modules import EnrollmentFulfillmentModule
from ecommerce.extensions.fulfillment.status import LINE
from ecommerce.extensions.fulfillment.tests.mixins import FulfillmentTestMixin
from ecommerce.extensions.voucher.utils import create_vouchers
from ecommerce.tests.testcases import TestCase
JSON = 'application/json'
LOGGER_NAME = 'ecommerce.extensions.analytics.utils'
Applicator = get_class('offer.utils', 'Applicator')
Benefit = get_model('offer', 'Benefit')
Catalog = get_model('catalogue', 'Catalog')
ProductAttribute = get_model("catalogue", "ProductAttribute")
User = get_user_model()
ProductClass = get_model('catalogue', 'ProductClass')
StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher')
@ddt.ddt
......@@ -353,3 +359,35 @@ class EnrollmentFulfillmentModuleTests(CourseCatalogTestMixin, FulfillmentTestMi
# 'x-edx-ga-client-id' and 'x-forwarded-for'.
self.assertEqual(exp.request.headers.get('x-edx-ga-client-id'), '123.123')
self.assertEqual(exp.request.headers.get('x-forwarded-for'), '11.22.33.44')
def test_voucher_usage(self):
"""
Test that using a voucher applies offer discount to reduce order price
"""
catalog = Catalog.objects.create(partner=self.partner)
coupon_product_class, _ = ProductClass.objects.get_or_create(name='coupon')
coupon = factories.create_product(
product_class=coupon_product_class,
title='Test product'
)
stock_record = StockRecord.objects.filter(product=self.seat).first()
catalog.stock_records.add(stock_record)
vouchers = create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=100.00,
catalog=catalog,
coupon=coupon,
end_datetime=datetime.datetime.now(),
name="Test Voucher",
quantity=10,
start_datetime=datetime.datetime.now() + datetime.timedelta(days=30),
voucher_type=Voucher.SINGLE_USE
)
voucher = vouchers[0]
seat_basket = self.order.basket
Applicator().apply_offers(seat_basket, voucher.offers.all())
self.assertEqual(seat_basket.total_excl_tax, 0.00)
default_app_config = 'ecommerce.extensions.offer.config.OfferConfig'
from oscar.apps.offer.admin import * # pylint: disable=unused-import,wildcard-import,unused-wildcard-import
class RangeAdminExtended(admin.ModelAdmin):
list_display = ('name', 'catalog',)
raw_id_fields = ('catalog',)
admin.site.unregister(Range)
admin.site.register(Range, RangeAdminExtended)
from oscar.apps.offer import config
class OfferConfig(config.OfferConfig):
name = 'ecommerce.extensions.offer'
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catalogue', '0013_coupon_product_class'),
('offer', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='range',
name='catalog',
field=models.ForeignKey(related_name='ranges', blank=True, to='catalogue.Catalog', null=True),
),
]
# noinspection PyUnresolvedReferences
from django.db import models
from oscar.apps.offer.abstract_models import AbstractRange
class Range(AbstractRange):
catalog = models.ForeignKey('catalogue.Catalog', blank=True, null=True, related_name='ranges')
def contains_product(self, product):
if self.catalog:
return (
product.id in self.catalog.stock_records.values_list('product', flat=True) or
super(Range, self).contains_product(product)
)
return super(Range, self).contains_product(product)
contains = contains_product
def num_products(self):
return len(self.all_products())
def all_products(self):
if self.catalog:
catalog_products = [record.product for record in self.catalog.stock_records.all()]
return catalog_products + list(super(Range, self).all_products())
return super(Range, self).all_products()
from oscar.apps.offer.models import * # noqa pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position
from oscar.core.loading import get_model
from oscar.test import factories
from ecommerce.tests.testcases import TestCase
Catalog = get_model('catalogue', 'Catalog')
class RangeTests(TestCase):
def setUp(self):
super(RangeTests, self).setUp()
self.range = factories.RangeFactory()
self.range_with_catalog = factories.RangeFactory()
self.catalog = Catalog.objects.create(partner=self.partner)
self.product = factories.create_product()
self.range.add_product(self.product)
self.range_with_catalog.catalog = self.catalog
self.stock_record = factories.create_stockrecord(self.product, num_in_stock=2)
self.catalog.stock_records.add(self.stock_record)
def test_range_contains_product(self):
"""
contains_product(product) should return Boolean value
"""
self.assertTrue(self.range.contains_product(self.product))
self.assertTrue(self.range_with_catalog.contains_product(self.product))
not_in_range_product = factories.create_product()
self.assertFalse(self.range.contains_product(not_in_range_product))
self.assertFalse(self.range.contains_product(not_in_range_product))
def test_range_number_of_products(self):
"""
num_products() should return number of num_of_products
"""
self.assertEqual(self.range.num_products(), 1)
self.assertEqual(self.range_with_catalog.num_products(), 1)
def test_range_all_products(self):
"""
all_products() should return a list of products in range
"""
self.assertIn(self.product, self.range.all_products())
self.assertEqual(len(self.range.all_products()), 1)
self.assertIn(self.product, self.range_with_catalog.all_products())
self.assertEqual(len(self.range_with_catalog.all_products()), 1)
......@@ -23,6 +23,7 @@ class PaymentEvent(AbstractPaymentEvent):
class Line(AbstractLine):
history = HistoricalRecords()
# If two models with the same name are declared within an app, Django will only use the first one.
# noinspection PyUnresolvedReferences
from oscar.apps.order.models import * # noqa pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position,wrong-import-order,ungrouped-imports
default_app_config = 'ecommerce.extensions.voucher.config.VoucherConfig'
from oscar.apps.voucher.admin import * # pylint: disable=unused-import,wildcard-import,unused-wildcard-import
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from oscar.apps.voucher import config
class VoucherConfig(config.VoucherConfig):
name = 'ecommerce.extensions.voucher'
def ready(self): # pragma: no cover
if settings.VOUCHER_CODE_LENGTH < 1:
raise ImproperlyConfigured("VOUCHER_CODE_LENGTH must be a positive number.")
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
from decimal import Decimal
class Migration(migrations.Migration):
dependencies = [
('order', '0001_initial'),
('offer', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Voucher',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(verbose_name='Name', max_length=128, help_text='This will be shown in the checkout and basket once the voucher is entered')),
('code', models.CharField(max_length=128, verbose_name='Code', unique=True, db_index=True, help_text='Case insensitive / No spaces allowed')),
('usage', models.CharField(default='Multi-use', max_length=128, verbose_name='Usage', choices=[('Single use', 'Can be used once by one customer'), ('Multi-use', 'Can be used multiple times by multiple customers'), ('Once per customer', 'Can only be used once per customer')])),
('start_datetime', models.DateTimeField(verbose_name='Start datetime')),
('end_datetime', models.DateTimeField(verbose_name='End datetime')),
('num_basket_additions', models.PositiveIntegerField(default=0, verbose_name='Times added to basket')),
('num_orders', models.PositiveIntegerField(default=0, verbose_name='Times on orders')),
('total_discount', models.DecimalField(default=Decimal('0.00'), max_digits=12, decimal_places=2, verbose_name='Total discount')),
('date_created', models.DateField(auto_now_add=True)),
('offers', models.ManyToManyField(related_name='vouchers', verbose_name='Offers', to='offer.ConditionalOffer')),
],
options={
'verbose_name_plural': 'Vouchers',
'get_latest_by': 'date_created',
'verbose_name': 'Voucher',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='VoucherApplication',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_created', models.DateField(auto_now_add=True, verbose_name='Date Created')),
('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
('user', models.ForeignKey(null=True, verbose_name='User', to=settings.AUTH_USER_MODEL, blank=True)),
('voucher', models.ForeignKey(verbose_name='Voucher', related_name='applications', to='voucher.Voucher')),
],
options={
'verbose_name_plural': 'Voucher Applications',
'verbose_name': 'Voucher Application',
'abstract': False,
},
bases=(models.Model,),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catalogue', '0013_coupon_product_class'),
('voucher', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CouponVouchers',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('coupon', models.ForeignKey(related_name='coupon_vouchers', to='catalogue.Product')),
('vouchers', models.ManyToManyField(related_name='coupon_vouchers', to='voucher.Voucher', blank=True)),
],
),
]
# noinspection PyUnresolvedReferences
from django.db import models
class CouponVouchers(models.Model):
coupon = models.ForeignKey('catalogue.Product', related_name='coupon_vouchers')
vouchers = models.ManyToManyField('voucher.Voucher', blank=True, related_name='coupon_vouchers')
# noinspection PyUnresolvedReferences
from oscar.apps.voucher.models import * # noqa pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position
import datetime
from django.db import IntegrityError
from django.test import override_settings
from oscar.core.loading import get_model
from oscar.test import factories
from ecommerce.extensions.voucher.utils import create_vouchers
from ecommerce.tests.testcases import TestCase
Benefit = get_model('offer', 'Benefit')
Catalog = get_model('catalogue', 'Catalog')
CouponVouchers = get_model('voucher', 'CouponVouchers')
Product = get_model('catalogue', 'Product')
ProductClass = get_model('catalogue', 'ProductClass')
StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher')
VOUCHER_CODE_LENGTH = 1
class UtilTests(TestCase):
course_id = 'edX/DemoX/Demo_Course'
certificate_type = 'test-certificate-type'
provider = None
def setUp(self):
super(UtilTests, self).setUp()
self.catalog = Catalog.objects.create(partner=self.partner)
self.coupon_product_class, _ = ProductClass.objects.get_or_create(name='coupon')
self.coupon = factories.create_product(
product_class=self.coupon_product_class,
title='Test product'
)
self.stock_record = factories.create_stockrecord(self.coupon, num_in_stock=2)
self.catalog.stock_records.add(self.stock_record)
def test_create_vouchers(self):
"""
Test voucher creation
"""
vouchers = create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=100.00,
catalog=self.catalog,
coupon=self.coupon,
end_datetime=datetime.date(2015, 10, 30),
name="Test voucher",
quantity=10,
start_datetime=datetime.date(2015, 10, 1),
voucher_type=Voucher.SINGLE_USE
)
self.assertEqual(len(vouchers), 10)
voucher = vouchers[0]
voucher_offer = voucher.offers.first()
coupon_voucher = CouponVouchers.objects.get(coupon=self.coupon)
self.assertEqual(voucher_offer.benefit.type, Benefit.PERCENTAGE)
self.assertEqual(voucher_offer.benefit.value, 100.00)
self.assertEqual(voucher_offer.benefit.range.catalog, self.catalog)
self.assertEqual(len(coupon_voucher.vouchers.all()), 10)
self.assertEqual(voucher.end_datetime, datetime.date(2015, 10, 30))
self.assertEqual(voucher.start_datetime, datetime.date(2015, 10, 1))
self.assertEqual(voucher.usage, Voucher.SINGLE_USE)
@override_settings(VOUCHER_CODE_LENGTH=VOUCHER_CODE_LENGTH)
def test_regenerate_voucher_code(self):
"""
Test that voucher code will be regenerated if it already exists
"""
for code in 'BCDFGHJKL':
create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=100.00,
catalog=self.catalog,
coupon=self.coupon,
end_datetime=datetime.date(2015, 10, 30),
name="Test voucher",
quantity=1,
start_datetime=datetime.date(2015, 10, 1),
voucher_type=Voucher.SINGLE_USE,
code=code
)
for _ in range(20):
voucher = create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=100.00,
catalog=self.catalog,
coupon=self.coupon,
end_datetime=datetime.date(2015, 10, 30),
name="Test voucher",
quantity=1,
start_datetime=datetime.date(2015, 10, 1),
voucher_type=Voucher.SINGLE_USE
)
self.assertTrue(Voucher.objects.filter(code__iexact=voucher[0].code).exists())
@override_settings(VOUCHER_CODE_LENGTH=0)
def test_nonpositive_voucher_code_length(self):
"""
Test that setting a voucher code length to a nonpositive integer value
raises a ValueError
"""
with self.assertRaises(ValueError):
create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=100.00,
catalog=self.catalog,
coupon=self.coupon,
end_datetime=datetime.date(2015, 10, 30),
name="Test voucher",
quantity=1,
start_datetime=datetime.date(2015, 10, 1),
voucher_type=Voucher.SINGLE_USE
)
def test_create_discount_coupon(self):
"""
Test discount voucher creation with specified code
"""
discount_vouchers = create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=25.00,
catalog=self.catalog,
coupon=self.coupon,
end_datetime=datetime.date(2015, 10, 30),
name="Discount code",
quantity=1,
start_datetime=datetime.date(2015, 10, 1),
voucher_type=Voucher.SINGLE_USE,
code="XMASC0DE"
)
self.assertEqual(len(discount_vouchers), 1)
self.assertEqual(discount_vouchers[0].code, "XMASC0DE")
with self.assertRaises(IntegrityError):
create_vouchers(
benefit_type=Benefit.PERCENTAGE,
benefit_value=35.00,
catalog=self.catalog,
coupon=self.coupon,
end_datetime=datetime.date(2015, 10, 30),
name="Discount name",
quantity=1,
start_datetime=datetime.date(2015, 10, 1),
voucher_type=Voucher.SINGLE_USE,
code="XMASC0DE"
)
"""Order Utility Classes. """
import logging
import random
import string # pylint: disable=deprecated-module
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
logger = logging.getLogger(__name__)
Benefit = get_model('offer', 'Benefit')
Condition = get_model('offer', 'Condition')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
CouponVouchers = get_model('voucher', 'CouponVouchers')
Range = get_model('offer', 'Range')
Voucher = get_model('voucher', 'Voucher')
def _get_or_create_offer(product_range, benefit_type, benefit_value):
"""
Return an offer for a catalog with condition and benefit.
If offer doesn't exist, new offer will be created and associated with
provided Offer condition and benefit.
Args:
product_range (Range): Range of products associated with condition
benefit_type (str): Type of benefit associated with the offer
benefit_value (Decimal): Value of benefit associated with the offer
Returns:
Offer
"""
offer_condition, __ = Condition.objects.get_or_create(
range=product_range,
type=Condition.COUNT,
value=1,
)
offer_benefit, __ = Benefit.objects.get_or_create(
range=product_range,
type=benefit_type,
value=benefit_value,
max_affected_items=1,
)
offer_name = "{}-{}".format(offer_benefit.type, offer_benefit.value)
offer, __ = ConditionalOffer.objects.get_or_create(
name=offer_name,
offer_type=ConditionalOffer.VOUCHER,
condition=offer_condition,
benefit=offer_benefit,
)
return offer
def _generate_code_string(length):
"""
Create a string of random characters of specified length
Args:
length (int): Defines the length of randomly generated string
Raises:
ValueError raised if length is less than one.
Returns:
str
"""
if length < 1:
raise ValueError("Voucher code length must be a positive number.")
chars = [
char for char in string.ascii_uppercase + string.digits
if char not in 'AEIOU1'
]
voucher_code = string.join((random.choice(chars) for i in range(length)), '')
if Voucher.objects.filter(code__iexact=voucher_code).exists():
return _generate_code_string(length)
return voucher_code
def _create_new_voucher(code, coupon, end_datetime, name, offer, start_datetime, voucher_type):
"""
Creates a voucher.
If randomly generated voucher code already exists, new code will be generated and reverified.
Args:
code (str): Code associated with vouchers. If not provided, one will be generated.
coupon (Product): Coupon product associated with voucher.
end_datetime (datetime): Voucher end date.
name (str): Voucher name.
offer (Offer): Offer associated with voucher.
start_datetime (datetime): Voucher start date.
voucher_type (str): Voucher usage.
Returns:
Voucher
"""
voucher_code = code or _generate_code_string(settings.VOUCHER_CODE_LENGTH)
voucher = Voucher.objects.create(
name=name,
code=voucher_code,
usage=voucher_type,
start_datetime=start_datetime,
end_datetime=end_datetime
)
voucher.offers.add(offer)
coupon_voucher, __ = CouponVouchers.objects.get_or_create(coupon=coupon)
coupon_voucher.vouchers.add(voucher)
return voucher
def create_vouchers(
benefit_type,
benefit_value,
catalog,
coupon,
end_datetime,
name,
quantity,
start_datetime,
voucher_type,
code=None):
"""
Create vouchers
Args:
benefit_type (str): Type of benefit associated with vouchers.
benefit_value (Decimal): Value of benefit associated with vouchers.
catalog (Catalog): Catalog associated with range of products
to which a voucher can be applied to
coupon (Coupon): Coupon entity associated with vouchers.
end_datetime (datetime): End date for voucher offer
name (str): Voucher name
quantity (int): Number of vouchers to be created.
start_datetime (datetime): Start date for voucher offer.
voucher_type (str): Type of voucher.
code (str): Code associated with vouchers. Defaults to None.
Returns:
List[Voucher]
"""
logger.info("Creating [%d] vouchers catalog [%s]", quantity, catalog.id)
vouchers = []
range_name = (_('Range for {catalog_name}').format(catalog_name=catalog.name))
product_range, __ = Range.objects.get_or_create(
name=range_name,
catalog=catalog,
)
offer = _get_or_create_offer(
product_range=product_range,
benefit_type=benefit_type,
benefit_value=benefit_value
)
for __ in range(quantity):
voucher = _create_new_voucher(
coupon=coupon,
end_datetime=end_datetime,
offer=offer,
start_datetime=start_datetime,
voucher_type=voucher_type,
code=code,
name=name
)
vouchers.append(voucher)
return vouchers
......@@ -30,9 +30,11 @@ OSCAR_APPS = [
'ecommerce.extensions.dashboard',
'ecommerce.extensions.dashboard.orders',
'ecommerce.extensions.dashboard.users',
'ecommerce.extensions.offer',
'ecommerce.extensions.order',
'ecommerce.extensions.partner',
'ecommerce.extensions.payment',
'ecommerce.extensions.voucher',
])
# END APP CONFIGURATION
......@@ -247,4 +249,7 @@ OSCAR_DASHBOARD_NAVIGATION = [
# Default timeout for Enrollment API calls
ENROLLMENT_FULFILLMENT_TIMEOUT = 7
# Coupon code length
VOUCHER_CODE_LENGTH = 8
THUMBNAIL_DEBUG = False
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment