add Order model fields for receipt generation

This commit is contained in:
Jason Bau
2013-08-08 23:01:29 -07:00
committed by Diana Huang
parent 1f6bdca6dd
commit ab1452cb1a
4 changed files with 213 additions and 9 deletions

View File

@@ -0,0 +1,183 @@
# -*- 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):
# Deleting field 'Order.nonce'
db.delete_column('shoppingcart_order', 'nonce')
# Adding field 'Order.bill_to_first'
db.add_column('shoppingcart_order', 'bill_to_first',
self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_last'
db.add_column('shoppingcart_order', 'bill_to_last',
self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_street1'
db.add_column('shoppingcart_order', 'bill_to_street1',
self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_street2'
db.add_column('shoppingcart_order', 'bill_to_street2',
self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_city'
db.add_column('shoppingcart_order', 'bill_to_city',
self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_postalcode'
db.add_column('shoppingcart_order', 'bill_to_postalcode',
self.gf('django.db.models.fields.CharField')(max_length=16, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_country'
db.add_column('shoppingcart_order', 'bill_to_country',
self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_ccnum'
db.add_column('shoppingcart_order', 'bill_to_ccnum',
self.gf('django.db.models.fields.CharField')(max_length=8, null=True, blank=True),
keep_default=False)
# Adding field 'Order.bill_to_cardtype'
db.add_column('shoppingcart_order', 'bill_to_cardtype',
self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True),
keep_default=False)
# Adding field 'Order.processor_reply_dump'
db.add_column('shoppingcart_order', 'processor_reply_dump',
self.gf('django.db.models.fields.TextField')(null=True, blank=True),
keep_default=False)
# Adding field 'OrderItem.currency'
db.add_column('shoppingcart_orderitem', 'currency',
self.gf('django.db.models.fields.CharField')(default='usd', max_length=8),
keep_default=False)
def backwards(self, orm):
# Adding field 'Order.nonce'
db.add_column('shoppingcart_order', 'nonce',
self.gf('django.db.models.fields.CharField')(default='defaultNonce', max_length=128),
keep_default=False)
# Deleting field 'Order.bill_to_first'
db.delete_column('shoppingcart_order', 'bill_to_first')
# Deleting field 'Order.bill_to_last'
db.delete_column('shoppingcart_order', 'bill_to_last')
# Deleting field 'Order.bill_to_street1'
db.delete_column('shoppingcart_order', 'bill_to_street1')
# Deleting field 'Order.bill_to_street2'
db.delete_column('shoppingcart_order', 'bill_to_street2')
# Deleting field 'Order.bill_to_city'
db.delete_column('shoppingcart_order', 'bill_to_city')
# Deleting field 'Order.bill_to_postalcode'
db.delete_column('shoppingcart_order', 'bill_to_postalcode')
# Deleting field 'Order.bill_to_country'
db.delete_column('shoppingcart_order', 'bill_to_country')
# Deleting field 'Order.bill_to_ccnum'
db.delete_column('shoppingcart_order', 'bill_to_ccnum')
# Deleting field 'Order.bill_to_cardtype'
db.delete_column('shoppingcart_order', 'bill_to_cardtype')
# Deleting field 'Order.processor_reply_dump'
db.delete_column('shoppingcart_order', 'processor_reply_dump')
# Deleting field 'OrderItem.currency'
db.delete_column('shoppingcart_orderitem', 'currency')
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.order': {
'Meta': {'object_name': 'Order'},
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'null': 'True', 'blank': 'True'}),
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'purchase_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'},
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'line_cost': ('django.db.models.fields.FloatField', [], {'default': '0.0'}),
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
'unit_cost': ('django.db.models.fields.FloatField', [], {'default': '0.0'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.paidcourseregistration': {
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
}
}
complete_apps = ['shoppingcart']

View File

@@ -19,20 +19,29 @@ class Order(models.Model):
"""
This is the model for an order. Before purchase, an Order and its related OrderItems are used
as the shopping cart.
THERE SHOULD ONLY EVER BE ZERO OR ONE ORDER WITH STATUS='cart' PER USER.
FOR ANY USER, THERE SHOULD ONLY EVER BE ZERO OR ONE ORDER WITH STATUS='cart'.
"""
user = models.ForeignKey(User, db_index=True)
status = models.CharField(max_length=32, default='cart', choices=ORDER_STATUSES)
# Because we allow an external service to tell us when something is purchased, and our order numbers
# are their pk and therefore predicatble, let's protect against
# forged/replayed replies with a nonce.
nonce = models.CharField(max_length=128)
purchase_time = models.DateTimeField(null=True, blank=True)
# Now we store data needed to generate a reasonable receipt
# These fields only make sense after the purchase
bill_to_first = models.CharField(max_length=64, null=True, blank=True)
bill_to_last = models.CharField(max_length=64, null=True, blank=True)
bill_to_street1 = models.CharField(max_length=128, null=True, blank=True)
bill_to_street2 = models.CharField(max_length=128, null=True, blank=True)
bill_to_city = models.CharField(max_length=64, null=True, blank=True)
bill_to_postalcode = models.CharField(max_length=16, null=True, blank=True)
bill_to_country = models.CharField(max_length=64, null=True, blank=True)
bill_to_ccnum = models.CharField(max_length=8, null=True, blank=True) # last 4 digits
bill_to_cardtype = models.CharField(max_length=32, null=True, blank=True)
# a JSON dump of the CC processor response, for completeness
processor_reply_dump = models.TextField(null=True, blank=True)
@classmethod
def get_cart_for_user(cls, user):
"""
Use this to enforce the property that at most 1 order per user has status = 'cart'
Always use this to preserve the property that at most 1 order per user has status = 'cart'
"""
order, created = cls.objects.get_or_create(user=user, status='cart')
return order
@@ -41,6 +50,15 @@ class Order(models.Model):
def total_cost(self):
return sum([i.line_cost for i in self.orderitem_set.all()])
@property
def currency(self):
"""Assumes that all cart items are in the same currency"""
items = self.orderitem_set.all()
if not items:
return 'usd'
else:
return items[0].currency
def purchase(self):
"""
Call to mark this order as purchased. Iterates through its OrderItems and calls
@@ -72,6 +90,7 @@ class OrderItem(models.Model):
unit_cost = models.FloatField(default=0.0)
line_cost = models.FloatField(default=0.0) # qty * unit_cost
line_desc = models.CharField(default="Misc. Item", max_length=1024)
currency = models.CharField(default="usd", max_length=8) # lower case ISO currency codes
def add_to_order(self, *args, **kwargs):
"""
@@ -118,7 +137,7 @@ class PaidCourseRegistration(OrderItem):
course_id = models.CharField(max_length=128, db_index=True)
@classmethod
def add_to_order(cls, order, course_id, cost):
def add_to_order(cls, order, course_id, cost, currency='usd'):
"""
A standardized way to create these objects, with sensible defaults filled in.
Will update the cost if called on an order that already carries the course.
@@ -134,6 +153,7 @@ class PaidCourseRegistration(OrderItem):
item.unit_cost = cost
item.line_cost = cost
item.line_desc = "Registration for Course {0}".format(course_id)
item.currency = currency
item.save()
return item

View File

@@ -50,7 +50,7 @@ def show_cart(request):
params = OrderedDict()
params['comment'] = 'Stanford OpenEdX Purchase'
params['amount'] = amount
params['currency'] = 'usd'
params['currency'] = cart.currency
params['orderPage_transactionType'] = 'sale'
params['orderNumber'] = "{0:d}".format(cart.id)
params['billTo_email'] = request.user.email

View File

@@ -10,12 +10,13 @@
% if shoppingcart_items:
<table>
<thead>
<tr><td>Qty</td><td>Description</td><td>Unit Price</td><td>Price</td></tr>
<tr><td>Qty</td><td>Description</td><td>Unit Price</td><td>Price</td><td>Currency</td></tr>
</thead>
<tbody>
% for item in shoppingcart_items:
<tr><td>${item.qty}</td><td>${item.line_desc}</td>
<td>${"{0:0.2f}".format(item.unit_cost)}</td><td>${"{0:0.2f}".format(item.line_cost)}</td>
<td>${item.currency.upper()}</td>
<td><a data-item-id="${item.id}" class='remove_line_item' href='#'>[x]</a></td></tr>
% endfor
<tr><td></td><td></td><td></td><td>Total Amount</td></tr>