Finalize stock feature

This commit is contained in:
Nathan Chapman 2022-10-29 22:53:32 -06:00
parent 157296db2b
commit 0e1f32d5b9
11 changed files with 111 additions and 28 deletions

View File

@ -4,7 +4,10 @@
{% block content %} {% block content %}
<article> <article>
<header class="object__header"> <header class="object__header">
<div>
<h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1> <h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1>
<p>Date: {{ order.created_at }}</p>
</div>
<div class="object__menu"> <div class="object__menu">
<a class="action-button action-button--warning" href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a> <a class="action-button action-button--warning" href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
<span class="order__status order__status--{{order.status}}">{{order.get_status_display}} ({{order.total_quantity_fulfilled}} / {{order.total_quantity_ordered}})</span> <span class="order__status order__status--{{order.status}}">{{order.get_status_display}} ({{order.total_quantity_fulfilled}} / {{order.total_quantity_ordered}})</span>
@ -102,7 +105,7 @@
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br> <span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
{% endif %} {% endif %}
<span>Shipping: ${{order.shipping_total}}</span><br> <span>Shipping: ${{order.shipping_total}}</span><br>
<span>Total: ${{order.get_total_price_after_discount}}</span> <span>Total: ${{order.total_amount}}</span>
</p> </p>
</div> </div>
</section> </section>

View File

@ -10,7 +10,7 @@
{% csrf_token %} {% csrf_token %}
{{form.as_p}} {{form.as_p}}
<p class="form__submit"> <p class="form__submit">
<input class="action-button" type="submit" value="Create rate"> or <a href="{% url 'dashboard:rate-detail' rate.pk %}">cancel</a> <input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:rate-detail' rate.pk %}">cancel</a>
</p> </p>
</form> </form>
</section> </section>

View File

@ -5,21 +5,30 @@
<article> <article>
<header class="object__header"> <header class="object__header">
<h1><img src="{% static 'images/warehouse.png' %}" alt=""> Stock</h1> <h1><img src="{% static 'images/warehouse.png' %}" alt=""> Stock</h1>
<p><strong>Total in warehouse</strong> = available stock + unfulfilled</p>
</header> </header>
<section class="object__panel"> <section class="object__list">
<div class="object__item panel__header"> <div class="object__item panel__header object__item--col5">
<h4>Products</h4> <span>Product</span>
<span>SKU</span>
<span>Available Stock</span>
<span>Total in warehouse</span>
</div> </div>
<div class="panel__item">
{% for variant in variant_list %} {% for variant in variant_list %}
<form action=""> <div class="object__item object__item--col5">
<h3>{{ variant }}</h3> {% with product=variant.product %}
<p>Variant ID: {{ variant.pk }}</p> <figure class="item__figure">
<p>Total in warehouse: {{ variant.stock }}</p> <img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</form> <figcaption><strong>{{variant}}</strong></figcaption>
{% endfor %} </figure>
<span>{{ variant.sku }}</span>
<span>{{ variant.stock }}</span>
<span>{{ variant.total_in_warehouse }}</span>
<a href="{% url 'dashboard:variant-restock' product.pk variant.pk %}" class="action-button">Restock &rarr;</a>
{% endwith %}
</div> </div>
{% endfor %}
</section> </section>
</article> </article>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Restock variant</h1>
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:variant-restock' product.pk variant.pk %}">
{% csrf_token %}
{{form.as_p}}
<p>Total in warehouse: {{ variant.total_in_warehouse }}</p>
<p class="form__submit">
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:stock' %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -191,6 +191,11 @@ urlpatterns = [
views.ProductVariantDeleteView.as_view(), views.ProductVariantDeleteView.as_view(),
name='variant-delete' name='variant-delete'
), ),
path(
'restock/',
views.ProductVariantStockUpdateView.as_view(),
name='variant-restock'
),
])), ])),
])), ])),
])), ])),

View File

@ -18,7 +18,8 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import ( from django.db.models import (
Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value,
ExpressionWrapper, IntegerField
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@ -109,9 +110,14 @@ class StockView(ListView):
def get_queryset(self): def get_queryset(self):
object_list = ProductVariant.objects.filter( object_list = ProductVariant.objects.filter(
track_inventory=True track_inventory=True
).prefetch_related('order_lines', 'product').annotate(
total_in_warehouse=F('stock') + Coalesce(Sum('order_lines__quantity', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)
) - Sum('order_lines__quantity_fulfilled', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)), 0)
).order_by('product') ).order_by('product')
# quantity
# quantity_fulfilled
return object_list return object_list
@ -460,6 +466,38 @@ class ProductVariantDeleteView(SuccessMessageMixin, DeleteView):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductVariantStockUpdateView(LoginRequiredMixin, UpdateView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
success_message = 'ProductVariant saved.'
success_url = reverse_lazy('dashboard:stock')
template_name = 'dashboard/variant_restock.html'
fields = [
'stock',
]
context_object_name = 'variant'
def get_queryset(self):
queryset = ProductVariant.objects.annotate(
total_in_warehouse=F('stock') + Coalesce(Sum('order_lines__quantity', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)
) - Sum('order_lines__quantity_fulfilled', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)), 0)
).prefetch_related('order_lines', 'product')
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['product'] = Product.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
form.instance.product = Product.objects.get(pk=self.kwargs['pk'])
return super().form_valid(form)
class ProductOptionDetailView(LoginRequiredMixin, DetailView): class ProductOptionDetailView(LoginRequiredMixin, DetailView):
model = ProductOption model = ProductOption
template_name = 'dashboard/option_detail.html' template_name = 'dashboard/option_detail.html'
@ -512,7 +550,7 @@ class CustomerListView(LoginRequiredMixin, ListView):
'orders' 'orders'
).annotate( ).annotate(
num_orders=Count('orders') num_orders=Count('orders')
) ).order_by('first_name', 'last_name')
return object_list return object_list

View File

@ -76,6 +76,7 @@ class Cart:
# TODO: abstract this to a function that will check the max amount of item in the cart # TODO: abstract this to a function that will check the max amount of item in the cart
if len(self) <= 20: if len(self) <= 20:
self.check_item_stock_quantities(request)
self.save() self.save()
else: else:
messages.warning(request, "Cart is full: 20 items or less.") messages.warning(request, "Cart is full: 20 items or less.")
@ -96,6 +97,14 @@ class Cart:
self.session.modified = True self.session.modified = True
logger.info(f'\nCart:\n{self.cart}\n') logger.info(f'\nCart:\n{self.cart}\n')
def check_item_stock_quantities(self, request):
for item in self:
if item['variant'].track_inventory:
if item['quantity'] > item['variant'].stock:
messages.warning(request, 'Quantity added exceeds available stock.')
item['quantity'] = item['variant'].stock
self.save()
def remove(self, pk): def remove(self, pk):
self.cart.pop(pk) self.cart.pop(pk)
self.save() self.save()
@ -160,10 +169,7 @@ class Cart:
container, container,
str(self.session.get('shipping_address')['postal_code']) str(self.session.get('shipping_address')['postal_code'])
) )
try:
logger.info('wafd')
except TypeError as e:
return Decimal('0.00')
usps = USPSApi(settings.USPS_USER_ID, test=True) usps = USPSApi(settings.USPS_USER_ID, test=True)
try: try:
@ -173,12 +179,12 @@ class Cart:
'Could not connect to USPS, try again.' 'Could not connect to USPS, try again.'
) )
logger.error(validation.result) logger.info(validation.result)
package = dict(validation.result['RateV4Response']['Package']) package = dict(validation.result['RateV4Response']['Package'])
if 'Error' not in package: if 'Error' not in package:
rate = package['Postage']['CommercialRate'] rate = package['Postage']['CommercialRate']
else: else:
logger.error("USPS Rate error") logger.error('USPS Rate error')
rate = '0.00' rate = '0.00'
return Decimal(rate) return Decimal(rate)
else: else:

View File

@ -11,7 +11,7 @@ from localflavor.us.us_states import USPS_CHOICES
from usps import USPSApi, Address from usps import USPSApi, Address
from captcha.fields import CaptchaField from captcha.fields import CaptchaField
from core.models import Order from core.models import Order, ProductVariant
from core import CoffeeGrind, ShippingContainer from core import CoffeeGrind, ShippingContainer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -62,7 +62,7 @@
</tr> </tr>
<tr> <tr>
<th>Total</th> <th>Total</th>
<td><strong>${{order.get_total_price_after_discount}}</strong></td> <td><strong>${{order.total_amount}}</strong></td>
</tr> </tr>
</table> </table>
</section> </section>

View File

@ -290,6 +290,11 @@ class CheckoutShippingView(FormView):
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:order-create') reverse('storefront:order-create')
) )
elif len(self.get_containers(request)) == 1:
self.request.session['shipping_container'] = self.get_containers(request)[0]
return HttpResponseRedirect(
reverse('storefront:order-create')
)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_form(self, form_class=None): def get_form(self, form_class=None):

View File

@ -33,12 +33,10 @@
<img src="{% static 'images/cubes.png' %}" alt=""> <img src="{% static 'images/cubes.png' %}" alt="">
Catalog Catalog
</a> </a>
<!--
<a href="{% url 'dashboard:stock' %}"> <a href="{% url 'dashboard:stock' %}">
<img src="{% static 'images/warehouse.png' %}" alt=""> <img src="{% static 'images/warehouse.png' %}" alt="">
Stock Stock
</a> </a>
-->
<a href="{% url 'dashboard:order-list' %}"> <a href="{% url 'dashboard:order-list' %}">
<img src="{% static 'images/box.png' %}" alt=""> <img src="{% static 'images/box.png' %}" alt="">
Orders Orders