From b61114babb5cb2eccad11bb8cd329760400079e1 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Tue, 22 Aug 2023 21:57:28 -0600 Subject: [PATCH] Finalize wholesale orders --- .../0010_wholesaleorder_is_cancelled.py | 18 +++ .../0011_alter_wholesaleorder_options.py | 17 +++ ...e_fulfilled_wholesaleorder_is_fulfilled.py | 18 +++ core/models.py | 4 +- core/tasks.py | 15 ++- dashboard/forms.py | 19 +++ .../dashboard/wholesale_order/_table.html | 28 +++++ .../wholesale_order/cancel_form.html | 26 +++++ .../dashboard/wholesale_order/detail.html | 108 ++++++++++++++++++ .../wholesale_order/fulfill_form.html | 26 +++++ .../dashboard/wholesale_order/list.html | 40 +++++++ dashboard/urls.py | 24 ++++ dashboard/views.py | 53 +++++++++ static/images/pallet.png | Bin 0 -> 4419 bytes static/styles/main.css | 7 ++ .../templates/storefront/wholesale_order.html | 12 +- .../storefront/wholesale_order_detail.html | 29 +++-- storefront/views.py | 21 +++- templates/base.html | 10 +- templates/dashboard.html | 4 + .../wholesale_order_confirmation.email | 84 ++++++++++++++ 21 files changed, 536 insertions(+), 27 deletions(-) create mode 100644 core/migrations/0010_wholesaleorder_is_cancelled.py create mode 100644 core/migrations/0011_alter_wholesaleorder_options.py create mode 100644 core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py create mode 100644 dashboard/templates/dashboard/wholesale_order/_table.html create mode 100644 dashboard/templates/dashboard/wholesale_order/cancel_form.html create mode 100644 dashboard/templates/dashboard/wholesale_order/detail.html create mode 100644 dashboard/templates/dashboard/wholesale_order/fulfill_form.html create mode 100644 dashboard/templates/dashboard/wholesale_order/list.html create mode 100644 static/images/pallet.png create mode 100644 templates/templated_email/storefront/wholesale_order_confirmation.email diff --git a/core/migrations/0010_wholesaleorder_is_cancelled.py b/core/migrations/0010_wholesaleorder_is_cancelled.py new file mode 100644 index 0000000..0d8db2a --- /dev/null +++ b/core/migrations/0010_wholesaleorder_is_cancelled.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.6 on 2023-08-06 18:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_wholesaleorder'), + ] + + operations = [ + migrations.AddField( + model_name='wholesaleorder', + name='is_cancelled', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/migrations/0011_alter_wholesaleorder_options.py b/core/migrations/0011_alter_wholesaleorder_options.py new file mode 100644 index 0000000..16460f2 --- /dev/null +++ b/core/migrations/0011_alter_wholesaleorder_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.6 on 2023-08-06 18:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_wholesaleorder_is_cancelled'), + ] + + operations = [ + migrations.AlterModelOptions( + name='wholesaleorder', + options={'ordering': ['-created_at'], 'verbose_name': 'wholesale order', 'verbose_name_plural': 'wholesale orders'}, + ), + ] diff --git a/core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py b/core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py new file mode 100644 index 0000000..d9d9ab5 --- /dev/null +++ b/core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.6 on 2023-08-23 01:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_alter_wholesaleorder_options'), + ] + + operations = [ + migrations.RenameField( + model_name='wholesaleorder', + old_name='fulfilled', + new_name='is_fulfilled', + ), + ] diff --git a/core/models.py b/core/models.py index 68d940c..0de6882 100644 --- a/core/models.py +++ b/core/models.py @@ -754,7 +754,8 @@ class WholesaleOrder(models.Model): size=2, ) - fulfilled = models.BooleanField(default=False) + is_fulfilled = models.BooleanField(default=False) + is_cancelled = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -762,4 +763,5 @@ class WholesaleOrder(models.Model): class Meta: verbose_name = "wholesale order" verbose_name_plural = "wholesale orders" + ordering = ['-created_at'] diff --git a/core/tasks.py b/core/tasks.py index 02fa9d3..13e60ae 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -11,8 +11,9 @@ logger = get_task_logger(__name__) CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation' SHIP_ORDER_TEMPLATE = 'storefront/order_shipped' -ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel' +ORDER_CANCEL_TEMPLATE = 'storefront/order_cancel' ORDER_REFUND_TEMPLATE = 'storefront/order_refund' +CONFIRM_WHOLESALE_ORDER_TEMPLATE = 'storefront/wholesale_order_confirmation' @shared_task(name='send_order_confirmation_email') @@ -27,6 +28,18 @@ def send_order_confirmation_email(order): logger.info(f"Order confirmation email sent to {order['email']}") +@shared_task(name='send_wholesale_order_confirmation_email') +def send_wholesale_order_confirmation_email(order): + send_templated_mail( + template_name=CONFIRM_WHOLESALE_ORDER_TEMPLATE, + from_email=SiteSettings.load().order_from_email, + recipient_list=[order['email']], + context=order + ) + + logger.info(f"WholesaleOrder confirmation email sent to {order['email']}") + + @shared_task(name='send_order_shipped_email') def send_order_shipped_email(data): send_templated_mail( diff --git a/dashboard/forms.py b/dashboard/forms.py index ea2e5fc..4f3351e 100644 --- a/dashboard/forms.py +++ b/dashboard/forms.py @@ -4,6 +4,7 @@ from django import forms from core import OrderStatus from core.models import ( ProductVariant, + WholesaleOrder, Order, OrderLine, ShippingRate, @@ -118,6 +119,24 @@ OrderTrackingFormset = forms.inlineformset_factory( ) +class WholesaleOrderCancelForm(forms.ModelForm): + class Meta: + model = WholesaleOrder + fields = ['is_cancelled'] + widgets = { + 'is_cancelled': forms.HiddenInput() + } + + +class WholesaleOrderFulfillForm(forms.ModelForm): + class Meta: + model = WholesaleOrder + fields = ['is_fulfilled'] + widgets = { + 'is_fulfilled': forms.HiddenInput() + } + + class ProductPhotoForm(forms.ModelForm): class Meta: model = ProductPhoto diff --git a/dashboard/templates/dashboard/wholesale_order/_table.html b/dashboard/templates/dashboard/wholesale_order/_table.html new file mode 100644 index 0000000..0545927 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/_table.html @@ -0,0 +1,28 @@ + + + Order No. + Date + Customer + Fulfillment + + + + {% for order in order_list %} + + No. {{ order.pk }} + {{ order.created_at|date:"D, M j Y" }} + {{ order.customer.get_full_name }} + +
+ {% if order.is_fulfilled %} + Fulfilled + {% elif order.is_cancelled %} + Cancelled + {% else %} + Unfulfilled + {% endif %} +
+ + + {% endfor %} + diff --git a/dashboard/templates/dashboard/wholesale_order/cancel_form.html b/dashboard/templates/dashboard/wholesale_order/cancel_form.html new file mode 100644 index 0000000..f8eb1c9 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/cancel_form.html @@ -0,0 +1,26 @@ +{% extends 'dashboard.html' %} + +{% block head_title %}Cancel Wholesale Order No. {{ order.pk }} | {% endblock %} + +{% block content %} +
+

+ ← Back +

+
+

Cancel Wholesale Order No. {{order.pk}}

+
+
+
+

Are you sure you want to cancel Wholesale Order No. {{ order.pk }}?

+
+
+ {% csrf_token %} + {{ form.as_p }} +

+ +

+
+
+
+{% endblock content %} diff --git a/dashboard/templates/dashboard/wholesale_order/detail.html b/dashboard/templates/dashboard/wholesale_order/detail.html new file mode 100644 index 0000000..21c663b --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/detail.html @@ -0,0 +1,108 @@ +{% extends 'dashboard.html' %} +{% load static %} + +{% block head_title %}Order No. {{ order.pk }} | {% endblock %} + +{% block content %} +
+

+ ← Back to wholesale +

+
+

Wholesale Order No. {{order.pk}}

+ {% if perms.core.cancel_order and not order.is_cancelled %} + Cancel order + {% endif %} +
+
+
+

Details

+
+
+
Date
+
{{ order.created_at }}
+ +
Customer
+
+ {{order.customer.get_full_name}}  + {{order.customer.email}} ↗ +
+ +
Status
+
+ {% if order.is_fulfilled %} + Fulfilled + {% elif order.is_cancelled %} + Cancelled + {% else %} + Unfulfilled + {% endif %} +
+
+
+
+
+

