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",