diff --git a/src/core/__init__.py b/src/core/__init__.py index 0643f2b..38676a8 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -71,3 +71,27 @@ class ShippingMethodType: (PRICE_BASED, "Price based shipping"), (WEIGHT_BASED, "Weight based shipping"), ] + +class ShippingService: + FIRST_CLASS = "FIRST CLASS" + PRIORITY = "PRIORITY" + PRIORITY_COMMERCIAL = "PRIORITY COMMERCIAL" + + CHOICES = [ + (FIRST_CLASS, "First Class"), + (PRIORITY, "Priority"), + (PRIORITY_COMMERCIAL, "Priority Commercial") + ] + +class ShippingContainer: + LG_FLAT_RATE_BOX = "LG FLAT RATE BOX" + REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA" + REGIONAL_RATE_BOX_B = "REGIONALRATEBOXB" + VARIABLE = "VARIABLE" + + CHOICES = [ + (LG_FLAT_RATE_BOX, "Flate Rate Box - Large"), + (REGIONAL_RATE_BOX_A, "Regional Rate Box A"), + (REGIONAL_RATE_BOX_B, "Regional Rate Box B"), + (VARIABLE, "Variable") + ] diff --git a/src/core/migrations/0006_alter_order_options_order_shipping_total.py b/src/core/migrations/0006_alter_order_options_order_shipping_total.py new file mode 100644 index 0000000..1af1363 --- /dev/null +++ b/src/core/migrations/0006_alter_order_options_order_shipping_total.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.2 on 2022-04-24 16:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_alter_product_options_product_sorting'), + ] + + operations = [ + migrations.AlterModelOptions( + name='order', + options={'ordering': ('-created_at',)}, + ), + migrations.AddField( + model_name='order', + name='shipping_total', + field=models.DecimalField(decimal_places=2, default=0, max_digits=5), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index 60d809b..dabd4df 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -9,6 +9,8 @@ from django.conf import settings from django.utils import timezone from django.urls import reverse from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.serializers.json import DjangoJSONEncoder +from django.forms.models import model_to_dict from django_measurement.models import MeasurementField @@ -25,6 +27,10 @@ from .weight import WeightUnits, zero_weight logger = logging.getLogger(__name__) +class ProductEncoder(DjangoJSONEncoder): + def default(self, obj): + logger.info(f"\n{obj}\n") + return super().default(obj) class ProductManager(models.Manager): def get_queryset(self): @@ -69,7 +75,7 @@ class Product(models.Model): try: return self.productphoto_set.all()[1] except IndexError: - pass + return 'No image' class Meta: ordering = ['sorting', 'name'] @@ -200,6 +206,7 @@ class Order(models.Model): on_delete=models.SET_NULL, ) + coupon = models.ForeignKey( Coupon, related_name='orders', @@ -207,6 +214,12 @@ class Order(models.Model): null=True ) + shipping_total = models.DecimalField( + max_digits=5, + decimal_places=2, + default=0 + ) + total_net_amount = models.DecimalField( max_digits=10, decimal_places=2, @@ -237,7 +250,7 @@ class Order(models.Model): return Decimal('0') def get_total_price_after_discount(self): - return round(self.total_net_amount - self.get_discount(), 2) + return round((self.total_net_amount - self.get_discount()) + self.shipping_total, 2) def get_absolute_url(self): return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) diff --git a/src/core/usps.py b/src/core/usps.py index 64e6f34..c4e60df 100644 --- a/src/core/usps.py +++ b/src/core/usps.py @@ -19,20 +19,20 @@ class USPSApiWithRate(USPSApi): class Rate: - def __init__(self, usps, box, **kwargs): + def __init__(self, usps, request, **kwargs): xml = etree.Element('RateV4Request', {'USERID': usps.api_user_id}) etree.SubElement(xml, 'Revision').text = '2' package = etree.SubElement(xml, 'Package', {'ID': '0'}) - etree.SubElement(package, 'Service').text = box['service'] - etree.SubElement(package, 'ZipOrigination').text = box['zip_origination'] - etree.SubElement(package, 'ZipDestination').text = box['zip_destination'] - etree.SubElement(package, 'Pounds').text = box['pounds'] - etree.SubElement(package, 'Ounces').text = box['ounces'] - etree.SubElement(package, 'Container').text = box['container'] - etree.SubElement(package, 'Width').text = box['width'] - etree.SubElement(package, 'Length').text = box['length'] - etree.SubElement(package, 'Height').text = box['height'] - etree.SubElement(package, 'Girth').text = box['girth'] - etree.SubElement(package, 'Machinable').text = box['machinable'] + etree.SubElement(package, 'Service').text = request['service'] + etree.SubElement(package, 'ZipOrigination').text = request['zip_origination'] + etree.SubElement(package, 'ZipDestination').text = request['zip_destination'] + etree.SubElement(package, 'Pounds').text = request['pounds'] + etree.SubElement(package, 'Ounces').text = request['ounces'] + etree.SubElement(package, 'Container').text = request['container'] + etree.SubElement(package, 'Width').text = request['width'] + etree.SubElement(package, 'Length').text = request['length'] + etree.SubElement(package, 'Height').text = request['height'] + etree.SubElement(package, 'Girth').text = request['girth'] + etree.SubElement(package, 'Machinable').text = request['machinable'] self.result = usps.send_request('rate', xml) diff --git a/src/dashboard/templates/dashboard/order_detail.html b/src/dashboard/templates/dashboard/order_detail.html index 995eb32..6226116 100644 --- a/src/dashboard/templates/dashboard/order_detail.html +++ b/src/dashboard/templates/dashboard/order_detail.html @@ -103,11 +103,12 @@

