🚚 Rename app main to bbs

🚚 Move settings into main
➕ Add django-rename-app
This commit is contained in:
nikolark committed 2022-02-20 00:39:16 +01:00
1 parent e1eaf3df98
commit 9b903f42d6
39 files changed
+82 -73

No files matched your search

View File
Whitespace-only changes.
+15
View File
@@ -0,0 +1,15 @@
from django.contrib import admin
from bbs.models import *
class ProductAdmin(admin.ModelAdmin):
pass
admin.site.register(Product, ProductAdmin)
admin.site.register(InventoryTransaction)
admin.site.register(Order)
admin.site.register(OrderLine)
admin.site.register(Transaction)
admin.site.register(UserProfile)
View File
Whitespace-only changes.
+22
View File
@@ -0,0 +1,22 @@
import strawberry
@strawberry.interface
class Error:
message: str
@strawberry.field()
def error(self) -> bool:
return True
@strawberry.type
class FieldError(Error):
field: str
@strawberry.type
class FormErrors:
fields: list[FieldError]
message: str = ""
error: bool = True
View File
Whitespace-only changes.
+137
View File
@@ -0,0 +1,137 @@
import strawberry
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import transaction
from django.shortcuts import get_object_or_404
from strawberry import ID
from bbs import models
from bbs.api.errors import Error, FieldError, FormErrors
from bbs.api.types.inventory import Transaction
from bbs.api.types.shop import Order
UserModel = get_user_model()
@strawberry.input
class OrderLineInput:
product_id: ID
amount: int
@strawberry.type
class CreateOrderSuccess:
order: Order
@strawberry.type
class InsufficientFunds(Error):
amount_lacking: float
CreateOrderResponse = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors])
@strawberry.mutation
@transaction.atomic
def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> CreateOrderResponse:
# clean and validate
try:
user = UserModel.objects.get(pk=customer_id)
except (UserModel.DoesNotExist, ValueError):
user = None
if user is None and not is_external:
message = "Du har ikke valgt hvem som skal kjøpe."
return FormErrors(
message=message,
fields=[FieldError(field="customer_id", message=message)],
)
if not order_lines:
message = "Du har ikke valgt hva du skal kjøpe."
return FormErrors(
message=message,
fields=[FieldError(field="order_lines", message=message)],
)
ols = []
order_sum = 0
for ol in order_lines:
product = get_object_or_404(models.Product, pk=ol.product_id)
price = product.sale_price_ext if is_external else product.sale_price_int
ols.append(models.OrderLine(product=product, amount=ol.amount, unit_price=price))
order_sum += ol.amount * price
product.inventory_amount -= ol.amount
product.save()
if not is_external and user.profile.balance <= order_sum:
return InsufficientFunds(
message="Du har ikke nok penger på bok.", amount_lacking=abs(order_sum - user.profile.balance)
)
# create
order = models.Order.objects.create(customer=user, order_sum=order_sum)
for ol in ols:
ol.order = order
models.OrderLine.objects.bulk_create(ols)
# update balance
if not is_external:
user.profile.balance -= order_sum
user.profile.save()
return CreateOrderSuccess(order=order)
@strawberry.type
class CreateDepositSuccess:
transaction: Transaction
@strawberry.type
class AboveMaxSaldo(Error):
max_saldo: float
above: float
CreateDepositResponse = strawberry.union("CreateDepositResponse", [CreateDepositSuccess, AboveMaxSaldo, FormErrors])
@strawberry.mutation
@transaction.atomic
def create_deposit(user_id: ID, amount: int) -> CreateDepositResponse:
# clean and validate
try:
user = UserModel.objects.get(pk=user_id)
except (UserModel.DoesNotExist, ValueError):
message = "Du har ikke valgt hvem som sette inn penger."
return FormErrors(
message=message,
fields=[FieldError(field="user_id", message=message)],
)
if amount <= 0:
message = "Du har ikke valgt beløp som skal settes inn."
return FormErrors(
message=message,
fields=[FieldError(field="amount", message=message)],
)
# 1337 is special <3
if amount + user.profile.balance > settings.BBS_SALDO_MAX and amount + user.profile.balance != 1337:
return AboveMaxSaldo(
message="Du prøver å sette inn for mye penger",
max_saldo=settings.BBS_SALDO_MAX,
above=amount + user.profile.balance - settings.BBS_SALDO_MAX,
)
trans = models.Transaction.objects.create(user=user, amount=amount)
profile = trans.user.profile
profile.balance += trans.amount
profile.save()
return CreateDepositSuccess(transaction=trans)
@strawberry.type
class ShopMutations:
create_order = create_order
create_deposit = create_deposit
+32
View File
@@ -0,0 +1,32 @@
import strawberry
import strawberry_django
from django.contrib.auth import get_user_model
from django.db import transaction
from strawberry_django import auto
from bbs import models
from bbs.api.types.users import User
DjangoUser = get_user_model()
@strawberry_django.input(DjangoUser)
class UserInput:
email: auto
first_name: auto
last_name: auto
@strawberry.mutation
@transaction.atomic
def register_mutation(input: UserInput) -> User:
user = DjangoUser.objects.create_user(
first_name=input.first_name, last_name=input.last_name, email=input.email, username=input.email
)
models.UserProfile.objects.create(user=user)
return user
@strawberry.type
class UsersMutations:
create_user = register_mutation
View File
Whitespace-only changes.
+9
View File
@@ -0,0 +1,9 @@
import strawberry
import strawberry_django
from bbs.api.types.inventory import Transaction, TransactionOrdering
@strawberry.type
class InventoryQueries:
transaction_list: list[Transaction] = strawberry_django.field(pagination=True, order=TransactionOrdering)
+31
View File
@@ -0,0 +1,31 @@
import strawberry
import strawberry_django
from bbs import models
from bbs.api.types.shop import Order, OrderOrdering, OrderStats, OrderStatsByTime, Point, Product, ProductOrderStat
@strawberry.type
class ShopQueries:
all_products: list[Product] = strawberry_django.field()
all_orders: list[Order] = strawberry_django.field()
order_list: list[Order] = strawberry_django.field(pagination=True, order=OrderOrdering)
@strawberry.field
def order_stats(self) -> OrderStats:
return OrderStats(
**{
period: [
OrderStatsByTime(**stat)
for stat in models.Order.objects.count_by_created_period(period).order_by("period")
]
for period in ["yearly", "monthly", "daily", "hourly"]
}
)
@strawberry.field
def product_stats(self) -> list[ProductOrderStat]:
return [
ProductOrderStat(product_name=stat["product_name"], data=[Point(**p) for p in stat["data"]])
for stat in models.OrderLine.objects.product_order_stats().as_series()
]
+15
View File
@@ -0,0 +1,15 @@
import strawberry
import strawberry_django
from django.contrib.auth.models import User as DjangoUser
from strawberry import ID
from bbs.api.types.users import User
@strawberry.type
class UsersQueries:
all_users: list[User] = strawberry_django.field()
@strawberry.field
def user(self, user_id: ID) -> User:
return DjangoUser.objects.get(pk=user_id)
+20
View File
@@ -0,0 +1,20 @@
import strawberry
from bbs.api.mutations.shop import ShopMutations
from bbs.api.mutations.users import UsersMutations
from bbs.api.queries.inventory import InventoryQueries
from bbs.api.queries.shop import ShopQueries
from bbs.api.queries.users import UsersQueries
@strawberry.type
class Query(InventoryQueries, ShopQueries, UsersQueries):
pass
@strawberry.type
class Mutation(ShopMutations, UsersMutations):
pass
schema = strawberry.Schema(query=Query, mutation=Mutation)
View File
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
import strawberry_django
from strawberry_django import auto
from bbs import models
from bbs.api.types.users import User
@strawberry_django.ordering.order(models.Transaction)
class TransactionOrdering:
created: auto
@strawberry_django.type(models.Transaction)
class Transaction:
id: auto
user: "User"
amount: auto
created: auto
+73
View File
@@ -0,0 +1,73 @@
import strawberry
import strawberry_django
from strawberry_django import auto
from bbs import models
from bbs.api.types.utils import JSONScalar
@strawberry_django.filters.filter(models.Product)
class ProductFilter:
id: auto
active: auto
@strawberry_django.type(models.Product, filters=ProductFilter)
class Product:
id: auto
name: auto
image_url: str
sale_price_int: auto
sale_price_ext: auto
# FIXME: stricter types
user_counts: JSONScalar
@strawberry_django.ordering.order(models.Order)
class OrderOrdering:
created: auto
@strawberry_django.type(models.Order)
class Order:
id: auto
customer: strawberry.LazyType["User", "bbs.api.types.users"]
order_sum: auto
created: auto
orderlines: list["OrderLine"]
is_external: bool
@strawberry_django.type(models.OrderLine)
class OrderLine:
id: auto
order: Order
product: Product
amount: auto
unit_price: auto
@strawberry.type
class OrderStatsByTime:
period: int
count: str
@strawberry.type
class OrderStats:
yearly: list[OrderStatsByTime]
monthly: list[OrderStatsByTime]
daily: list[OrderStatsByTime]
hourly: list[OrderStatsByTime]
@strawberry.type
class Point:
x: str
y: int
@strawberry.type
class ProductOrderStat:
product_name: str
data: list[Point]
+46
View File
@@ -0,0 +1,46 @@
from datetime import datetime
import strawberry
import strawberry_django
from django.contrib.auth import get_user_model
from strawberry_django import auto
from bbs import models
from bbs.api.types.shop import Point, ProductOrderStat
UserModel = get_user_model()
@strawberry.type
class TopMonth:
period: str
count: int
sum: int
@strawberry_django.type(models.UserProfile)
class UserProfile:
id: auto
balance: auto
image: auto
last_purchase_date: datetime
order_sum_total: float
@strawberry.field()
def top_months(self) -> list[TopMonth]:
return [TopMonth(**month) for month in self.top_months()]
@strawberry.field()
def product_order_stats(self) -> list[ProductOrderStat]:
return [
ProductOrderStat(product_name=stat["product_name"], data=[Point(**p) for p in stat["data"]])
for stat in self.product_order_stats().as_series()
]
@strawberry_django.type(UserModel)
class User:
id: auto
first_name: auto
last_name: auto
profile: UserProfile
+11
View File
@@ -0,0 +1,11 @@
import json
from typing import Any, NewType
import strawberry
JSONScalar = strawberry.scalar(
NewType("JSONScalar", Any),
serialize=lambda v: v,
parse_value=lambda v: json.loads(v),
description="The GenericScalar scalar type represents a generic GraphQL scalar value that could be: List or Object.",
)
+10
View File
@@ -0,0 +1,10 @@
from django.apps import AppConfig
class BBSAppConfig(AppConfig):
name = "bbs"
verbose_name = "Biceps Bar System"
def ready(self):
# all models are loaded, could attach signals
pass
+21
View File
@@ -0,0 +1,21 @@
from django import forms
from bbs.models import InventoryTransaction, Product
class InventoryTransactionForm(forms.ModelForm):
unit_price = forms.FloatField(required=True, localize=True)
def __init__(self, *args, **kwargs):
# Only show active products
super(InventoryTransactionForm, self).__init__(*args, **kwargs)
self.fields["product"].queryset = Product.objects.filter(active=True)
class Meta:
model = InventoryTransaction
fields = ["product", "amount", "unit_price", "comment"]
class DateRangeForm(forms.Form):
start_time = forms.DateTimeField(required=False, label="Fra")
end_time = forms.DateTimeField(required=False, label="Til")
+1
View File
@@ -0,0 +1 @@
__author__ = "Nikolai"
+1
View File
@@ -0,0 +1 @@
__author__ = "Nikolai"
+23
View File
@@ -0,0 +1,23 @@
from pathlib import Path
from django.conf import settings
from django.core.management import BaseCommand, CommandParser
from strawberry.printer import print_schema
from bbs.api.schema import schema as schema_symbol
class Command(BaseCommand):
help = " Write graphql schema to file or stdout"
def add_arguments(self, parser: CommandParser):
parser.add_argument("--filename", default="schema.graphql", help="`-` prints to stdout")
def handle(self, *args, filename=None, **options):
schema_output = print_schema(schema_symbol)
if filename != "-":
with Path(settings.BASE_DIR, filename).open("w+") as fp:
fp.write(schema_output)
self.stdout.write(self.style.SUCCESS("OK"))
else:
print(schema_output)
+20
View File
@@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand, CommandParser
from bbs.models import UserProfile
class Command(BaseCommand):
"""Set profile image URL for all users"""
def add_arguments(self, parser: CommandParser):
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--force", action="store_true")
def handle(self, *args, dry_run=False, force=False, **options):
for profile in UserProfile.objects.all().order_by("id"):
if dry_run:
print(profile.user.email, profile.image_url())
else:
if force or not profile.image:
profile.image = profile.image_url()
profile.save()
+185
View File
@@ -0,0 +1,185 @@
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="InventoryTransaction",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("amount", models.FloatField(verbose_name="Antall")),
("unit_price", models.FloatField(verbose_name="Enhetspris")),
("comment", models.TextField(verbose_name="Kommentar", blank=True)),
("created", models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name="Order",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("order_sum", models.FloatField(db_column="sum")),
("created", models.DateTimeField(auto_now_add=True)),
(
"customer",
models.ForeignKey(
related_name="orders",
on_delete=models.CASCADE,
blank=True,
to=settings.AUTH_USER_MODEL,
null=True,
),
),
],
),
migrations.CreateModel(
name="OrderLine",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("amount", models.IntegerField()),
("unit_price", models.FloatField(verbose_name="Enhetspris")),
(
"order",
models.ForeignKey(
on_delete=models.CASCADE,
related_name="orderlines",
to="bbs.Order",
),
),
],
),
migrations.CreateModel(
name="Product",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=64)),
("sale_price_int", models.FloatField()),
("sale_price_ext", models.FloatField()),
("description", models.CharField(max_length=256, blank=True)),
("volume_liter", models.FloatField(null=True)),
("alcohol_percent", models.FloatField(null=True)),
("image", models.ImageField(upload_to="products", blank=True)),
("customer_support", models.CharField(max_length=32, blank=True)),
("inventory_amount", models.FloatField(default=0)),
("active", models.BooleanField(default=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="Transaction",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("amount", models.FloatField()),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=models.CASCADE,
related_name="transactions",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="UserProfile",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("balance", models.FloatField(default=0.0)),
("image", models.URLField(blank=True)),
(
"user",
models.OneToOneField(
on_delete=models.CASCADE,
related_name="profile",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name="orderline",
name="product",
field=models.ForeignKey(on_delete=models.CASCADE, related_name="orderlines", to="bbs.Product"),
),
migrations.AddField(
model_name="inventorytransaction",
name="product",
field=models.ForeignKey(
on_delete=models.CASCADE,
related_name="transactions",
verbose_name="Produkt",
to="bbs.Product",
),
),
migrations.AddField(
model_name="inventorytransaction",
name="user",
field=models.ForeignKey(
on_delete=models.CASCADE,
related_name="inventory_transactions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterUniqueTogether(
name="orderline",
unique_together=set([("order", "product")]),
),
]
View File
Whitespace-only changes.
Executable
+247
View File
@@ -0,0 +1,247 @@
import hashlib
from datetime import datetime
from itertools import groupby
from typing import Type
from django.conf import settings
from django.db import models
from django.db.models import Count, F, Sum
from django.db.models.functions import (
Extract,
ExtractHour,
ExtractIsoWeekDay,
ExtractMonth,
ExtractYear,
TruncDay,
TruncMonth,
)
EXTERNAL_USER = "Ekstern"
def _format_counts(per_user):
per_user = list(per_user)
for row in per_user:
if row.get("order__customer__first_name") is None:
who = EXTERNAL_USER
else:
first_name = row.pop("order__customer__first_name")
last_name = row.pop("order__customer__last_name")
last_name_initial = f" {last_name[0]}" if last_name else ""
who = f"{first_name}{last_name_initial}"
row["name"] = who
return per_user
class Product(models.Model):
name = models.CharField(max_length=64)
sale_price_int = models.FloatField()
sale_price_ext = models.FloatField()
description = models.CharField(max_length=256, blank=True)
volume_liter = models.FloatField(null=True)
alcohol_percent = models.FloatField(null=True)
image = models.ImageField(upload_to="products", blank=True)
customer_support = models.CharField(max_length=32, blank=True)
inventory_amount = models.FloatField(default=0)
active = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
@property
def wholesale_unit_price(self):
"""
This is not accurate.
"""
if self.inventory_amount <= 0:
return 0
unit_prices = self.transactions.order_by("created").values("amount", "unit_price")
sum_units = 0
for up in unit_prices:
sum_units += up["amount"]
if self.inventory_amount <= sum_units:
return up["unit_price"]
if len(unit_prices) > 0:
return unit_prices[0]["unit_price"]
return 0
@property
def wholesale_value(self):
return self.wholesale_unit_price * self.inventory_amount
@property
def image_url(self):
return self.image.url if self.image else ""
def user_counts(self):
name_fields = ["order__customer__first_name", "order__customer__last_name"]
per_user = OrderLine.objects.filter(product=self)
per_user = per_user.values(*name_fields).annotate(count=Sum("amount")).order_by("-count")
return _format_counts(per_user)
def __str__(self):
return self.name
class Meta:
ordering = ["name"]
class OrderManager(models.Manager):
def count_by_created(self, extract_class: Type[Extract]):
period = extract_class("created")
return self.get_queryset().annotate(period=period).values("period").annotate(count=Count("id"))
def count_by_created_period(self, period: str):
periods = {"yearly": ExtractYear, "monthly": ExtractMonth, "daily": ExtractIsoWeekDay, "hourly": ExtractHour}
assert period in periods
return self.count_by_created(periods[period])
class Order(models.Model):
customer = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="orders",
)
order_sum = models.FloatField(db_column="sum")
created = models.DateTimeField(auto_now_add=True)
objects = OrderManager()
def is_external(self):
return self.customer is None
def reciept(self):
return ", ".join([str(ol) for ol in self.orderlines.all()])
def __str__(self):
return f"{self.customer or EXTERNAL_USER}: {self.order_sum} kr"
class OrderLineManager(models.QuerySet):
def product_order_stats(self):
return (
self.annotate(date=TruncDay("order__created"))
.values("date", product_name=F("product__name"))
.annotate(count=Sum("amount"))
.order_by("date")
)
def as_series(self):
datas = list(self)
datas.sort(key=lambda x: x["product_name"])
return [
{
"product_name": key,
"data": [{"x": item["date"].date().isoformat(), "y": item["count"]} for item in list(group)],
}
for key, group in groupby(datas, key=lambda x: x["product_name"])
]
class OrderLine(models.Model):
order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines")
product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines")
amount = models.IntegerField()
unit_price = models.FloatField(verbose_name="Enhetspris")
objects = OrderLineManager.as_manager()
@property
def price(self):
return self.amount * self.unit_price
def __str__(self):
return f"{self.amount} {self.product} ({self.unit_price} kr per)"
class Meta:
unique_together = ("order", "product")
class Transaction(models.Model):
"""Money"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions")
amount = models.FloatField()
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.id}: { self.user}: {self.amount}"
class InventoryTransaction(models.Model):
"""Goods"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="inventory_transactions",
)
product = models.ForeignKey(
"bbs.Product",
on_delete=models.CASCADE,
related_name="transactions",
verbose_name="Produkt",
)
amount = models.FloatField(verbose_name="Antall")
unit_price = models.FloatField(verbose_name="Enhetspris")
comment = models.TextField(verbose_name="Kommentar", blank=True)
created = models.DateTimeField(auto_now_add=True)
@property
def price(self):
return self.amount * self.unit_price
def __str__(self):
return f"{self.id}: {self.user}: {self.product}. {self.amount:.0f} stykker ({self.unit_price} kr per)"
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
balance = models.FloatField(default=0.0)
image = models.URLField(blank=True)
def last_purchase(self):
return self.user.orders.order_by("-created").first()
def last_purchase_date(self) -> datetime:
last = self.last_purchase()
last_p = last.created if last else datetime.min
return last_p
def top_months(self, limit=5):
return (
Order.objects.filter(customer_id=self.user_id)
.annotate(period=TruncMonth("created"))
.values("period")
.annotate(sum=Sum("order_sum"), count=Count("pk"))
.order_by("-sum")[:limit]
)
def product_order_stats(self):
return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
def order_sum_total(self) -> float:
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
def image_url(self) -> str:
if self.user.email:
md5_hash = hashlib.md5(self.user.email.lower().strip().encode()).hexdigest()
return f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon"
return ""
def save(self, **kwargs):
if not self.image:
self.image = self.image_url()
super().save(**kwargs)
def __str__(self):
return f"{self.user}"
+147
View File
@@ -0,0 +1,147 @@
# Django settings for bbs project.
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
DEBUG = True
ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
MANAGERS = ADMINS
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "db.sqlite3",
},
}
TIME_ZONE = "Europe/Oslo"
LANGUAGE_CODE = "nb"
USE_I18N = True
USE_L10N = True
SITE_ID = 1
# Media and uploads
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
# Static files
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATIC_URL = "/static/"
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = "osv$uy-ea$*%lc)+xiwt5@2b7au%u*c!pa6hmh#vl)*+tp%fss"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(BASE_DIR, "templates"),
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.request",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
]
},
},
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
]
ROOT_URLCONF = "bbs.urls"
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.admin",
"django_extensions",
"easy_thumbnails",
"corsheaders",
"django_rename_app",
]
LOCAL_APPS = [
"bbs",
]
INSTALLED_APPS += LOCAL_APPS
LOGIN_REDIRECT_URL = "/"
SESSION_COOKIE_AGE = 1209600 # 15 minutes
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
BBS_SALDO_MAX = 600
BBS_LIMIT_DEPOSITS = False
# Thumbnails
THUMBNAIL_ALIASES = {
"": {
"product": {"size": (64, 64), "crop": True},
},
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"root": {
"level": "INFO",
"handlers": ["console"],
},
"formatters": {
"verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"django.db.backends": {
"level": "ERROR",
"handlers": ["console"],
"propagate": False,
},
},
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
CORS_ALLOWED_ORIGINS = [
"https://bbs.neuf.no",
"http://localhost:8000",
"http://localhost:3000",
]
try:
from .local_settings import *
except ImportError:
pass
+48
View File
@@ -0,0 +1,48 @@
import pytest
from django.test import Client
from strawberry.django.test import GraphQLTestClient
from bbs.models import Order, Product
@pytest.fixture(name="gql_client")
def gql_client_fixture(client: Client):
yield GraphQLTestClient(client)
@pytest.fixture(name="product")
def product_fix(db):
return Product.objects.create(name="borg", sale_price_ext=35, sale_price_int=30)
def test_create_order(product, gql_client):
assert Order.objects.count() == 0
variables = dict(customerId="external", isExternal=True, orderLines=[{"productId": product.id, "amount": 1}])
query = """
mutation CreateOrder($customerId: ID!, $isExternal: Boolean!, $orderLines: [OrderLineInput!]!) {
createOrder(customerId: $customerId, isExternal: $isExternal, orderLines: $orderLines) {
... on CreateOrderSuccess {
order {
id
orderSum
orderlines {
id
product {
id
}
}
}
}
}
}
"""
res = gql_client.query(query, variables)
assert res
assert not res.errors
assert res.data
order_lines = res.data.get("createOrder", {}).get("order", {}).get("orderlines", [])
assert order_lines
assert order_lines[0]["product"]["id"] == str(product.id)
assert Order.objects.count() == 1
o = Order.objects.first()
assert o.orderlines.first().product.id == product.id
+25
View File
@@ -0,0 +1,25 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path, re_path
from strawberry.django.views import GraphQLView
from bbs.api.schema import schema
from bbs.views.reports import inventory, inventory_add, report
admin.autodiscover()
urlpatterns = [
# TODO: migrate reports to SPA
re_path(r"^inventory/$", inventory, name="inventory"),
re_path(r"^inventory/add/$", inventory_add, name="inventory-add"),
re_path(r"^report$", report, name="report"),
path("accounts/login/", LoginView.as_view(), name="django.contrib.auth.views.login"),
path("accounts/logout/", LogoutView.as_view(), name="django.contrib.auth.views.logout"),
path("admin/", admin.site.urls),
path("graphql/", GraphQLView.as_view(schema=schema), name="graphql"),
]
urlpatterns += static("/media", document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+5
View File
@@ -0,0 +1,5 @@
def format_username(firstname: str, lastname: str):
first = firstname.lower().strip().replace(" ", "")[:6].encode("ascii", "ignore").decode("utf-8")
last = lastname.lower().strip().replace(" ", "")[:6].encode("ascii", "ignore").decode("utf-8")
return f"{first}{last}"
View File
Whitespace-only changes.
+145
View File
@@ -0,0 +1,145 @@
from datetime import datetime, timedelta
from itertools import groupby
from django.contrib import messages
from django.shortcuts import redirect, render
from bbs.forms import DateRangeForm, InventoryTransactionForm
from bbs.models import InventoryTransaction, Order, Product, Transaction, UserProfile
def report(request):
# FIXME: split this up in smaller pieces
datetime_now = datetime.now()
start_time = datetime.now() - timedelta(days=30)
# start of day, 1 month
start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0)
# end of day
end_time = datetime.now().replace(hour=23, minute=59, second=59, microsecond=999)
if request.GET.get("start_time", False) or request.GET.get("end_time", False):
form = DateRangeForm(request.GET)
if form.is_valid():
start_time = form.cleaned_data["start_time"]
# end of day
end_time = form.cleaned_data["end_time"].replace(hour=23, minute=59, second=59, microsecond=999)
else:
form = DateRangeForm(
initial={
"start_time": start_time.strftime("%Y-%m-%d"),
"end_time": end_time.strftime("%Y-%m-%d"),
}
)
# Inventory
products = (
Product.objects.filter(transactions__created__range=[start_time, end_time])
.values("name", "transactions__unit_price", "transactions__amount")
.order_by("name")
)
# group by name and sum
inventory_products = {}
for k, g in groupby(products, lambda x: x["name"]):
group = []
for el in g:
el["transactions__price"] = el["transactions__amount"] * el["transactions__unit_price"]
group.append(el)
inventory_products.update(
{
k: {
"name": k,
"transactions": group,
"transactions_sum": sum(map(lambda x: x["transactions__price"], group)),
"transactions_units": sum(map(lambda x: x["transactions__amount"], group)),
}
}
)
itrans = InventoryTransaction.objects.filter(created__range=[start_time, end_time])
inv_in = sum(map(lambda x: x[1].get("transactions_sum", 0), inventory_products.items()))
# out
out_products = (
Product.objects.filter(orderlines__order__created__range=[start_time, end_time])
.values("name", "orderlines__unit_price", "orderlines__amount")
.order_by("name")
)
# group by name and sum
for k, g in groupby(out_products, lambda x: x["name"]):
group = []
for el in g:
el["orderlines__price"] = el["orderlines__amount"] * el["orderlines__unit_price"]
group.append(el)
product = {
"name": k,
"orderlines": group,
"orderlines_sum": sum(map(lambda x: x["orderlines__price"], group)),
"orderlines_units": sum(map(lambda x: x["orderlines__amount"], group)),
}
if k in inventory_products:
# add diff
inventory_products[k].update(product)
else:
inventory_products.update({k: product})
for p in inventory_products.items():
p[1]["value_diff"] = p[1].get("transactions_sum", 0) - p[1].get("orderlines_sum", 0)
p[1]["units_diff"] = p[1].get("transactions_units", 0) - p[1].get("orderlines_units", 0)
inv_out = sum(map(lambda x: x[1].get("orderlines_sum", 0), inventory_products.items()))
inv_diff = inv_in - inv_out
# Deposits and sales
saldo_profiles = sum(UserProfile.objects.all().values_list("balance", flat=True))
deposits = sum(Transaction.objects.all().values_list("amount", flat=True))
purchases = sum(Order.objects.all().values_list("order_sum", flat=True))
saldo = deposits - purchases
deposits_start = sum(Transaction.objects.filter(created__lt=start_time).values_list("amount", flat=True))
purchases_start = sum(Order.objects.filter(created__lt=start_time).values_list("order_sum", flat=True))
saldo_start = deposits_start - purchases_start
deposits_end = sum(Transaction.objects.filter(created__lt=end_time).values_list("amount", flat=True))
purchases_end = sum(Order.objects.filter(created__lt=end_time).values_list("order_sum", flat=True))
saldo_end = deposits_end - purchases_end
deposits_diff = deposits_end - deposits_start
purchases_diff = purchases_end - purchases_start
saldo_diff = saldo_end - saldo_start
return render(request, "report.html", locals())
def inventory(request):
products = Product.objects.all().order_by("-active", "inventory_amount", "name")
transactions = InventoryTransaction.objects.all().order_by("-created")
products_active = products.filter(active=True)
wholesale_value = sum([p.wholesale_value for p in products_active])
num_products = sum(products_active.values_list("inventory_amount", flat=True))
return render(request, "inventory.html", locals())
def inventory_add(request):
if request.method == "POST":
form = InventoryTransactionForm(data=request.POST)
if form.is_valid():
itrans = form.save(commit=False)
# Add amount and user, then save
itrans.product.inventory_amount += itrans.amount
itrans.user = request.user
itrans.product.save()
itrans.save()
messages.success(request, "Satt inn {0} stk {1}.".format(itrans.amount, itrans.product))
return redirect("inventory-add")
else:
messages.error(request, "Feil med skjemaet, se under.")
form = InventoryTransactionForm(data=request.POST)
else:
form = InventoryTransactionForm()
return render(request, "inventory_add.html", locals())
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bbs.settings")
application = get_wsgi_application()