From e80dc604a734fdf824d747e0ec954be600378b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 17 Feb 2025 23:25:19 +0100 Subject: [PATCH] Improve docs management and add invoices repo (#51) * Create a repository app for documents, first step * Step two (broken) * Complete the implementation of document repository and add invoices * Add support for invoices * Update submodules --- arduino/dcc-ex.github.io | 2 +- arduino/vim-arduino | 2 +- ram/bookshelf/admin.py | 51 ++- .../0023_delete_basebookdocument.py | 17 + ram/bookshelf/models.py | 17 +- ram/metadata/admin.py | 49 +-- ...nt_tags_delete_decoderdocument_and_more.py | 24 ++ ram/metadata/models.py | 25 -- ram/ram/__init__.py | 2 +- ram/ram/models.py | 14 +- ram/ram/settings.py | 1 + ram/repository/__init__.py | 0 ram/repository/admin.py | 248 ++++++++++++ ram/repository/apps.py | 6 + ram/repository/migrations/0001_initial.py | 361 ++++++++++++++++++ ...t_remove_basebookdocument_book_and_more.py | 157 ++++++++ ram/repository/migrations/__init__.py | 0 ram/repository/models.py | 90 +++++ ram/repository/tests.py | 3 + ram/repository/views.py | 3 + ram/roster/admin.py | 52 +-- .../0036_delete_rollingstockdocument.py | 17 + ram/roster/models.py | 16 +- 23 files changed, 997 insertions(+), 160 deletions(-) create mode 100644 ram/bookshelf/migrations/0023_delete_basebookdocument.py create mode 100644 ram/metadata/migrations/0024_remove_genericdocument_tags_delete_decoderdocument_and_more.py create mode 100644 ram/repository/__init__.py create mode 100644 ram/repository/admin.py create mode 100644 ram/repository/apps.py create mode 100644 ram/repository/migrations/0001_initial.py create mode 100644 ram/repository/migrations/0002_invoicedocument_remove_basebookdocument_book_and_more.py create mode 100644 ram/repository/migrations/__init__.py create mode 100644 ram/repository/models.py create mode 100644 ram/repository/tests.py create mode 100644 ram/repository/views.py create mode 100644 ram/roster/migrations/0036_delete_rollingstockdocument.py diff --git a/arduino/dcc-ex.github.io b/arduino/dcc-ex.github.io index 9acc446..a0f886b 160000 --- a/arduino/dcc-ex.github.io +++ b/arduino/dcc-ex.github.io @@ -1 +1 @@ -Subproject commit 9acc446358e24a62d1ec1616bc32e0de9d5b4f3a +Subproject commit a0f886b69ff23ae8f3a391a9e8584554c286111e diff --git a/arduino/vim-arduino b/arduino/vim-arduino index 111db61..2ded67c 160000 --- a/arduino/vim-arduino +++ b/arduino/vim-arduino @@ -1 +1 @@ -Subproject commit 111db616db21d4f925691f1517792953f7671647 +Subproject commit 2ded67cdf09bb07c4805d9e93d478095ed3d8606 diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py index 72cece5..0e8a88b 100644 --- a/ram/bookshelf/admin.py +++ b/ram/bookshelf/admin.py @@ -8,10 +8,10 @@ from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from ram.admin import publish, unpublish from ram.utils import generate_csv from portal.utils import get_site_conf +from repository.models import BookDocument, CatalogDocument from bookshelf.models import ( BaseBookProperty, BaseBookImage, - BaseBookDocument, Book, Author, Publisher, @@ -28,13 +28,6 @@ class BookImageInline(SortableInlineAdminMixin, admin.TabularInline): verbose_name = "Image" -class BookDocInline(admin.TabularInline): - model = BaseBookDocument - min_num = 0 - extra = 0 - classes = ["collapse"] - - class BookPropertyInline(admin.TabularInline): model = BaseBookProperty min_num = 0 @@ -44,6 +37,17 @@ class BookPropertyInline(admin.TabularInline): verbose_name_plural = "Properties" +class BookDocInline(admin.TabularInline): + model = BookDocument + min_num = 0 + extra = 0 + classes = ["collapse"] + + +class CatalogDocInline(BookDocInline): + model = CatalogDocument + + @admin.register(Book) class BookAdmin(SortableAdminBase, admin.ModelAdmin): inlines = ( @@ -60,7 +64,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): "published", ) autocomplete_fields = ("authors", "publisher", "shop") - readonly_fields = ("creation_time", "updated_time") + readonly_fields = ("invoices", "creation_time", "updated_time") search_fields = ("title", "publisher__name", "authors__last_name") list_filter = ("publisher__name", "authors") @@ -89,6 +93,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): "shop", "purchase_date", "price", + "invoices", ) }, ), @@ -115,6 +120,17 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): ) return form + @admin.display(description="Invoices") + def invoices(self, obj): + if obj.invoice.exists(): + html = "
".join( + "{}".format( + i.file.url, i + ) for i in obj.invoice.all()) + else: + html = "-" + return format_html(html) + @admin.display(description="Publisher") def get_publisher(self, obj): return obj.publisher.name @@ -200,7 +216,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): inlines = ( BookPropertyInline, BookImageInline, - BookDocInline, + CatalogDocInline, ) list_display = ( "__str__", @@ -210,7 +226,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): "published", ) autocomplete_fields = ("manufacturer",) - readonly_fields = ("creation_time", "updated_time") + readonly_fields = ("invoices", "creation_time", "updated_time") search_fields = ("manufacturer__name", "years", "scales__scale") list_filter = ("manufacturer__name", "publication_year", "scales__scale") @@ -236,8 +252,10 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): "Purchase data", { "fields": ( + "shop", "purchase_date", "price", + "invoices", ) }, ), @@ -264,6 +282,17 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): ) return form + @admin.display(description="Invoices") + def invoices(self, obj): + if obj.invoice.exists(): + html = "
".join( + "{}".format( + i.file.url, i + ) for i in obj.invoice.all()) + else: + html = "-" + return format_html(html) + def download_csv(modeladmin, request, queryset): header = [ "Catalog", diff --git a/ram/bookshelf/migrations/0023_delete_basebookdocument.py b/ram/bookshelf/migrations/0023_delete_basebookdocument.py new file mode 100644 index 0000000..cd3123e --- /dev/null +++ b/ram/bookshelf/migrations/0023_delete_basebookdocument.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.4 on 2025-02-09 13:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0022_basebook_shop"), + ("repository", "0001_initial"), + ] + + operations = [ + migrations.DeleteModel( + name="BaseBookDocument", + ), + ] diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index b059b4e..fba7aa5 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -6,7 +6,7 @@ from django.urls import reverse from django_countries.fields import CountryField from ram.utils import DeduplicatedStorage -from ram.models import BaseModel, Image, Document, PropertyInstance +from ram.models import BaseModel, Image, PropertyInstance from metadata.models import Scale, Manufacturer, Shop, Tag @@ -89,21 +89,6 @@ class BaseBookImage(Image): ) -class BaseBookDocument(Document): - book = models.ForeignKey( - BaseBook, on_delete=models.CASCADE, related_name="document" - ) - - class Meta: - verbose_name_plural = "Documents" - constraints = [ - models.UniqueConstraint( - fields=["book", "file"], - name="unique_book_file" - ) - ] - - class BaseBookProperty(PropertyInstance): book = models.ForeignKey( BaseBook, diff --git a/ram/metadata/admin.py b/ram/metadata/admin.py index 4592d4c..c017394 100644 --- a/ram/metadata/admin.py +++ b/ram/metadata/admin.py @@ -2,18 +2,16 @@ from django.contrib import admin from django.utils.html import format_html from adminsortable2.admin import SortableAdminMixin -from ram.admin import publish, unpublish +from repository.models import DecoderDocument from metadata.models import ( Property, Decoder, - DecoderDocument, Scale, Shop, Manufacturer, Company, Tag, RollingStockType, - GenericDocument, ) @@ -88,51 +86,6 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin): search_fields = ("type", "category") -@admin.register(GenericDocument) -class GenericDocumentAdmin(admin.ModelAdmin): - readonly_fields = ("size", "creation_time", "updated_time") - list_display = ( - "__str__", - "description", - "private", - "size", - "download", - ) - search_fields = ( - "description", - "file", - ) - fieldsets = ( - ( - None, - { - "fields": ( - "private", - "description", - "file", - "size", - "tags", - ) - }, - ), - ( - "Notes", - {"classes": ("collapse",), "fields": ("notes",)}, - ), - ( - "Audit", - { - "classes": ("collapse",), - "fields": ( - "creation_time", - "updated_time", - ), - }, - ), - ) - actions = [publish, unpublish] - - @admin.register(Shop) class ShopAdmin(admin.ModelAdmin): list_display = ("name", "on_line", "active") diff --git a/ram/metadata/migrations/0024_remove_genericdocument_tags_delete_decoderdocument_and_more.py b/ram/metadata/migrations/0024_remove_genericdocument_tags_delete_decoderdocument_and_more.py new file mode 100644 index 0000000..dc7ea56 --- /dev/null +++ b/ram/metadata/migrations/0024_remove_genericdocument_tags_delete_decoderdocument_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.4 on 2025-02-09 13:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("metadata", "0023_shop"), + ("repository", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="genericdocument", + name="tags", + ), + migrations.DeleteModel( + name="DecoderDocument", + ), + migrations.DeleteModel( + name="GenericDocument", + ), + ] diff --git a/ram/metadata/models.py b/ram/metadata/models.py index be193f1..29a3ae8 100644 --- a/ram/metadata/models.py +++ b/ram/metadata/models.py @@ -6,9 +6,6 @@ from django.dispatch.dispatcher import receiver from django.core.exceptions import ValidationError from django_countries.fields import CountryField -from tinymce import models as tinymce - -from ram.models import Document from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.managers import PublicManager @@ -132,20 +129,6 @@ class Decoder(models.Model): image_thumbnail.short_description = "Preview" -class DecoderDocument(Document): - decoder = models.ForeignKey( - Decoder, on_delete=models.CASCADE, related_name="document" - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["decoder", "file"], - name="unique_decoder_file" - ) - ] - - def calculate_ratio(ratio): try: num, den = ratio.split(":") @@ -239,14 +222,6 @@ class Tag(models.Model): ) -class GenericDocument(Document): - notes = tinymce.HTMLField(blank=True) - tags = models.ManyToManyField(Tag, blank=True) - - class Meta: - verbose_name_plural = "Generic Documents" - - class Shop(models.Model): name = models.CharField(max_length=128, unique=True) country = CountryField(blank=True) diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index ec8f297..909b501 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.16.9" +__version__ = "0.17.0" __version__ += git_suffix(__file__) diff --git a/ram/ram/models.py b/ram/ram/models.py index 9153bf6..ed71aac 100644 --- a/ram/ram/models.py +++ b/ram/ram/models.py @@ -27,11 +27,6 @@ class Document(models.Model): description = models.CharField(max_length=128, blank=True) file = models.FileField( upload_to="files/", - storage=DeduplicatedStorage(), - ) - private = models.BooleanField( - default=False, - help_text="Document will be visible only to logged users", ) creation_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) @@ -61,8 +56,17 @@ class Document(models.Model): 'Link'.format(self.file.url) ) + +class PrivateDocument(Document): + private = models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ) objects = PublicManager() + class Meta: + abstract = True + class Image(models.Model): order = models.PositiveIntegerField(default=0, blank=False, null=False) diff --git a/ram/ram/settings.py b/ram/ram/settings.py index 4b7a20b..5071c9d 100644 --- a/ram/ram/settings.py +++ b/ram/ram/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = [ "portal", # "driver", # uncomment this to enable the "driver" API "metadata", + "repository", "roster", "consist", "bookshelf", diff --git a/ram/repository/__init__.py b/ram/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ram/repository/admin.py b/ram/repository/admin.py new file mode 100644 index 0000000..546e311 --- /dev/null +++ b/ram/repository/admin.py @@ -0,0 +1,248 @@ +from django.contrib import admin + +from ram.admin import publish, unpublish +from repository.models import ( + GenericDocument, + InvoiceDocument, + BookDocument, + CatalogDocument, + DecoderDocument, + RollingStockDocument +) + + +@admin.register(GenericDocument) +class GenericDocumentAdmin(admin.ModelAdmin): + readonly_fields = ("size", "creation_time", "updated_time") + list_display = ( + "__str__", + "description", + "private", + "size", + "download", + ) + search_fields = ( + "description", + "file", + ) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "description", + "file", + "size", + "tags", + ) + }, + ), + ( + "Notes", + {"classes": ("collapse",), "fields": ("notes",)}, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) + actions = [publish, unpublish] + + +@admin.register(InvoiceDocument) +class InvoiceDocumentAdmin(admin.ModelAdmin): + readonly_fields = ("size", "creation_time", "updated_time") + list_display = ( + "__str__", + "description", + "date", + "shop", + "size", + "download", + ) + search_fields = ( + "rolling_stock__manufacturer__name", + "rolling_stock__item_number", + "book__title", + "catalog__manufacturer__name", + "shop__name", + "description", + "file", + ) + autocomplete_fields = ("rolling_stock", "book", "catalog", "shop") + fieldsets = ( + ( + None, + { + "fields": ( + "rolling_stock", + "book", + "catalog", + "description", + "date", + "shop", + "file", + "size", + ) + }, + ), + ( + "Notes", + {"classes": ("collapse",), "fields": ("notes",)}, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) + + +@admin.register(BookDocument) +class BookDocumentAdmin(admin.ModelAdmin): + readonly_fields = ("size",) + list_display = ( + "__str__", + "book", + "description", + "private", + "size", + "download", + ) + search_fields = ( + "book__title", + "description", + "file", + ) + autocomplete_fields = ("book",) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "book", + "description", + "file", + "size", + ) + }, + ), + ) + actions = [publish, unpublish] + + +@admin.register(CatalogDocument) +class CatalogDocumentAdmin(admin.ModelAdmin): + readonly_fields = ("size",) + list_display = ( + "__str__", + "catalog", + "description", + "private", + "size", + "download", + ) + search_fields = ( + "catalog__title", + "description", + "file", + ) + autocomplete_fields = ("catalog",) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "catalog", + "description", + "file", + "size", + ) + }, + ), + ) + actions = [publish, unpublish] + + +@admin.register(DecoderDocument) +class DecoderDocumentAdmin(admin.ModelAdmin): + readonly_fields = ("size",) + list_display = ( + "__str__", + "decoder", + "description", + "private", + "size", + "download", + ) + search_fields = ( + "decoder__name", + "decoder__manufacturer__name", + "description", + "file", + ) + autocomplete_fields = ("decoder",) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "decoder", + "description", + "file", + "size", + ) + }, + ), + ) + actions = [publish, unpublish] + + +@admin.register(RollingStockDocument) +class RollingStockDocumentAdmin(admin.ModelAdmin): + readonly_fields = ("size",) + list_display = ( + "__str__", + "rolling_stock", + "description", + "private", + "size", + "download", + ) + search_fields = ( + "rolling_stock__rolling_class__identifier", + "rolling_stock__item_number", + "description", + "file", + ) + autocomplete_fields = ("rolling_stock",) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "rolling_stock", + "description", + "file", + "size", + ) + }, + ), + ) + actions = [publish, unpublish] diff --git a/ram/repository/apps.py b/ram/repository/apps.py new file mode 100644 index 0000000..10bc534 --- /dev/null +++ b/ram/repository/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RepositoryConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "repository" diff --git a/ram/repository/migrations/0001_initial.py b/ram/repository/migrations/0001_initial.py new file mode 100644 index 0000000..06c5e9a --- /dev/null +++ b/ram/repository/migrations/0001_initial.py @@ -0,0 +1,361 @@ +# Generated by Django 5.1.4 on 2025-02-09 13:04 + +import django.db.models.deletion +import ram.utils +import tinymce.models +from django.db import migrations, models + + +def migrate_document(apps, schema_editor): + document = apps.get_model("metadata", "GenericDocument") + document_new = apps.get_model("repository", "GenericDocument") + for d in document.objects.all(): + n = document_new.objects.create( + notes=d.notes, + description=d.description, + file=d.file, + private=d.private, + creation_time=d.creation_time, + updated_time=d.updated_time, + ) + for t in d.tags.all(): + n.tags.add(t) + + +def migrate_decoder(apps, schema_editor): + dcc_document = apps.get_model("metadata", "DecoderDocument") + dcc_document_new = apps.get_model("repository", "DecoderDocument") + for d in dcc_document.objects.all(): + dcc_document_new.objects.create( + decoder=d.decoder, + description=d.description, + file=d.file, + private=d.private, + creation_time=d.creation_time, + updated_time=d.updated_time, + ) + + +def migrate_rollingstock(apps, schema_editor): + rs_document = apps.get_model("roster", "RollingStockDocument") + rs_document_new = apps.get_model("repository", "RollingStockDocument") + for d in rs_document.objects.all(): + rs_document_new.objects.create( + rolling_stock=d.rolling_stock, + description=d.description, + file=d.file, + private=d.private, + creation_time=d.creation_time, + updated_time=d.updated_time, + ) + + +def migrate_book(apps, schema_editor): + book_document = apps.get_model("bookshelf", "BaseBookDocument") + book_document_new = apps.get_model("repository", "BaseBookDocument") + catalog_document_new = apps.get_model("repository", "CatalogDocument") + for d in book_document.objects.all(): + if hasattr(d.book, "book"): + book_document_new.objects.create( + book=d.book.book, + description=d.description, + file=d.file, + private=d.private, + creation_time=d.creation_time, + updated_time=d.updated_time, + ) + else: + catalog_document_new.objects.create( + catalog=d.book.catalog, + description=d.description, + file=d.file, + private=d.private, + creation_time=d.creation_time, + updated_time=d.updated_time, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0022_basebook_shop"), + ("metadata", "0023_shop"), + ("roster", "0035_alter_rollingstock_shop"), + ] + + operations = [ + migrations.CreateModel( + name="BaseBookDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="new_document", + to="bookshelf.basebook", + ), + ), + ], + options={ + "verbose_name_plural": "Documents", + "abstract": False, + }, + ), + migrations.CreateModel( + name="BookDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="document", + to="bookshelf.book", + ), + ), + ], + options={ + "verbose_name_plural": "Book documents", + "constraints": [ + models.UniqueConstraint( + fields=("book", "file"), name="unique_book_file" + ) + ], + }, + ), + migrations.CreateModel( + name="CatalogDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ( + "catalog", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="document", + to="bookshelf.catalog", + ), + ), + ], + options={ + "verbose_name_plural": "Catalog documents", + "constraints": [ + models.UniqueConstraint( + fields=("catalog", "file"), name="unique_catalog_file" + ) + ], + }, + ), + migrations.CreateModel( + name="GenericDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ("notes", tinymce.models.HTMLField(blank=True)), + ( + "tags", + models.ManyToManyField( + blank=True, related_name="new_document", to="metadata.tag" + ), + ), + ], + options={ + "verbose_name_plural": "Generic Documents", + }, + ), + migrations.CreateModel( + name="RollingStockDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "rolling_stock", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="new_document", + to="roster.rollingstock", + ), + ), + ], + options={ + "verbose_name_plural": "Documents", + "abstract": False, + }, + ), + migrations.CreateModel( + name="DecoderDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "decoder", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="new_document", + to="metadata.decoder", + ), + ), + ], + options={ + "verbose_name_plural": "Documents", + "abstract": False, + }, + ), + migrations.RunPython( + migrate_document, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + migrate_decoder, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + migrate_rollingstock, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + migrate_book, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/ram/repository/migrations/0002_invoicedocument_remove_basebookdocument_book_and_more.py b/ram/repository/migrations/0002_invoicedocument_remove_basebookdocument_book_and_more.py new file mode 100644 index 0000000..1716fa4 --- /dev/null +++ b/ram/repository/migrations/0002_invoicedocument_remove_basebookdocument_book_and_more.py @@ -0,0 +1,157 @@ +# Generated by Django 5.1.4 on 2025-02-09 23:10 + +import django.db.models.deletion +import tinymce.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0023_delete_basebookdocument"), + ( + "metadata", + "0024_remove_genericdocument_tags_delete_decoderdocument_and_more", + ), + ("repository", "0001_initial"), + ("roster", "0036_delete_rollingstockdocument"), + ] + + operations = [ + migrations.CreateModel( + name="InvoiceDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ("private", models.BooleanField(default=True, editable=False)), + ("date", models.DateField()), + ("file", models.FileField(upload_to="files/invoices/")), + ("notes", tinymce.models.HTMLField(blank=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.RemoveField( + model_name="basebookdocument", + name="book", + ), + migrations.AlterModelOptions( + name="decoderdocument", + options={}, + ), + migrations.AlterModelOptions( + name="genericdocument", + options={"verbose_name_plural": "Generic documents"}, + ), + migrations.AlterModelOptions( + name="rollingstockdocument", + options={}, + ), + migrations.AlterField( + model_name="bookdocument", + name="file", + field=models.FileField(upload_to="files/"), + ), + migrations.AlterField( + model_name="catalogdocument", + name="file", + field=models.FileField(upload_to="files/"), + ), + migrations.AlterField( + model_name="decoderdocument", + name="decoder", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="document", + to="metadata.decoder", + ), + ), + migrations.AlterField( + model_name="decoderdocument", + name="file", + field=models.FileField(upload_to="files/"), + ), + migrations.AlterField( + model_name="genericdocument", + name="file", + field=models.FileField(upload_to="files/"), + ), + migrations.AlterField( + model_name="genericdocument", + name="tags", + field=models.ManyToManyField( + blank=True, related_name="document", to="metadata.tag" + ), + ), + migrations.AlterField( + model_name="rollingstockdocument", + name="file", + field=models.FileField(upload_to="files/"), + ), + migrations.AlterField( + model_name="rollingstockdocument", + name="rolling_stock", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="document", + to="roster.rollingstock", + ), + ), + migrations.AddConstraint( + model_name="decoderdocument", + constraint=models.UniqueConstraint( + fields=("decoder", "file"), name="unique_decoder_file" + ), + ), + migrations.AddConstraint( + model_name="rollingstockdocument", + constraint=models.UniqueConstraint( + fields=("rolling_stock", "file"), name="unique_stock_file" + ), + ), + migrations.AddField( + model_name="invoicedocument", + name="book", + field=models.ManyToManyField( + blank=True, related_name="invoice", to="bookshelf.book" + ), + ), + migrations.AddField( + model_name="invoicedocument", + name="catalog", + field=models.ManyToManyField( + blank=True, related_name="invoice", to="bookshelf.catalog" + ), + ), + migrations.AddField( + model_name="invoicedocument", + name="rolling_stock", + field=models.ManyToManyField( + blank=True, related_name="invoice", to="roster.rollingstock" + ), + ), + migrations.AddField( + model_name="invoicedocument", + name="shop", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="metadata.shop", + ), + ), + migrations.DeleteModel( + name="BaseBookDocument", + ), + ] diff --git a/ram/repository/migrations/__init__.py b/ram/repository/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ram/repository/models.py b/ram/repository/models.py new file mode 100644 index 0000000..00f8a0b --- /dev/null +++ b/ram/repository/models.py @@ -0,0 +1,90 @@ +from django.db import models +from django.core.exceptions import ValidationError + +from tinymce import models as tinymce + +from ram.models import PrivateDocument +from metadata.models import Decoder, Shop, Tag +from roster.models import RollingStock +from bookshelf.models import Book, Catalog + + +class GenericDocument(PrivateDocument): + notes = tinymce.HTMLField(blank=True) + tags = models.ManyToManyField(Tag, blank=True, related_name="document") + + class Meta: + verbose_name_plural = "Generic documents" + + +class InvoiceDocument(PrivateDocument): + private = models.BooleanField(default=True, editable=False) + rolling_stock = models.ManyToManyField( + RollingStock, related_name="invoice", blank=True + ) + book = models.ManyToManyField(Book, related_name="invoice", blank=True) + catalog = models.ManyToManyField( + Catalog, related_name="invoice", blank=True + ) + date = models.DateField() + shop = models.ForeignKey( + Shop, on_delete=models.SET_NULL, null=True, blank=True + ) + file = models.FileField( + upload_to="files/invoices/", + ) + notes = tinymce.HTMLField(blank=True) + + +class DecoderDocument(PrivateDocument): + decoder = models.ForeignKey( + Decoder, on_delete=models.CASCADE, related_name="document" + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["decoder", "file"], name="unique_decoder_file" + ) + ] + + +class BookDocument(PrivateDocument): + book = models.ForeignKey( + Book, on_delete=models.CASCADE, related_name="document" + ) + + class Meta: + verbose_name_plural = "Book documents" + constraints = [ + models.UniqueConstraint( + fields=["book", "file"], name="unique_book_file" + ) + ] + + +class CatalogDocument(PrivateDocument): + catalog = models.ForeignKey( + Catalog, on_delete=models.CASCADE, related_name="document" + ) + + class Meta: + verbose_name_plural = "Catalog documents" + constraints = [ + models.UniqueConstraint( + fields=["catalog", "file"], name="unique_catalog_file" + ) + ] + + +class RollingStockDocument(PrivateDocument): + rolling_stock = models.ForeignKey( + RollingStock, on_delete=models.CASCADE, related_name="document" + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["rolling_stock", "file"], name="unique_stock_file" + ) + ] diff --git a/ram/repository/tests.py b/ram/repository/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/ram/repository/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ram/repository/views.py b/ram/repository/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/ram/repository/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/ram/roster/admin.py b/ram/roster/admin.py index 86e52c7..f1962aa 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -8,13 +8,13 @@ from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from ram.admin import publish, unpublish from ram.utils import generate_csv +from repository.models import RollingStockDocument from portal.utils import get_site_conf from roster.models import ( RollingClass, RollingClassProperty, RollingStock, RollingStockImage, - RollingStockDocument, RollingStockProperty, RollingStockJournal, ) @@ -76,42 +76,8 @@ class RollingStockJournalInline(admin.TabularInline): classes = ["collapse"] -@admin.register(RollingStockDocument) -class RollingStockDocumentAdmin(admin.ModelAdmin): - readonly_fields = ("size",) - list_display = ( - "__str__", - "rolling_stock", - "description", - "private", - "size", - "download", - ) - search_fields = ( - "rolling_stock__rolling_class__identifier", - "rolling_stock__item_number", - "description", - "file", - ) - autocomplete_fields = ("rolling_stock",) - fieldsets = ( - ( - None, - { - "fields": ( - "private", - "rolling_stock", - "description", - "file", - "size", - ) - }, - ), - ) - - @admin.register(RollingStockJournal) -class RollingJournalDocumentAdmin(admin.ModelAdmin): +class RollingJournalAdmin(admin.ModelAdmin): list_display = ( "__str__", "date", @@ -152,7 +118,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): RollingStockJournalInline, ) autocomplete_fields = ("rolling_class", "shop") - readonly_fields = ("preview", "creation_time", "updated_time") + readonly_fields = ("preview", "invoices", "creation_time", "updated_time") list_display = ( "__str__", "address", @@ -223,6 +189,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "shop", "purchase_date", "price", + "invoices", ) }, ), @@ -249,6 +216,17 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): ) return form + @admin.display(description="Invoices") + def invoices(self, obj): + if obj.invoice.exists(): + html = "
".join( + "{}".format( + i.file.url, i + ) for i in obj.invoice.all()) + else: + html = "-" + return format_html(html) + def download_csv(modeladmin, request, queryset): header = [ "Name", diff --git a/ram/roster/migrations/0036_delete_rollingstockdocument.py b/ram/roster/migrations/0036_delete_rollingstockdocument.py new file mode 100644 index 0000000..3234a3e --- /dev/null +++ b/ram/roster/migrations/0036_delete_rollingstockdocument.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.4 on 2025-02-09 13:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("roster", "0035_alter_rollingstock_shop"), + ("repository", "0001_initial"), + ] + + operations = [ + migrations.DeleteModel( + name="RollingStockDocument", + ), + ] diff --git a/ram/roster/models.py b/ram/roster/models.py index 999819b..72fc261 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -8,7 +8,7 @@ from django.dispatch import receiver from tinymce import models as tinymce -from ram.models import BaseModel, Document, Image, PropertyInstance +from ram.models import BaseModel, Image, PropertyInstance from ram.utils import DeduplicatedStorage, slugify from ram.managers import PublicManager from metadata.models import ( @@ -169,20 +169,6 @@ def pre_save_internal_fields(sender, instance, *args, **kwargs): instance.item_number_slug = slugify(instance.item_number) -class RollingStockDocument(Document): - rolling_stock = models.ForeignKey( - RollingStock, on_delete=models.CASCADE, related_name="document" - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["rolling_stock", "file"], - name="unique_stock_file" - ) - ] - - def rolling_stock_image_upload(instance, filename): return os.path.join( "images",