@@ -237,3 +237,35 @@ class AdminRevenueTests(TestCase):
|
|||||||
self.assertEqual(len(seller_data), 1)
|
self.assertEqual(len(seller_data), 1)
|
||||||
self.assertEqual(seller_data[0]['total_revenue'], Decimal('1100.00'))
|
self.assertEqual(seller_data[0]['total_revenue'], Decimal('1100.00'))
|
||||||
self.assertEqual(seller_data[0]['platform_fees'], Decimal('30.70'))
|
self.assertEqual(seller_data[0]['platform_fees'], Decimal('30.70'))
|
||||||
|
|
||||||
|
class CardListStockTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Create Data
|
||||||
|
self.user = User.objects.create_user(username='stock_seller', password='password')
|
||||||
|
self.seller = Seller.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
store_name='Stock Store',
|
||||||
|
slug='stock-store'
|
||||||
|
)
|
||||||
|
self.game = Game.objects.create(name='Stock Game', slug='stock-game')
|
||||||
|
self.set = Set.objects.create(game=self.game, name='Stock Set')
|
||||||
|
self.card = Card.objects.create(set=self.set, name='Stock Card')
|
||||||
|
|
||||||
|
def test_stock_aggregation(self):
|
||||||
|
# 1. No listings
|
||||||
|
url = reverse('store:card_list')
|
||||||
|
response = self.client.get(url, {'hide_out_of_stock': 'off'})
|
||||||
|
# Expect card in context
|
||||||
|
self.assertEqual(len(response.context['page_obj']), 1)
|
||||||
|
self.assertEqual(response.context['page_obj'][0].total_quantity, 0)
|
||||||
|
|
||||||
|
# 2. Add listings
|
||||||
|
CardListing.objects.create(card=self.card, seller=self.seller, price=10, quantity=5, status='listed')
|
||||||
|
CardListing.objects.create(card=self.card, seller=self.seller, price=10, quantity=3, status='listed')
|
||||||
|
|
||||||
|
# Add a sold listing (should be ignored)
|
||||||
|
CardListing.objects.create(card=self.card, seller=self.seller, price=10, quantity=2, status='sold')
|
||||||
|
|
||||||
|
response = self.client.get(url, {'hide_out_of_stock': 'off'})
|
||||||
|
# Should be 5 + 3 = 8
|
||||||
|
self.assertEqual(response.context['page_obj'][0].total_quantity, 8)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from django.db.models import Q
|
|||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from .models import Card, Game, Set, Cart, CartItem, CardListing, PackListing, VirtualPack, Order, OrderItem, Seller, Bounty, BountyOffer, SellerReport
|
from .models import Card, Game, Set, Cart, CartItem, CardListing, PackListing, VirtualPack, Order, OrderItem, Seller, Bounty, BountyOffer, SellerReport
|
||||||
|
from django.db.models import Sum, Value
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from .forms import SellerRegistrationForm, CardListingForm, PackListingForm, AddCardListingForm, SellerEditForm, BountyForm, BountyOfferForm, BulkListingForm
|
from .forms import SellerRegistrationForm, CardListingForm, PackListingForm, AddCardListingForm, SellerEditForm, BountyForm, BountyOfferForm, BulkListingForm
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
import random
|
import random
|
||||||
@@ -19,6 +21,14 @@ def index(request):
|
|||||||
def card_list(request):
|
def card_list(request):
|
||||||
cards = Card.objects.all().select_related('set', 'set__game').prefetch_related('listings')
|
cards = Card.objects.all().select_related('set', 'set__game').prefetch_related('listings')
|
||||||
|
|
||||||
|
# Annotate with total quantity of listed items
|
||||||
|
cards = cards.annotate(
|
||||||
|
total_quantity=Coalesce(
|
||||||
|
Sum('listings__quantity', filter=Q(listings__status='listed')),
|
||||||
|
Value(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Filtering
|
# Filtering
|
||||||
game_slug = request.GET.get('game')
|
game_slug = request.GET.get('game')
|
||||||
if game_slug:
|
if game_slug:
|
||||||
@@ -43,7 +53,7 @@ def card_list(request):
|
|||||||
hide_oos = 'on'
|
hide_oos = 'on'
|
||||||
|
|
||||||
if hide_oos == 'on':
|
if hide_oos == 'on':
|
||||||
cards = cards.filter(listings__quantity__gt=0, listings__status='listed').distinct()
|
cards = cards.filter(total_quantity__gt=0)
|
||||||
|
|
||||||
# Simple logic: only show cards that have listings or show all?
|
# Simple logic: only show cards that have listings or show all?
|
||||||
# Let's show all for browsing, but indicate stock.
|
# Let's show all for browsing, but indicate stock.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<img src="{{ listing.image.url }}" alt="Listing Image" style="width: 50px; height: 70px; object-fit: cover; border-radius: 4px; border: 1px solid #444;">
|
<img src="{{ listing.image.url }}" alt="Listing Image" style="width: 50px; height: 70px; object-fit: cover; border-radius: 4px; border: 1px solid #444;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span style="font-weight: 700; font-size: 1.25rem;">
|
<span style="font-weight: 700; font-size: 1.25rem;">
|
||||||
{{ listing.condition }}</span>
|
{{ listing.get_condition_display }}</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size: 0.875rem; color: var(--muted-text-color);">Condition</div>
|
<div style="font-size: 0.875rem; color: var(--muted-text-color);">Condition</div>
|
||||||
{% if listing.is_foil %}
|
{% if listing.is_foil %}
|
||||||
|
|||||||
@@ -146,7 +146,11 @@
|
|||||||
<span style="color: #64748b;">Out of Stock</span>
|
<span style="color: #64748b;">Out of Stock</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<span id="stock-{{ card.uuid }}" class="stock-counter" data-card-id="{{ card.uuid }}" style="font-size: 0.75rem; color: #94a3b8; margin-left: auto;">...</span>
|
{% if card.total_quantity > 0 %}
|
||||||
|
<span style="font-size: 0.75rem; color: #10b981; margin-left: auto;">{{ card.total_quantity }} in stock</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="font-size: 0.75rem; color: #ef4444; margin-left: auto;">Out of Stock</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -178,26 +182,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const stockCounters = document.querySelectorAll('.stock-counter');
|
|
||||||
stockCounters.forEach(counter => {
|
|
||||||
const cardId = counter.getAttribute('data-card-id');
|
|
||||||
fetch(`/store/api/stock/${cardId}/`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.total_stock > 0) {
|
|
||||||
counter.textContent = `${data.total_stock} in stock`;
|
|
||||||
counter.style.color = '#10b981'; // green
|
|
||||||
} else {
|
|
||||||
counter.textContent = 'Out of Stock';
|
|
||||||
counter.style.color = '#ef4444'; // red
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
counter.textContent = 'Stock unknown';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user