Commit ac72e262 by Afzal Wali

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.
parent 710c2110
......@@ -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()
......@@ -719,6 +711,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):
"""
The template that should be used when there's only one item in the order
......@@ -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">
......
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