Finalize stock feature
This commit is contained in:
parent
157296db2b
commit
0e1f32d5b9
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 →</a>
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
19
src/dashboard/templates/dashboard/variant_restock.html
Normal file
19
src/dashboard/templates/dashboard/variant_restock.html
Normal 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 %}
|
||||||
@ -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'
|
||||||
|
),
|
||||||
])),
|
])),
|
||||||
])),
|
])),
|
||||||
])),
|
])),
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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__)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user