Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ad0309a
Started
Zaph-x Sep 30, 2021
b205dfc
Added minimal inventory functionality to stregsystemet
Zaph-x Oct 4, 2021
084d4ca
black
Zaph-x Oct 4, 2021
07681d4
self insight
Zaph-x Oct 4, 2021
2a3ad64
Fixed item count handling
Zaph-x Oct 6, 2021
3f10c15
Inventory item model can now manage the active state of product model
Zaph-x Oct 6, 2021
2bd090c
Ran black
Zaph-x Oct 6, 2021
e5e14c8
Tests 🥳
Zaph-x Oct 6, 2021
8a2fca0
Added inventory history
Zaph-x Oct 6, 2021
fe8d138
added tests
Zaph-x Oct 6, 2021
7d682e5
Added desired amount to inventoryitem to make life easier for the buyer
Zaph-x Oct 6, 2021
b717d8f
Updated test to reflect changes in save method. Updated admin view wi…
Zaph-x Oct 6, 2021
e3c76b8
Made inventoryitemtests its own thing
Zaph-x Oct 6, 2021
d314abb
Fixed indentation
Zaph-x Oct 6, 2021
317b0e8
Added crude loss calculation
Zaph-x Oct 6, 2021
95e0e41
Added migration
Zaph-x Oct 6, 2021
ca8dee5
Updated loss calculation. Keep getting warning. Feels sad
Zaph-x Oct 6, 2021
f870cf6
Added tests to root out bugs
Zaph-x Oct 7, 2021
e8f10f2
Merge remote-tracking branch 'origin/next' into inventory
Zaph-x Oct 7, 2021
6810c48
Added sold out date to inventory item history
Zaph-x Oct 8, 2021
4d67985
Added tests
Zaph-x Oct 8, 2021
4ebf681
Black
Zaph-x Oct 8, 2021
a827915
Removed vscode folder
Zaph-x Oct 8, 2021
3bef8de
Migrations 👌👌
Zaph-x Oct 8, 2021
00e2ae4
Migrations 👌👌
Zaph-x Oct 8, 2021
a0196b5
Yeeted insignificant lines, as they no longer had a purpose
Zaph-x Oct 8, 2021
b85591a
If start date is set, we wish to update this
Zaph-x Oct 8, 2021
bda8aba
Updated if statement for start date update
Zaph-x Oct 8, 2021
eaeef96
Fixed tests after hours of testing
Zaph-x Oct 13, 2021
6ac1c88
Fixed single test assert statement
Zaph-x Oct 13, 2021
3b2f1d1
mega commit
Zaph-x Nov 24, 2021
4186892
Merge branch 'next' into inventory
falkecarlsen Oct 27, 2023
df0ea23
apply black
falkecarlsen Oct 27, 2023
d082791
fixup migration
falkecarlsen Oct 27, 2023
96cff93
fixup clash of datetime obj uses in Models and comment out non-workin…
falkecarlsen Oct 27, 2023
8064abe
Revert unrelated format changes
krestenlaust May 1, 2024
4f79966
Merge branch 'next' into inventory
krestenlaust May 1, 2024
955f409
Format
krestenlaust May 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ htmlcov/
# ignore mobilepay api token
stregsystem/management/commands/tokens.json

.vscode/

