Add product category for color hue

This commit is contained in:
Gabriel Augendre 2023-04-02 19:33:24 +02:00
parent 24a46d398d
commit 5998a280ba
8 changed files with 236 additions and 20 deletions

View file

@ -3,7 +3,14 @@ from django.contrib.admin import register
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from solo.admin import SingletonModelAdmin from solo.admin import SingletonModelAdmin
from purchase.models import Basket, BasketItem, Cache, PaymentMethod, Product from purchase.models import (
Basket,
BasketItem,
Cache,
PaymentMethod,
Product,
ProductCategory,
)
from purchase.templatetags.purchase import currency from purchase.templatetags.purchase import currency
@ -13,15 +20,16 @@ class ProductAdmin(admin.ModelAdmin):
"name", "name",
"display_order", "display_order",
"initials", "initials",
"category",
"unit_price", "unit_price",
"sold", "sold",
"turnover", "turnover",
] ]
list_editable = ["display_order", "initials"] list_editable = ["display_order"]
search_fields = ["name"] search_fields = ["name"]
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).with_sold().with_turnover() return super().get_queryset(request).with_category().with_sold().with_turnover()
@admin.display(description=_("unit price")) @admin.display(description=_("unit price"))
def unit_price(self, instance: Product): def unit_price(self, instance: Product):
@ -49,6 +57,12 @@ class PaymentMethodAdmin(admin.ModelAdmin):
return currency(instance.turnover) return currency(instance.turnover)
@register(ProductCategory)
class ProductCategoryAdmin(admin.ModelAdmin):
list_display = ["name", "color_hue"]
search_fields = ["name"]
class BasketItemInline(admin.TabularInline): class BasketItemInline(admin.TabularInline):
model = BasketItem model = BasketItem
fields = ["product", "quantity", "price"] fields = ["product", "quantity", "price"]

View file