Items

+ Fulfill order → +
+ + + + + + + + + + + + {% for qty in order.brazil %} + + {% endfor %} + + + + {% for qty in order.dantes_tornado %} + + {% endfor %} + + + {% for qty in order.decaf %} + + {% endfor %} + + + {% for qty in order.ethiopia %} + + {% endfor %} + + + {% for qty in order.loop_d_loop %} + + {% endfor %} + + + {% for qty in order.moka_java %} + + {% endfor %} + + + {% for qty in order.nicaragua %} + + {% endfor %} + + + {% for qty in order.pantomime %} + + {% endfor %} + + + {% for qty in order.sumatra %} + + {% endfor %} + + +
Product16 oz.5 lb.
Brazil{% if qty %}Qty: {{ qty }}{% endif %}
Dante's Tornado{% if qty %}Qty: {{ qty }}{% endif %}
Decaf{% if qty %}Qty: {{ qty }}{% endif %}
Ethiopia{% if qty %}Qty: {{ qty }}{% endif %}
Loop d' Loop{% if qty %}Qty: {{ qty }}{% endif %}
Moka Java{% if qty %}Qty: {{ qty }}{% endif %}
Nicaragua{% if qty %}Qty: {{ qty }}{% endif %}
Pantomime{% if qty %}Qty: {{ qty }}{% endif %}
Sumatra{% if qty %}Qty: {{ qty }}{% endif %}
+
+
+{% endblock content %} diff --git a/dashboard/templates/dashboard/wholesale_order/fulfill_form.html b/dashboard/templates/dashboard/wholesale_order/fulfill_form.html new file mode 100644 index 0000000..db2c147 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/fulfill_form.html @@ -0,0 +1,26 @@ +{% extends 'dashboard.html' %} + +{% block head_title %}Fulfill Wholesale Order No. {{ order.pk }} | {% endblock %} + +{% block content %} +
+

+ ← Back +

+
+

Fulfill Wholesale Order No. {{order.pk}}

+
+
+
+

Are you sure you want to fulfill Wholesale Order No. {{ order.pk }}?

+
+
+ {% csrf_token %} + {{ form.as_p }} +

+ +

+
+
+
+{% endblock content %} diff --git a/dashboard/templates/dashboard/wholesale_order/list.html b/dashboard/templates/dashboard/wholesale_order/list.html new file mode 100644 index 0000000..ee8a9f0 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/list.html @@ -0,0 +1,40 @@ +{% extends 'dashboard.html' %} +{% load static %} + +{% block head_title %}Wholesale Orders | {% endblock %} + +{% block content %} +
+
+

Wholesale Orders

