Skip to content

Commit

Permalink
Merge pull request #51 from JoGorska/05-checkout
Browse files Browse the repository at this point in the history
05 checkout
  • Loading branch information
JoGorska authored Apr 3, 2022
2 parents a896e92 + 26e5ece commit 536fd5c
Show file tree
Hide file tree
Showing 18 changed files with 484 additions and 2 deletions.
9 changes: 9 additions & 0 deletions bonsai_shop/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
'home',
'trees',
'trolley',
'checkout',

'crispy_forms',

]

Expand All @@ -67,6 +70,8 @@

ROOT_URLCONF = 'bonsai_shop.urls'

CRISPY_TEMPLATE_PACK = 'bootstrap4'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
Expand All @@ -84,6 +89,10 @@
'django.template.context_processors.media',
'trolley.contexts.trolley_contents',
],
'builtins': [
'crispy_forms.templatetags.crispy_forms_tags',
'crispy_forms.templatetags.crispy_forms_field',
]
},
},
]
Expand Down
1 change: 1 addition & 0 deletions bonsai_shop/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@
path('', include('home.urls')),
path('trees/', include('trees.urls')),
path('trolley/', include('trolley.urls')),
path('checkout/', include('checkout.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Empty file added checkout/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions checkout/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.contrib import admin
from .models import Order, OrderLineItem


class OrderLineItemAdminInline(admin.TabularInline):
'''
admin for line item model
admin can add and edit line items from inside the order model
when we look at the order, we will see the list of editable line items
without going to line item interface
'''
model = OrderLineItem
readonly_fields = ('lineitem_total',)


class OrderAdmin(admin.ModelAdmin):
'''
admin for order model
'''
# this allows to vied and edit inline items inside each order
inlines = (OrderLineItemAdminInline,)

readonly_fields = ('order_number', 'date',
'delivery_cost', 'order_total',
'grand_total', 'original_bag', 'stripe_pid',)
# this settings dictates the order in which the columns are displayed
# in admin
fields = ('order_number', 'date', 'full_name',
'email', 'phone_number', 'country',
'postcode', 'town_or_city', 'street_address1',
'street_address2', 'county', 'delivery_cost',
'order_total', 'grand_total', 'original_bag', 'stripe_pid',)
# restrict the columns that show up in order list
list_display = ('order_number', 'date', 'full_name',
'order_total', 'delivery_cost',
'grand_total',)
# most recent orders at the top
ordering = ('-date',)


admin.site.register(Order, OrderAdmin)
13 changes: 13 additions & 0 deletions checkout/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.apps import AppConfig


class CheckoutConfig(AppConfig):
'''
checkout config with method overiding ready method
and imorting signals
'''
default_auto_field = 'django.db.models.BigAutoField'
name = 'checkout'

def ready(self):
import checkout.signals
54 changes: 54 additions & 0 deletions checkout/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from django import forms
from .models import Order


class OrderForm(forms.ModelForm):
'''
order form
'''
class Meta:
'''
meta data - which model the form reffers to
which fields from the model we want to render
NOT rendering the fields that will be automaticaly calculated
'''
model = Order
fields = ('full_name', 'email', 'phone_number',
'street_address1', 'street_address2',
'town_or_city', 'postcode', 'country',
'county',)

def __init__(self, *args, **kwargs):
"""
overrides form __init__ method
Add placeholders and classes, remove auto-generated
labels and set autofocus on first field
"""
super().__init__(*args, **kwargs)
# adds nice placeholders
placeholders = {
'full_name': 'Full Name',
'email': 'Email Address',
'phone_number': 'Phone Number',
'postcode': 'Postal Code',
'town_or_city': 'Town or City',
'street_address1': 'Street Address 1',
'street_address2': 'Street Address 2',
'county': 'County',
}
# sets autofocus on full name field
self.fields['full_name'].widget.attrs['autofocus'] = True
# makes country readonly
self.fields['country'].widget.attrs['readonly'] = True

for field in self.fields:
if field != 'country':
if self.fields[field].required:
# ads a star for fields that are required on the model
placeholder = f'{placeholders[field]} *'
else:
placeholder = placeholders[field]
# sets attributes for place holder and class and removes labels
self.fields[field].widget.attrs['placeholder'] = placeholder
self.fields[field].widget.attrs['class'] = 'stripe-style-input'
self.fields[field].label = False
48 changes: 48 additions & 0 deletions checkout/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 3.2 on 2022-04-03 18:05

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('trees', '0009_auto_20220329_0820'),
]

operations = [
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_number', models.CharField(editable=False, max_length=32)),
('full_name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254)),
('phone_number', models.CharField(max_length=20)),
('country', models.CharField(default='GB', max_length=2)),
('postcode', models.CharField(blank=True, max_length=20, null=True)),
('town_or_city', models.CharField(max_length=40)),
('street_address1', models.CharField(max_length=80)),
('street_address2', models.CharField(blank=True, max_length=80, null=True)),
('county', models.CharField(blank=True, max_length=80, null=True)),
('date', models.DateTimeField(auto_now_add=True)),
('delivery_cost', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('order_total', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('grand_total', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('original_bag', models.TextField(default='')),
('stripe_pid', models.CharField(default='', max_length=254)),
],
),
migrations.CreateModel(
name='OrderLineItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.IntegerField(default=0)),
('lineitem_total', models.DecimalField(decimal_places=2, editable=False, max_digits=6)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lineitems', to='checkout.order')),
('tree', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='trees.tree')),
],
),
]
Empty file added checkout/migrations/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions checkout/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import uuid

from django.db import models
from django.db.models import Sum
from django.conf import settings

from trees.models import Tree


class Order(models.Model):
"""
order model
"""
order_number = models.CharField(max_length=32, null=False, editable=False)

full_name = models.CharField(max_length=50, null=False, blank=False)

email = models.EmailField(max_length=254, null=False, blank=False)
phone_number = models.CharField(max_length=20, null=False, blank=False)
# set to GB
country = models.CharField(default='GB', max_length=2, null=False, blank=False)
postcode = models.CharField(max_length=20, null=True, blank=True)
town_or_city = models.CharField(max_length=40, null=False, blank=False)
street_address1 = models.CharField(max_length=80, null=False, blank=False)
street_address2 = models.CharField(max_length=80, null=True, blank=True)
county = models.CharField(max_length=80, null=True, blank=True)
date = models.DateTimeField(auto_now_add=True)
delivery_cost = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
order_total = models.DecimalField(max_digits=10, decimal_places=2, null=False, default=0)
grand_total = models.DecimalField(max_digits=10, decimal_places=2, null=False, default=0)
# in case if customer makes the same purchase twice
original_bag = models.TextField(null=False, blank=False, default='')
stripe_pid = models.CharField(max_length=254, null=False, blank=False, default='')

def _generate_order_number(self):
"""
Generate a random, unique order number using UUID
_ before name means it is private method to be used only inside this class
"""
return uuid.uuid4().hex.upper()

def update_total(self):
"""
Update grand total each time a line item is added,
accounting for delivery costs.
"""
# or 0 is to prevent setting order_total to none if we manually
# delete all orders from the database
self.order_total = self.lineitems.aggregate(Sum('lineitem_total'))['lineitem_total__sum'] or 0
if self.order_total < settings.FREE_DELIVERY_THRESHOLD:
self.delivery_cost = self.order_total * settings.STANDARD_DELIVERY_PERCENTAGE / 100
else:
self.delivery_cost = 0
self.grand_total = self.order_total + self.delivery_cost
self.save()

def save(self, *args, **kwargs):
"""
Override the original save method to set the order number
if it hasn't been set already.
"""
if not self.order_number:
self.order_number = self._generate_order_number()
super().save(*args, **kwargs)

def __str__(self):
return self.order_number


class OrderLineItem(models.Model):
"""
individual shopping bag item, relating to specific order from the order model
total cost for this specific item
the function will iterate through order and get each item as a seperate line item
this one will update delivery cost, order total and grand total
"""
order = models.ForeignKey(Order, null=False, blank=False, on_delete=models.CASCADE, related_name='lineitems')
tree = models.ForeignKey(Tree, null=False, blank=False, on_delete=models.CASCADE)
quantity = models.IntegerField(null=False, blank=False, default=0)
lineitem_total = models.DecimalField(max_digits=6, decimal_places=2, null=False, blank=False, editable=False)

def save(self, *args, **kwargs):
"""
Override the original save method to set the lineitem total
and update the order total.
"""
self.lineitem_total = self.tree.price * self.quantity
super().save(*args, **kwargs)

def __str__(self):
return f'Slug {self.tree.slug} on order {self.order.order_number}'
24 changes: 24 additions & 0 deletions checkout/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

from .models import OrderLineItem

@receiver(post_save, sender=OrderLineItem)
def update_on_save(sender, instance, created, **kwargs):
"""
handles signals from post save event
sender of the signal - update/ create order line item
instance of the model - sends it
created - boleon if this is a new instance or update
receiver decorator received post save signal from orderlineitem
"""
instance.order.update_total()


@receiver(post_delete, sender=OrderLineItem)
def update_on_delete(sender, instance, **kwargs):
"""
Update order total on lineitem delete
"""
instance.order.update_total()
Loading

0 comments on commit 536fd5c

Please sign in to comment.