🚚 Rename app main to bbs
🚚 Move settings into main ➕ Add django-rename-app
This commit is contained in:
1 parent
e1eaf3df98
commit
9b903f42d6
39 files changed
+82
-73
No files matched your search
Executable
Whitespace-only changes.
@@ -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)
|
||||
Whitespace-only changes.
@@ -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
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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
|
||||
Whitespace-only changes.
@@ -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)
|
||||
@@ -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()
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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")
|
||||
@@ -0,0 +1 @@
|
||||
__author__ = "Nikolai"
|
||||
@@ -0,0 +1 @@
|
||||
__author__ = "Nikolai"
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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")]),
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
Executable
+247
@@ -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}"
|
||||
Executable
+147
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
@@ -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}"
|
||||
Whitespace-only changes.
@@ -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())
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user