Commit 22af2b75 by Awais Qureshi Committed by Will Daly

ECOM-890: Update invoice data model.

ECOM-891: Allow tracking of invoice transactions.

Authors: Awais Qureshi and Aamir Khan
parent 6fa7815f
...@@ -299,8 +299,14 @@ class DashboardTest(ModuleStoreTestCase): ...@@ -299,8 +299,14 @@ class DashboardTest(ModuleStoreTestCase):
recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A", recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A",
course_id=self.course.id, is_valid=False course_id=self.course.id, is_valid=False
) )
invoice_item = shoppingcart.models.CourseRegistrationCodeInvoiceItem.objects.create(
invoice=sale_invoice_1,
qty=1,
unit_price=1234.32,
course_id=self.course.id
)
course_reg_code = shoppingcart.models.CourseRegistrationCode( course_reg_code = shoppingcart.models.CourseRegistrationCode(
code="abcde", course_id=self.course.id, created_by=self.user, invoice=sale_invoice_1, mode_slug='honor' code="abcde", course_id=self.course.id, created_by=self.user, invoice=sale_invoice_1, invoice_item=invoice_item, mode_slug='honor'
) )
course_reg_code.save() course_reg_code.save()
......
...@@ -465,8 +465,8 @@ def is_course_blocked(request, redeemed_registration_codes, course_key): ...@@ -465,8 +465,8 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
# registration codes may be generated via Bulk Purchase Scenario # registration codes may be generated via Bulk Purchase Scenario
# we have to check only for the invoice generated registration codes # we have to check only for the invoice generated registration codes
# that their invoice is valid or not # that their invoice is valid or not
if redeemed_registration.invoice: if redeemed_registration.invoice_item:
if not getattr(redeemed_registration.invoice, 'is_valid'): if not getattr(redeemed_registration.invoice_item.invoice, 'is_valid'):
blocked = True blocked = True
# disabling email notifications for unpaid registration courses # disabling email notifications for unpaid registration courses
Optout.objects.get_or_create(user=request.user, course_id=course_key) Optout.objects.get_or_create(user=request.user, course_id=course_key)
......
...@@ -27,6 +27,7 @@ import string # pylint: disable=deprecated-module ...@@ -27,6 +27,7 @@ import string # pylint: disable=deprecated-module
import random import random
import unicodecsv import unicodecsv
import urllib import urllib
import decimal
from student import auth from student import auth
from student.roles import CourseSalesAdminRole from student.roles import CourseSalesAdminRole
from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator
...@@ -49,7 +50,14 @@ from django_comment_common.models import ( ...@@ -49,7 +50,14 @@ from django_comment_common.models import (
) )
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
from courseware.models import StudentModule from courseware.models import StudentModule
from shoppingcart.models import Coupon, CourseRegistrationCode, RegistrationCodeRedemption, Invoice, CourseMode from shoppingcart.models import (
Coupon,
CourseRegistrationCode,
RegistrationCodeRedemption,
Invoice,
CourseMode,
CourseRegistrationCodeInvoiceItem,
)
from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user
import instructor_task.api import instructor_task.api
from instructor_task.api_helper import AlreadyRunningError from instructor_task.api_helper import AlreadyRunningError
...@@ -885,9 +893,13 @@ def sale_validation(request, course_id): ...@@ -885,9 +893,13 @@ def sale_validation(request, course_id):
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try: try:
obj_invoice = Invoice.objects.select_related('is_valid').get(id=invoice_number, course_id=course_id) obj_invoice = CourseRegistrationCodeInvoiceItem.objects.select_related('invoice').get(
except Invoice.DoesNotExist: invoice_id=invoice_number,
return HttpResponseNotFound(_("Invoice number '{0}' does not exist.").format(invoice_number)) course_id=course_id
)
obj_invoice = obj_invoice.invoice
except CourseRegistrationCodeInvoiceItem.DoesNotExist: # Check for old type invoices
return HttpResponseNotFound(_("Invoice number '{0}' does not exist.".format(invoice_number)))
if event_type == "invalidate": if event_type == "invalidate":
return invalidate_invoice(obj_invoice) return invalidate_invoice(obj_invoice)
...@@ -1053,7 +1065,7 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument ...@@ -1053,7 +1065,7 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument
return instructor_analytics.csvs.create_csv_response('Coupons.csv', header, data_rows) return instructor_analytics.csvs.create_csv_response('Coupons.csv', header, data_rows)
def save_registration_code(user, course_id, mode_slug, invoice=None, order=None): def save_registration_code(user, course_id, mode_slug, invoice=None, order=None, invoice_item=None):
""" """
recursive function that generate a new code every time and saves in the Course Registration Table recursive function that generate a new code every time and saves in the Course Registration Table
if validation check passes if validation check passes
...@@ -1064,6 +1076,7 @@ def save_registration_code(user, course_id, mode_slug, invoice=None, order=None) ...@@ -1064,6 +1076,7 @@ def save_registration_code(user, course_id, mode_slug, invoice=None, order=None)
mode_slug (str): The Course Mode Slug associated with any enrollment made by these codes. mode_slug (str): The Course Mode Slug associated with any enrollment made by these codes.
invoice (Invoice): (Optional) The associated invoice for this code. invoice (Invoice): (Optional) The associated invoice for this code.
order (Order): (Optional) The associated order for this code. order (Order): (Optional) The associated order for this code.
invoice_item (CourseRegistrationCodeInvoiceItem) : (Optional) The associated CourseRegistrationCodeInvoiceItem
Returns: Returns:
The newly created CourseRegistrationCode. The newly created CourseRegistrationCode.
...@@ -1074,7 +1087,9 @@ def save_registration_code(user, course_id, mode_slug, invoice=None, order=None) ...@@ -1074,7 +1087,9 @@ def save_registration_code(user, course_id, mode_slug, invoice=None, order=None)
# check if the generated code is in the Coupon Table # check if the generated code is in the Coupon Table
matching_coupons = Coupon.objects.filter(code=code, is_active=True) matching_coupons = Coupon.objects.filter(code=code, is_active=True)
if matching_coupons: if matching_coupons:
return save_registration_code(user, course_id, invoice, order) return save_registration_code(
user, course_id, mode_slug, invoice=invoice, order=order, invoice_item=invoice_item
)
course_registration = CourseRegistrationCode( course_registration = CourseRegistrationCode(
code=code, code=code,
...@@ -1082,13 +1097,16 @@ def save_registration_code(user, course_id, mode_slug, invoice=None, order=None) ...@@ -1082,13 +1097,16 @@ def save_registration_code(user, course_id, mode_slug, invoice=None, order=None)
created_by=user, created_by=user,
invoice=invoice, invoice=invoice,
order=order, order=order,
mode_slug=mode_slug mode_slug=mode_slug,
invoice_item=invoice_item
) )
try: try:
course_registration.save() course_registration.save()
return course_registration return course_registration
except IntegrityError: except IntegrityError:
return save_registration_code(user, course_id, invoice, order) return save_registration_code(
user, course_id, mode_slug, invoice=invoice, order=order, invoice_item=invoice_item
)
def registration_codes_csv(file_name, codes_list, csv_type=None): def registration_codes_csv(file_name, codes_list, csv_type=None):
...@@ -1130,11 +1148,13 @@ def get_registration_codes(request, course_id): # pylint: disable=unused-argume ...@@ -1130,11 +1148,13 @@ def get_registration_codes(request, course_id): # pylint: disable=unused-argume
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
#filter all the course registration codes #filter all the course registration codes
registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id).order_by('invoice__company_name') registration_codes = CourseRegistrationCode.objects.filter(
course_id=course_id
).order_by('invoice_item__invoice__company_name')
company_name = request.POST['download_company_name'] company_name = request.POST['download_company_name']
if company_name: if company_name:
registration_codes = registration_codes.filter(invoice__company_name=company_name) registration_codes = registration_codes.filter(invoice_item__invoice__company_name=company_name)
csv_type = 'download' csv_type = 'download'
return registration_codes_csv("Registration_Codes.csv", registration_codes, csv_type) return registration_codes_csv("Registration_Codes.csv", registration_codes, csv_type)
...@@ -1160,7 +1180,21 @@ def generate_registration_codes(request, course_id): ...@@ -1160,7 +1180,21 @@ def generate_registration_codes(request, course_id):
company_name = request.POST['company_name'] company_name = request.POST['company_name']
company_contact_name = request.POST['company_contact_name'] company_contact_name = request.POST['company_contact_name']
company_contact_email = request.POST['company_contact_email'] company_contact_email = request.POST['company_contact_email']
sale_price = request.POST['sale_price'] unit_price = request.POST['unit_price']
try:
unit_price = (
decimal.Decimal(unit_price)
).quantize(
decimal.Decimal('.01'),
rounding=decimal.ROUND_DOWN
)
except decimal.InvalidOperation:
return HttpResponse(
status=400,
content=_(u"Could not parse amount as a decimal")
)
recipient_name = request.POST['recipient_name'] recipient_name = request.POST['recipient_name']
recipient_email = request.POST['recipient_email'] recipient_email = request.POST['recipient_email']
address_line_1 = request.POST['address_line_1'] address_line_1 = request.POST['address_line_1']
...@@ -1177,6 +1211,7 @@ def generate_registration_codes(request, course_id): ...@@ -1177,6 +1211,7 @@ def generate_registration_codes(request, course_id):
recipient_list.append(request.user.email) recipient_list.append(request.user.email)
invoice_copy = True invoice_copy = True
sale_price = unit_price * course_code_number
UserPreference.set_preference(request.user, INVOICE_KEY, invoice_copy) UserPreference.set_preference(request.user, INVOICE_KEY, invoice_copy)
sale_invoice = Invoice.objects.create( sale_invoice = Invoice.objects.create(
total_amount=sale_price, total_amount=sale_price,
...@@ -1197,6 +1232,13 @@ def generate_registration_codes(request, course_id): ...@@ -1197,6 +1232,13 @@ def generate_registration_codes(request, course_id):
customer_reference_number=customer_reference_number customer_reference_number=customer_reference_number
) )
invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
invoice=sale_invoice,
qty=course_code_number,
unit_price=unit_price,
course_id=course_id
)
course = get_course_by_id(course_id, depth=0) course = get_course_by_id(course_id, depth=0)
paid_modes = CourseMode.paid_modes_for_course(course_id) paid_modes = CourseMode.paid_modes_for_course(course_id)
...@@ -1217,7 +1259,7 @@ def generate_registration_codes(request, course_id): ...@@ -1217,7 +1259,7 @@ def generate_registration_codes(request, course_id):
registration_codes = [] registration_codes = []
for __ in range(course_code_number): # pylint: disable=redefined-outer-name for __ in range(course_code_number): # pylint: disable=redefined-outer-name
generated_registration_code = save_registration_code( generated_registration_code = save_registration_code(
request.user, course_id, course_mode.slug, sale_invoice, order=None request.user, course_id, course_mode.slug, invoice=sale_invoice, order=None, invoice_item=invoice_item
) )
registration_codes.append(generated_registration_code) registration_codes.append(generated_registration_code)
...@@ -1309,13 +1351,17 @@ def active_registration_codes(request, course_id): # pylint: disable=unused-arg ...@@ -1309,13 +1351,17 @@ def active_registration_codes(request, course_id): # pylint: disable=unused-arg
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
# find all the registration codes in this course # find all the registration codes in this course
registration_codes_list = CourseRegistrationCode.objects.filter(course_id=course_id).order_by('invoice__company_name') registration_codes_list = CourseRegistrationCode.objects.filter(
course_id=course_id
).order_by('invoice_item__invoice__company_name')
company_name = request.POST['active_company_name'] company_name = request.POST['active_company_name']
if company_name: if company_name:
registration_codes_list = registration_codes_list.filter(invoice__company_name=company_name) registration_codes_list = registration_codes_list.filter(invoice_item__invoice__company_name=company_name)
# find the redeemed registration codes if any exist in the db # find the redeemed registration codes if any exist in the db
code_redemption_set = RegistrationCodeRedemption.objects.select_related('registration_code').filter(registration_code__course_id=course_id) code_redemption_set = RegistrationCodeRedemption.objects.select_related(
'registration_code', 'registration_code__invoice_item__invoice'
).filter(registration_code__course_id=course_id)
if code_redemption_set.exists(): if code_redemption_set.exists():
redeemed_registration_codes = [code.registration_code.code for code in code_redemption_set] redeemed_registration_codes = [code.registration_code.code for code in code_redemption_set]
# exclude the redeemed registration codes from the registration codes list and you will get # exclude the redeemed registration codes from the registration codes list and you will get
...@@ -1346,11 +1392,11 @@ def spent_registration_codes(request, course_id): # pylint: disable=unused-argu ...@@ -1346,11 +1392,11 @@ def spent_registration_codes(request, course_id): # pylint: disable=unused-argu
# you will get a list of all the spent(Redeemed) Registration Codes # you will get a list of all the spent(Redeemed) Registration Codes
spent_codes_list = CourseRegistrationCode.objects.filter( spent_codes_list = CourseRegistrationCode.objects.filter(
course_id=course_id, code__in=redeemed_registration_codes course_id=course_id, code__in=redeemed_registration_codes
).order_by('invoice__company_name') ).order_by('invoice_item__invoice__company_name').select_related('invoice_item__invoice')
company_name = request.POST['spent_company_name'] company_name = request.POST['spent_company_name']
if company_name: if company_name:
spent_codes_list = spent_codes_list.filter(invoice__company_name=company_name) # pylint: disable=maybe-no-member spent_codes_list = spent_codes_list.filter(invoice_item__invoice__company_name=company_name) # pylint: disable=maybe-no-member
csv_type = 'spent' csv_type = 'spent'
return registration_codes_csv("Spent_Registration_Codes.csv", spent_codes_list, csv_type) return registration_codes_csv("Spent_Registration_Codes.csv", spent_codes_list, csv_type)
......
...@@ -6,7 +6,7 @@ Serve miscellaneous course and student data ...@@ -6,7 +6,7 @@ Serve miscellaneous course and student data
import json import json
from shoppingcart.models import ( from shoppingcart.models import (
PaidCourseRegistration, CouponRedemption, Invoice, CourseRegCodeItem, PaidCourseRegistration, CouponRedemption, Invoice, CourseRegCodeItem,
OrderTypes, RegistrationCodeRedemption, CourseRegistrationCode OrderTypes, RegistrationCodeRedemption, CourseRegistrationCode, CourseRegistrationCodeInvoiceItem
) )
from django.db.models import Q from django.db.models import Q
from django.conf import settings from django.conf import settings
...@@ -110,22 +110,25 @@ def sale_record_features(course_id, features): ...@@ -110,22 +110,25 @@ def sale_record_features(course_id, features):
{'company_name': 'group_C', 'total_codes': '3', total_amount:'total_amount3 in decimal'.} {'company_name': 'group_C', 'total_codes': '3', total_amount:'total_amount3 in decimal'.}
] ]
""" """
sales = Invoice.objects.filter(course_id=course_id) sales = CourseRegistrationCodeInvoiceItem.objects.select_related('invoice').filter(course_id=course_id)
def sale_records_info(sale, features): def sale_records_info(sale, features):
""" convert sales records to dictionary """ """
Convert sales records to dictionary
"""
invoice = sale.invoice
sale_features = [x for x in SALE_FEATURES if x in features] sale_features = [x for x in SALE_FEATURES if x in features]
course_reg_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features] course_reg_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features]
# Extracting sale information # Extracting sale information
sale_dict = dict((feature, getattr(sale, feature)) sale_dict = dict((feature, getattr(invoice, feature))
for feature in sale_features) for feature in sale_features)
total_used_codes = RegistrationCodeRedemption.objects.filter( total_used_codes = RegistrationCodeRedemption.objects.filter(
registration_code__in=sale.courseregistrationcode_set.all() registration_code__in=sale.courseregistrationcode_set.all()
).count() ).count()
sale_dict.update({"invoice_number": getattr(sale, 'id')}) sale_dict.update({"invoice_number": getattr(invoice, 'id')})
sale_dict.update({"total_codes": sale.courseregistrationcode_set.all().count()}) sale_dict.update({"total_codes": sale.courseregistrationcode_set.all().count()})
sale_dict.update({'total_used_codes': total_used_codes}) sale_dict.update({'total_used_codes': total_used_codes})
...@@ -261,11 +264,11 @@ def course_registration_features(features, registration_codes, csv_type): ...@@ -261,11 +264,11 @@ def course_registration_features(features, registration_codes, csv_type):
course_registration_dict = dict((feature, getattr(registration_code, feature)) for feature in registration_features) course_registration_dict = dict((feature, getattr(registration_code, feature)) for feature in registration_features)
course_registration_dict['company_name'] = None course_registration_dict['company_name'] = None
if registration_code.invoice: if registration_code.invoice_item:
course_registration_dict['company_name'] = getattr(registration_code.invoice, 'company_name') course_registration_dict['company_name'] = getattr(registration_code.invoice_item.invoice, 'company_name')
course_registration_dict['redeemed_by'] = None course_registration_dict['redeemed_by'] = None
if registration_code.invoice: if registration_code.invoice_item:
sale_invoice = Invoice.objects.get(id=registration_code.invoice_id) sale_invoice = registration_code.invoice_item.invoice
course_registration_dict['invoice_id'] = sale_invoice.id course_registration_dict['invoice_id'] = sale_invoice.id
course_registration_dict['purchaser'] = sale_invoice.recipient_name course_registration_dict['purchaser'] = sale_invoice.recipient_name
course_registration_dict['customer_reference_number'] = sale_invoice.customer_reference_number course_registration_dict['customer_reference_number'] = sale_invoice.customer_reference_number
......
...@@ -11,7 +11,7 @@ from student.tests.factories import UserFactory, CourseModeFactory ...@@ -11,7 +11,7 @@ from student.tests.factories import UserFactory, CourseModeFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.models import ( from shoppingcart.models import (
CourseRegistrationCode, RegistrationCodeRedemption, Order, CourseRegistrationCode, RegistrationCodeRedemption, Order,
Invoice, Coupon, CourseRegCodeItem, CouponRedemption Invoice, Coupon, CourseRegCodeItem, CouponRedemption, CourseRegistrationCodeInvoiceItem
) )
from course_modes.models import CourseMode from course_modes.models import CourseMode
from instructor_analytics.basic import ( from instructor_analytics.basic import (
...@@ -147,10 +147,16 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): ...@@ -147,10 +147,16 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
company_contact_email='test@company.com', recipient_name='Testw_1', recipient_email='test2@test.com', company_contact_email='test@company.com', recipient_name='Testw_1', recipient_email='test2@test.com',
customer_reference_number='2Fwe23S', internal_reference="ABC", course_id=self.course.id customer_reference_number='2Fwe23S', internal_reference="ABC", course_id=self.course.id
) )
invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
invoice=sale_invoice,
qty=1,
unit_price=1234.32,
course_id=self.course.id
)
for i in range(5): for i in range(5):
course_code = CourseRegistrationCode( course_code = CourseRegistrationCode(
code="test_code{}".format(i), course_id=self.course.id.to_deprecated_string(), code="test_code{}".format(i), course_id=self.course.id.to_deprecated_string(),
created_by=self.instructor, invoice=sale_invoice, mode_slug='honor' created_by=self.instructor, invoice=sale_invoice, invoice_item=invoice_item, mode_slug='honor'
) )
course_code.save() course_code.save()
...@@ -272,7 +278,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): ...@@ -272,7 +278,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
kwargs={'course_id': self.course.id.to_deprecated_string()}) kwargs={'course_id': self.course.id.to_deprecated_string()})
data = { data = {
'total_registration_codes': 12, 'company_name': 'Test Group', 'sale_price': 122.45, 'total_registration_codes': 12, 'company_name': 'Test Group', 'unit_price': 122.45,
'company_contact_name': 'TestName', 'company_contact_email': 'test@company.com', 'recipient_name': 'Test123', 'company_contact_name': 'TestName', 'company_contact_email': 'test@company.com', 'recipient_name': 'Test123',
'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street', 'address_line_2': '', 'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street', 'address_line_2': '',
'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '', 'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '',
...@@ -306,11 +312,17 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): ...@@ -306,11 +312,17 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
) )
self.assertIn( self.assertIn(
course_registration['company_name'], course_registration['company_name'],
[getattr(registration_code.invoice, 'company_name') for registration_code in registration_codes] [
getattr(registration_code.invoice_item.invoice, 'company_name')
for registration_code in registration_codes
]
) )
self.assertIn( self.assertIn(
course_registration['invoice_id'], course_registration['invoice_id'],
[registration_code.invoice_id for registration_code in registration_codes] [
registration_code.invoice_item.invoice_id
for registration_code in registration_codes
]
) )
def test_coupon_codes_features(self): def test_coupon_codes_features(self):
......
""" """Django admin interface for the shopping cart models. """
Allows django admin site to add PaidCourseRegistrationAnnotations
"""
from ratelimitbackend import admin from ratelimitbackend import admin
from shoppingcart.models import ( from shoppingcart.models import (
PaidCourseRegistrationAnnotation, Coupon, DonationConfiguration PaidCourseRegistrationAnnotation,
Coupon,
DonationConfiguration,
Invoice,
CourseRegistrationCodeInvoiceItem,
InvoiceTransaction
) )
...@@ -49,6 +52,128 @@ class SoftDeleteCouponAdmin(admin.ModelAdmin): ...@@ -49,6 +52,128 @@ class SoftDeleteCouponAdmin(admin.ModelAdmin):
really_delete_selected.short_description = "Delete s selected entries" really_delete_selected.short_description = "Delete s selected entries"
class CourseRegistrationCodeInvoiceItemInline(admin.StackedInline):
"""Admin for course registration code invoice items.
Displayed inline within the invoice admin UI.
"""
model = CourseRegistrationCodeInvoiceItem
extra = 0
can_delete = False
readonly_fields = (
'qty',
'unit_price',
'currency',
'course_id',
)
def has_add_permission(self, request):
return False
class InvoiceTransactionInline(admin.StackedInline):
"""Admin for invoice transactions.
Displayed inline within the invoice admin UI.
"""
model = InvoiceTransaction
extra = 0
readonly_fields = (
'created',
'modified',
'created_by',
'last_modified_by'
)
class InvoiceAdmin(admin.ModelAdmin):
"""Admin for invoices.
This is intended for the internal finance team
to be able to view and update invoice information,
including payments and refunds.
"""
date_hierarchy = 'created'
can_delete = False
readonly_fields = ('created', 'modified')
search_fields = (
'internal_reference',
'customer_reference_number',
'company_name',
)
fieldsets = (
(
None, {
'fields': (
'internal_reference',
'customer_reference_number',
'created',
'modified',
)
}
),
(
'Billing Information', {
'fields': (
'company_name',
'company_contact_name',
'company_contact_email',
'recipient_name',
'recipient_email',
'address_line_1',
'address_line_2',
'address_line_3',
'city',
'state',
'zip',
'country'
)
}
)
)
readonly_fields = (
'internal_reference',
'customer_reference_number',
'created',
'modified',
'company_name',
'company_contact_name',
'company_contact_email',
'recipient_name',
'recipient_email',
'address_line_1',
'address_line_2',
'address_line_3',
'city',
'state',
'zip',
'country'
)
inlines = [
CourseRegistrationCodeInvoiceItemInline,
InvoiceTransactionInline
]
def save_formset(self, request, form, formset, change):
"""Save the user who created and modified invoice transactions. """
instances = formset.save(commit=False)
for instance in instances:
if isinstance(instance, InvoiceTransaction):
if not hasattr(instance, 'created_by'):
instance.created_by = request.user
instance.last_modified_by = request.user
instance.save()
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
admin.site.register(PaidCourseRegistrationAnnotation) admin.site.register(PaidCourseRegistrationAnnotation)
admin.site.register(Coupon, SoftDeleteCouponAdmin) admin.site.register(Coupon, SoftDeleteCouponAdmin)
admin.site.register(DonationConfiguration) admin.site.register(DonationConfiguration)
admin.site.register(Invoice, InvoiceAdmin)
...@@ -18,7 +18,7 @@ from django.conf import settings ...@@ -18,7 +18,7 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail from django.core.mail import send_mail
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _, ugettext_lazy
from django.db import transaction from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -773,11 +773,11 @@ class OrderItem(TimeStampedModel): ...@@ -773,11 +773,11 @@ class OrderItem(TimeStampedModel):
self.save() self.save()
class Invoice(models.Model): class Invoice(TimeStampedModel):
""" """
This table capture all the information needed to support "invoicing" This table capture all the information needed to support "invoicing"
which is when a user wants to purchase Registration Codes, which is when a user wants to purchase Registration Codes,
but will not do so via a Credit Card transaction. but will not do so via a Credit Card transaction.
""" """
company_name = models.CharField(max_length=255, db_index=True) company_name = models.CharField(max_length=255, db_index=True)
company_contact_name = models.CharField(max_length=255) company_contact_name = models.CharField(max_length=255)
...@@ -785,16 +785,39 @@ class Invoice(models.Model): ...@@ -785,16 +785,39 @@ class Invoice(models.Model):
recipient_name = models.CharField(max_length=255) recipient_name = models.CharField(max_length=255)
recipient_email = models.CharField(max_length=255) recipient_email = models.CharField(max_length=255)
address_line_1 = models.CharField(max_length=255) address_line_1 = models.CharField(max_length=255)
address_line_2 = models.CharField(max_length=255, null=True) address_line_2 = models.CharField(max_length=255, null=True, blank=True)
address_line_3 = models.CharField(max_length=255, null=True) address_line_3 = models.CharField(max_length=255, null=True, blank=True)
city = models.CharField(max_length=255, null=True) city = models.CharField(max_length=255, null=True)
state = models.CharField(max_length=255, null=True) state = models.CharField(max_length=255, null=True)
zip = models.CharField(max_length=15, null=True) zip = models.CharField(max_length=15, null=True)
country = models.CharField(max_length=64, null=True) country = models.CharField(max_length=64, null=True)
course_id = CourseKeyField(max_length=255, db_index=True)
# This field has been deprecated.
# The total amount can now be calculated as the sum
# of each invoice item associated with the invoice.
# For backwards compatibility, this field is maintained
# and written to during invoice creation.
total_amount = models.FloatField() total_amount = models.FloatField()
internal_reference = models.CharField(max_length=255, null=True)
customer_reference_number = models.CharField(max_length=63, null=True) # This field has been deprecated in order to support
# invoices for items that are not course-related.
# Although this field is still maintained for backwards
# compatibility, you should use CourseRegistrationCodeInvoiceItem
# to look up the course ID for purchased redeem codes.
course_id = CourseKeyField(max_length=255, db_index=True)
internal_reference = models.CharField(
max_length=255,
null=True,
blank=True,
help_text=ugettext_lazy("Internal reference code for this invoice.")
)
customer_reference_number = models.CharField(
max_length=63,
null=True,
blank=True,
help_text=ugettext_lazy("Customer's reference code for this invoice.")
)
is_valid = models.BooleanField(default=True) is_valid = models.BooleanField(default=True)
def generate_pdf_invoice(self, course, course_price, quantity, sale_price): def generate_pdf_invoice(self, course, course_price, quantity, sale_price):
...@@ -824,6 +847,125 @@ class Invoice(models.Model): ...@@ -824,6 +847,125 @@ class Invoice(models.Model):
return pdf_buffer return pdf_buffer
def __unicode__(self):
label = (
unicode(self.internal_reference)
if self.internal_reference
else u"No label"
)
created = (
self.created.strftime("%Y-%m-%d") # pylint: disable=no-member
if self.created
else u"No date"
)
return u"{label} ({date_created})".format(
label=label, date_created=created
)
INVOICE_TRANSACTION_STATUSES = (
# A payment/refund is in process, but money has not yet been transferred
('started', 'started'),
# A payment/refund has completed successfully
# This should be set ONLY once money has been successfully exchanged.
('completed', 'completed'),
# A payment/refund was promised, but was cancelled before
# money had been transferred. An example would be
# cancelling a refund check before the recipient has
# a chance to deposit it.
('cancelled', 'cancelled')
)
class InvoiceTransaction(TimeStampedModel):
"""Record payment and refund information for invoices.
There are two expected use cases:
1) We send an invoice to someone, and they send us a check.
We then manually create an invoice transaction to represent
the payment.
2) We send an invoice to someone, and they pay us. Later, we
need to issue a refund for the payment. We manually
create a transaction with a negative amount to represent
the refund.
"""
invoice = models.ForeignKey(Invoice)
amount = models.DecimalField(
default=0.0, decimal_places=2, max_digits=30,
help_text=ugettext_lazy(
"The amount of the transaction. Use positive amounts for payments"
" and negative amounts for refunds."
)
)
currency = models.CharField(
default="usd",
max_length=8,
help_text=ugettext_lazy("Lower-case ISO currency codes")
)
comments = models.TextField(
null=True,
blank=True,
help_text=ugettext_lazy("Optional: provide additional information for this transaction")
)
status = models.CharField(
max_length=32,
default='started',
choices=INVOICE_TRANSACTION_STATUSES,
help_text=ugettext_lazy(
"The status of the payment or refund. "
"'started' means that payment is expected, but money has not yet been transferred. "
"'completed' means that the payment or refund was received. "
"'cancelled' means that payment or refund was expected, but was cancelled before money was transferred. "
)
)
created_by = models.ForeignKey(User)
last_modified_by = models.ForeignKey(User, related_name='last_modified_by_user')
class InvoiceItem(TimeStampedModel):
"""
This is the basic interface for invoice items.
Each invoice item represents a "line" in the invoice.
For example, in an invoice for course registration codes,
there might be an invoice item representing 10 registration
codes for the DemoX course.
"""
objects = InheritanceManager()
invoice = models.ForeignKey(Invoice, db_index=True)
qty = models.IntegerField(
default=1,
help_text=ugettext_lazy("The number of items sold.")
)
unit_price = models.DecimalField(
default=0.0,
decimal_places=2,
max_digits=30,
help_text=ugettext_lazy("The price per item sold, including discounts.")
)
currency = models.CharField(
default="usd",
max_length=8,
help_text=ugettext_lazy("Lower-case ISO currency codes")
)
class CourseRegistrationCodeInvoiceItem(InvoiceItem):
"""
This is an invoice item that represents a payment for
a course registration.
"""
course_id = CourseKeyField(max_length=128, db_index=True)
class CourseRegistrationCode(models.Model): class CourseRegistrationCode(models.Model):
""" """
...@@ -835,9 +977,14 @@ class CourseRegistrationCode(models.Model): ...@@ -835,9 +977,14 @@ class CourseRegistrationCode(models.Model):
created_by = models.ForeignKey(User, related_name='created_by_user') created_by = models.ForeignKey(User, related_name='created_by_user')
created_at = models.DateTimeField(default=datetime.now(pytz.utc)) created_at = models.DateTimeField(default=datetime.now(pytz.utc))
order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order") order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order")
invoice = models.ForeignKey(Invoice, null=True)
mode_slug = models.CharField(max_length=100, null=True) mode_slug = models.CharField(max_length=100, null=True)
# For backwards compatibility, we maintain the FK to "invoice"
# In the future, we will remove this in favor of the FK
# to "invoice_item" (which can be used to look up the invoice).
invoice = models.ForeignKey(Invoice, null=True)
invoice_item = models.ForeignKey(CourseRegistrationCodeInvoiceItem, null=True)
class RegistrationCodeRedemption(models.Model): class RegistrationCodeRedemption(models.Model):
""" """
...@@ -1227,7 +1374,7 @@ class CourseRegCodeItem(OrderItem): ...@@ -1227,7 +1374,7 @@ class CourseRegCodeItem(OrderItem):
# is in another PR (for another feature) # is in another PR (for another feature)
from instructor.views.api import save_registration_code from instructor.views.api import save_registration_code
for i in range(total_registration_codes): # pylint: disable=unused-variable for i in range(total_registration_codes): # pylint: disable=unused-variable
save_registration_code(self.user, self.course_id, self.mode, invoice=None, order=self.order) save_registration_code(self.user, self.course_id, self.mode, order=self.order)
log.info("Enrolled {0} in paid course {1}, paid ${2}" log.info("Enrolled {0} in paid course {1}, paid ${2}"
.format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=no-member .format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=no-member
......
...@@ -1518,7 +1518,7 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): ...@@ -1518,7 +1518,7 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
data = { data = {
'total_registration_codes': 12, 'company_name': 'Test Group', 'company_contact_name': 'Test@company.com', 'total_registration_codes': 12, 'company_name': 'Test Group', 'company_contact_name': 'Test@company.com',
'company_contact_email': 'Test@company.com', 'sale_price': 122.45, 'recipient_name': 'Test123', 'company_contact_email': 'Test@company.com', 'unit_price': 122.45, 'recipient_name': 'Test123',
'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street', 'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street',
'address_line_2': '', 'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '', 'address_line_2': '', 'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '',
'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': '' 'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': ''
......
...@@ -1664,7 +1664,7 @@ input[name="subject"] { ...@@ -1664,7 +1664,7 @@ input[name="subject"] {
height: auto; height: auto;
} }
} }
li#generate-registration-modal-field-country ~ li#generate-registration-modal-field-total-price, li#generate-registration-modal-field-country ~ li#generate-registration-modal-field-unit-price,
li#generate-registration-modal-field-country ~ li#generate-registration-modal-field-internal-reference { li#generate-registration-modal-field-country ~ li#generate-registration-modal-field-internal-reference {
@include margin-left(0px !important); @include margin-left(0px !important);
@include margin-right(15px !important); @include margin-right(15px !important);
......
...@@ -296,7 +296,7 @@ ...@@ -296,7 +296,7 @@
var total_registration_codes = $('input[name="total_registration_codes"]').val(); var total_registration_codes = $('input[name="total_registration_codes"]').val();
var recipient_name = $('input[name="recipient_name"]').val(); var recipient_name = $('input[name="recipient_name"]').val();
var recipient_email = $('input[name="recipient_email"]').val(); var recipient_email = $('input[name="recipient_email"]').val();
var sale_price = $('input[name="sale_price"]').val(); var unit_price = $('input[name="unit_price"]').val();
var company_name = $('input[name="company_name"]').val(); var company_name = $('input[name="company_name"]').val();
var company_contact_name = $('input[name="company_contact_name"]').val(); var company_contact_name = $('input[name="company_contact_name"]').val();
var company_contact_email = $('input[name="company_contact_email"]').val(); var company_contact_email = $('input[name="company_contact_email"]').val();
...@@ -305,91 +305,91 @@ ...@@ -305,91 +305,91 @@
if (company_name == '') { if (company_name == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the company name'); registration_code_error.text("${_('Please enter the company name')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (($.isNumeric(company_name))) { if (($.isNumeric(company_name))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the non-numeric value for company name'); registration_code_error.text("${_('Please enter the non-numeric value for company name')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (company_contact_name == '') { if (company_contact_name == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the company contact name'); registration_code_error.text("${_('Please enter the company contact name')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (($.isNumeric(company_contact_name))) { if (($.isNumeric(company_contact_name))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the non-numeric value for company contact name'); registration_code_error.text("${_('Please enter the non-numeric value for company contact name')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (company_contact_email == '') { if (company_contact_email == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the company contact email'); registration_code_error.text("${_('Please enter the company contact email')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (!(validateEmail(company_contact_email))) { if (!(validateEmail(company_contact_email))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the valid email address'); registration_code_error.text("${_('Please enter the valid email address')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (recipient_name == '') { if (recipient_name == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the recipient name'); registration_code_error.text("${_('Please enter the recipient name')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (($.isNumeric(recipient_name))) { if (($.isNumeric(recipient_name))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the non-numeric value for recipient name'); registration_code_error.text("${_('Please enter the non-numeric value for recipient name')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (recipient_email == '') { if (recipient_email == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the recipient email'); registration_code_error.text("${_('Please enter the recipient email')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (!(validateEmail(recipient_email))) { if (!(validateEmail(recipient_email))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the valid email address'); registration_code_error.text("${_('Please enter the valid email address')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (address_line == '') { if (address_line == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the billing address'); registration_code_error.text("${_('Please enter the billing address')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
if (sale_price == '') { if (unit_price == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the sale price'); registration_code_error.text("${_('Please enter the unit price')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false return false
} }
if (!($.isNumeric(sale_price))) { if (!($.isNumeric(unit_price))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the numeric value for sale price'); registration_code_error.text("${_('Please enter the numeric value for unit price')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false return false
} }
if (total_registration_codes == '') { if (total_registration_codes == '') {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the total registration codes'); registration_code_error.text("${_('Please enter the number of enrollment codes')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false return false
} }
if (!($.isNumeric(total_registration_codes))) { if (!($.isNumeric(total_registration_codes))) {
registration_code_error.attr('style', 'display: block !important'); registration_code_error.attr('style', 'display: block !important');
registration_code_error.text('Please enter the numeric value for total registration codes'); registration_code_error.text("${_('Please enter the numeric value for number of enrollment codes')}");
generate_registration_button.removeAttr('disabled'); generate_registration_button.removeAttr('disabled');
return false; return false;
} }
...@@ -600,7 +600,7 @@ ...@@ -600,7 +600,7 @@
$('input[name="country"]').val(''); $('input[name="country"]').val('');
$('input[name="customer_reference_number"]').val(''); $('input[name="customer_reference_number"]').val('');
$('input[name="recipient_name"]').val(''); $('input[name="recipient_name"]').val('');
$('input[name="sale_price"]').val(''); $('input[name="unit_price"]').val('');
$('input[name="recipient_email"]').val(''); $('input[name="recipient_email"]').val('');
$('input[name="company_contact_name"]').val(''); $('input[name="company_contact_name"]').val('');
$('input[name="company_contact_email"]').val(''); $('input[name="company_contact_email"]').val('');
......
...@@ -102,12 +102,12 @@ ...@@ -102,12 +102,12 @@
</span> </span>
</li> </li>
<div class="clearfix"></div> <div class="clearfix"></div>
<li class="field required text" id="generate-registration-modal-field-total-price"> <li class="field required text" id="generate-registration-modal-field-unit-price">
<label for="id_sale_price" class="required text">${_("Sale Price")}</label> <label for="id_unit_price" class="required text">${_("Unit Price")}</label>
<input class="field required" id="id_sale_price" type="text" name="sale_price" <input class="field required" id="id_unit_price" type="text" name="unit_price"
aria-required="true" /> aria-required="true" />
<span class="tip-text"> <span class="tip-text">
${_("The total price for all enrollments purchased")} ${_("The price per enrollment purchased")}
</span> </span>
</li> </li>
<li class="field required text" id="generate-registration-modal-field-total-codes"> <li class="field required text" id="generate-registration-modal-field-total-codes">
......
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