# ignore default log file location
stregsystem.log
48 changes: 48 additions & 0 deletions stregsystem/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from stregsystem.models import (
Category,
InventoryItem,
InventoryItemHistory,
Member,
News,
Payment,
Expand Down Expand Up @@ -169,6 +171,50 @@ def activated(self, product):
activated.boolean = True


class InventoryAdmin(admin.ModelAdmin):
search_fields = ('name',)
list_display = (
'activated',
'name',
'quantity',
'desired_amount',
)
fields = (
'name',
'active',
(
'desired_amount',
'quantity',
),
'products',
)

def activated(self, inventory_item: InventoryItem) -> bool:
return inventory_item.is_active()

activated.boolean = True


class InventoryHistoryAdmin(admin.ModelAdmin):
search_fields = ('count_date',)
list_display = (
'item',
'old_quantity',
'new_quantity',
'count_date',
'sold_out',
)
readonly_fields = (
'item',
'old_quantity',
'new_quantity',
'loss',
'count_date',
'sold_out',
'sold_out_date',
)


class NamedProductAdmin(admin.ModelAdmin):
search_fields = (
'name',
Expand Down Expand Up @@ -345,6 +391,8 @@ def has_delete_permission(self, request, obj=None):
admin.site.register(Sale, SaleAdmin)
admin.site.register(Member, MemberAdmin)
admin.site.register(Payment, PaymentAdmin)
admin.site.register(InventoryItem, InventoryAdmin)
admin.site.register(InventoryItemHistory, InventoryHistoryAdmin)
admin.site.register(News)
admin.site.register(Product, ProductAdmin)
admin.site.register(NamedProduct, NamedProductAdmin)
Expand Down
45 changes: 45 additions & 0 deletions stregsystem/migrations/0018_inventoryitem_inventoryitemhistory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 2.2.28 on 2023-10-27 11:55

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('stregsystem', '0017_auto_20220511_1738'),
]

operations = [
migrations.CreateModel(
name='InventoryItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
('quantity', models.PositiveIntegerField(default=0)),
('desired_amount', models.IntegerField(default=0)),
('active', models.BooleanField(default=False)),
('products', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='stregsystem.Product')),
],
options={
'verbose_name_plural': 'Inventory',
},
),
migrations.CreateModel(
name='InventoryItemHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('new_quantity', models.IntegerField(default=0)),
('old_quantity', models.IntegerField(default=0)),
('count_date', models.DateField(default=django.utils.timezone.now)),
('sold_out', models.BooleanField(default=False)),
('sold_out_date', models.DateField(blank=True, null=True)),
('loss', models.IntegerField(default=0)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_item_history', to='stregsystem.InventoryItem')),
],
options={
'verbose_name_plural': 'Inventory History',
},
),
]
132 changes: 130 additions & 2 deletions stregsystem/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import datetime
import re
from collections import Counter
from datetime import datetime, date, timedelta
from email.utils import parseaddr
from smtplib import OLDSTYLE_AUTH
from unittest.case import expectedFailure

from django.contrib.admin.models import LogEntry, ADDITION, CHANGE
from django.contrib.auth.models import User
from django.core.validators import RegexValidator
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.db.models import Count
from django.db.models.query_utils import Q
from django.utils import timezone

from stregsystem.caffeine import Intake, CAFFEINE_TIME_INTERVAL, current_caffeine_in_body_compound_interest
Expand Down Expand Up @@ -135,6 +138,9 @@ def execute(self):
s = Sale(member=self.member, product=item.product, room=self.room, price=item.product.price)
s.save()

if item.product.quantity == 0 and item.product.start_date is not None:
item.product.sold_out_date = date.today()

# Bought (used above) is automatically calculated, so we don't need
# to update it
# We changed the user balance, so save that
Expand Down Expand Up @@ -307,7 +313,7 @@ def is_leading_coffee_addict(self):
coffee_category = [6]