@ -5,6 +5,7 @@
"created_at": "2022-04-26T20:30:22.959Z", "created_at": "2022-04-26T20:30:22.959Z",
"updated_at": "2023-04-02T13:47:16.252Z", "updated_at": "2023-04-02T13:47:16.252Z",
"name": "Clou", "name": "Clou",
"category": 1,
"unit_price_cents": 140, "unit_price_cents": 140,
"initials": "C", "initials": "C",
"display_order": 1 "display_order": 1
@ -16,6 +17,7 @@
"created_at": "2022-04-26T20:30:22.959Z", "created_at": "2022-04-26T20:30:22.959Z",
"updated_at": "2023-04-02T14:16:24.038Z", "updated_at": "2023-04-02T14:16:24.038Z",
"name": "Villard'Ain", "name": "Villard'Ain",
"category": 1,
"unit_price_cents": 310, "unit_price_cents": 310,
"initials": "V", "initials": "V",
"display_order": 3 "display_order": 3
@ -27,6 +29,7 @@
"created_at": "2022-04-26T20:30:22.959Z", "created_at": "2022-04-26T20:30:22.959Z",
"updated_at": "2023-04-02T14:16:24.046Z", "updated_at": "2023-04-02T14:16:24.046Z",
"name": "Herbier", "name": "Herbier",
"category": 1,
"unit_price_cents": 360, "unit_price_cents": 360,
"initials": "Hb", "initials": "Hb",
"display_order": 9 "display_order": 9
@ -36,8 +39,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2022-04-26T20:30:22.959Z", "created_at": "2022-04-26T20:30:22.959Z",
"updated_at": "2023-04-02T14:16:24.053Z", "updated_at": "2023-04-02T17:27:16.954Z",
"name": "Blanc vache", "name": "Blanc vache",
"category": 3,
"unit_price_cents": 360, "unit_price_cents": 360,
"initials": "BV", "initials": "BV",
"display_order": 15 "display_order": 15
@ -49,6 +53,7 @@
"created_at": "2022-04-28T16:08:08.298Z", "created_at": "2022-04-28T16:08:08.298Z",
"updated_at": "2023-04-02T14:16:24.048Z", "updated_at": "2023-04-02T14:16:24.048Z",
"name": "Ap\u00e9rich\u00e8vres", "name": "Ap\u00e9rich\u00e8vres",
"category": 1,
"unit_price_cents": 360, "unit_price_cents": 360,
"initials": "Ac", "initials": "Ac",
"display_order": 7 "display_order": 7
@ -60,6 +65,7 @@
"created_at": "2022-04-28T16:08:16.542Z", "created_at": "2022-04-28T16:08:16.542Z",
"updated_at": "2023-04-02T13:47:52.614Z", "updated_at": "2023-04-02T13:47:52.614Z",
"name": "Clougert", "name": "Clougert",
"category": 1,
"unit_price_cents": 470, "unit_price_cents": 470,
"initials": "CM", "initials": "CM",
"display_order": 6 "display_order": 6
@ -71,6 +77,7 @@
"created_at": "2022-04-28T16:08:20.471Z", "created_at": "2022-04-28T16:08:20.471Z",
"updated_at": "2023-04-02T14:16:24.050Z", "updated_at": "2023-04-02T14:16:24.050Z",
"name": "Brique", "name": "Brique",
"category": 1,
"unit_price_cents": 310, "unit_price_cents": 310,
"initials": "Bq", "initials": "Bq",
"display_order": 4 "display_order": 4
@ -82,6 +89,7 @@
"created_at": "2022-04-28T16:08:20.471Z", "created_at": "2022-04-28T16:08:20.471Z",
"updated_at": "2023-04-02T14:16:24.051Z", "updated_at": "2023-04-02T14:16:24.051Z",
"name": "Tomme de ch\u00e8vre", "name": "Tomme de ch\u00e8vre",
"category": 1,
"unit_price_cents": 0, "unit_price_cents": 0,
"initials": "T", "initials": "T",
"display_order": 50 "display_order": 50
@ -91,8 +99,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T12:47:15.522Z", "created_at": "2023-04-02T12:47:15.522Z",
"updated_at": "2023-04-02T14:16:24.070Z", "updated_at": "2023-04-02T17:27:16.961Z",
"name": "Cr\u00eapes chocolat", "name": "Cr\u00eapes chocolat",
"category": 4,
"unit_price_cents": 200, "unit_price_cents": 200,
"initials": "CrC", "initials": "CrC",
"display_order": 18 "display_order": 18
@ -104,6 +113,7 @@
"created_at": "2023-04-02T13:02:38.149Z", "created_at": "2023-04-02T13:02:38.149Z",
"updated_at": "2023-04-02T14:16:24.043Z", "updated_at": "2023-04-02T14:16:24.043Z",
"name": "Caprinoux", "name": "Caprinoux",
"category": 1,
"unit_price_cents": 100, "unit_price_cents": 100,
"initials": "Cap", "initials": "Cap",
"display_order": 5 "display_order": 5
@ -115,6 +125,7 @@
"created_at": "2023-04-02T13:31:13.712Z", "created_at": "2023-04-02T13:31:13.712Z",
"updated_at": "2023-04-02T14:16:24.055Z", "updated_at": "2023-04-02T14:16:24.055Z",
"name": "Clou affin\u00e9", "name": "Clou affin\u00e9",
"category": 1,
"unit_price_cents": 160, "unit_price_cents": 160,
"initials": "CA", "initials": "CA",
"display_order": 2 "display_order": 2
@ -126,6 +137,7 @@
"created_at": "2023-04-02T13:32:12.289Z", "created_at": "2023-04-02T13:32:12.289Z",
"updated_at": "2023-04-02T14:16:24.057Z", "updated_at": "2023-04-02T14:16:24.057Z",
"name": "Ap\u00e9rich\u00e8vres frais", "name": "Ap\u00e9rich\u00e8vres frais",
"category": 1,
"unit_price_cents": 250, "unit_price_cents": 250,
"initials": "AcF", "initials": "AcF",
"display_order": 8 "display_order": 8
@ -137,6 +149,7 @@
"created_at": "2023-04-02T13:32:29.365Z", "created_at": "2023-04-02T13:32:29.365Z",
"updated_at": "2023-04-02T14:16:24.059Z", "updated_at": "2023-04-02T14:16:24.059Z",
"name": "Blanc ch\u00e8vre", "name": "Blanc ch\u00e8vre",
"category": 1,
"unit_price_cents": 480, "unit_price_cents": 480,
"initials": "BC", "initials": "BC",
"display_order": 10 "display_order": 10
@ -146,8 +159,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T13:32:41.308Z", "created_at": "2023-04-02T13:32:41.308Z",
"updated_at": "2023-04-02T14:16:24.060Z", "updated_at": "2023-04-02T17:27:16.948Z",
"name": "Gros de vache", "name": "Gros de vache",
"category": 3,
"unit_price_cents": 200, "unit_price_cents": 200,
"initials": "GV", "initials": "GV",
"display_order": 13 "display_order": 13
@ -157,8 +171,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T13:32:48.404Z", "created_at": "2023-04-02T13:32:48.404Z",
"updated_at": "2023-04-02T14:16:24.062Z", "updated_at": "2023-04-02T17:27:16.952Z",
"name": "P'tit clou", "name": "P'tit clou",
"category": 3,
"unit_price_cents": 60, "unit_price_cents": 60,
"initials": "PC", "initials": "PC",
"display_order": 14 "display_order": 14
@ -168,8 +183,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T13:33:12.184Z", "created_at": "2023-04-02T13:33:12.184Z",
"updated_at": "2023-04-02T13:47:52.633Z", "updated_at": "2023-04-02T17:27:16.957Z",
"name": "Ap\u00e9rivache", "name": "Ap\u00e9rivache",
"category": 3,
"unit_price_cents": 300, "unit_price_cents": 300,
"initials": "AV", "initials": "AV",
"display_order": 16 "display_order": 16
@ -181,6 +197,7 @@
"created_at": "2023-04-02T13:33:24.357Z", "created_at": "2023-04-02T13:33:24.357Z",
"updated_at": "2023-04-02T14:16:24.064Z", "updated_at": "2023-04-02T14:16:24.064Z",
"name": "Fromage fort", "name": "Fromage fort",
"category": 1,
"unit_price_cents": 360, "unit_price_cents": 360,
"initials": "FF", "initials": "FF",
"display_order": 11 "display_order": 11
@ -192,6 +209,7 @@
"created_at": "2023-04-02T13:33:49.793Z", "created_at": "2023-04-02T13:33:49.793Z",
"updated_at": "2023-04-02T14:16:24.066Z", "updated_at": "2023-04-02T14:16:24.066Z",
"name": "Lait de ch\u00e8vre", "name": "Lait de ch\u00e8vre",
"category": 1,
"unit_price_cents": 200, "unit_price_cents": 200,
"initials": "L", "initials": "L",
"display_order": 12 "display_order": 12
@ -201,8 +219,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T13:33:54.405Z", "created_at": "2023-04-02T13:33:54.405Z",
"updated_at": "2023-04-02T14:16:24.068Z", "updated_at": "2023-04-02T17:27:16.959Z",
"name": "Verre", "name": "Verre",
"category": 4,
"unit_price_cents": 100, "unit_price_cents": 100,
"initials": "Ve", "initials": "Ve",
"display_order": 17 "display_order": 17
@ -212,8 +231,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T13:34:34.802Z", "created_at": "2023-04-02T13:34:34.802Z",
"updated_at": "2023-04-02T14:16:24.072Z", "updated_at": "2023-04-02T17:27:16.963Z",
"name": "Cr\u00eape sucre", "name": "Cr\u00eape sucre",
"category": 4,
"unit_price_cents": 150, "unit_price_cents": 150,
"initials": "CrS", "initials": "CrS",
"display_order": 19 "display_order": 19
@ -223,8 +243,9 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T13:34:44.535Z", "created_at": "2023-04-02T13:34:44.535Z",
"updated_at": "2023-04-02T14:16:24.073Z", "updated_at": "2023-04-02T17:27:16.966Z",
"name": "Cr\u00eape nature", "name": "Cr\u00eape nature",
"category": 4,
"unit_price_cents": 140, "unit_price_cents": 140,
"initials": "CrN", "initials": "CrN",
"display_order": 20 "display_order": 20
@ -234,11 +255,42 @@
"model": "purchase.product", "model": "purchase.product",
"fields": { "fields": {
"created_at": "2023-04-02T14:02:56.130Z", "created_at": "2023-04-02T14:02:56.130Z",
"updated_at": "2023-04-02T14:16:24.075Z", "updated_at": "2023-04-02T17:27:16.968Z",
"name": "Facture", "name": "Facture",
"category": 4,
"unit_price_cents": 0, "unit_price_cents": 0,
"initials": "Fa", "initials": "Fa",
"display_order": 51 "display_order": 51
} }
},
{
"model": "purchase.productcategory",
"pk": 1,
"fields": {
"created_at": "2023-04-02T17:24:02.168Z",
"updated_at": "2023-04-02T17:30:07.195Z",
"name": "Ch\u00e8vre",
"color_hue": 145
}
},
{
"model": "purchase.productcategory",
"pk": 3,
"fields": {
"created_at": "2023-04-02T17:25:41.912Z",
"updated_at": "2023-04-02T17:30:04.093Z",
"name": "Vache",
"color_hue": 276
}
},
{
"model": "purchase.productcategory",
"pk": 4,
"fields": {
"created_at": "2023-04-02T17:25:57.992Z",
"updated_at": "2023-04-02T17:29:59.720Z",
"name": "Autres",
"color_hue": 38
}
} }
] ]