+
+
+
+
+ + {% include 'dashboard/wholesale_order/_table.html' with order_list=order_list %} + + + + + +
+ +
+
+
+{% endblock content %} diff --git a/dashboard/urls.py b/dashboard/urls.py index f2ab9d6..11813c2 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -254,4 +254,28 @@ urlpatterns = [ name='customer-update' ), ])), + + # Wholesale Orders + path( + 'wholesale-orders/', + views.WholesaleOrderListView.as_view(), + name='wholesale-order-list' + ), + path('wholesale-orders//', include([ + path( + '', + views.WholesaleOrderDetailView.as_view(), + name='wholesale-order-detail' + ), + path( + 'fulfill/', + views.WholesaleOrderFulfillView.as_view(), + name='wholesale-order-fulfill' + ), + path( + 'cancel/', + views.WholesaleOrderCancelView.as_view(), + name='wholesale-order-cancel' + ), + ])), ] diff --git a/dashboard/views.py b/dashboard/views.py index ffffee5..b589202 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -43,6 +43,7 @@ from core.models import ( Transaction, TrackingNumber, Coupon, + WholesaleOrder, SiteSettings ) @@ -57,6 +58,8 @@ from .forms import ( OrderLineFormset, OrderCancelForm, OrderTrackingFormset, + WholesaleOrderFulfillForm, + WholesaleOrderCancelForm, CouponForm, ProductPhotoForm ) @@ -723,3 +726,53 @@ class CustomerUpdateView( def get_success_url(self): return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk}) + + +class WholesaleOrderListView(LoginRequiredMixin, ListView): + model = WholesaleOrder + template_name = 'dashboard/wholesale_order/list.html' + context_object_name = 'order_list' + paginate_by = 50 + + +class WholesaleOrderDetailView( + LoginRequiredMixin, DetailView +): + model = WholesaleOrder + context_object_name = 'order' + template_name = 'dashboard/wholesale_order/detail.html' + + +class WholesaleOrderCancelView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + permission_required = 'core.cancel_order' + model = WholesaleOrder + context_object_name = 'order' + template_name = 'dashboard/wholesale_order/cancel_form.html' + form_class = WholesaleOrderCancelForm + success_message = 'Wholesale Order canceled.' + initial = { + 'is_cancelled': True + } + + def get_success_url(self): + return reverse('dashboard:wholesale-order-detail', kwargs={'pk': self.object.pk}) + + +class WholesaleOrderFulfillView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + permission_required = 'core.change_wholesaleorder' + model = WholesaleOrder + context_object_name = 'order' + template_name = 'dashboard/wholesale_order/fulfill_form.html' + form_class = WholesaleOrderFulfillForm + success_message = 'Wholesale Order fulfilled.' + initial = { + 'is_fulfilled': True + } + + def get_success_url(self): + return reverse('dashboard:wholesale-order-detail', kwargs={'pk': self.object.pk}) + diff --git a/static/images/pallet.png b/static/images/pallet.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc9599bd5a651deae1a8603f66255bb243674ee GIT binary patch literal 4419 zcmeHKdr(x@8NZ7Nl*q;hND_6rF11nW-TT=0?XC#y0vlPgDk$X9=I(vqmEFC|-3z;r z2|=x*ZKl>qYOS+LY2vFylL0C& zg-8}W08*2VXcP=I%{V(vGYy#rV_Tuk23x@>?IF!{wSNxmc}e>PuoovWiVvYlLvo;x zLnFb?g}xJ-(4JW9_h2iZEcJUK4MFC>(`aAr@%nV6fxrz$6Zqq#k-}{hZqX4WWwKB> z0qbiz#v)RfHayrj1o3SVN=tp7(o$Vm5_uuOAxJF#%uZ)T&+_?afBg6{cj2a{nGJou zCj%UtP;yoJL*o!Nr7J#cOjf8)r@ySH8+ zKGDA8^=QLOL(lx}{XZVat;`rsA58Dh`C076SWEvU`|Ua9HMOg^y}zBwc=8v+JOA4C zt{7jT{j!-q)5l-iR&b`|cbkW_JXgBtCH>C}JIlTpiNBkD9lfw&-?_)`UBzbPA01ls zTL1jw?E7kdlBFRxe7t=6+`*$8_U`Vx*m|j_=;p9nI)C*~NI|co)3?yxdZ%dkFNbU6 z@$mC+dpf>6d%UP@;}yfjLj(W3_eRkAXj`{2yzkgoA7flBNM?I?&GxR39G@(> zar4$?m5+a-2EZso*;sv5b*TS+lu&_J`IpEJaQVdcaSM^P+HGxeOT^K*4V z$f>`5@(+5Qz&iEo%wF6ZF6Gt>6)h6Cre$Rn(^AjaS$)y6Y)6a&f*>c;x>zs}icm49 zUgf30R)#UXPKC(zPJIm|QmH6$I+MXCQvTsaDWGnLWiT$l1pyj?Rf!2pDpxM$p-50C1jDKqh&=%*3;dW^6Jk?l)a{H9 z1l&`+6VRi3SHXbtdMTI4G%DeFTu!~RKE;ZRz*4Gdx8j72H?ye4%rK~l=S`@cVeP2d zgj;BW^V1A(A4la0MPxd}a0(TW8wB8?jf9mmTKp*M=lrP2!V{?9Oj=NaF`8{gqXpR6 zaTJeA0%Ro}7$22F#R3&eGk$_IK@6mY11dj((tfiar7gIPG@C8F)rzZBEJKxvQjmt@ z6oPaehlN9R>VzUVRqXRP^`s#IRY!aQTIRvQsjn15jj=Ikl@R3C$h4v+VX+fNlhte? zEG8pvH;#eUa#92`QQ=gJr#hn?3k912VQD2#fj}LHy-=kRN6VsAC5i#3K2>&8(_Xkv zEG^S6TIPT>Rfwxb3o(jAIZlMH(0N5x;G4e*ty~_R1MqO<6+#5oZ&rsAH);*nka$Ww z1_bpg>2&I)plBwsg$UimB@PR?5>w22I#kC&^+=}c=(zB8sX(v8bvS(k)gGYRN=&bUmk>0tklUHW2B* z1;dgX#*~^dx?>0SKbkmHz_=mm+Hzo00*EamNaWCy%xW%yT=yO4h?-;PrU zo`;d|gez(z2%_y#nxWzyu1g|P zO@FYjfjv0m?5cC*{(n8YKYL_dx8?NdSp}89-e}ld9@$mvE4(ta?bfFU7reT?WM0qG z2L^Z7oz8o!_t*!H`PH8-+3bvSbC(|JFlaukdG&>NKKNwq(-|d)`s3|8 section { grid-template-columns: repeat(2, 1fr); gap: 3rem; } + +.wholesale-detail, +.wholesale-fields { + width: fit-content; +} .wholesale-field { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; + margin-bottom: 1rem; } + diff --git a/storefront/templates/storefront/wholesale_order.html b/storefront/templates/storefront/wholesale_order.html index dc97cce..d97ab13 100644 --- a/storefront/templates/storefront/wholesale_order.html +++ b/storefront/templates/storefront/wholesale_order.html @@ -6,17 +6,23 @@

