200 lines
8.3 KiB
Python
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")
|