Files
edx-platform/lms/djangoapps/shoppingcart/reports.py
Muhammad Shoaib b555c869bf SOL-794 Detailed Enrollment Report
- added the abstract and concrete layers of enrollment report provider
- created a celery task.
-added the button in the e-commerce reports section

added the enrollment data backend

added the payment data and start writing the test cases.

updated the code with the feedback suggestions and wrote some test cases.
- all the downloadable reports are now visible in the ecommerce download section.
Pending instructor tasks is also visible in the ecommerce section

added the fields in the user profile information
changed the report store configuration key
added the new http endpoint for financial reports to add more permissions for finance_admin to access.

fix quality issues

added test cases to check csv content data

rebased with master and resolved conflicts

changed the log messages

added the changes as per code clintonb suggestions during code review

updated the test cases for the finance_admin decorator

changes suggested by clinton.

Created and moved Table level filters to the Custom Manager for the CourseEnrollment model.

ecommerce.js file was loaded twice in the instructor_dashboard.js fixed the issues

added the registration code column in the csv

added the full gender in the csv file

Update data sources and add display name translations for the report columns

fix meta name

Make sure the reports section does not appear on non whitelabel courses

pylint fixes

expand out enumerated values
2015-05-22 15:16:57 -04:00

281 lines
10 KiB
Python

