Commit 31ea513b by Vedran Karačić

Merge pull request #719 from edx/vkaracic/enrollment-code-fulfill

Enrollment code fulfillment module
parents a33e8298 e8c90828
...@@ -7,7 +7,9 @@ from django.utils.timezone import now ...@@ -7,7 +7,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import httpretty import httpretty
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test.factories import ConditionalOfferFactory, OrderFactory, RangeFactory, VoucherFactory from oscar.test.factories import (
ConditionalOfferFactory, OrderFactory, OrderLineFactory, RangeFactory, VoucherFactory
)
from oscar.test.utils import RequestFactory from oscar.test.utils import RequestFactory
import pytz import pytz
...@@ -25,6 +27,8 @@ Basket = get_model('basket', 'Basket') ...@@ -25,6 +27,8 @@ Basket = get_model('basket', 'Basket')
Benefit = get_model('offer', 'Benefit') Benefit = get_model('offer', 'Benefit')
Catalog = get_model('catalogue', 'Catalog') Catalog = get_model('catalogue', 'Catalog')
Course = get_model('courses', 'Course') Course = get_model('courses', 'Course')
Product = get_model('catalogue', 'Product')
OrderLineVouchers = get_model('voucher', 'OrderLineVouchers')
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher') Voucher = get_model('voucher', 'Voucher')
VoucherApplication = get_model('voucher', 'VoucherApplication') VoucherApplication = get_model('voucher', 'VoucherApplication')
...@@ -324,3 +328,38 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin ...@@ -324,3 +328,38 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
basket.vouchers.add(Voucher.objects.get(code=COUPON_CODE)) basket.vouchers.add(Voucher.objects.get(code=COUPON_CODE))
httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200) httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200)
self.assert_redemption_page_redirects(get_lms_url()) self.assert_redemption_page_redirects(get_lms_url())
class EnrollmentCodeCsvViewTests(TestCase):
""" Tests for the EnrollmentCodeCsvView view. """
path = 'coupons:enrollment_code_csv'
def setUp(self):
super(EnrollmentCodeCsvViewTests, self).setUp()
self.user = self.create_user()
self.client.login(username=self.user.username, password=self.password)
def test_invalid_order_number(self):
""" Verify a 404 error is raised for an invalid order number. """
response = self.client.get(reverse(self.path, args=['INVALID']))
self.assertEqual(response.status_code, 404)
def test_invalid_user(self):
""" Verify an unauthorized request is redirected to the LMS dashboard. """
order = OrderFactory()
order.user = self.create_user()
response = self.client.get(reverse(self.path, args=[order.number]))
self.assertEqual(response.status_code, 302)
redirect_location = get_lms_url('dashboard')
self.assertEqual(response['location'], redirect_location)
def test_successful_response(self):
""" Verify a successful response is returned. """
voucher = VoucherFactory(code='ENROLLMENT')
order = OrderFactory(user=self.user)
line = OrderLineFactory(order=order)
order_line_vouchers = OrderLineVouchers.objects.create(line=line)
order_line_vouchers.vouchers.add(voucher)
response = self.client.get(reverse(self.path, args=[order.number]))
self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-type'], 'text/csv')
...@@ -5,5 +5,10 @@ from ecommerce.coupons import views ...@@ -5,5 +5,10 @@ from ecommerce.coupons import views
urlpatterns = [ urlpatterns = [
url(r'^offer/$', views.CouponOfferView.as_view(), name='offer'), url(r'^offer/$', views.CouponOfferView.as_view(), name='offer'),
url(r'^redeem/$', views.CouponRedeemView.as_view(), name='redeem'), url(r'^redeem/$', views.CouponRedeemView.as_view(), name='redeem'),
url(
r'^enrollment_code_csv/(?P<number>[-\w]+)/$',
views.EnrollmentCodeCsvView.as_view(),
name='enrollment_code_csv'
),
url(r'^(.*)$', views.CouponAppView.as_view(), name='app'), url(r'^(.*)$', views.CouponAppView.as_view(), name='app'),
] ]
from __future__ import unicode_literals from __future__ import unicode_literals
import logging
import csv
from decimal import Decimal from decimal import Decimal
import logging
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from oscar.core.loading import get_class, get_model
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from edx_rest_api_client.exceptions import SlumberHttpBaseException from edx_rest_api_client.exceptions import SlumberHttpBaseException
from oscar.core.loading import get_class, get_model
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_ecommerce_url, get_lms_url
from ecommerce.core.views import StaffOnlyMixin from ecommerce.core.views import StaffOnlyMixin
from ecommerce.extensions.api import exceptions from ecommerce.extensions.api import exceptions
from ecommerce.extensions.analytics.utils import prepare_analytics_data from ecommerce.extensions.analytics.utils import prepare_analytics_data
...@@ -29,6 +32,8 @@ Applicator = get_class('offer.utils', 'Applicator') ...@@ -29,6 +32,8 @@ Applicator = get_class('offer.utils', 'Applicator')
Basket = get_model('basket', 'Basket') Basket = get_model('basket', 'Basket')
Benefit = get_model('offer', 'Benefit') Benefit = get_model('offer', 'Benefit')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
OrderLineVouchers = get_model('voucher', 'OrderLineVouchers')
Order = get_model('order', 'Order')
Selector = get_class('partner.strategy', 'Selector') Selector = get_class('partner.strategy', 'Selector')
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher') Voucher = get_model('voucher', 'Voucher')
...@@ -143,7 +148,11 @@ class CouponOfferView(TemplateView): ...@@ -143,7 +148,11 @@ class CouponOfferView(TemplateView):
} }
course['image_url'] = get_lms_url(course['media']['course_image']['uri']) course['image_url'] = get_lms_url(course['media']['course_image']['uri'])
benefit = voucher.offers.first().benefit benefit = voucher.offers.first().benefit
stock_record = benefit.range.catalog.stock_records.first() # Note (multi-courses): fix this to work for all stock records / courses.
if benefit.range.catalog:
stock_record = benefit.range.catalog.stock_records.first()
else:
stock_record = StockRecord.objects.get(product=benefit.range.included_products.first())
price = stock_record.price_excl_tax price = stock_record.price_excl_tax
benefit_value = format_benefit_value(benefit) benefit_value = format_benefit_value(benefit)
if benefit.type == 'Percentage': if benefit.type == 'Percentage':
...@@ -216,3 +225,73 @@ class CouponRedeemView(EdxOrderPlacementMixin, View): ...@@ -216,3 +225,73 @@ class CouponRedeemView(EdxOrderPlacementMixin, View):
return HttpResponseRedirect(reverse('basket:summary')) return HttpResponseRedirect(reverse('basket:summary'))
return HttpResponseRedirect(get_lms_url('')) return HttpResponseRedirect(get_lms_url(''))
class EnrollmentCodeCsvView(View):
""" Download enrollment code CSV file view. """
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(EnrollmentCodeCsvView, self).dispatch(*args, **kwargs)
def get(self, request, number):
"""
Creates a CSV for the order. The structure of the CSV looks like this:
> Order Number:,EDX-100001
> Seat in Demo with verified certificate (and ID verification)
> Code,Redemption URL
> J4HDI5OAUGCSUJJ3,ecommerce.server?code=J4HDI5OAUGCSUJJ3
> OZCRR6WXLWGAFWZR,ecommerce.server?code=OZCRR6WXLWGAFWZR
> 6KPYL6IO6Y3XL7SI,ecommerce.server?code=6KPYL6IO6Y3XL7SI
> NPIJWIKNLRURYVU2,ecommerce.server?code=NPIJWIKNLRURYVU2
> 6SZULKPZQYACAODC,ecommerce.server?code=6SZULKPZQYACAODC
>
Args:
request (Request): The GET request
number (str): Number of the order
Returns:
HttpResponse
Raises:
Http404: When an order number for a non-existing order is passed.
PermissionDenied: When a user tries to download a CSV for an order that he did not make.
"""
try:
order = Order.objects.get(number=number)
except Order.DoesNotExist:
raise Http404('Order not found.')
if request.user != order.user and not request.user.is_staff:
raise PermissionDenied
file_name = 'Enrollment code CSV order num {}'.format(order.number)
file_name = '{filename}.csv'.format(filename=slugify(file_name))
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={filename}'.format(filename=file_name)
redeem_url = get_ecommerce_url(reverse('coupons:offer'))
voucher_field_names = ('Code', 'Redemption URL')
voucher_writer = csv.DictWriter(response, fieldnames=voucher_field_names)
writer = csv.writer(response)
writer.writerow(('Order Number:', order.number))
writer.writerow([])
order_line_vouchers = OrderLineVouchers.objects.filter(line__order=order)
for order_line_voucher in order_line_vouchers:
writer.writerow([order_line_voucher.line.product.title])
voucher_writer.writeheader()
for voucher in order_line_voucher.vouchers.all():
voucher_writer.writerow({
voucher_field_names[0]: voucher.code,
voucher_field_names[1]: '{url}?code={code}'.format(url=redeem_url, code=voucher.code)
})
writer.writerow([])
return response
...@@ -31,10 +31,10 @@ from ecommerce.invoice.models import Invoice ...@@ -31,10 +31,10 @@ from ecommerce.invoice.models import Invoice
Basket = get_model('basket', 'Basket') Basket = get_model('basket', 'Basket')
Catalog = get_model('catalogue', 'Catalog') Catalog = get_model('catalogue', 'Catalog')
Category = get_model('catalogue', 'Category') Category = get_model('catalogue', 'Category')
ProductCategory = get_model('catalogue', 'ProductCategory')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Order = get_model('order', 'Order') Order = get_model('order', 'Order')
Product = get_model('catalogue', 'Product') Product = get_model('catalogue', 'Product')
ProductCategory = get_model('catalogue', 'ProductCategory')
ProductClass = get_model('catalogue', 'ProductClass') ProductClass = get_model('catalogue', 'ProductClass')
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher') Voucher = get_model('voucher', 'Voucher')
......
...@@ -4,20 +4,31 @@ Fulfillment Modules are designed to allow specific fulfillment logic based on th ...@@ -4,20 +4,31 @@ Fulfillment Modules are designed to allow specific fulfillment logic based on th
in an Order. in an Order.
""" """
import abc import abc
import datetime
import json import json
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from oscar.core.loading import get_model
from rest_framework import status from rest_framework import status
import requests import requests
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from ecommerce.core.url_utils import get_lms_enrollment_api_url from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME
from ecommerce.core.url_utils import get_ecommerce_url, get_lms_enrollment_api_url, get_lms_url
from ecommerce.courses.models import Course
from ecommerce.courses.utils import mode_for_seat from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.analytics.utils import audit_log, parse_tracking_context from ecommerce.extensions.analytics.utils import audit_log, parse_tracking_context
from ecommerce.extensions.fulfillment.status import LINE from ecommerce.extensions.fulfillment.status import LINE
from ecommerce.extensions.voucher.models import OrderLineVouchers
from ecommerce.extensions.voucher.utils import create_vouchers
from ecommerce.notifications.notifications import send_notification
Benefit = get_model('offer', 'Benefit')
Product = get_model('catalogue', 'Product')
Range = get_model('offer', 'Range')
Voucher = get_model('voucher', 'Voucher')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -291,7 +302,7 @@ class CouponFulfillmentModule(BaseFulfillmentModule): ...@@ -291,7 +302,7 @@ class CouponFulfillmentModule(BaseFulfillmentModule):
Check whether the product in line is a Coupon Check whether the product in line is a Coupon
Args: Args:
line (Line): Defines the length of randomly generated string line (Line): Line to be considered.
Returns: Returns:
True if the line contains product of product class Coupon. True if the line contains product of product class Coupon.
...@@ -338,3 +349,115 @@ class CouponFulfillmentModule(BaseFulfillmentModule): ...@@ -338,3 +349,115 @@ class CouponFulfillmentModule(BaseFulfillmentModule):
True, if the product is revoked; otherwise, False. True, if the product is revoked; otherwise, False.
""" """
raise NotImplementedError("Revoke method not implemented!") raise NotImplementedError("Revoke method not implemented!")
class EnrollmentCodeFulfillmentModule(BaseFulfillmentModule):
def supports_line(self, line):
"""
Check whether the product in line is an Enrollment code.
Args:
line (Line): Line to be considered.
Returns:
True if the line contains an Enrollment code.
False otherwise.
"""
return line.product.get_product_class().name == ENROLLMENT_CODE_PRODUCT_CLASS_NAME
def get_supported_lines(self, lines):
""" Return a list of lines containing Enrollment code products that can be fulfilled.
Args:
lines (List of Lines): Order Lines, associated with purchased products in an Order.
Returns:
A supported list of unmodified lines associated with an Enrollment code product.
"""
return [line for line in lines if self.supports_line(line)]
def fulfill_product(self, order, lines):
""" Fulfills the purchase of an Enrollment code product.
For each line creates number of vouchers equal to that line's quantity. Creates a new OrderLineVouchers
object to tie the order with the created voucher and adds the vouchers to the coupon's total vouchers.
Args:
order (Order): The Order associated with the lines to be fulfilled.
lines (List of Lines): Order Lines, associated with purchased products in an Order.
Returns:
The original set of lines, with new statuses set based on the success or failure of fulfillment.
"""
msg = "Attempting to fulfill '{product_class}' product types for order [{order_number}]".format(
product_class=ENROLLMENT_CODE_PRODUCT_CLASS_NAME,
order_number=order.number
)
logger.info(msg)
for line in lines:
name = 'Enrollment Code Range for {}'.format(line.product.attr.course_key)
seat = Product.objects.filter(
attributes__name='course_key',
attribute_values__value_text=line.product.attr.course_key
).get(
attributes__name='certificate_type',
attribute_values__value_text=line.product.attr.seat_type
)
_range, created = Range.objects.get_or_create(name=name)
if created:
_range.add_product(seat)
vouchers = create_vouchers(
name='Enrollment code voucher [{}]'.format(line.product.title),
benefit_type=Benefit.PERCENTAGE,
benefit_value=100,
catalog=None,
coupon=seat,
end_datetime=settings.ENROLLMENT_CODE_EXIPRATION_DATE,
quantity=line.quantity,
start_datetime=datetime.datetime.now(),
voucher_type=Voucher.SINGLE_USE,
_range=_range
)
line_vouchers = OrderLineVouchers.objects.create(line=line)
for voucher in vouchers:
line_vouchers.vouchers.add(voucher)
line.set_status(LINE.COMPLETE)
self.send_email(order)
logger.info("Finished fulfilling 'Enrollment code' product types for order [%s]", order.number)
return order, lines
def revoke_line(self, line):
""" Revokes the specified line.
Args:
line (Line): Order Line to be revoked.
Returns:
True, if the product is revoked; otherwise, False.
"""
raise NotImplementedError("Revoke method not implemented!")
def send_email(self, order):
""" Sends an email with enrollment code order information. """
# Note (multi-courses): Change from a course_name to a list of course names.
product = order.lines.first().product
course = Course.objects.get(id=product.attr.course_key)
send_notification(
order.user,
'ORDER_WITH_CSV',
context={
'contact_url': get_lms_url('/contact'),
'course_name': course.name,
'download_csv_link': get_ecommerce_url(reverse('coupons:enrollment_code_csv', args=[order.number])),
'enrollment_code_title': product.title,
'order_number': order.number,
'partner_name': order.site.siteconfiguration.partner.name,
'lms_url': get_lms_url(),
'receipt_page_url': get_lms_url('{}?orderNum={}'.format(settings.RECEIPT_PAGE_PATH, order.number)),
},
site=order.site
)
...@@ -12,13 +12,19 @@ from oscar.test.newfactories import UserFactory, BasketFactory ...@@ -12,13 +12,19 @@ from oscar.test.newfactories import UserFactory, BasketFactory
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH
from ecommerce.core.tests import toggle_switch
from ecommerce.core.url_utils import get_lms_enrollment_api_url from ecommerce.core.url_utils import get_lms_enrollment_api_url
from ecommerce.courses.models import Course from ecommerce.courses.models import Course
from ecommerce.courses.utils import mode_for_seat from ecommerce.courses.utils import mode_for_seat
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.fulfillment.modules import CouponFulfillmentModule, EnrollmentFulfillmentModule from ecommerce.extensions.fulfillment.modules import (
CouponFulfillmentModule, EnrollmentCodeFulfillmentModule, EnrollmentFulfillmentModule
)
from ecommerce.extensions.fulfillment.status import LINE from ecommerce.extensions.fulfillment.status import LINE
from ecommerce.extensions.fulfillment.tests.mixins import FulfillmentTestMixin from ecommerce.extensions.fulfillment.tests.mixins import FulfillmentTestMixin
from ecommerce.extensions.voucher.models import OrderLineVouchers
from ecommerce.extensions.voucher.utils import create_vouchers from ecommerce.extensions.voucher.utils import create_vouchers
from ecommerce.tests.mixins import CouponMixin from ecommerce.tests.mixins import CouponMixin
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -29,7 +35,8 @@ LOGGER_NAME = 'ecommerce.extensions.analytics.utils' ...@@ -29,7 +35,8 @@ LOGGER_NAME = 'ecommerce.extensions.analytics.utils'
Applicator = get_class('offer.utils', 'Applicator') Applicator = get_class('offer.utils', 'Applicator')
Benefit = get_model('offer', 'Benefit') Benefit = get_model('offer', 'Benefit')
Catalog = get_model('catalogue', 'Catalog') Catalog = get_model('catalogue', 'Catalog')
ProductAttribute = get_model("catalogue", "ProductAttribute") Product = get_model('catalogue', 'Product')
ProductAttribute = get_model('catalogue', 'ProductAttribute')
ProductClass = get_model('catalogue', 'ProductClass') ProductClass = get_model('catalogue', 'ProductClass')
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher') Voucher = get_model('voucher', 'Voucher')
...@@ -427,3 +434,50 @@ class CouponFulfillmentModuleTest(CouponMixin, FulfillmentTestMixin, TestCase): ...@@ -427,3 +434,50 @@ class CouponFulfillmentModuleTest(CouponMixin, FulfillmentTestMixin, TestCase):
line = self.order.lines.first() line = self.order.lines.first()
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
CouponFulfillmentModule().revoke_line(line) CouponFulfillmentModule().revoke_line(line)
class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase):
""" Test Enrollment code fulfillment. """
QUANTITY = 5
def setUp(self):
super(EnrollmentCodeFulfillmentModuleTests, self).setUp()
toggle_switch(ENROLLMENT_CODE_SWITCH, True)
course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
user = UserFactory()
basket = BasketFactory()
basket.add_product(enrollment_code, self.QUANTITY)
self.order = factories.create_order(number=1, basket=basket, user=user)
def test_supports_line(self):
"""Test that support_line returns True for Enrollment code lines."""
line = self.order.lines.first()
supports_line = EnrollmentCodeFulfillmentModule().supports_line(line)
self.assertTrue(supports_line)
order = factories.create_order()
unsupported_line = order.lines.first()
supports_line = EnrollmentCodeFulfillmentModule().supports_line(unsupported_line)
self.assertFalse(supports_line)
def test_get_supported_lines(self):
"""Test that Enrollment code lines where returned."""
lines = self.order.lines.all()
supported_lines = EnrollmentCodeFulfillmentModule().get_supported_lines(lines)
self.assertListEqual(supported_lines, list(lines))
def test_fulfill_product(self):
"""Test fulfilling an Enrollment code product."""
self.assertEqual(OrderLineVouchers.objects.count(), 0)
lines = self.order.lines.all()
__, completed_lines = EnrollmentCodeFulfillmentModule().fulfill_product(self.order, lines)
self.assertEqual(completed_lines[0].status, LINE.COMPLETE)
self.assertEqual(OrderLineVouchers.objects.count(), 1)
self.assertEqual(OrderLineVouchers.objects.first().vouchers.count(), self.QUANTITY)
def test_revoke_line(self):
line = self.order.lines.first()
with self.assertRaises(NotImplementedError):
EnrollmentCodeFulfillmentModule().revoke_line(line)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0009_auto_20150709_1205'),
('voucher', '0002_couponvouchers'),
]
operations = [
migrations.CreateModel(
name='OrderLineVouchers',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('line', models.ForeignKey(related_name='order_line_vouchers', to='order.Line')),
('vouchers', models.ManyToManyField(related_name='order_line_vouchers', to='voucher.Voucher', blank=True)),
],
),
]
...@@ -6,5 +6,10 @@ class CouponVouchers(models.Model): ...@@ -6,5 +6,10 @@ class CouponVouchers(models.Model):
coupon = models.ForeignKey('catalogue.Product', related_name='coupon_vouchers') coupon = models.ForeignKey('catalogue.Product', related_name='coupon_vouchers')
vouchers = models.ManyToManyField('voucher.Voucher', blank=True, related_name='coupon_vouchers') vouchers = models.ManyToManyField('voucher.Voucher', blank=True, related_name='coupon_vouchers')
class OrderLineVouchers(models.Model):
line = models.ForeignKey('order.Line', related_name='order_line_vouchers')
vouchers = models.ManyToManyField('voucher.Voucher', related_name='order_line_vouchers')
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from oscar.apps.voucher.models import * # noqa pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position from oscar.apps.voucher.models import * # noqa pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position
import httpretty
from django.db import IntegrityError from django.db import IntegrityError
from django.test import override_settings from django.test import override_settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import httpretty
from oscar.templatetags.currency_filters import currency from oscar.templatetags.currency_filters import currency
from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wildcard-import from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wildcard-import
......
...@@ -4,7 +4,6 @@ import datetime ...@@ -4,7 +4,6 @@ import datetime
import hashlib import hashlib
import logging import logging
import uuid import uuid
import pytz
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -14,6 +13,7 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -14,6 +13,7 @@ from django.utils.translation import ugettext_lazy as _
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.templatetags.currency_filters import currency from oscar.templatetags.currency_filters import currency
import pytz
from ecommerce.core.url_utils import get_ecommerce_url from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.invoice.models import Invoice from ecommerce.invoice.models import Invoice
...@@ -232,7 +232,7 @@ def _get_or_create_offer(product_range, benefit_type, benefit_value, coupon_id=N ...@@ -232,7 +232,7 @@ def _get_or_create_offer(product_range, benefit_type, benefit_value, coupon_id=N
max_affected_items=1, max_affected_items=1,
) )
offer_name = "Catalog [{}]-{}-{}".format(product_range.catalog.id, offer_benefit.type, offer_benefit.value) offer_name = "Catalog [{}]-{}-{}".format(product_range.id, offer_benefit.type, offer_benefit.value)
if max_uses: if max_uses:
# Offer needs to be unique for each multi-use coupon. # Offer needs to be unique for each multi-use coupon.
offer_name = "{} (Coupon [{}] - max_uses:{})".format(offer_name, coupon_id, max_uses) offer_name = "{} (Coupon [{}] - max_uses:{})".format(offer_name, coupon_id, max_uses)
...@@ -320,7 +320,8 @@ def create_vouchers( ...@@ -320,7 +320,8 @@ def create_vouchers(
voucher_type, voucher_type,
coupon_id=None, coupon_id=None,
code=None, code=None,
max_uses=None): max_uses=None,
_range=None):
""" """
Create vouchers. Create vouchers.
...@@ -340,16 +341,19 @@ def create_vouchers( ...@@ -340,16 +341,19 @@ def create_vouchers(
Returns: Returns:
List[Voucher] List[Voucher]
""" """
logger.info("Creating [%d] vouchers catalog [%s]", quantity, catalog.id)
vouchers = [] vouchers = []
range_name = (_('Range for {catalog_name}').format(catalog_name=catalog.name)) if catalog is None and _range:
product_range, __ = Range.objects.get_or_create( # We don't use a catalog for enrollment codes.
name=range_name, logger.info("Creating [%d] enrollment code vouchers", quantity)
catalog=catalog, product_range = _range
) else:
logger.info("Creating [%d] vouchers catalog [%s]", quantity, catalog.id)
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( offer = _get_or_create_offer(
product_range=product_range, product_range=product_range,
......
...@@ -80,6 +80,7 @@ OSCAR_ORDER_STATUS_CASCADE = { ...@@ -80,6 +80,7 @@ OSCAR_ORDER_STATUS_CASCADE = {
FULFILLMENT_MODULES = [ FULFILLMENT_MODULES = [
'ecommerce.extensions.fulfillment.modules.EnrollmentFulfillmentModule', 'ecommerce.extensions.fulfillment.modules.EnrollmentFulfillmentModule',
'ecommerce.extensions.fulfillment.modules.CouponFulfillmentModule', 'ecommerce.extensions.fulfillment.modules.CouponFulfillmentModule',
'ecommerce.extensions.fulfillment.modules.EnrollmentCodeFulfillmentModule',
] ]
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
......
"""Common settings and globals.""" """Common settings and globals."""
import datetime
import os import os
from os.path import basename, normpath from os.path import basename, normpath
from sys import path from sys import path
...@@ -517,6 +518,12 @@ THEME_CACHE_TIMEOUT = 30 * 60 ...@@ -517,6 +518,12 @@ THEME_CACHE_TIMEOUT = 30 * 60
# End Theme settings # End Theme settings
EDX_DRF_EXTENSIONS = { EDX_DRF_EXTENSIONS = {
'JWT_PAYLOAD_USER_ATTRIBUTES': ('full_name', 'email', 'tracking_context',), 'JWT_PAYLOAD_USER_ATTRIBUTES': ('full_name', 'email', 'tracking_context',),
} }
# Enrollment codes voucher end datetime used for setting the end dates for vouchers
# created for the Enrollment code products.
ENROLLMENT_CODE_EXIPRATION_DATE = datetime.datetime.now() + datetime.timedelta(weeks=520)
{% extends 'customer/email_base.html' %}
{% load i18n %}
{% block body %}
<tr>
<table class="cn-body">
<!-- Message header -->
<tr align="left" class="cn-img-wrapper">
<td colspan="2" align="left" class="cn-img-wrapper cn-full-width" style="font-size: 20px; vertical-align: middle; padding-left: 20px; color: #127eb1;">
<img src="http://gallery.mailchimp.com/1822a33c054dc20e223ca40e2/images/logo_web_edx_studio01aad778f34f.png"
height="30" alt="{{partner_name}} Logo" border="0" hspace="0" vspace="0"
class="platform-img">
{% trans "Order Confirmation" %}
</td>
</tr>
<tr>
<td class="cn-content-clear" colspan="2"></td>
</tr>
<!-- Message sub-header -->
<tr class="force-col" valign="middle" style="background-color: #f3f3f3;">
<td style="padding: 20px;">
<table border="0" cellspacing="0" cellpadding="0" width="280" align="left" class="col-2">
<tr>
<td align="left" valign="top" style="font-size:13px; line-height: 20px; color: #aaaaaa;">
{% trans "Order confirmation for" %}:
</td>
</tr>
<tr>
<td align="left" valign="top" style="line-height: 26px; font-size:24px; color: #002D40;">
{{ order_number }}
</td>
</tr>
</table>
</td>
<td style="padding: 20px;">
<table border="0" cellspacing="0" cellpadding="10" width="100" align="right" class="col-2">
<tr>
<td align="center" valign="middle"
style="font-size:13px; background-color: #127eb1; border-radius: 5px; box-shadow: 0 2px #002D40;">
<a href="{{ receipt_page_url }}" style="color: #ffffff; text-decoration: none;">
{% trans "View Order Information" %}
</a>
</td>
</tr>
</table>
</td>
<tr>
<!-- Message Body -->
<tr class="cn-content cn-full-width">
<td colspan="2">
<p>{% blocktrans %}Dear {{full_name}},{% endblocktrans %}</p>
<p>{% blocktrans %}Thank you for purchasing access to {{course_name}}.
Please click <a href="{{download_csv_link}}">HERE</a> to download a CSV file with the enrollment codes for this course.
Once you have the codes you can distribute them to your team. {% endblocktrans %}</p>
<p>{% blocktrans %}To view your payment information, please visit {{receipt_page_url}}.{% endblocktrans %}</p>
<p>{% blocktrans %}To explore other courses, please visit {{lms_url}}.{% endblocktrans %}</p>
<p>{% trans "Thank you," %}</p>
<p>{% blocktrans %}The {{partner_name}} team{% endblocktrans %}</p>
</td>
</tr>
<tr>
<td class="cn-content-clear cn-footer" colspan="2"></td>
</tr>
<!-- Footer content -->
<tr class="cn-full-width">
<td class="cn-footer-content" colspan="2">
<p>{% blocktrans %}You received this message because you purchased enrollment codes for {{course_name}} on {{lms_url}}. If you have any questions, please visit {{contact_url}}.{% endblocktrans %}</p>
</td>
</tr>
<!-- Empty base footer -->
{% block footer %}{% endblock footer %}
</table>
</tr>
{% endblock body %}
{% load i18n %}
{% trans "Order confirmation for: " %}{{order_number}}
{% blocktrans %}Dear {{full_name}},{% endblocktrans %}
{% blocktrans %}Thank you for purchasing access to {{course_name}}. Please go to {{download_csv_link}} to download a CSV file with the enrollment codes for this course. Once you have the codes you can distribute them to your team. {% endblocktrans %}
{% blocktrans %}To explore other courses, please visit {{lms_url}}.{% endblocktrans %}
{% trans "Thank you," %}
{% blocktrans %}The {{partner_name}} team{% endblocktrans %}
{% blocktrans %}You received this message because you purchased enrollment codes for {{course_name}} on {{lms_url}}. If you have any questions, please visit {{contact_url}}.{% endblocktrans %}
{% load i18n %}
{% blocktrans %}{{partner_name}}: Order Confirmation: [{{order_number}}]{% endblocktrans %}
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