Fixed the empty list price issue.
Added columns to the CC purchases report. (added Qty and Total Discount. Moved the Total Amount to the last index). Coupon code report.
This commit is contained in:
@@ -94,11 +94,7 @@ class PaidCourseEnrollmentReportProvider(BaseAbstractEnrollmentReportProvider):
|
||||
coupon_codes = ", ".join(coupon_codes)
|
||||
registration_code_used = 'N/A'
|
||||
|
||||
if coupon_redemption.exists():
|
||||
list_price = paid_course_reg_item.list_price
|
||||
else:
|
||||
list_price = paid_course_reg_item.unit_cost
|
||||
|
||||
list_price = paid_course_reg_item.get_list_price()
|
||||
payment_amount = paid_course_reg_item.unit_cost
|
||||
coupon_codes_used = coupon_codes
|
||||
payment_status = paid_course_reg_item.status
|
||||
@@ -156,7 +152,7 @@ class PaidCourseEnrollmentReportProvider(BaseAbstractEnrollmentReportProvider):
|
||||
coupon_codes = [redemption.coupon.code for redemption in coupon_redemption]
|
||||
coupon_codes = ", ".join(coupon_codes)
|
||||
|
||||
list_price = order_item.list_price
|
||||
list_price = order_item.get_list_price()
|
||||
payment_amount = order_item.unit_cost
|
||||
coupon_codes_used = coupon_codes
|
||||
payment_status = order_item.status
|
||||
|
||||
@@ -75,7 +75,8 @@ EXPECTED_CSV_HEADER = (
|
||||
'"code","redeem_code_url","course_id","company_name","created_by","redeemed_by","invoice_id","purchaser",'
|
||||
'"customer_reference_number","internal_reference"'
|
||||
)
|
||||
EXPECTED_COUPON_CSV_HEADER = '"code","course_id","percentage_discount","code_redeemed_count","description"'
|
||||
EXPECTED_COUPON_CSV_HEADER = '"Coupon Code","Course Id","% Discount","Description","Expiration Date",' \
|
||||
'"Is Active","Code Redeemed Count","Total Discounted Seats","Total Discounted Amount"'
|
||||
|
||||
# ddt data for test cases involving reports
|
||||
REPORTS_DATA = (
|
||||
@@ -4246,13 +4247,20 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
# filter all the coupons
|
||||
for coupon in Coupon.objects.all():
|
||||
self.assertIn('"{code}","{course_id}","{discount}","0","{description}","{expiration_date}","True"'.format(
|
||||
code=coupon.code,
|
||||
course_id=coupon.course_id,
|
||||
discount=coupon.percentage_discount,
|
||||
description=coupon.description,
|
||||
expiration_date=coupon.display_expiry_date
|
||||
), response.content)
|
||||
self.assertIn(
|
||||
'"{coupon_code}","{course_id}","{discount}","{description}","{expiration_date}","{is_active}",'
|
||||
'"{code_redeemed_count}","{total_discounted_seats}","{total_discounted_amount}"'.format(
|
||||
coupon_code=coupon.code,
|
||||
course_id=coupon.course_id,
|
||||
discount=coupon.percentage_discount,
|
||||
description=coupon.description,
|
||||
expiration_date=coupon.display_expiry_date,
|
||||
is_active=coupon.is_active,
|
||||
code_redeemed_count="0",
|
||||
total_discounted_seats="0",
|
||||
total_discounted_amount="0",
|
||||
), response.content
|
||||
)
|
||||
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
|
||||
@@ -951,7 +951,6 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume
|
||||
('company_name', 'Company Name'),
|
||||
('company_contact_name', 'Company Contact Name'),
|
||||
('company_contact_email', 'Company Contact Email'),
|
||||
('total_amount', 'Total Amount'),
|
||||
('logged_in_username', 'Login Username'),
|
||||
('logged_in_email', 'Login User Email'),
|
||||
('purchase_time', 'Date of Sale'),
|
||||
@@ -967,8 +966,11 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume
|
||||
('order_type', 'Order Type'),
|
||||
('status', 'Order Item Status'),
|
||||
('coupon_code', 'Coupon Code'),
|
||||
('unit_cost', 'Unit Price'),
|
||||
('list_price', 'List Price'),
|
||||
('unit_cost', 'Unit Price'),
|
||||
('quantity', 'Quantity'),
|
||||
('total_discount', 'Total Discount'),
|
||||
('total_amount', 'Total Amount Paid'),
|
||||
]
|
||||
|
||||
db_columns = [x[0] for x in query_features]
|
||||
@@ -1198,11 +1200,22 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument
|
||||
coupons = Coupon.objects.filter(course_id=course_id)
|
||||
|
||||
query_features = [
|
||||
'code', 'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date', 'is_active'
|
||||
('code', _('Coupon Code')),
|
||||
('course_id', _('Course Id')),
|
||||
('percentage_discount', _('% Discount')),
|
||||
('description', _('Description')),
|
||||
('expiration_date', _('Expiration Date')),
|
||||
('is_active', _('Is Active')),
|
||||
('code_redeemed_count', _('Code Redeemed Count')),
|
||||
('total_discounted_seats', _('Total Discounted Seats')),
|
||||
('total_discounted_amount', _('Total Discounted Amount')),
|
||||
]
|
||||
coupons_list = instructor_analytics.basic.coupon_codes_features(query_features, coupons)
|
||||
header, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, query_features)
|
||||
return instructor_analytics.csvs.create_csv_response('Coupons.csv', header, data_rows)
|
||||
db_columns = [x[0] for x in query_features]
|
||||
csv_columns = [x[1] for x in query_features]
|
||||
|
||||
coupons_list = instructor_analytics.basic.coupon_codes_features(db_columns, coupons, course_id)
|
||||
__, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, db_columns)
|
||||
return instructor_analytics.csvs.create_csv_response('Coupons.csv', csv_columns, data_rows)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -72,6 +72,7 @@ def sale_order_record_features(course_id, features):
|
||||
|
||||
quantity = int(getattr(purchased_course, 'qty'))
|
||||
unit_cost = float(getattr(purchased_course, 'unit_cost'))
|
||||
sale_order_dict.update({"quantity": quantity})
|
||||
sale_order_dict.update({"total_amount": quantity * unit_cost})
|
||||
|
||||
sale_order_dict.update({"logged_in_username": purchased_course.order.user.username})
|
||||
@@ -80,6 +81,13 @@ def sale_order_record_features(course_id, features):
|
||||
# Extracting OrderItem information of unit_cost, list_price and status
|
||||
order_item_dict = dict((feature, getattr(purchased_course, feature, None))
|
||||
for feature in order_item_features)
|
||||
|
||||
order_item_dict['list_price'] = purchased_course.get_list_price()
|
||||
|
||||
sale_order_dict.update(
|
||||
{"total_discount": (order_item_dict['list_price'] - order_item_dict['unit_cost']) * quantity}
|
||||
)
|
||||
|
||||
order_item_dict.update({"coupon_code": 'N/A'})
|
||||
|
||||
coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order_id=purchased_course.order_id)
|
||||
@@ -235,7 +243,7 @@ def list_may_enroll(course_key, features):
|
||||
return [extract_student(student, features) for student in may_enroll_and_unenrolled]
|
||||
|
||||
|
||||
def coupon_codes_features(features, coupons_list):
|
||||
def coupon_codes_features(features, coupons_list, course_id):
|
||||
"""
|
||||
Return list of Coupon Codes as dictionaries.
|
||||
|
||||
@@ -254,13 +262,33 @@ def coupon_codes_features(features, coupons_list):
|
||||
coupon_features = [x for x in COUPON_FEATURES if x in features]
|
||||
|
||||
coupon_dict = dict((feature, getattr(coupon, feature)) for feature in coupon_features)
|
||||
coupon_dict['code_redeemed_count'] = coupon.couponredemption_set.filter(
|
||||
coupon_redemptions = coupon.couponredemption_set.filter(
|
||||
order__status="purchased"
|
||||
).count()
|
||||
)
|
||||
|
||||
# we have to capture the redeemed_by value in the case of the downloading and spent registration
|
||||
coupon_dict['code_redeemed_count'] = coupon_redemptions.count()
|
||||
|
||||
seats_purchased_using_coupon = 0
|
||||
total_discounted_amount = 0
|
||||
for coupon_redemption in coupon_redemptions:
|
||||
cart_items = coupon_redemption.order.orderitem_set.select_subclasses()
|
||||
found_items = []
|
||||
for item in cart_items:
|
||||
if getattr(item, 'course_id', None):
|
||||
if item.course_id == course_id:
|
||||
found_items.append(item)
|
||||
for order_item in found_items:
|
||||
seats_purchased_using_coupon += order_item.qty
|
||||
discounted_amount_for_item = float(
|
||||
order_item.list_price * order_item.qty) * (float(coupon.percentage_discount) / 100)
|
||||
total_discounted_amount += discounted_amount_for_item
|
||||
|
||||
coupon_dict['total_discounted_seats'] = seats_purchased_using_coupon
|
||||
coupon_dict['total_discounted_amount'] = total_discounted_amount
|
||||
|
||||
# We have to capture the redeemed_by value in the case of the downloading and spent registration
|
||||
# codes csv. In the case of active and generated registration codes the redeemed_by value will be None.
|
||||
# They have not been redeemed yet
|
||||
# They have not been redeemed yet
|
||||
|
||||
coupon_dict['expiration_date'] = coupon.display_expiry_date
|
||||
coupon_dict['course_id'] = coupon_dict['course_id'].to_deprecated_string()
|
||||
|
||||
@@ -189,7 +189,7 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
|
||||
self.assertEqual(sale_record['total_used_codes'], 0)
|
||||
self.assertEqual(sale_record['total_codes'], 5)
|
||||
|
||||
def test_sale_order_features(self):
|
||||
def test_sale_order_features_with_discount(self):
|
||||
"""
|
||||
Test Order Sales Report CSV
|
||||
"""
|
||||
@@ -267,6 +267,78 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
|
||||
self.assertEqual(sale_order_record['status'], item.status)
|
||||
self.assertEqual(sale_order_record['coupon_code'], coupon_redemption[0].coupon.code)
|
||||
|
||||
def test_sale_order_features_without_discount(self):
|
||||
"""
|
||||
Test Order Sales Report CSV
|
||||
"""
|
||||
query_features = [
|
||||
('id', 'Order Id'),
|
||||
('company_name', 'Company Name'),
|
||||
('company_contact_name', 'Company Contact Name'),
|
||||
('company_contact_email', 'Company Contact Email'),
|
||||
('total_amount', 'Total Amount'),
|
||||
('total_codes', 'Total Codes'),
|
||||
('total_used_codes', 'Total Used Codes'),
|
||||
('logged_in_username', 'Login Username'),
|
||||
('logged_in_email', 'Login User Email'),
|
||||
('purchase_time', 'Date of Sale'),
|
||||
('customer_reference_number', 'Customer Reference Number'),
|
||||
('recipient_name', 'Recipient Name'),
|
||||
('recipient_email', 'Recipient Email'),
|
||||
('bill_to_street1', 'Street 1'),
|
||||
('bill_to_street2', 'Street 2'),
|
||||
('bill_to_city', 'City'),
|
||||
('bill_to_state', 'State'),
|
||||
('bill_to_postalcode', 'Postal Code'),
|
||||
('bill_to_country', 'Country'),
|
||||
('order_type', 'Order Type'),
|
||||
('status', 'Order Item Status'),
|
||||
('coupon_code', 'Coupon Code'),
|
||||
('unit_cost', 'Unit Price'),
|
||||
('list_price', 'List Price'),
|
||||
('codes', 'Registration Codes'),
|
||||
('course_id', 'Course Id'),
|
||||
('quantity', 'Quantity'),
|
||||
('total_discount', 'Total Discount'),
|
||||
('total_amount', 'Total Amount Paid'),
|
||||
]
|
||||
# add the coupon code for the course
|
||||
order = Order.get_cart_for_user(self.instructor)
|
||||
order.order_type = 'business'
|
||||
order.save()
|
||||
order.add_billing_details(
|
||||
company_name='Test Company',
|
||||
company_contact_name='Test',
|
||||
company_contact_email='test@123',
|
||||
recipient_name='R1', recipient_email='',
|
||||
customer_reference_number='PO#23'
|
||||
)
|
||||
CourseRegCodeItem.add_to_order(order, self.course.id, 4)
|
||||
order.purchase()
|
||||
|
||||
# get the updated item
|
||||
item = order.orderitem_set.all().select_subclasses()[0]
|
||||
|
||||
db_columns = [x[0] for x in query_features]
|
||||
sale_order_records_list = sale_order_record_features(self.course.id, db_columns)
|
||||
|
||||
for sale_order_record in sale_order_records_list:
|
||||
self.assertEqual(sale_order_record['recipient_email'], order.recipient_email)
|
||||
self.assertEqual(sale_order_record['recipient_name'], order.recipient_name)
|
||||
self.assertEqual(sale_order_record['company_name'], order.company_name)
|
||||
self.assertEqual(sale_order_record['company_contact_name'], order.company_contact_name)
|
||||
self.assertEqual(sale_order_record['company_contact_email'], order.company_contact_email)
|
||||
self.assertEqual(sale_order_record['customer_reference_number'], order.customer_reference_number)
|
||||
self.assertEqual(sale_order_record['unit_cost'], item.unit_cost)
|
||||
# Make sure list price is not None and matches the unit price since no discount was applied.
|
||||
self.assertIsNotNone(sale_order_record['list_price'])
|
||||
self.assertEqual(sale_order_record['list_price'], item.unit_cost)
|
||||
self.assertEqual(sale_order_record['status'], item.status)
|
||||
self.assertEqual(sale_order_record['coupon_code'], 'N/A')
|
||||
self.assertEqual(sale_order_record['total_amount'], item.unit_cost * item.qty)
|
||||
self.assertEqual(sale_order_record['total_discount'], 0)
|
||||
self.assertEqual(sale_order_record['quantity'], item.qty)
|
||||
|
||||
|
||||
class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
|
||||
""" Test basic course registration codes analytics functions. """
|
||||
@@ -340,7 +412,8 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
|
||||
|
||||
def test_coupon_codes_features(self):
|
||||
query_features = [
|
||||
'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date'
|
||||
'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date',
|
||||
'total_discounted_amount', 'total_discounted_seats'
|
||||
]
|
||||
for i in range(10):
|
||||
coupon = Coupon(
|
||||
@@ -366,7 +439,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
|
||||
Q(expiration_date__gt=datetime.datetime.now(pytz.UTC)) |
|
||||
Q(expiration_date__isnull=True)
|
||||
)
|
||||
active_coupons_list = coupon_codes_features(query_features, active_coupons)
|
||||
active_coupons_list = coupon_codes_features(query_features, active_coupons, self.course.id)
|
||||
self.assertEqual(len(active_coupons_list), len(active_coupons))
|
||||
for active_coupon in active_coupons_list:
|
||||
self.assertEqual(set(active_coupon.keys()), set(query_features))
|
||||
|
||||
@@ -220,9 +220,8 @@ class Order(models.Model):
|
||||
Reset the items price state in the user cart
|
||||
"""
|
||||
for item in self.orderitem_set.all(): # pylint: disable=no-member
|
||||
if item.list_price:
|
||||
if item.is_discounted:
|
||||
item.unit_cost = item.list_price
|
||||
item.list_price = None
|
||||
item.save()
|
||||
|
||||
def clear(self):
|
||||
@@ -300,19 +299,12 @@ class Order(models.Model):
|
||||
"""
|
||||
items_data = []
|
||||
for item in order_items:
|
||||
if item.list_price is not None:
|
||||
discount_price = item.list_price - item.unit_cost
|
||||
price = item.list_price
|
||||
else:
|
||||
discount_price = 0
|
||||
price = item.unit_cost
|
||||
|
||||
item_total = item.qty * item.unit_cost
|
||||
items_data.append({
|
||||
'item_description': item.pdf_receipt_display_name,
|
||||
'quantity': item.qty,
|
||||
'list_price': price,
|
||||
'discount': discount_price,
|
||||
'list_price': item.get_list_price(),
|
||||
'discount': item.get_list_price() - item.unit_cost,
|
||||
'item_total': item_total
|
||||
})
|
||||
pdf_buffer = BytesIO()
|
||||
@@ -718,6 +710,23 @@ class OrderItem(TimeStampedModel):
|
||||
"""
|
||||
return OrderItemSubclassPK(type(self), self.pk)
|
||||
|
||||
@property
|
||||
def is_discounted(self):
|
||||
"""
|
||||
Returns True if the item a discount coupon has been applied to the OrderItem and False otherwise.
|
||||
Earlier, the OrderItems were stored with an empty list_price if a discount had not been applied.
|
||||
Now we consider the item to be non discounted if list_price is None or list_price == unit_cost. In
|
||||
these lines, an item is discounted if it's non-None and list_price and unit_cost mismatch.
|
||||
This should work with both new and old records.
|
||||
"""
|
||||
return self.list_price and self.list_price != self.unit_cost
|
||||
|
||||
def get_list_price(self):
|
||||
"""
|
||||
Returns the unit_cost if no discount has been applied, or the list_price if it is defined.
|
||||
"""
|
||||
return self.list_price if self.list_price else self.unit_cost
|
||||
|
||||
@property
|
||||
def single_item_receipt_template(self):
|
||||
"""
|
||||
@@ -1449,6 +1458,7 @@ class PaidCourseRegistration(OrderItem):
|
||||
item.mode = course_mode.slug
|
||||
item.qty = 1
|
||||
item.unit_cost = cost
|
||||
item.list_price = cost
|
||||
item.line_desc = _(u'Registration for Course: {course_name}').format(
|
||||
course_name=course.display_name_with_default)
|
||||
item.currency = currency
|
||||
@@ -1602,6 +1612,7 @@ class CourseRegCodeItem(OrderItem):
|
||||
item.status = order.status
|
||||
item.mode = course_mode.slug
|
||||
item.unit_cost = cost
|
||||
item.list_price = cost
|
||||
item.qty = qty
|
||||
item.line_desc = _(u'Enrollment codes for Course: {course_name}').format(
|
||||
course_name=course.display_name_with_default)
|
||||
@@ -1803,6 +1814,7 @@ class CertificateItem(OrderItem):
|
||||
item.status = order.status
|
||||
item.qty = 1
|
||||
item.unit_cost = cost
|
||||
item.list_price = cost
|
||||
course_name = modulestore().get_course(course_id).display_name
|
||||
# Translators: In this particular case, mode_name refers to a
|
||||
# particular mode (i.e. Honor Code Certificate, Verified Certificate, etc)
|
||||
|
||||
@@ -430,6 +430,40 @@ class OrderItemTest(TestCase):
|
||||
self.assertDictEqual({item.pk_with_subclass: set([])}, inst_dict)
|
||||
self.assertEquals(set([]), inst_set)
|
||||
|
||||
def test_is_discounted(self):
|
||||
"""
|
||||
This tests the is_discounted property of the OrderItem
|
||||
"""
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
item = OrderItem(user=self.user, order=cart)
|
||||
|
||||
item.list_price = None
|
||||
item.unit_cost = 100
|
||||
self.assertFalse(item.is_discounted)
|
||||
|
||||
item.list_price = 100
|
||||
item.unit_cost = 100
|
||||
self.assertFalse(item.is_discounted)
|
||||
|
||||
item.list_price = 100
|
||||
item.unit_cost = 90
|
||||
self.assertTrue(item.is_discounted)
|
||||
|
||||
def test_get_list_price(self):
|
||||
"""
|
||||
This tests the get_list_price() method of the OrderItem
|
||||
"""
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
item = OrderItem(user=self.user, order=cart)
|
||||
|
||||
item.list_price = None
|
||||
item.unit_cost = 100
|
||||
self.assertEqual(item.get_list_price(), item.unit_cost)
|
||||
|
||||
item.list_price = 200
|
||||
item.unit_cost = 100
|
||||
self.assertEqual(item.get_list_price(), item.list_price)
|
||||
|
||||
|
||||
class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -664,8 +664,10 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
for item in items:
|
||||
if item.id == reg_item.id:
|
||||
self.assertEquals(item.unit_cost, self.get_discount(self.cost))
|
||||
self.assertEquals(item.list_price, self.cost)
|
||||
elif item.id == cert_item.id:
|
||||
self.assertEquals(item.list_price, None)
|
||||
self.assertEquals(item.list_price, self.cost)
|
||||
self.assertEquals(item.unit_cost, self.cost)
|
||||
|
||||
# Delete the discounted item, corresponding coupon redemption should
|
||||
# be removed for that particular discounted item
|
||||
|
||||
@@ -320,7 +320,7 @@ from microsite_configuration import microsite
|
||||
<div class="three-col">
|
||||
% if item.status == "purchased":
|
||||
<div class="col-1">
|
||||
% if item.list_price != None:
|
||||
% if item.is_discounted:
|
||||
<div class="price">${_('Price per student:')} <span class="line-through"> ${currency_symbol}${"{0:0.2f}".format(item.list_price)}</span>
|
||||
</div>
|
||||
<div class="price green">${_('Discount Applied:')} <span> ${currency_symbol}${"{0:0.2f}".format(item.unit_cost)} </span></div>
|
||||
@@ -338,7 +338,7 @@ from microsite_configuration import microsite
|
||||
</div>
|
||||
% elif item.status == "refunded":
|
||||
<div class="col-1">
|
||||
% if item.list_price != None:
|
||||
% if item.is_discounted:
|
||||
<div class="price">${_('Price per student:')} <span class="line-through"> ${currency_symbol}${"{0:0.2f}".format(item.list_price)}</span>
|
||||
</div>
|
||||
<div class="price green">${_('Discount Applied:')} <span><del> ${currency_symbol}${"{0:0.2f}".format(item.unit_cost)}
|
||||
|
||||
@@ -78,7 +78,7 @@ from django.utils.translation import ungettext
|
||||
<hr>
|
||||
<div class="three-col">
|
||||
<div class="col-1">
|
||||
% if item.list_price != None:
|
||||
% if item.is_discounted:
|
||||
<% discount_applied = True %>
|
||||
<div class="price">${_('Price per student:')}
|
||||
<span class="line-through">
|
||||
|
||||
Reference in New Issue
Block a user