Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
362de62e2f | ||
|
|
ff09985944 | ||
|
|
d76c911d4b | ||
|
|
e60473e7ed | ||
|
|
d596c31353 | ||
|
|
90558a3a28 | ||
|
|
57a6c412d5 | ||
|
|
19f58211e7 | ||
|
|
0726754678 | ||
|
|
480fba870a | ||
|
|
ed08c60380 |
No files matched your search
@@ -26,6 +26,7 @@ npm start
|
||||
- improve product listing (light purple background)
|
||||
- Stats: products over time
|
||||
- inventory views
|
||||
- Stats: Implement users tab
|
||||
|
||||
|
||||
## TODO-old
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from decimal import Decimal
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@@ -25,7 +26,7 @@ class UserProfile:
|
||||
balance: auto
|
||||
image: auto
|
||||
last_purchase_date: datetime
|
||||
order_sum_total: float
|
||||
order_sum_total: Decimal
|
||||
|
||||
@strawberry.field()
|
||||
def top_months(self) -> List[TopMonth]:
|
||||
|
||||
+3
-1
@@ -2,9 +2,11 @@ from django import forms
|
||||
|
||||
from bbs.models import InventoryTransaction, Product
|
||||
|
||||
MAX_DIGITS = 9
|
||||
DECIMAL_PLACES = 1
|
||||
|
||||
class InventoryTransactionForm(forms.ModelForm):
|
||||
unit_price = forms.FloatField(required=True, localize=True)
|
||||
unit_price = forms.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, required=True, localize=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Only show active products
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-05 19:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bbs", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="inventorytransaction",
|
||||
name="amount",
|
||||
field=models.IntegerField(verbose_name="Antall"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="inventorytransaction",
|
||||
name="unit_price",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="orderline",
|
||||
name="unit_price",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="alcohol_percent",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="inventory_amount",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="sale_price_ext",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="sale_price_int",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="volume_liter",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transaction",
|
||||
name="amount",
|
||||
field=models.DecimalField(decimal_places=1, max_digits=9),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="balance",
|
||||
field=models.DecimalField(decimal_places=1, default=0, max_digits=9),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.2 on 2023-06-28 19:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bbs', '0002_alter_inventorytransaction_amount_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='order_sum',
|
||||
field=models.DecimalField(db_column='sum', decimal_places=1, max_digits=9),
|
||||
),
|
||||
]
|
||||
+20
-12
@@ -2,6 +2,7 @@ import hashlib
|
||||
from datetime import datetime
|
||||
from itertools import groupby
|
||||
from typing import Type
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
@@ -17,6 +18,8 @@ from django.db.models.functions import (
|
||||
)
|
||||
|
||||
EXTERNAL_USER = "Ekstern"
|
||||
MAX_DIGITS = 9
|
||||
DECIMAL_PLACES = 1
|
||||
|
||||
|
||||
def _format_counts(per_user):
|
||||
@@ -36,14 +39,15 @@ def _format_counts(per_user):
|
||||
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=64)
|
||||
sale_price_int = models.FloatField()
|
||||
sale_price_ext = models.FloatField()
|
||||
sale_price_int = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
||||
sale_price_ext = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
||||
description = models.CharField(max_length=256, blank=True)
|
||||
volume_liter = models.FloatField(null=True)
|
||||
alcohol_percent = models.FloatField(null=True)
|
||||
volume_liter = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
|
||||
alcohol_percent = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, 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)
|
||||
# FIXME: rename to inventory_quantity
|
||||
inventory_amount = models.IntegerField(default=0)
|
||||
active = models.BooleanField(default=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
@@ -109,7 +113,7 @@ class Order(models.Model):
|
||||
blank=True,
|
||||
related_name="orders",
|
||||
)
|
||||
order_sum = models.FloatField(db_column="sum")
|
||||
order_sum = models.DecimalField(db_column="sum", max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
objects = OrderManager()
|
||||
@@ -148,8 +152,10 @@ class OrderLineManager(models.QuerySet):
|
||||
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")
|
||||
# FIXME: rename to quantity
|
||||
amount = models.IntegerField()
|
||||
unit_price = models.FloatField(verbose_name="Enhetspris")
|
||||
# FIXME: rename to price
|
||||
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
|
||||
|
||||
objects = OrderLineManager.as_manager()
|
||||
|
||||
@@ -168,7 +174,7 @@ class Transaction(models.Model):
|
||||
"""Money"""
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions")
|
||||
amount = models.FloatField()
|
||||
amount = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
@@ -189,8 +195,10 @@ class InventoryTransaction(models.Model):
|
||||
related_name="transactions",
|
||||
verbose_name="Produkt",
|
||||
)
|
||||
amount = models.FloatField(verbose_name="Antall")
|
||||
unit_price = models.FloatField(verbose_name="Enhetspris")
|
||||
# FIXME: rename to quantity
|
||||
amount = models.IntegerField(verbose_name="Antall")
|
||||
# FIXME: rename to price
|
||||
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
|
||||
comment = models.TextField(verbose_name="Kommentar", blank=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -204,7 +212,7 @@ class InventoryTransaction(models.Model):
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
|
||||
balance = models.FloatField(default=0.0)
|
||||
balance = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, default=0)
|
||||
image = models.URLField(blank=True)
|
||||
|
||||
def last_purchase(self):
|
||||
@@ -227,7 +235,7 @@ class UserProfile(models.Model):
|
||||
def product_order_stats(self):
|
||||
return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
|
||||
|
||||
def order_sum_total(self) -> float:
|
||||
def order_sum_total(self) -> Decimal:
|
||||
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
|
||||
|
||||
def image_url(self) -> str:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/brick.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NavLink } from "react-router-dom";
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
|
||||
const LOGOUT_URL = `${import.meta.env.VITE_API_URL}/`.replace("://", "://none:none@");
|
||||
|
||||
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
|
||||
|
||||
@@ -48,6 +49,11 @@ export const Header = (): JSX.Element => {
|
||||
Admin
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={LOGOUT_URL} className={styles.navLink}>
|
||||
Logg ut
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -51,11 +51,11 @@ const Log = () => {
|
||||
<td>
|
||||
{(order.orderlines ?? []).map((line) => (
|
||||
<div key={line.id}>
|
||||
{line.amount}x {line.product.name}
|
||||
{Number(line.amount)}x {line.product.name}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td>{order.orderSum}</td>
|
||||
<td>{Number(order.orderSum)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -104,7 +104,7 @@ const Log = () => {
|
||||
<td>
|
||||
{trans.user.firstName} {trans.user.lastName}
|
||||
</td>
|
||||
<td>{trans.amount}</td>
|
||||
<td>{Number(trans.amount)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useToast } from "../../components/ToastProvider";
|
||||
import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types";
|
||||
import { activeUserFilter } from "../../utils/users";
|
||||
import {formatCurrency} from "../../utils/currency";
|
||||
|
||||
const USER_EXTERNAL = "external";
|
||||
|
||||
@@ -53,7 +54,7 @@ export const useRegister = () => {
|
||||
setOrder(newOrder);
|
||||
};
|
||||
|
||||
const total = sumTotal(products, order, selectedUser === USER_EXTERNAL);
|
||||
const total = formatCurrency(sumTotal(products, order, selectedUser === USER_EXTERNAL));
|
||||
|
||||
const onSubmit = async () => {
|
||||
let res;
|
||||
|
||||
@@ -25,7 +25,7 @@ export const StatsNav = () => (
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink className="disabled" to="/stats/users" title="TODO">
|
||||
<NavLink className="disabled" to="/stats/users" title="TODO" hidden={true}>
|
||||
Folk
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { MouseEventHandler } from "react";
|
||||
|
||||
import { AllUsersQuery } from "../../types";
|
||||
import styles from "./UserCard.module.css";
|
||||
import {formatCurrency} from "../../utils/currency";
|
||||
|
||||
type UserCardProps = {
|
||||
user: AllUsersQuery["allUsers"][0] | null;
|
||||
@@ -13,6 +14,7 @@ type UserCardProps = {
|
||||
/** If user is null, then it's the special external user */
|
||||
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => {
|
||||
const classes = clsx(styles.user, { [styles.active]: active });
|
||||
const userBalanceString = formatCurrency(Number(user?.profile?.balance));
|
||||
return (
|
||||
<button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}>
|
||||
<div className={styles.avatarWrap}>
|
||||
@@ -27,7 +29,7 @@ export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.E
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
{user?.firstName || "Ekstern"}
|
||||
<span className={styles.balance}>{user?.profile?.balance || ""}</span>
|
||||
<span className={styles.balance}>{userBalanceString}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function formatCurrency(value: number) {
|
||||
if (isNaN(value)) {
|
||||
return "";
|
||||
} else {
|
||||
return value.toFixed(2).replace(/\.00$/, '');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user