Skip to content

Commit

Permalink
store: optimize
Browse files Browse the repository at this point in the history
  • Loading branch information
Aiq0 committed Dec 16, 2024
1 parent 9cc4c2c commit 4286376
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 187 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- "8000:8000"
volumes:
- ./sortiment/:/app/sortiment/
- ./sortiment/uploads/:/app/uploads/
environment:
- DEBUG=True
depends_on:
Expand Down
250 changes: 126 additions & 124 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ python = "^3.12"
django = "5.0.6"
gunicorn = "22.0.0"
django-feather = "0.3.0"
django-debug-toolbar = "4.4.2"
django-debug-toolbar = "4.4.6"
django-htmx = "1.17.3"
django-ipware = "7.0.1"
django-widget-tweaks = "1.5.0"
Expand Down
10 changes: 2 additions & 8 deletions sortiment/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import * as Turbo from "@hotwired/turbo"
window.Stimulus = Application.start()

import BarcodeController from "./controllers/barcode"
Stimulus.register("barcode", BarcodeController)
import ImportController from "./controllers/import"
Stimulus.register("import", ImportController)
import MenuToggleController from "./controllers/menu-toggle"
Stimulus.register("barcode", BarcodeController)
Stimulus.register("import", ImportController)
Stimulus.register("menu-toggle", MenuToggleController)

Turbo.start()
Expand All @@ -17,9 +17,3 @@ document.addEventListener("turbo:before-fetch-request", (event) => {
const token = tokenMeta.getAttribute("value")
event.detail.fetchOptions.headers["X-CSRFToken"] = token
})

document.addEventListener("turbo:load", (event) => {
if (typeof window.djdt !== "undefined") {
djdt.init()
}
})
7 changes: 7 additions & 0 deletions sortiment/sortiment/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
"OPTIONS": {"MAX_ENTRIES": 5000},
}
}

if DEBUG:
import socket
Expand Down
4 changes: 2 additions & 2 deletions sortiment/sortiment/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""

from debug_toolbar.toolbar import debug_toolbar_urls
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
Expand All @@ -25,8 +26,7 @@
path("users/", include("users.urls")),
path("store/", include("store.urls")),
path("", lambda request: redirect("user_list")),
]
] + debug_toolbar_urls()

if settings.DEBUG:
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
93 changes: 56 additions & 37 deletions sortiment/store/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from datetime import datetime, timedelta
from typing import Iterable

from django.contrib.auth.models import AbstractUser, User
from django.db.models import Sum
from django.contrib.auth.models import User
from django.db.models import F, FloatField, Max, Q, Sum
from django.db.models.functions import ExtractDay
from django.utils import timezone

from store.models import Product, Warehouse, WarehouseEvent, WarehouseState
Expand All @@ -19,71 +20,89 @@ class InventoryQuantities:
def get_inventory_quantities(warehouse: Warehouse) -> dict[int, InventoryQuantities]:
quantities = defaultdict(lambda: InventoryQuantities(0, 0))

local_states = WarehouseState.objects.filter(warehouse=warehouse)
for state in local_states:
quantities[state.product_id].local = state.quantity

