Files
arbit/inventory/views.py
2026-05-18 14:08:13 -04:00

200 lines
8.3 KiB
Python

from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import DecimalField, ExpressionWrapper, F, Q, Sum
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils import timezone
from django.views.generic import CreateView, DetailView, TemplateView, UpdateView
from .forms import ItemForm, ItemFilterForm, ItemNoteForm, ItemSoldForm, ItemTemplateForm
from .models import Item, ItemNote, ItemTemplate
from .services import PricingSuggestionService
import decimal
class DashboardView(LoginRequiredMixin, TemplateView):
template_name = "inventory/dashboard.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
item_filter = ItemFilterForm(self.request.GET or None)
items = Item.objects.select_related("created_by", "template").order_by("-created_at")
if item_filter.is_valid():
query = item_filter.cleaned_data.get("query")
status = item_filter.cleaned_data.get("status")
category = item_filter.cleaned_data.get("category")
created_by = item_filter.cleaned_data.get("created_by")
min_purchase_price = item_filter.cleaned_data.get("min_purchase_price")
max_purchase_price = item_filter.cleaned_data.get("max_purchase_price")
min_profit = item_filter.cleaned_data.get("min_profit")
if query:
items = items.filter(
Q(title__icontains=query)
| Q(brand__icontains=query)
| Q(notes__icontains=query)
| Q(category__icontains=query)
)
if status:
items = items.filter(status=status)
if category:
items = items.filter(category__icontains=category)
if min_purchase_price is not None:
items = items.filter(purchase_price__gte=min_purchase_price)
if max_purchase_price is not None:
items = items.filter(purchase_price__lte=max_purchase_price)
if min_profit is not None:
items = items.annotate(
calculated_profit=ExpressionWrapper(
F("sold_price") - F("purchase_price") - F("ebay_fee") - F("shipping_cost"),
output_field=DecimalField(max_digits=10, decimal_places=2),
)
).filter(
sold_price__isnull=False,
calculated_profit__gte=min_profit,
)
if created_by:
items = items.filter(created_by__username__icontains=created_by)
context["items"] = items[:100]
context["templates"] = ItemTemplate.objects.filter(is_active=True).order_by("name")
context["item_filter"] = item_filter
context["item_form"] = ItemForm()
context["template_form"] = ItemTemplateForm()
inventory_cost = Item.objects.aggregate(total=Sum("purchase_price"))["total"] or 0
context["stats"] = {
"item_count": Item.objects.count(),
"profit_total": sum((item.profit or 0) for item in Item.objects.all()),
"sold_count": Item.objects.filter(status=Item.Status.SOLD).count(),
"inventory_value": inventory_cost,
}
return context
class ItemCreateView(LoginRequiredMixin, CreateView):
model = Item
form_class = ItemForm
template_name = "inventory/item_form.html"
success_url = reverse_lazy("dashboard")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
template_id = self.request.GET.get("template") or self.request.POST.get("template")
template_obj = None
if template_id:
try:
template_obj = ItemTemplate.objects.get(pk=int(template_id))
except Exception:
template_obj = None
context["template_obj"] = template_obj
return context
def form_valid(self, form):
form.instance.created_by = self.request.user
template = form.cleaned_data.get("template")
properties = {}
# If template defines extra fields, read them from POST and coerce types
if template and template.field_definitions:
for fd in template.field_definitions:
key = fd.get("name")
ftype = fd.get("type", "text")
raw = self.request.POST.get(f"prop_{key}")
if raw is None or raw == "":
value = None
else:
if ftype == "number":
try:
value = decimal.Decimal(raw)
# JSONField cannot store Decimal directly; store as string to preserve precision
value = str(value)
except Exception:
value = raw
elif ftype == "boolean":
value = raw in ("on", "true", "1", "True")
else:
value = raw
properties[key] = value
form.instance.properties = properties
if template:
if not form.cleaned_data.get("category"):
form.instance.category = template.category
if not form.cleaned_data.get("purchase_price") and template.default_purchase_price is not None:
form.instance.purchase_price = template.default_purchase_price
if not form.cleaned_data.get("estimated_resale_price") and template.default_estimated_resale_price is not None:
form.instance.estimated_resale_price = template.default_estimated_resale_price
if not form.cleaned_data.get("notes") and template.default_notes:
form.instance.notes = template.default_notes
return super().form_valid(form)
class ItemDetailView(LoginRequiredMixin, DetailView):
model = Item
template_name = "inventory/item_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["edit_form"] = ItemForm(instance=self.object)
context["sold_form"] = ItemSoldForm(instance=self.object)
context["note_form"] = ItemNoteForm()
context["notes"] = self.object.notes_history.select_related("created_by").order_by("-created_at")
context["price_estimates"] = self.object.price_estimates.all()
context["pricing_suggestion"] = PricingSuggestionService().suggest_for_item(self.object)
return context
class ItemUpdateView(LoginRequiredMixin, UpdateView):
model = Item
form_class = ItemForm
template_name = "inventory/item_form.html"
def get_success_url(self):
return self.object.get_absolute_url()
class ItemMarkSoldView(LoginRequiredMixin, UpdateView):
model = Item
form_class = ItemSoldForm
template_name = "inventory/item_sold_form.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.status = Item.Status.SOLD
if self.object.sold_at is None:
self.object.sold_at = timezone.now()
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return self.object.get_absolute_url()
class ItemNoteCreateView(LoginRequiredMixin, CreateView):
model = ItemNote
form_class = ItemNoteForm
template_name = "inventory/note_form.html"
success_url = reverse_lazy("dashboard")
def form_valid(self, form):
form.instance.item = get_object_or_404(Item, pk=self.kwargs["pk"])
form.instance.created_by = self.request.user
response = super().form_valid(form)
return HttpResponseRedirect(self.object.item.get_absolute_url())
def get_success_url(self):
return self.object.item.get_absolute_url()
class ItemTemplateCreateView(LoginRequiredMixin, CreateView):
model = ItemTemplate
form_class = ItemTemplateForm
template_name = "inventory/template_form.html"
success_url = reverse_lazy("dashboard")
class ItemTemplateUpdateView(LoginRequiredMixin, UpdateView):
model = ItemTemplate
form_class = ItemTemplateForm
template_name = "inventory/template_form.html"
success_url = reverse_lazy("dashboard")