Wholesale

- {% if user.is_authenticated %} + {% if user.is_authenticated and perms.core.add_wholesaleorder %}

New wholesale order

+

Orders

{% for order in user.wholesale_orders.all %} - + {% empty %} + + + {% endfor %}
{{ order.created_at }} - + + No. {{ order.pk }} | {{ order.created_at }} +
No wholesale order placed yet.
{% else %} diff --git a/storefront/templates/storefront/wholesale_order_detail.html b/storefront/templates/storefront/wholesale_order_detail.html index b6d53a1..afffd10 100644 --- a/storefront/templates/storefront/wholesale_order_detail.html +++ b/storefront/templates/storefront/wholesale_order_detail.html @@ -3,65 +3,64 @@ {% block content %}
-

← Back

+

← Back

Wholesale Order No. {{order.pk}}

Placed on {{order.created_at|date:"M j, Y"}}

- +
- - + + - + {% for qty in order.brazil %} {% endfor %} - - - + + {% for qty in order.dantes_tornado %} {% endfor %} - + {% for qty in order.decaf %} {% endfor %} - + {% for qty in order.ethiopia %} {% endfor %} - + {% for qty in order.loop_d_loop %} {% endfor %} - + {% for qty in order.moka_java %} {% endfor %} - + {% for qty in order.nicaragua %} {% endfor %} - + {% for qty in order.pantomime %} {% endfor %} - + {% for qty in order.sumatra %} {% endfor %} diff --git a/storefront/views.py b/storefront/views.py index d9df71b..06ff166 100644 --- a/storefront/views.py +++ b/storefront/views.py @@ -39,6 +39,7 @@ from moneyed import Money, USD from accounts.models import User from accounts.utils import get_or_create_customer from accounts.forms import CustomerUpdateForm, CustomerShippingAddressUpdateForm +from core.tasks import send_wholesale_order_confirmation_email from core.models import ( ProductCategory, Product, ProductVariant, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate, @@ -850,9 +851,9 @@ class WholesaleOrderView(TemplateView): class WholesaleOrderCreateView( - PermissionRequiredMixin, LoginRequiredMixin, CreateView, SuccessMessageMixin + PermissionRequiredMixin, LoginRequiredMixin, SuccessMessageMixin, CreateView ): - permission_required = "core.create_wholesaleorder" + permission_required = "core.add_wholesaleorder" model = WholesaleOrder template_name = 'storefront/wholesale_order_create_form.html' form_class = WholesaleOrderCreateForm @@ -867,6 +868,22 @@ class WholesaleOrderCreateView( def form_valid(self, form): form.instance.customer = self.request.user + self.object = form.save() + order = { + 'order_id': self.object.pk, + 'full_name': self.object.customer.get_full_name(), + 'email': self.object.customer.email, + 'brazil': self.object.brazil, + 'dantes_tornado': self.object.dantes_tornado, + 'decaf': self.object.decaf, + 'ethiopia': self.object.ethiopia, + 'loop_d_loop': self.object.loop_d_loop, + 'moka_java': self.object.moka_java, + 'nicaragua': self.object.nicaragua, + 'pantomime': self.object.pantomime, + 'sumatra': self.object.sumatra + } + send_wholesale_order_confirmation_email.delay(order) return super().form_valid(form) diff --git a/templates/base.html b/templates/base.html index e3568f6..f65242d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -87,11 +87,11 @@
{% if messages %} -
- {% for message in messages %} -

{{ message }}

- {% endfor %} -
+
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
{% endif %} {% block content %} {% endblock content %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 0ed2c7f..5b7d2c6 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -48,6 +48,10 @@ Orders + + + Wholesale + Customers diff --git a/templates/templated_email/storefront/wholesale_order_confirmation.email b/templates/templated_email/storefront/wholesale_order_confirmation.email new file mode 100644 index 0000000..e85a4e5 --- /dev/null +++ b/templates/templated_email/storefront/wholesale_order_confirmation.email @@ -0,0 +1,84 @@ +{% block subject %}PT Coffee: Confirmation for Wholesale Order No. {{ order_id }}{% endblock %} +{% block html %} + + + +

Thank you for your order!

+ +

Hi {{ full_name }}, your order details are below.

+ +

Order Summary

+
Product16 oz5 lb16 oz.5 lb.
BrazilBrazil{% if qty %}Qty: {{ qty }}{% endif %}
Dante's Tornado
Dante's Tornado{% if qty %}Qty: {{ qty }}{% endif %}
DecafDecaf{% if qty %}Qty: {{ qty }}{% endif %}
EthiopiaEthiopia{% if qty %}Qty: {{ qty }}{% endif %}
Loop d' LoopLoop d' Loop{% if qty %}Qty: {{ qty }}{% endif %}
Moka JavaMoka Java{% if qty %}Qty: {{ qty }}{% endif %}
NicaraguaNicaragua{% if qty %}Qty: {{ qty }}{% endif %}
PantomimePantomime{% if qty %}Qty: {{ qty }}{% endif %}
SumatraSumatra{% if qty %}Qty: {{ qty }}{% endif %}
+ + + + + + + + + + + {% for qty in brazil %} + + {% endfor %} + + + {% for qty in dantes_tornado %} + + {% endfor %} + + + {% for qty in decaf %} + + {% endfor %} + + + {% for qty in ethiopia %} + + {% endfor %} + + + {% for qty in loop_d_loop %} + + {% endfor %} + + + {% for qty in moka_java %} + + {% endfor %} + + + {% for qty in nicaragua %} + + {% endfor %} + + + {% for qty in pantomime %} + + {% endfor %} + + + {% for qty in sumatra %} + + {% endfor %} + + +
Product16 oz.5 lb.
Brazil{% if qty %}Qty: {{ qty }}{% endif %}
Dante's Tornado{% if qty %}Qty: {{ qty }}{% endif %}
Decaf{% if qty %}Qty: {{ qty }}{% endif %}
Ethiopia{% if qty %}Qty: {{ qty }}{% endif %}
Loop d' Loop{% if qty %}Qty: {{ qty }}{% endif %}
Moka Java{% if qty %}Qty: {{ qty }}{% endif %}
Nicaragua{% if qty %}Qty: {{ qty }}{% endif %}
Pantomime{% if qty %}Qty: {{ qty }}{% endif %}
Sumatra{% if qty %}Qty: {{ qty }}{% endif %}
+ +

If you have any questions, send us an email at support@ptcoffee.com.

+ +

Thanks,
+ Port Townsend Roasting Co.

+{% endblock %}