From d9f2d6f39b36946eb912e64cce0700b1d3211a95 Mon Sep 17 00:00:00 2001 From: Aiq0 <66842415+Aiq0@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:23:58 +0200 Subject: [PATCH 1/3] feat: add option to specify multiple eans per product --- sortiment/store/admin.py | 12 +++- sortiment/store/forms.py | 61 +++++++++++++++- .../store/migrations/0016_product_eans.py | 72 +++++++++++++++++++ sortiment/store/models.py | 26 ++++++- .../store/templates/products/_search.html | 2 +- .../store/templates/store/_products_list.html | 2 +- .../store/templates/store/inventory.html | 4 +- sortiment/store/templates/store/products.html | 2 +- sortiment/store/views/settings.py | 12 ++-- sortiment/store/views/storefront.py | 10 +-- 10 files changed, 182 insertions(+), 21 deletions(-) create mode 100644 sortiment/store/migrations/0016_product_eans.py diff --git a/sortiment/store/admin.py b/sortiment/store/admin.py index 320fe69..c6637d3 100644 --- a/sortiment/store/admin.py +++ b/sortiment/store/admin.py @@ -5,10 +5,15 @@ from sortiment.store.models import Reset +class ProductEANInline(admin.TabularInline): + model = models.ProductEAN + extra = 1 + + class ProductAdmin(ModelAdmin): list_display = ( "name", - "barcode", + "ean_list", "price", "is_unlimited", "is_dummy", @@ -17,6 +22,11 @@ class ProductAdmin(ModelAdmin): "is_unlimited", "is_dummy", ) + inlines = [ProductEANInline] + + @admin.display(description="EAN kódy") + def ean_list(self, obj): + return ", ".join(obj.ean_codes) class WarehouseAdmin(ModelAdmin): diff --git a/sortiment/store/forms.py b/sortiment/store/forms.py index c153e14..3b0c020 100644 --- a/sortiment/store/forms.py +++ b/sortiment/store/forms.py @@ -1,18 +1,75 @@ +import re + from django import forms from django.core.exceptions import ValidationError +from django.db import transaction from django.forms import DecimalField, Form, IntegerField, ModelForm, NumberInput -from .models import Product, Warehouse, WarehouseState +from .models import Product, ProductEAN, Warehouse, WarehouseState + +EAN_SPLIT_RE = re.compile(r"[\s,]+") class ProductForm(ModelForm): + eans = forms.CharField( + label="EAN kódy", + help_text="Zadaj jeden alebo viac EAN kódov, každý na samostatný riadok.", + widget=forms.Textarea(attrs={"rows": 3, "placeholder": "Jeden EAN na riadok"}), + ) + class Meta: model = Product - fields = ["name", "barcode", "image", "is_unlimited", "tags", "price"] + fields = ["name", "image", "is_unlimited", "tags", "price"] widgets = { "price": NumberInput(attrs={"min": 0, "step": 0.01}), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields["eans"].initial = "\n".join(self.instance.ean_codes) + + def clean_eans(self): + raw = self.cleaned_data["eans"] + codes = [code for code in EAN_SPLIT_RE.split(raw) if code] + if not codes: + raise ValidationError("Zadaj aspoň jeden EAN kód.") + + seen = set() + for code in codes: + if len(code) > 32: + raise ValidationError( + f"EAN kód {code} je príliš dlhý (max. 32 znakov)." + ) + if code in seen: + raise ValidationError(f"EAN kód {code} je zadaný viackrát.") + seen.add(code) + + existing = ProductEAN.objects.filter(code__in=codes) + if self.instance.pk: + existing = existing.exclude(product=self.instance) + if existing.exists(): + code = existing.first().code + raise ValidationError(f"EAN kód {code} už patrí inému produktu.") + + return codes + + @transaction.atomic + def save(self, commit=True): + product = super().save(commit=commit) + if commit: + codes = self.cleaned_data["eans"] + product.eans.exclude(code__in=codes).delete() + existing = set(product.eans.values_list("code", flat=True)) + ProductEAN.objects.bulk_create( + [ + ProductEAN(product=product, code=code) + for code in codes + if code not in existing + ] + ) + return product + class DiscardForm(Form): quantity = IntegerField(min_value=0, label="Počet kusov") diff --git a/sortiment/store/migrations/0016_product_eans.py b/sortiment/store/migrations/0016_product_eans.py new file mode 100644 index 0000000..92c5f39 --- /dev/null +++ b/sortiment/store/migrations/0016_product_eans.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2.14 on 2026-10-22 16:45 + +import django.db.models.deletion +from django.db import migrations, models + + +def copy_barcodes(apps, schema_editor): + Product = apps.get_model("store", "Product") + ProductEAN = apps.get_model("store", "ProductEAN") + + ProductEAN.objects.bulk_create( + [ + ProductEAN(product_id=product.id, code=product.barcode) + for product in Product.objects.exclude(barcode="") + ] + ) + + +def restore_barcodes(apps, schema_editor): + Product = apps.get_model("store", "Product") + ProductEAN = apps.get_model("store", "ProductEAN") + + seen_products = set() + for ean in ProductEAN.objects.order_by("id"): + if ean.product_id in seen_products: + continue + seen_products.add(ean.product_id) + Product.objects.filter(id=ean.product_id).update(barcode=ean.code) + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0015_auto_20240703_2338"), + ] + + operations = [ + migrations.CreateModel( + name="ProductEAN", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField(max_length=32, unique=True, verbose_name="EAN"), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="eans", + to="store.product", + verbose_name="produkt", + ), + ), + ], + options={ + "ordering": ["id"], + }, + ), + migrations.RunPython(copy_barcodes, restore_barcodes), + migrations.RemoveField( + model_name="product", + name="barcode", + ), + ] diff --git a/sortiment/store/models.py b/sortiment/store/models.py index 31cf983..4d98f17 100644 --- a/sortiment/store/models.py +++ b/sortiment/store/models.py @@ -52,7 +52,6 @@ def __str__(self): class Product(models.Model): id: int name = models.CharField(max_length=128, verbose_name="názov") - barcode = models.CharField(max_length=32, unique=True, verbose_name="čiarový kód") image = models.FileField(null=True, verbose_name="obrázok") price = models.DecimalField( max_digits=6, decimal_places=2, verbose_name="predajná cena" @@ -64,18 +63,39 @@ class Product(models.Model): def __str__(self): return f"{self.name} ({self.price} €)" + @property + def ean_codes(self): + return [ean.code for ean in self.eans.all()] + @staticmethod def generate_one_time_product(price, barcode): - product, created = Product.objects.get_or_create( + ean = ProductEAN.objects.filter(code=barcode).first() + if ean: + return ean.product + + product = Product.objects.create( price=price, name="Jednorazová položka", is_unlimited=True, - barcode=barcode, is_dummy=True, ) + ProductEAN.objects.create(product=product, code=barcode) return product +class ProductEAN(models.Model): + product = models.ForeignKey( + Product, on_delete=models.CASCADE, related_name="eans", verbose_name="produkt" + ) + code = models.CharField(max_length=32, unique=True, verbose_name="EAN") + + class Meta: + ordering = ["id"] + + def __str__(self): + return self.code + + class WarehouseState(models.Model): warehouse = models.ForeignKey( Warehouse, on_delete=models.CASCADE, verbose_name="sklad" diff --git a/sortiment/store/templates/products/_search.html b/sortiment/store/templates/products/_search.html index 7c97c66..7e22b53 100644 --- a/sortiment/store/templates/products/_search.html +++ b/sortiment/store/templates/products/_search.html @@ -1,7 +1,7 @@ {% load icon %} {% if not product %}