- Subtotal: {{order.total_net_amount}}
+ Subtotal: ${{order.total_net_amount}}
{% if order.coupon %} Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}
{% endif %} - Total: {{order.get_total_price_after_discount}} + Shipping: ${{order.shipping_total}}
+ Total: ${{order.get_total_price_after_discount}}

diff --git a/src/dashboard/templates/dashboard/order_list.html b/src/dashboard/templates/dashboard/order_list.html index b620945..8c787bf 100644 --- a/src/dashboard/templates/dashboard/order_list.html +++ b/src/dashboard/templates/dashboard/order_list.html @@ -22,7 +22,7 @@
{{order.get_status_display}}
- ${{order.total_net_amount}} + ${{order.get_total_price_after_discount}} {% empty %} No orders diff --git a/src/ptcoffee/config.py b/src/ptcoffee/config.py index 0ca241c..f4c127a 100644 --- a/src/ptcoffee/config.py +++ b/src/ptcoffee/config.py @@ -21,6 +21,7 @@ CACHE_CONFIG = { PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '') PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '') USPS_USER_ID = os.environ.get('USPS_USER_ID', '639NATHA3105') +DEFAULT_ZIP_ORIGINATION = os.environ.get('DEFAULT_ZIP_ORIGINATION', '98368') ANYMAIL_CONFIG = { 'MAILGUN_API_KEY': os.environ.get('MAILGUN_API_KEY', ''), diff --git a/src/ptcoffee/settings.py b/src/ptcoffee/settings.py index dd462df..83cee0d 100644 --- a/src/ptcoffee/settings.py +++ b/src/ptcoffee/settings.py @@ -93,6 +93,7 @@ DATABASES = { CACHES = {'default': CACHE_CONFIG} SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators diff --git a/src/static/images/site_banner.jpg b/src/static/images/site_banner.jpg new file mode 100644 index 0000000..d78da3c Binary files /dev/null and b/src/static/images/site_banner.jpg differ diff --git a/src/static/styles/main.css b/src/static/styles/main.css index d217e48..3a7da9a 100644 --- a/src/static/styles/main.css +++ b/src/static/styles/main.css @@ -7,6 +7,8 @@ --yellow-color: #f8a911; --yellow-alt-color: #ffce6f; --yellow-dark-color: #b27606; + --red-color: #d43131; + --green-color: #3ea165; --default-border: 2px solid var(--gray-color); } @@ -48,6 +50,7 @@ h1, h2, h3, h4, h5 { h1 { margin-top: 0; + font-family: 'Vollkorn', serif; font-size: 2.488rem; } @@ -443,6 +446,53 @@ section:not(:last-child) { flex-direction: column; } +/* Site Banner + ========================================================================== */ +.site__banner { + background-color: rgba(0, 0, 0, 0.44); + background-blend-mode: multiply; + background-image: url("/static/images/site_banner.jpg"); + background-size: cover; + background-position: center; + color: white; + text-align: center; + padding: 8rem 1rem; + font-family: 'Vollkorn', serif; +} +.site__banner h1 { + font-size: 3.5rem; +} + +.site__banner p { + text-transform: lowercase; + font-variant: small-caps; + font-size: 2rem; +} + +/* Messages + ========================================================================== */ +.messages { + text-align: center; + font-weight: bold; + margin-bottom: 0 !important; +} + +.messages p { + margin-bottom: 0; +} + +.messages .success { + background-color: var(--green-color); +} + +.messages .warning { + background-color: var(--yellow-color); +} + +.messages .error { + background-color: var(--red-color); +} + /* Site Cart ========================================================================== */ .site__cart { diff --git a/src/storefront/cart.py b/src/storefront/cart.py index 2a73eb9..25979f5 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -1,18 +1,23 @@ import logging + from decimal import Decimal from django.conf import settings -from core.models import Product, OrderLine, Coupon -from .payments import CreateOrder +from django.contrib import messages +from core.models import Product, OrderLine, Coupon from core.usps import USPSApiWithRate from core import ( DiscountValueType, VoucherType, TransactionStatus, OrderStatus, - ShippingMethodType + ShippingMethodType, + ShippingService, + ShippingContainer ) +from .payments import CreateOrder + logger = logging.getLogger(__name__) class Cart: @@ -24,7 +29,7 @@ class Cart: cart = self.session[settings.CART_SESSION_ID] = {} self.cart = cart - def add(self, product, quantity=1, grind='', update_quantity=False): + def add(self, request, product, quantity=1, grind='', update_quantity=False): product_id = str(product.id) if product_id not in self.cart: self.cart[product_id] = { @@ -37,7 +42,10 @@ class Cart: self.cart[product_id]['quantity'] = quantity else: self.cart[product_id]['quantity'] += quantity - self.save() + if len(self) <= 20: + self.save() + else: + messages.warning(request, "Cart is full: 20 items or less.") def save(self): self.session[settings.CART_SESSION_ID] = self.cart @@ -65,36 +73,23 @@ class Cart: return sum(item['quantity'] for item in self.cart.values()) def get_total_weight(self): - return sum([item['product'].weight.value * item['quantity'] for item in self.cart.values()]) + return sum([item['product'].weight.value * item['quantity'] for item in self]) def get_shipping_box(self): - logger.debug(len(self)) - + logger.info(len(self)) if len(self) > 6 and len(self) <= 10: - return "LG FLAT RATE BOX" + return ShippingContainer.LG_FLAT_RATE_BOX elif len(self) > 2 and len(self) <= 6: - return "REGIONALRATEBOXB" + return ShippingContainer.REGIONAL_RATE_BOX_B elif len(self) <= 2: - return "REGIONALRATEBOXA" + return ShippingContainer.REGIONAL_RATE_BOX_A else: - return "VARIABLE" + return ShippingContainer.VARIABLE def get_shipping_cost(self): - box = { - 'service': 'PRIORITY COMMERCIAL', - 'zip_origination': '98368', - 'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}', - 'pounds': '0', - 'ounces': f'{self.get_total_weight()}', - 'container': f'{self.get_shipping_box()}', - 'width': '', - 'length': '', - 'height': '', - 'girth': '', - 'machinable': 'TRUE' - } + usps_rate_request = self.build_usps_rate_request() usps = USPSApiWithRate(settings.USPS_USER_ID, test=True) - validation = usps.get_rate(box) + validation = usps.get_rate(usps_rate_request) return Decimal(validation.result['RateV4Response']['Package']['Postage']['CommercialRate']) def get_total_price(self): @@ -108,6 +103,22 @@ class Cart: pass self.session.modified = True + def build_usps_rate_request(self): + return \ + { + 'service': ShippingService.PRIORITY_COMMERCIAL, + 'zip_origination': settings.DEFAULT_ZIP_ORIGINATION, + 'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}', + 'pounds': '0', + 'ounces': f'{self.get_total_weight()}', + 'container': f'{self.get_shipping_box()}', + 'width': '', + 'length': '', + 'height': '', + 'girth': '', + 'machinable': 'TRUE' + } + def build_order_params(self): return \ { diff --git a/src/storefront/forms.py b/src/storefront/forms.py index 14d4124..0dedc5d 100644 --- a/src/storefront/forms.py +++ b/src/storefront/forms.py @@ -96,9 +96,11 @@ class OrderCreateForm(forms.ModelForm): model = Order fields = ( 'total_net_amount', + 'shipping_total', ) widgets = { - 'total_net_amount': forms.HiddenInput() + 'total_net_amount': forms.HiddenInput(), + 'shipping_total': forms.HiddenInput() } class CouponApplyForm(forms.Form): diff --git a/src/storefront/templates/storefront/contact_form.html b/src/storefront/templates/storefront/contact_form.html index b5ddadf..68c2e89 100644 --- a/src/storefront/templates/storefront/contact_form.html +++ b/src/storefront/templates/storefront/contact_form.html @@ -5,13 +5,9 @@

Contact us

Problem with your online order or have a question?

-

- Please contact us, we’re happy to help you over the phone
- (360) 385-5856 between 8:00 am and 10:00 pm Pacific Time. -

+

Please contact us, we’re happy to help you.

-

Or send us a message using the form below and we'll email you back as soon as we can.

{% csrf_token %} {{form.as_p}} diff --git a/src/storefront/templates/storefront/product_list.html b/src/storefront/templates/storefront/product_list.html index bc374b3..d0b3822 100644 --- a/src/storefront/templates/storefront/product_list.html +++ b/src/storefront/templates/storefront/product_list.html @@ -6,6 +6,10 @@ {% endblock %} {% block content %} +
+

Better, not Bitter

+

ORGANIC COFFEE, SLOW ROASTED, ITALIAN STYLE

+
{% for product in product_list %} diff --git a/src/storefront/templates/storefront/reviews.html b/src/storefront/templates/storefront/reviews.html index 4c242f4..ec87d2e 100644 --- a/src/storefront/templates/storefront/reviews.html +++ b/src/storefront/templates/storefront/reviews.html @@ -4,7 +4,7 @@ {% block content %}
-

Reviews

+

Reviews

diff --git a/src/storefront/tests.py b/src/storefront/tests.py index 4fb9587..29c3f48 100644 --- a/src/storefront/tests.py +++ b/src/storefront/tests.py @@ -44,6 +44,7 @@ class CartTest(TestCase): request = response.wsgi_request cart = Cart(request) cart.add( + request=request, product=self.product, quantity=1, update_quantity=False diff --git a/src/storefront/views.py b/src/storefront/views.py index 9f2524f..8f73d53 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -17,6 +17,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.messages.views import SuccessMessageMixin from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST +from django.forms.models import model_to_dict from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment @@ -26,6 +27,7 @@ from accounts.utils import get_or_create_customer from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm from core.models import Product, Order, Transaction, OrderLine, Coupon from core.forms import ShippingMethodForm +from core import OrderStatus from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm from .cart import Cart @@ -61,6 +63,7 @@ class CartAddProductView(SingleObjectMixin, FormView): form = self.get_form() if form.is_valid(): cart.add( + request=request, product=self.get_object(), grind=form.cleaned_data['grind'], quantity=form.cleaned_data['quantity'] @@ -85,6 +88,7 @@ class CartUpdateProductView(SingleObjectMixin, FormView): form = self.get_form() if form.is_valid(): cart.add( + request=request, product=self.get_object(), quantity=form.cleaned_data['quantity'], update_quantity=form.cleaned_data['update'] @@ -178,7 +182,8 @@ class OrderCreateView(CreateView): def get_initial(self): cart = Cart(self.request) initial = { - 'total_net_amount': cart.get_total_price() + 'total_net_amount': cart.get_total_price(), + 'shipping_total': cart.get_shipping_cost() } if self.request.user.is_authenticated: @@ -210,13 +215,14 @@ class OrderCreateView(CreateView): cart = Cart(self.request) shipping_address = self.request.session.get('shipping_address') form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) + form.instance.status = OrderStatus.DRAFT self.object = form.save() bulk_list = cart.build_bulk_list(self.object) objs = OrderLine.objects.bulk_create(bulk_list) response = cart.create_order() data = response.result.__dict__['_dict'] - cart.clear() + self.request.session['order_id'] = self.object.pk return JsonResponse(data) @@ -224,8 +230,12 @@ class OrderCreateView(CreateView): def paypal_order_transaction_capture(request, transaction_id): if request.method =="POST": data = CaptureOrder().capture_order(transaction_id) - - transaction = Transaction.objects.get(order__pk=request.session.get('order_id')) + cart = Cart(request) + cart.clear() + order = Order.objects.get(pk=request.session.get('order_id')) + order.status = OrderStatus.UNFULFILLED + order.save() + transaction = Transaction.objects.get(order=order) transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id'] transaction.status = data['status'] transaction.save() diff --git a/src/templates/base.html b/src/templates/base.html index 80aaa1d..2ed1b62 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -72,7 +72,14 @@ -
+
+ {% if messages %} +
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+ {% endif %} {% block content %} {% endblock content %}
@@ -81,8 +88,7 @@

Problem with your online order or have a question?
- Please contact us, we’re happy to help you over the phone
- (360) 385-5856 between 8:00 am and 10:00 pm Pacific Time.
+ Please contact us, we’re happy to help you.

854 East Park Ave. Suite 1, Port Townsend, WA 98368

diff --git a/src/templates/templated_email/storefront/order_shipped.email b/src/templates/templated_email/storefront/order_shipped.email index 9537410..6922f70 100644 --- a/src/templates/templated_email/storefront/order_shipped.email +++ b/src/templates/templated_email/storefront/order_shipped.email @@ -2,7 +2,7 @@ {% block plain %} Great news! Your recent order #{{order_id}} has shipped - {{tracking_id}} + Your USPS tracking ID: {{tracking_id}} Thanks, Port Townsend Coffee @@ -11,7 +11,7 @@ {% block html %}

Great news! Your recent order #{{order_id}} has shipped

-

{{tracking_id}}

+

Your USPS tracking ID: {{tracking_id}}

Thanks,
Port Townsend Coffee