global_states = WarehouseState.objects.values("product_id").annotate(
total=Sum("quantity")
states = WarehouseState.objects.values("product_id").annotate(
local=Sum("quantity", filter=Q(warehouse=warehouse), default=0),
total=Sum("quantity"),
)
for state in global_states:
quantities[state["product_id"]].total = state["total"]

for state in states:
quantities[state["product_id"]] = InventoryQuantities(
state["local"], state["total"]
)

return quantities


def get_purchases(warehouse: Warehouse) -> dict[int, list[WarehouseEvent]]:
purchases = defaultdict(list)
@dataclass
class PurchaseStats:
global_priority: float = 0
user_priority: float = 0
last_purchase: datetime = datetime.min


def get_purchases(warehouse: Warehouse, user: User) -> dict[int, PurchaseStats]:
purchases = defaultdict(lambda: PurchaseStats())

cutoff = timezone.now() - timedelta(days=60)
events = WarehouseEvent.objects.filter(
warehouse=warehouse,
type=WarehouseEvent.EventType.PURCHASE,
timestamp__gte=cutoff,
).order_by("-timestamp")

events = (
WarehouseEvent.objects.filter(
warehouse=warehouse,
type=WarehouseEvent.EventType.PURCHASE,
timestamp__gte=cutoff,
)
.values("product_id")
.annotate(
global_priority=Sum(
-F("quantity")
* 0.95 ** ExtractDay(timezone.now().date() - F("timestamp")),
output_field=FloatField(),
),
user_priority=Sum(
-F("quantity")
* 0.95 ** ExtractDay(timezone.now().date() - F("timestamp")),
output_field=FloatField(),
filter=Q(user=user),
default=0,
),
last_purchase=Max("timestamp"),
)
)

for event in events:
purchases[event.product_id].append(event)
purchases[event["product_id"]] = PurchaseStats(
event["global_priority"], event["user_priority"], event["last_purchase"]
)

return purchases


@dataclass
class AnnotatedProduct:
product: Product
stats: PurchaseStats
local_quantity: int = 0
total_quantity: int = 0
last_purchase: datetime | None = None
global_priority: float = 0
user_priority: float = 0


def get_priority_value(e: WarehouseEvent):
return -e.quantity * 0.95 ** (timezone.now() - e.timestamp).days


def annotate_products(
products: Iterable[Product], warehouse: Warehouse, user: AbstractUser | None = None
products: Iterable[Product], warehouse: Warehouse, user: User
) -> list[AnnotatedProduct]:
quantities = get_inventory_quantities(warehouse)
purchases = get_purchases(warehouse)
purchases = get_purchases(warehouse, user)

annotated: list[AnnotatedProduct] = []
for p in products:
ap = AnnotatedProduct(p)
ap = AnnotatedProduct(p, purchases[p.id])

if not p.is_unlimited:
ap.local_quantity = quantities[p.id].local
ap.total_quantity = quantities[p.id].total

purchase_log = purchases[p.id]
if purchase_log:
ap.last_purchase = purchase_log[0].timestamp

ap.global_priority = sum(map(get_priority_value, purchase_log))
user_purchases = list(filter(lambda e: e.user_id == user.id, purchase_log))
if user_purchases:
ap.user_priority = sum(map(get_priority_value, user_purchases))

annotated.append(ap)
return annotated

Expand All @@ -98,15 +117,15 @@ def product_sort_key(p: AnnotatedProduct):

return (
availability,
p.user_priority,
p.global_priority,
p.last_purchase if p.last_purchase else datetime.min,
p.stats.user_priority,
p.stats.global_priority,
p.stats.last_purchase,
p.product.name,
)


def get_product_list(
products: Iterable[Product], warehouse: Warehouse, user: User | None = None
products: Iterable[Product], warehouse: Warehouse, user: User
) -> list[AnnotatedProduct]:
annotated = annotate_products(products, warehouse, user)
return sorted(annotated, key=product_sort_key, reverse=True)
2 changes: 1 addition & 1 deletion sortiment/store/templates/store/_item_image.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load icon %}
<div class="w-full flex items-center justify-center aspect-square rounded-md bg-white border overflow-hidden">
{% if product.image %}
<img class="max-w-full max-h-full" src="{{ product.image.url }}">
<img class="max-w-full max-h-full" src="{{ product.image.url }}" loading="lazy">
{% else %}
{% icon "help-circle" class="w-1/2 h-1/2 text-blue-300" %}
{% endif %}
Expand Down
32 changes: 18 additions & 14 deletions sortiment/store/templates/store/_products_list.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
{% load cache %}

{% if products %}
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 p-6">
{% for p in products %}
<a class="hover:bg-gray-50 p-2 rounded-lg cursor-pointer {% if not p.product.is_unlimited and p.local_quantity <= 0 %} opacity-30 grayscale hover:opacity-100 {% endif %}" role="button" href="{% url 'store:cart_add' p.product.id %}" data-turbo-method="post" data-turbo-frame="turbo-cart" {% if forloop.first %}data-barcode-target="firstProduct"{% endif %}
{% if request.GET.query == p.product.barcode %}data-exact-match="1"{% endif %}>
{% include "store/_item_image.html" with product=p.product %}
<h2 class="text-lg font-bold mt-1">{{ p.product.name }}</h2>
{% cache 3600 "product_detail" p.product.id p.global_quantity %}
<a class="hover:bg-gray-50 p-2 rounded-lg cursor-pointer {% if not p.product.is_unlimited and p.local_quantity <= 0 %} opacity-30 grayscale hover:opacity-100 {% endif %}" role="button" href="{% url 'store:cart_add' p.product.id %}" data-turbo-method="post" data-turbo-frame="turbo-cart" {% if forloop.first %}data-barcode-target="firstProduct"{% endif %}
{% if request.GET.query == p.product.barcode %}data-exact-match="1"{% endif %}>
{% include "store/_item_image.html" with product=p.product %}
<h2 class="text-lg font-bold mt-1">{{ p.product.name }}</h2>

<div class="flex text-sm text-gray-700">
<div>{{ p.product.price }}&nbsp;&euro;</div>
<div class="ml-auto">
{% if p.product.is_unlimited %}
&infin;&nbsp;ks
{% else %}
{{ p.local_quantity }}/{{ p.total_quantity }}&nbsp;ks
{% endif %}
<div class="flex text-sm text-gray-700">
<div>{{ p.product.price }}&nbsp;&euro;</div>
<div class="ml-auto">
{% if p.product.is_unlimited %}
&infin;&nbsp;ks
{% else %}
{{ p.local_quantity }}/{{ p.total_quantity }}&nbsp;ks
{% endif %}
</div>
</div>
</div>
</a>
</a>
{% endcache %}
{% endfor %}
</div>
{% else %}
Expand Down

0 comments on commit 4286376

Please sign in to comment.