Add InvoiceHistory model to record changes to invoices, invoice items, and invoice transactions.
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'InvoiceHistory'
|
||||
db.create_table('shoppingcart_invoicehistory', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
|
||||
('invoice', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['shoppingcart.Invoice'])),
|
||||
('snapshot', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
))
|
||||
db.send_create_signal('shoppingcart', ['InvoiceHistory'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'InvoiceHistory'
|
||||
db.delete_table('shoppingcart_invoicehistory')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'shoppingcart.certificateitem': {
|
||||
'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.coupon': {
|
||||
'Meta': {'object_name': 'Coupon'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 2, 8, 0, 0)'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'expiration_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'shoppingcart.couponredemption': {
|
||||
'Meta': {'object_name': 'CouponRedemption'},
|
||||
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.courseregcodeitem': {
|
||||
'Meta': {'object_name': 'CourseRegCodeItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.courseregcodeitemannotation': {
|
||||
'Meta': {'object_name': 'CourseRegCodeItemAnnotation'},
|
||||
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.courseregistrationcode': {
|
||||
'Meta': {'object_name': 'CourseRegistrationCode'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 2, 8, 0, 0)'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']", 'null': 'True'}),
|
||||
'invoice_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCodeInvoiceItem']", 'null': 'True'}),
|
||||
'mode_slug': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_order'", 'null': 'True', 'to': "orm['shoppingcart.Order']"})
|
||||
},
|
||||
'shoppingcart.courseregistrationcodeinvoiceitem': {
|
||||
'Meta': {'object_name': 'CourseRegistrationCodeInvoiceItem', '_ormbases': ['shoppingcart.InvoiceItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'invoiceitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.InvoiceItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.donation': {
|
||||
'Meta': {'object_name': 'Donation', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'donation_type': ('django.db.models.fields.CharField', [], {'default': "'general'", 'max_length': '32'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.donationconfiguration': {
|
||||
'Meta': {'object_name': 'DonationConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.invoice': {
|
||||
'Meta': {'object_name': 'Invoice'},
|
||||
'address_line_1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'address_line_2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'total_amount': ('django.db.models.fields.FloatField', [], {}),
|
||||
'zip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'})
|
||||
},
|
||||
'shoppingcart.invoicehistory': {
|
||||
'Meta': {'object_name': 'InvoiceHistory'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']"}),
|
||||
'snapshot': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'})
|
||||
},
|
||||
'shoppingcart.invoiceitem': {
|
||||
'Meta': {'object_name': 'InvoiceItem'},
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']"}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
|
||||
'unit_price': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'})
|
||||
},
|
||||
'shoppingcart.invoicetransaction': {
|
||||
'Meta': {'object_name': 'InvoiceTransaction'},
|
||||
'amount': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']"}),
|
||||
'last_modified_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_modified_by_user'", 'to': "orm['auth.User']"}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'started'", 'max_length': '32'})
|
||||
},
|
||||
'shoppingcart.order': {
|
||||
'Meta': {'object_name': 'Order'},
|
||||
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
|
||||
'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order_type': ('django.db.models.fields.CharField', [], {'default': "'personal'", 'max_length': '32'}),
|
||||
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.orderitem': {
|
||||
'Meta': {'object_name': 'OrderItem'},
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
|
||||
'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
|
||||
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.paidcourseregistration': {
|
||||
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.paidcourseregistrationannotation': {
|
||||
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
|
||||
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.registrationcoderedemption': {
|
||||
'Meta': {'object_name': 'RegistrationCodeRedemption'},
|
||||
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']", 'null': 'True'}),
|
||||
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 2, 8, 0, 0)', 'null': 'True'}),
|
||||
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'registration_code': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCode']"})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['shoppingcart']
|
||||
@@ -4,6 +4,7 @@ from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
import json
|
||||
import analytics
|
||||
from io import BytesIO
|
||||
import pytz
|
||||
@@ -21,6 +22,8 @@ from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from model_utils.managers import InheritanceManager
|
||||
from model_utils.models import TimeStampedModel
|
||||
@@ -846,6 +849,48 @@ class Invoice(TimeStampedModel):
|
||||
|
||||
return pdf_buffer
|
||||
|
||||
def snapshot(self):
|
||||
"""Create a snapshot of the invoice.
|
||||
|
||||
A snapshot is a JSON-serializable representation
|
||||
of the invoice's state, including its line items
|
||||
and associated transactions (payments/refunds).
|
||||
|
||||
This is useful for saving the history of changes
|
||||
to the invoice.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
return {
|
||||
'internal_reference': self.internal_reference,
|
||||
'customer_reference': self.customer_reference_number,
|
||||
'is_valid': self.is_valid,
|
||||
'contact_info': {
|
||||
'company_name': self.company_name,
|
||||
'company_contact_name': self.company_contact_name,
|
||||
'company_contact_email': self.company_contact_email,
|
||||
'recipient_name': self.recipient_name,
|
||||
'recipient_email': self.recipient_email,
|
||||
'address_line_1': self.address_line_1,
|
||||
'address_line_2': self.address_line_2,
|
||||
'address_line_3': self.address_line_3,
|
||||
'city': self.city,
|
||||
'state': self.state,
|
||||
'zip': self.zip,
|
||||
'country': self.country,
|
||||
},
|
||||
'items': [
|
||||
item.snapshot()
|
||||
for item in InvoiceItem.objects.filter(invoice=self).select_subclasses()
|
||||
],
|
||||
'transactions': [
|
||||
trans.snapshot()
|
||||
for trans in InvoiceTransaction.objects.filter(invoice=self)
|
||||
],
|
||||
}
|
||||
|
||||
def __unicode__(self):
|
||||
label = (
|
||||
unicode(self.internal_reference)
|
||||
@@ -927,6 +972,24 @@ class InvoiceTransaction(TimeStampedModel):
|
||||
created_by = models.ForeignKey(User)
|
||||
last_modified_by = models.ForeignKey(User, related_name='last_modified_by_user')
|
||||
|
||||
def snapshot(self):
|
||||
"""Create a snapshot of the invoice transaction.
|
||||
|
||||
The returned dictionary is JSON-serializable.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
return {
|
||||
'amount': unicode(self.amount),
|
||||
'currency': self.currency,
|
||||
'comments': self.comments,
|
||||
'status': self.status,
|
||||
'created_by': self.created_by.username, # pylint: disable=no-member
|
||||
'last_modified_by': self.last_modified_by.username # pylint: disable=no-member
|
||||
}
|
||||
|
||||
|
||||
class InvoiceItem(TimeStampedModel):
|
||||
"""
|
||||
@@ -956,6 +1019,21 @@ class InvoiceItem(TimeStampedModel):
|
||||
help_text=ugettext_lazy("Lower-case ISO currency codes")
|
||||
)
|
||||
|
||||
def snapshot(self):
|
||||
"""Create a snapshot of the invoice item.
|
||||
|
||||
The returned dictionary is JSON-serializable.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
return {
|
||||
'qty': self.qty,
|
||||
'unit_price': unicode(self.unit_price),
|
||||
'currency': self.currency
|
||||
}
|
||||
|
||||
|
||||
class CourseRegistrationCodeInvoiceItem(InvoiceItem):
|
||||
"""
|
||||
@@ -965,6 +1043,89 @@ class CourseRegistrationCodeInvoiceItem(InvoiceItem):
|
||||
"""
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
|
||||
def snapshot(self):
|
||||
"""Create a snapshot of the invoice item.
|
||||
|
||||
This is the same as a snapshot for other invoice items,
|
||||
with the addition of a `course_id` field.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
snapshot = super(CourseRegistrationCodeInvoiceItem, self).snapshot()
|
||||
snapshot['course_id'] = unicode(self.course_id)
|
||||
return snapshot
|
||||
|
||||
|
||||
class InvoiceHistory(models.Model):
|
||||
"""History of changes to invoices.
|
||||
|
||||
This table stores snapshots of invoice state,
|
||||
including the associated line items and transactions
|
||||
(payments/refunds).
|
||||
|
||||
Entries in the table are created, but never deleted
|
||||
or modified.
|
||||
|
||||
We use Django signals to save history entries on change
|
||||
events. These signals are fired within a database
|
||||
transaction, so the history record is created only
|
||||
if the invoice change is successfully persisted.
|
||||
|
||||
"""
|
||||
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
invoice = models.ForeignKey(Invoice)
|
||||
|
||||
# JSON-serialized representation of the current state
|
||||
# of the invoice, including its line items and
|
||||
# transactions (payments/refunds).
|
||||
snapshot = models.TextField(blank=True)
|
||||
|
||||
@classmethod
|
||||
def save_invoice_snapshot(cls, invoice):
|
||||
"""Save a snapshot of the invoice's current state.
|
||||
|
||||
Arguments:
|
||||
invoice (Invoice): The invoice to save.
|
||||
|
||||
"""
|
||||
cls.objects.create(
|
||||
invoice=invoice,
|
||||
snapshot=json.dumps(invoice.snapshot())
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def snapshot_receiver(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
"""Signal receiver that saves a snapshot of an invoice.
|
||||
|
||||
Arguments:
|
||||
sender: Not used, but required by Django signals.
|
||||
instance (Invoice, InvoiceItem, or InvoiceTransaction)
|
||||
|
||||
"""
|
||||
if isinstance(instance, Invoice):
|
||||
InvoiceHistory.save_invoice_snapshot(instance)
|
||||
elif hasattr(instance, 'invoice'):
|
||||
InvoiceHistory.save_invoice_snapshot(instance.invoice)
|
||||
|
||||
class Meta: # pylint: disable=missing-docstring,old-style-class
|
||||
get_latest_by = "timestamp"
|
||||
|
||||
|
||||
# Hook up Django signals to record changes in the history table.
|
||||
# We record any change to an invoice, invoice item, or transaction.
|
||||
# We also record any deletion of a transaction, since users can delete
|
||||
# transactions via Django admin.
|
||||
# Note that we need to include *each* InvoiceItem subclass
|
||||
# here, since Django signals do not fire automatically for subclasses
|
||||
# of the "sender" class.
|
||||
post_save.connect(InvoiceHistory.snapshot_receiver, sender=Invoice)
|
||||
post_save.connect(InvoiceHistory.snapshot_receiver, sender=InvoiceItem)
|
||||
post_save.connect(InvoiceHistory.snapshot_receiver, sender=CourseRegistrationCodeInvoiceItem)
|
||||
post_save.connect(InvoiceHistory.snapshot_receiver, sender=InvoiceTransaction)
|
||||
post_delete.connect(InvoiceHistory.snapshot_receiver, sender=InvoiceTransaction)
|
||||
|
||||
|
||||
class CourseRegistrationCode(models.Model):
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,8 @@ Tests for the Shopping Cart Models
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
import sys
|
||||
import json
|
||||
import copy
|
||||
|
||||
import smtplib
|
||||
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
|
||||
@@ -18,15 +20,14 @@ from django.db import DatabaseError
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from shoppingcart.models import (
|
||||
Order, OrderItem, CertificateItem,
|
||||
InvalidCartItem, CourseRegistrationCode, PaidCourseRegistration, CourseRegCodeItem,
|
||||
Donation, OrderItemSubclassPK
|
||||
Donation, OrderItemSubclassPK,
|
||||
Invoice, CourseRegistrationCodeInvoiceItem, InvoiceTransaction, InvoiceHistory
|
||||
)
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
@@ -850,3 +851,164 @@ class DonationTest(ModuleStoreTestCase):
|
||||
# Verify that the donation is marked as purchased
|
||||
donation = Donation.objects.get(pk=donation.id)
|
||||
self.assertEqual(donation.status, "purchased")
|
||||
|
||||
|
||||
class InvoiceHistoryTest(TestCase):
|
||||
"""Tests for the InvoiceHistory model. """
|
||||
|
||||
INVOICE_INFO = {
|
||||
'is_valid': True,
|
||||
'internal_reference': 'Test Internal Ref Num',
|
||||
'customer_reference_number': 'Test Customer Ref Num',
|
||||
}
|
||||
|
||||
CONTACT_INFO = {
|
||||
'company_name': 'Test Company',
|
||||
'company_contact_name': 'Test Company Contact Name',
|
||||
'company_contact_email': 'test-contact@example.com',
|
||||
'recipient_name': 'Test Recipient Name',
|
||||
'recipient_email': 'test-recipient@example.com',
|
||||
'address_line_1': 'Test Address 1',
|
||||
'address_line_2': 'Test Address 2',
|
||||
'address_line_3': 'Test Address 3',
|
||||
'city': 'Test City',
|
||||
'state': 'Test State',
|
||||
'zip': '12345',
|
||||
'country': 'US',
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
invoice_data = copy.copy(self.INVOICE_INFO)
|
||||
invoice_data.update(self.CONTACT_INFO)
|
||||
self.invoice = Invoice.objects.create(total_amount="123.45", **invoice_data)
|
||||
self.course_key = CourseLocator('edX', 'DemoX', 'Demo_Course')
|
||||
self.user = UserFactory.create()
|
||||
|
||||
def test_invoice_contact_info_history(self):
|
||||
self._assert_history_invoice_info(
|
||||
is_valid=True,
|
||||
internal_ref=self.INVOICE_INFO['internal_reference'],
|
||||
customer_ref=self.INVOICE_INFO['customer_reference_number']
|
||||
)
|
||||
self._assert_history_contact_info(**self.CONTACT_INFO)
|
||||
self._assert_history_items([])
|
||||
self._assert_history_transactions([])
|
||||
|
||||
def test_invoice_history_items(self):
|
||||
# Create an invoice item
|
||||
CourseRegistrationCodeInvoiceItem.objects.create(
|
||||
invoice=self.invoice,
|
||||
qty=1,
|
||||
unit_price='123.45',
|
||||
course_id=self.course_key
|
||||
)
|
||||
self._assert_history_items([{
|
||||
'qty': 1,
|
||||
'unit_price': '123.45',
|
||||
'currency': 'usd',
|
||||
'course_id': unicode(self.course_key)
|
||||
}])
|
||||
|
||||
# Create a second invoice item
|
||||
CourseRegistrationCodeInvoiceItem.objects.create(
|
||||
invoice=self.invoice,
|
||||
qty=2,
|
||||
unit_price='456.78',
|
||||
course_id=self.course_key
|
||||
)
|
||||
self._assert_history_items([
|
||||
{
|
||||
'qty': 1,
|
||||
'unit_price': '123.45',
|
||||
'currency': 'usd',
|
||||
'course_id': unicode(self.course_key)
|
||||
},
|
||||
{
|
||||
'qty': 2,
|
||||
'unit_price': '456.78',
|
||||
'currency': 'usd',
|
||||
'course_id': unicode(self.course_key)
|
||||
}
|
||||
])
|
||||
|
||||
def test_invoice_history_transactions(self):
|
||||
# Create an invoice transaction
|
||||
first_transaction = InvoiceTransaction.objects.create(
|
||||
invoice=self.invoice,
|
||||
amount='123.45',
|
||||
currency='usd',
|
||||
comments='test comments',
|
||||
status='completed',
|
||||
created_by=self.user,
|
||||
last_modified_by=self.user
|
||||
)
|
||||
self._assert_history_transactions([{
|
||||
'amount': '123.45',
|
||||
'currency': 'usd',
|
||||
'comments': 'test comments',
|
||||
'status': 'completed',
|
||||
'created_by': self.user.username,
|
||||
'last_modified_by': self.user.username,
|
||||
}])
|
||||
|
||||
# Create a second invoice transaction
|
||||
second_transaction = InvoiceTransaction.objects.create(
|
||||
invoice=self.invoice,
|
||||
amount='456.78',
|
||||
currency='usd',
|
||||
comments='test more comments',
|
||||
status='started',
|
||||
created_by=self.user,
|
||||
last_modified_by=self.user
|
||||
)
|
||||
self._assert_history_transactions([
|
||||
{
|
||||
'amount': '123.45',
|
||||
'currency': 'usd',
|
||||
'comments': 'test comments',
|
||||
'status': 'completed',
|
||||
'created_by': self.user.username,
|
||||
'last_modified_by': self.user.username,
|
||||
},
|
||||
{
|
||||
'amount': '456.78',
|
||||
'currency': 'usd',
|
||||
'comments': 'test more comments',
|
||||
'status': 'started',
|
||||
'created_by': self.user.username,
|
||||
'last_modified_by': self.user.username,
|
||||
}
|
||||
])
|
||||
|
||||
# Delete the transactions
|
||||
first_transaction.delete()
|
||||
second_transaction.delete()
|
||||
self._assert_history_transactions([])
|
||||
|
||||
def _assert_history_invoice_info(self, is_valid=True, customer_ref=None, internal_ref=None):
|
||||
"""Check top-level invoice information in the latest history record. """
|
||||
latest = self._latest_history()
|
||||
self.assertEqual(latest['is_valid'], is_valid)
|
||||
self.assertEqual(latest['customer_reference'], customer_ref)
|
||||
self.assertEqual(latest['internal_reference'], internal_ref)
|
||||
|
||||
def _assert_history_contact_info(self, **kwargs):
|
||||
"""Check contact info in the latest history record. """
|
||||
contact_info = self._latest_history()['contact_info']
|
||||
for key, value in kwargs.iteritems():
|
||||
self.assertEqual(contact_info[key], value)
|
||||
|
||||
def _assert_history_items(self, expected_items):
|
||||
"""Check line item info in the latest history record. """
|
||||
items = self._latest_history()['items']
|
||||
self.assertItemsEqual(items, expected_items)
|
||||
|
||||
def _assert_history_transactions(self, expected_transactions):
|
||||
"""Check transactions (payments/refunds) in the latest history record. """
|
||||
transactions = self._latest_history()['transactions']
|
||||
self.assertItemsEqual(transactions, expected_transactions)
|
||||
|
||||
def _latest_history(self):
|
||||
"""Retrieve the snapshot from the latest history record. """
|
||||
latest = InvoiceHistory.objects.latest()
|
||||
return json.loads(latest.snapshot)
|
||||
|
||||
Reference in New Issue
Block a user