""" Objects and functions related to generating CSV reports """
from decimal import Decimal
import unicodecsv
from django.utils.translation import ugettext as _
from courseware.courses import get_course_by_id
from course_modes.models import CourseMode
from shoppingcart.models import CertificateItem, OrderItem
from student.models import CourseEnrollment
from util.query import use_read_replica_if_available
from xmodule.modulestore.django import modulestore
class Report(object):
"""
Base class for making CSV reports related to revenue, enrollments, etc
To make a different type of report, write a new subclass that implements
the methods rows and header.
"""
def __init__(self, start_date, end_date, start_word=None, end_word=None):
self.start_date = start_date
self.end_date = end_date
self.start_word = start_word
self.end_word = end_word
def rows(self):
"""
Performs database queries necessary for the report and eturns an generator of
lists, in which each list is a separate row of the report.
Arguments are start_date (datetime), end_date (datetime), start_word (str),
and end_word (str). Date comparisons are start_date <= [date of item] < end_date.
"""
raise NotImplementedError
def header(self):
"""
Returns the appropriate header based on the report type, in the form of a
list of strings.
"""
raise NotImplementedError
def write_csv(self, filelike):
"""
Given a file object to write to and {start/end date, start/end letter} bounds,
generates a CSV report of the appropriate type.
"""
items = self.rows()
writer = unicodecsv.writer(filelike, encoding="utf-8")
writer.writerow(self.header())
for item in items:
writer.writerow(item)
class RefundReport(Report):
"""
Subclass of Report, used to generate Refund Reports for finance purposes.
For each refund between a given start_date and end_date, we find the relevant
order number, customer name, date of transaction, date of refund, and any service
fees.
"""
def rows(self):
query1 = use_read_replica_if_available(
CertificateItem.objects.select_related('user__profile').filter(
status="refunded",
refund_requested_time__gte=self.start_date,
refund_requested_time__lt=self.end_date,
).order_by('refund_requested_time'))
query2 = use_read_replica_if_available(
CertificateItem.objects.select_related('user__profile').filter(
status="refunded",
refund_requested_time=None,
))
query = query1 | query2
for item in query:
yield [
item.order_id,
item.user.profile.name,
item.fulfilled_time,
item.refund_requested_time,
item.line_cost,
item.service_fee,
]
def header(self):
return [
_("Order Number"),
_("Customer Name"),
_("Date of Original Transaction"),
_("Date of Refund"),
_("Amount of Refund"),
_("Service Fees (if any)"),
]
class ItemizedPurchaseReport(Report):
"""
Subclass of Report, used to generate itemized purchase reports.
For all purchases (verified certificates, paid course registrations, etc) between
a given start_date and end_date, we find that purchase's time, order ID, status,
quantity, unit cost, total cost, currency, description, and related comments.
"""
def rows(self):
query = use_read_replica_if_available(
OrderItem.objects.filter(
status="purchased",
fulfilled_time__gte=self.start_date,
fulfilled_time__lt=self.end_date,
).order_by("fulfilled_time"))
for item in query:
yield [
item.fulfilled_time,
item.order_id, # pylint: disable=no-member
item.status,
item.qty,
item.unit_cost,
item.line_cost,
item.currency,
item.line_desc,
item.report_comments,
]
def header(self):
return [
_("Purchase Time"),
_("Order ID"),
_("Status"),
_("Quantity"),
_("Unit Cost"),
_("Total Cost"),
_("Currency"),
_("Description"),
_("Comments")
]
class CertificateStatusReport(Report):
"""
Subclass of Report, used to generate Certificate Status Reports for Ed Services.
For each course in each university whose name is within the range start_word and end_word,
inclusive, (i.e., the letter range H-J includes both Ithaca College and Harvard University), we
calculate the total enrollment, audit enrollment, honor enrollment, verified enrollment, total
gross revenue, gross revenue over the minimum, and total dollars refunded.
"""
def rows(self):
for course_id in course_ids_between(self.start_word, self.end_word):
# If the first letter of the university is between start_word and end_word, then we include
# it in the report. These comparisons are unicode-safe.
cur_course = get_course_by_id(course_id)
university = cur_course.org
course = cur_course.number + " " + cur_course.display_name_with_default # TODO add term (i.e. Fall 2013)?
counts = CourseEnrollment.objects.enrollment_counts(course_id)
total_enrolled = counts['total']
audit_enrolled = counts['audit']
honor_enrolled = counts['honor']
if counts['verified'] == 0:
verified_enrolled = 0
gross_rev = Decimal(0.00)
gross_rev_over_min = Decimal(0.00)
else:
verified_enrolled = counts['verified']
gross_rev = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'purchased', 'unit_cost')
gross_rev_over_min = gross_rev - (CourseMode.min_course_price_for_verified_for_currency(course_id, 'usd') * verified_enrolled)
num_verified_over_the_minimum = CertificateItem.verified_certificates_contributing_more_than_minimum(course_id)
# should I be worried about is_active here?
number_of_refunds = CertificateItem.verified_certificates_count(course_id, 'refunded')
if number_of_refunds == 0:
dollars_refunded = Decimal(0.00)
else:
dollars_refunded = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'refunded', 'unit_cost')
course_announce_date = ""
course_reg_start_date = ""
course_reg_close_date = ""
registration_period = ""
yield [
university,
course,
course_announce_date,
course_reg_start_date,
course_reg_close_date,
registration_period,
total_enrolled,
audit_enrolled,
honor_enrolled,
verified_enrolled,
gross_rev,
gross_rev_over_min,
num_verified_over_the_minimum,
number_of_refunds,
dollars_refunded
]
def header(self):
return [
_("University"),
_("Course"),
_("Course Announce Date"),
_("Course Start Date"),
_("Course Registration Close Date"),
_("Course Registration Period"),
_("Total Enrolled"),
_("Audit Enrollment"),
_("Honor Code Enrollment"),
_("Verified Enrollment"),
_("Gross Revenue"),
_("Gross Revenue over the Minimum"),
_("Number of Verified Students Contributing More than the Minimum"),
_("Number of Refunds"),
_("Dollars Refunded"),
]
class UniversityRevenueShareReport(Report):
"""
Subclass of Report, used to generate University Revenue Share Reports for finance purposes.
For each course in each university whose name is within the range start_word and end_word,
inclusive, (i.e., the letter range H-J includes both Ithaca College and Harvard University), we calculate
the total revenue generated by that particular course. This includes the number of transactions,
total payments collected, service fees, number of refunds, and total amount of refunds.
"""
def rows(self):
for course_id in course_ids_between(self.start_word, self.end_word):
cur_course = get_course_by_id(course_id)
university = cur_course.org
course = cur_course.number + " " + cur_course.display_name_with_default
total_payments_collected = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'purchased', 'unit_cost')
service_fees = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'purchased', 'service_fee')
num_refunds = CertificateItem.verified_certificates_count(course_id, "refunded")
amount_refunds = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'refunded', 'unit_cost')
num_transactions = (num_refunds * 2) + CertificateItem.verified_certificates_count(course_id, "purchased")
yield [
university,
course,
num_transactions,
total_payments_collected,
service_fees,
num_refunds,
amount_refunds
]
def header(self):
return [
_("University"),
_("Course"),
_("Number of Transactions"),
_("Total Payments Collected"),
_("Service Fees (if any)"),
_("Number of Successful Refunds"),
_("Total Amount of Refunds"),
]
def course_ids_between(start_word, end_word):
"""
Returns a list of all valid course_ids that fall alphabetically between start_word and end_word.
These comparisons are unicode-safe.
"""
valid_courses = []
for course in modulestore().get_courses():
course_id = course.id.to_deprecated_string()
if start_word.lower() <= course_id.lower() <= end_word.lower():
valid_courses.append(course.id)
return valid_courses