now = timezone.now()
start_of_week = now - datetime.timedelta(days=now.weekday()) - datetime.timedelta(hours=now.hour)
start_of_week = now - timedelta(days=now.weekday()) - timedelta(hours=now.hour)
user_with_most_coffees_bought = (
Member.objects.filter(
sale__timestamp__gt=start_of_week,
Expand Down Expand Up @@ -559,6 +565,7 @@ def __unicode__(self):
def __str__(self):
return active_str(self.active) + " " + self.name + " (" + money(self.price) + ")"

@transaction.atomic
def save(self, *args, **kwargs):
price_changed = True
if self.id:
Expand Down Expand Up @@ -593,6 +600,127 @@ def is_active(self):
return self.active and not expired and not out_of_stock


class InventoryItem(models.Model): # Skal bruges af TREO til at holde styr på tab og indkøb
# INFO: This model will keep track of inventory for every item in the system
# TODO Decide what to do when an item does not conform to a single category
# TODO Decide what to do when an item is only one category
# TODO Figure out how to extract data about loss in the system
# TODO Figure out how reimbursements will be handled, with the inventory system
name = models.CharField(max_length=64)
quantity = models.PositiveIntegerField(default=0)
desired_amount = models.IntegerField(default=0)
active = models.BooleanField(default=False)
products: Product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='inventory_items')

class Meta:
verbose_name_plural = "Inventory"

def __str__(self):
return active_str(self.active) + " " + self.name + " : " + str(self.quantity)

@transaction.atomic
def save(self, *args, **kwargs):
self.active = self.quantity > 0
__can_create = False

if self.id:
__can_create = True
# At initial creation we do not wish to create an inventory history record for the item
item: InventoryItem = InventoryItem.objects.get(id=self.pk)

old_quantity = item.quantity

# We do not wish to 💣 bomb 💣 the database with "non-important" log entries
if InventoryItemHistory.objects.filter(count_date=date.today(), item=self).exists():
inventory_history: InventoryItemHistory = InventoryItemHistory.objects.get(
count_date=date.today(), item=self
)
else:
inventory_history = InventoryItemHistory()

inventory_history.item = self
inventory_history.set_quantities(old_quantity, self.quantity)
if old_quantity:
inventory_history.calculate_loss()

inventory_history.save()

# We only want to update the start date if at least once day has passed to ensure updated inventory
if self.products.pk and (
self.products.start_date is None or self.products.start_date <= date.today() - timedelta(days=1)
):
self.products.start_date = date.today()

# Save own model before product list, to ensure active state is considered
super(InventoryItem, self).save(*args, **kwargs)
if not __can_create:
inventory_history = InventoryItemHistory()
inventory_history.item = self
inventory_history.set_quantities(0, self.quantity)
inventory_history.save()

# pls no abuse
# 4/10-2021 - made it un-abusable
self.products.quantity = sum(
[item.quantity for item in InventoryItem.objects.filter(products=self.products.pk) if item.active]
)

self.products.save()

def is_active(self):
return self.products.is_active()


class InventoryItemHistory(models.Model):
item: InventoryItem = models.ForeignKey(
InventoryItem, on_delete=models.CASCADE, related_name='inventory_item_history'
)
new_quantity = models.IntegerField(default=0)
old_quantity = models.IntegerField(default=0)
count_date = models.DateField(default=timezone.now)
sold_out = models.BooleanField(default=False)
sold_out_date = models.DateField(null=True, blank=True)
loss = models.IntegerField(default=0)

class Meta:
verbose_name_plural = "Inventory History"

def __str__(self) -> str:
return f'{self.item.name} ({self.old_quantity} -> {self.new_quantity})[{self.sold_out}] @ {self.count_date}'

def calculate_loss(
self,
) -> None:
if self.item is None or self.item.products is None or self.item.products.start_date is None:
self.loss = 0
return

if self.old_quantity > self.new_quantity and self.old_quantity - self.new_quantity > self.item.products.bought:
self.loss = self.old_quantity - self.new_quantity - self.item.products.bought
return

# If no sales are made, any difference in quantity is loss
self.loss = self.old_quantity - self.new_quantity if self.old_quantity > self.new_quantity else 0

def set_quantities(self, old_quantity: int, new_quantity: int) -> None:
self.old_quantity = old_quantity
self.new_quantity = new_quantity

@transaction.atomic
def save(self, *args, **kwargs):
self.sold_out = not (self.item.active and self.item.products.is_active())
self.item = InventoryItem.objects.get(id=self.item.pk)
if self.sold_out:
self.old_quantity = 0
try:
self.sold_out_date = Sale.objects.filter(product=self.item.products).latest('id').timestamp
except Exception:
self.sold_out_date = date.today()

super(InventoryItemHistory, self).save(*args, **kwargs)
assert self.pk


class NamedProduct(models.Model):
name = models.CharField(max_length=50, unique=True, validators=[RegexValidator(regex=r'^[^\d:\-_][\w\-]+$')])
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='named_id')
Expand Down
Loading