View file

@ -40,7 +40,7 @@ class BasketForm(forms.ModelForm):
for item in basket.items.all(): for item in basket.items.all():
products[item.product] = item.quantity products[item.product] = item.quantity
fields = [] fields = []
for product in Product.objects.with_fixed_price(): for product in Product.objects.with_category().with_fixed_price():
field_name = f"{PRICED_PREFIX}{product.id}" field_name = f"{PRICED_PREFIX}{product.id}"
self.fields.update( self.fields.update(
{ {

View file

@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-02 18:32+0200\n" "POT-Creation-Date: 2023-04-02 19:31+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -56,6 +56,26 @@ msgstr ""
msgid "payment methods" msgid "payment methods"
msgstr "" msgstr ""
#: purchase/models.py
msgid "color hue"
msgstr ""
#: purchase/models.py
msgid "Color hue in degrees (0-360)"
msgstr ""
#: purchase/models.py
msgid "product category"
msgstr ""
#: purchase/models.py
msgid "product categories"
msgstr ""
#: purchase/models.py
msgid "category"
msgstr ""
#: purchase/models.py #: purchase/models.py
msgid "unit price (cents)" msgid "unit price (cents)"
msgstr "" msgstr ""

View file

@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-02 18:32+0200\n" "POT-Creation-Date: 2023-04-02 19:31+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -55,6 +55,27 @@ msgstr "moyen de paiement"
msgid "payment methods" msgid "payment methods"
msgstr "moyens de paiement" msgstr "moyens de paiement"
#: purchase/models.py
msgid "color hue"
msgstr "teinte de couleur"
#: purchase/models.py
msgid "Color hue in degrees (0-360)"
msgstr "Teinte de couleur en degrés (0-360)"
#: purchase/models.py
#, fuzzy
msgid "product category"
msgstr "catégorie de produits"
#: purchase/models.py
msgid "product categories"
msgstr "catégories de produits"
#: purchase/models.py
msgid "category"
msgstr "catégorie"
#: purchase/models.py #: purchase/models.py
msgid "unit price (cents)" msgid "unit price (cents)"
msgstr "prix unitaire (centimes)" msgstr "prix unitaire (centimes)"

View file

@ -0,0 +1,62 @@
# Generated by Django 4.1.7 on 2023-04-02 17:21
import django.core.validators
from django.db import migrations, models
def create_default_categories(apps, schema_editor):
ProductCategory = apps.get_model("purchase", "ProductCategory") # noqa: N806
ProductCategory.objects.using(schema_editor.connection.alias).create(
name="default",
color_hue=0,
)
class Migration(migrations.Migration):
dependencies = [
("purchase", "0016_alter_paymentmethod_options"),
]
operations = [
migrations.CreateModel(
name="ProductCategory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="updated at"),
),
(
"name",
models.CharField(max_length=250, unique=True, verbose_name="name"),
),
(
"color_hue",
models.PositiveIntegerField(
help_text="Color hue in degrees (0-360)",
validators=[django.core.validators.MaxValueValidator(360)],
verbose_name="color hue",
),
),
],
options={
"abstract": False,
},
),
migrations.RunPython(
create_default_categories,
reverse_code=migrations.RunPython.noop,
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 4.1.7 on 2023-04-02 17:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("purchase", "0017_productcategory"),
]
operations = [
migrations.AddField(
model_name="product",
name="category",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.PROTECT,
to="purchase.productcategory",
verbose_name="category",
),
preserve_default=False,
),
]

View file

@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import uuid import uuid
from django.core.validators import MaxValueValidator
from django.db import models from django.db import models
from django.db.models import Avg, Count, F, Sum from django.db.models import Avg, Count, F, Sum
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@ -65,6 +65,22 @@ def default_product_display_order():
return last.display_order + 1 return last.display_order + 1
class ProductCategory(Model):
name = models.CharField(max_length=250, unique=True, verbose_name=_("name"))
color_hue = models.PositiveIntegerField(
validators=[MaxValueValidator(360)],
verbose_name=_("color hue"),
help_text=_("Color hue in degrees (0-360)"),
)
class Meta:
verbose_name = _("product category")
verbose_name_plural = _("product categories")
def __str__(self):
return self.name
class ProductQuerySet(models.QuerySet): class ProductQuerySet(models.QuerySet):
def with_turnover(self): def with_turnover(self):
return self.annotate( return self.annotate(
@ -83,6 +99,9 @@ class ProductQuerySet(models.QuerySet):
def with_no_fixed_price(self): def with_no_fixed_price(self):
return self.filter(unit_price_cents=0) return self.filter(unit_price_cents=0)
def with_category(self):
return self.select_related("category")
class ProductManager(models.Manager): class ProductManager(models.Manager):
def get_by_natural_key(self, name): def get_by_natural_key(self, name):
@ -91,6 +110,13 @@ class ProductManager(models.Manager):
class Product(Model): class Product(Model):
name = models.CharField(max_length=250, unique=True, verbose_name=_("name")) name = models.CharField(max_length=250, unique=True, verbose_name=_("name"))
category = models.ForeignKey(
ProductCategory,
on_delete=models.PROTECT,
verbose_name=_("category"),
null=False,
blank=False,
)
unit_price_cents = models.PositiveIntegerField( unit_price_cents = models.PositiveIntegerField(
verbose_name=_("unit price (cents)"), verbose_name=_("unit price (cents)"),
help_text=_( help_text=_(
@ -123,10 +149,7 @@ class Product(Model):
@property @property
def color_hue(self): def color_hue(self):
return int( return self.category.color_hue
hashlib.sha256(bytes(self.name, encoding="utf-8")).hexdigest()[:2],
base=16,
)
@property @property
def has_fixed_price(self) -> bool: def has_fixed_price(self) -> bool: