diff --git a/.gitignore b/.gitignore
index 2dc53ca..b929545 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
+
+# Drawio
+.*.drawio.bkp
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1e25492..fdc19da 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,18 +1,27 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
repos:
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.2.0
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: check-yaml
- - id: check-added-large-files
- - repo: https://github.com/asottile/reorder-python-imports
- rev: v3.12.0
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+- repo: https://github.com/pre-commit/mirrors-autopep8
+ rev: v2.0.4
hooks:
- - id: reorder-python-imports
- - repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.2.0
+ - id: autopep8
+- repo: https://github.com/asottile/add-trailing-comma
+ rev: v3.1.0
hooks:
- - id: ruff
- args: [ --fix, --exit-non-zero-on-fix ]
- - id: ruff-format
+ - id: add-trailing-comma
+- repo: https://github.com/asottile/reorder-python-imports
+ rev: v3.13.0
+ hooks:
+ - id: reorder-python-imports
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.5.5
+ hooks:
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..76d1b41
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Facturio
+- DB Schema: [dbdiagram.io](https://dbdiagram.io/d/Facturio-65c15011ac844320ae80e86e) -- TODO: Update
+- KanBoard: [board.katuwoss.dev](https://board.katuwoss.dev/public/board/b64e04ae8ba0cc2fa26712ccd555ea4e4290b8463a8ef6e3a19aa74829c4)
diff --git a/TODO.md b/TODO.md
deleted file mode 100644
index 787f06e..0000000
--- a/TODO.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# TODO
-
-## Localisation
-
-- [ ] It would be nice to centralize the translations to some well-chosen singular directory
-- [ ] Integrate it with some public translation project?
-
-## DevOps Functionality
-- [ ] I need to dockerize it
-- [ ] It should be easily buildable and publishable using some kind of CI/CD
-
-## Application Functionality
-
-- [ ] I should be able to connect one (or many) subjects to a user, so they can start generating as the one
- - This is meant so that the user is being the "connected subject"
-- [ ] I need to be able to create the invoices as the connected subject to any subject in current database
- - Automatically creating non-existing subjects might be nice to have
-- [ ] I should be able to keep the invoices even if the subject is deleted (or changed) with the data in the day of the
- creation
-- [ ] Generating QR Payments is nice to have
-- [ ] It would be great to automatically check `ares` for data changes from time to time
diff --git a/accounts/admin.py b/accounts/admin.py
index a578c41..b3077d3 100644
--- a/accounts/admin.py
+++ b/accounts/admin.py
@@ -7,4 +7,8 @@ from . import models
@admin.register(models.User)
class UserAdmin(BaseUserAdmin):
- fieldsets = (*BaseUserAdmin.fieldsets, (_('Subjects'), {'fields': ('subjects',)}))
+ fieldsets = (
+ *BaseUserAdmin.fieldsets,
+ (_('Subjects'), {'fields': ('supplier', 'customers')}),
+ )
+ filter_horizontal = (*BaseUserAdmin.filter_horizontal, 'customers')
diff --git a/accounts/forms.py b/accounts/forms.py
index c74fdb3..b26120a 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -7,9 +7,6 @@ from django.utils.translation import gettext_lazy as _
from .models import User
-# from .models import User
-
-
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -22,7 +19,8 @@ class LoginForm(AuthenticationForm):
class RegisterForm(UserCreationForm):
class Meta:
model = User
- fields = UserCreationForm.Meta.fields + ('first_name', 'last_name', 'email')
+ fields = UserCreationForm.Meta.fields + \
+ ('first_name', 'last_name', 'email')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
index 6a903e4..1e7e1d2 100644
--- a/accounts/migrations/0001_initial.py
+++ b/accounts/migrations/0001_initial.py
@@ -26,11 +26,15 @@ class Migration(migrations.Migration):
verbose_name='ID',
),
),
- ('password', models.CharField(max_length=128, verbose_name='password')),
+ (
+ 'password', models.CharField(
+ max_length=128, verbose_name='password',
+ ),
+ ),
(
'last_login',
models.DateTimeField(
- blank=True, null=True, verbose_name='last login'
+ blank=True, null=True, verbose_name='last login',
),
),
(
@@ -45,13 +49,13 @@ class Migration(migrations.Migration):
'username',
models.CharField(
error_messages={
- 'unique': 'A user with that username already exists.'
+ 'unique': 'A user with that username already exists.',
},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150,
unique=True,
validators=[
- django.contrib.auth.validators.UnicodeUsernameValidator()
+ django.contrib.auth.validators.UnicodeUsernameValidator(),
],
verbose_name='username',
),
@@ -59,19 +63,19 @@ class Migration(migrations.Migration):
(
'first_name',
models.CharField(
- blank=True, max_length=150, verbose_name='first name'
+ blank=True, max_length=150, verbose_name='first name',
),
),
(
'last_name',
models.CharField(
- blank=True, max_length=150, verbose_name='last name'
+ blank=True, max_length=150, verbose_name='last name',
),
),
(
'email',
models.EmailField(
- blank=True, max_length=254, verbose_name='email address'
+ blank=True, max_length=254, verbose_name='email address',
),
),
(
@@ -93,7 +97,7 @@ class Migration(migrations.Migration):
(
'date_joined',
models.DateTimeField(
- default=django.utils.timezone.now, verbose_name='date joined'
+ default=django.utils.timezone.now, verbose_name='date joined',
),
),
(
diff --git a/accounts/migrations/0002_alter_user_email_alter_user_first_name_and_more.py b/accounts/migrations/0002_alter_user_email_alter_user_first_name_and_more.py
index 1674e31..8fbdc3e 100644
--- a/accounts/migrations/0002_alter_user_email_alter_user_first_name_and_more.py
+++ b/accounts/migrations/0002_alter_user_email_alter_user_first_name_and_more.py
@@ -12,7 +12,9 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='user',
name='email',
- field=models.EmailField(max_length=254, verbose_name='email address'),
+ field=models.EmailField(
+ max_length=254, verbose_name='email address',
+ ),
),
migrations.AlterField(
model_name='user',
diff --git a/accounts/migrations/0005_remove_user_subjects_user_customers_user_suppliers.py b/accounts/migrations/0005_remove_user_subjects_user_customers_user_suppliers.py
new file mode 100644
index 0000000..769c756
--- /dev/null
+++ b/accounts/migrations/0005_remove_user_subjects_user_customers_user_suppliers.py
@@ -0,0 +1,31 @@
+# Generated by Django 5.0.1 on 2024-02-06 18:57
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('accounts', '0004_alter_user_subjects'),
+ ('subjects', '0003_alter_subject_options'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='user',
+ name='subjects',
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='customers',
+ field=models.ManyToManyField(
+ blank=True, related_name='+', to='subjects.subject',
+ ),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='suppliers',
+ field=models.ManyToManyField(
+ blank=True, related_name='+', to='subjects.subject',
+ ),
+ ),
+ ]
diff --git a/accounts/migrations/0006_remove_user_suppliers_user_supplier_and_more.py b/accounts/migrations/0006_remove_user_suppliers_user_supplier_and_more.py
new file mode 100644
index 0000000..c52bdbb
--- /dev/null
+++ b/accounts/migrations/0006_remove_user_suppliers_user_supplier_and_more.py
@@ -0,0 +1,36 @@
+# Generated by Django 5.0.1 on 2024-02-16 15:20
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('accounts', '0005_remove_user_subjects_user_customers_user_suppliers'),
+ ('subjects', '0003_alter_subject_options'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='user',
+ name='suppliers',
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='supplier',
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name='supplier',
+ to='subjects.subject',
+ ),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='customers',
+ field=models.ManyToManyField(
+ blank=True, to='subjects.subject', verbose_name='customers',
+ ),
+ ),
+ ]
diff --git a/accounts/models.py b/accounts/models.py
index e3e72b5..e9f7f14 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -12,7 +12,18 @@ class User(AbstractUser):
last_name = models.CharField(_('last name'), max_length=150)
email = models.EmailField(_('email address'))
- subjects = models.ManyToManyField(Subject, blank=True)
+ supplier = models.ForeignKey(
+ Subject, models.PROTECT, _('supplier'), blank=True, null=True,
+ )
+ customers = models.ManyToManyField(
+ Subject, blank=True, verbose_name=_('customers'),
+ )
class Meta(AbstractUser.Meta):
...
+
+ def _get_m2m_ids(self, field: str):
+ return list(getattr(self, field).values_list('id', flat=True))
+
+ def get_customers(self) -> list[int]:
+ return self._get_m2m_ids('customers')
diff --git a/accounts/templates/account/me.html b/accounts/templates/account/me.html
index db158ef..1e593f6 100644
--- a/accounts/templates/account/me.html
+++ b/accounts/templates/account/me.html
@@ -19,14 +19,27 @@
- {% if request.user.subjects.exists %}
+ {% if request.user.supplier %}
-
+
- {% for subject in request.user.subjects.all %}
- {{ subject.name }}
+ {% with supplier_data=request.user.supplier.get_latest_data %}
+ {{ supplier_data.name }}
+ {% endwith %}
+
+
+ {% endif %}
+ {% if request.user.customers.exists %}
+
+
+
+ {% for subject in request.user.customers.all %}
+ {% with customer_data=subject.get_latest_data %}
+ {{ customer_data.name }}
+ {% endwith %}
{% endfor %}
{% endif %}
+
{% endblock %}
diff --git a/accounts/views.py b/accounts/views.py
index f2185a0..a44bd8a 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -59,5 +59,4 @@ def auth_register(req: HttpRequest) -> HttpResponse:
@login_required
def me(req: HttpRequest) -> HttpResponse:
- print(req.user.username)
return render(req, 'account/me.html')
diff --git a/db_schema.drawio b/db_schema.drawio
new file mode 100644
index 0000000..fb01ed7
--- /dev/null
+++ b/db_schema.drawio
@@ -0,0 +1,556 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/db_schema_rework.drawio b/db_schema_rework.drawio
new file mode 100644
index 0000000..d062827
--- /dev/null
+++ b/db_schema_rework.drawio
@@ -0,0 +1,523 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/facturio/asgi.py b/facturio/asgi.py
index 26e9844..21ef554 100644
--- a/facturio/asgi.py
+++ b/facturio/asgi.py
@@ -10,6 +10,9 @@ import os
from django.core.asgi import get_asgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'facturio.settings')
+os.environ.setdefault(
+ 'DJANGO_SETTINGS_MODULE',
+ 'facturio.settings.development',
+)
application = get_asgi_application()
diff --git a/facturio/settings/__init__.py b/facturio/settings/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/facturio/settings.py b/facturio/settings/base.py
similarity index 92%
rename from facturio/settings.py
rename to facturio/settings/base.py
index 2785a13..deea0b5 100644
--- a/facturio/settings.py
+++ b/facturio/settings/base.py
@@ -12,18 +12,23 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
from pathlib import Path
from django.utils.translation import gettext_lazy as _
+from environ import Env
+
+env = Env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
-BASE_DIR = Path(__file__).resolve().parent.parent
+BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'django-insecure-+l+g3)q1zz2bz7=mz4ys6lhu5uj+=ucj34flm^clo4vb3(wmdp'
+SECRET_KEY = env(
+ 'SECRET_KEY', default='django-insecure-+l+g3)q1zz2bz7=mz4ys6lhu5uj+=ucj34flm^clo4vb3(wmdp',
+)
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = env.bool("DEBUG", default=True)
ALLOWED_HOSTS = ['*']
@@ -40,6 +45,7 @@ INSTALLED_APPS = [
'crispy_bootstrap5',
'accounts.apps.AccountConfig',
'subjects.apps.SubjectsConfig',
+ 'invoices.apps.InvoicesConfig',
]
MIDDLEWARE = [
@@ -80,7 +86,7 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
- }
+ },
}
# Password validation
diff --git a/facturio/settings/development.py b/facturio/settings/development.py
new file mode 100644
index 0000000..897c454
--- /dev/null
+++ b/facturio/settings/development.py
@@ -0,0 +1 @@
+from facturio.settings.base import * # noqa
diff --git a/facturio/settings/production.py b/facturio/settings/production.py
new file mode 100644
index 0000000..897c454
--- /dev/null
+++ b/facturio/settings/production.py
@@ -0,0 +1 @@
+from facturio.settings.base import * # noqa
diff --git a/facturio/urls.py b/facturio/urls.py
index cbfa486..f5bd9fb 100644
--- a/facturio/urls.py
+++ b/facturio/urls.py
@@ -15,6 +15,7 @@ urlpatterns = i18n_patterns(
path('', landing_page, name='main-page'),
path('accounts/', include('accounts.urls')),
path('subjects/', include('subjects.urls')),
+ path('invoices/', include('invoices.urls')),
path('admin/', admin.site.urls),
prefix_default_language=False,
)
diff --git a/facturio/wsgi.py b/facturio/wsgi.py
index e1c1dcb..84b1ae0 100644
--- a/facturio/wsgi.py
+++ b/facturio/wsgi.py
@@ -10,6 +10,9 @@ import os
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'facturio.settings')
+os.environ.setdefault(
+ 'DJANGO_SETTINGS_MODULE',
+ 'facturio.settings.development',
+)
application = get_wsgi_application()
diff --git a/invoices/__init__.py b/invoices/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/invoices/admin.py b/invoices/admin.py
new file mode 100644
index 0000000..e6f087c
--- /dev/null
+++ b/invoices/admin.py
@@ -0,0 +1,21 @@
+from django.contrib import admin
+
+from . import models
+
+
+class InvoiceItemInline(admin.TabularInline):
+ model = models.InvoiceItem
+ extra = 0
+ readonly_fields = [
+ 'amount',
+ 'amount_unit',
+ 'description',
+ 'price_for_amount',
+ ]
+ can_delete = False
+
+
+@admin.register(models.Invoice)
+class InvoiceAdmin(admin.ModelAdmin):
+ list_display = ['__str__', 'invoice_date']
+ inlines = [InvoiceItemInline]
diff --git a/invoices/apps.py b/invoices/apps.py
new file mode 100644
index 0000000..b68fb44
--- /dev/null
+++ b/invoices/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class InvoicesConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'invoices'
diff --git a/invoices/forms.py b/invoices/forms.py
new file mode 100644
index 0000000..aeb5b38
--- /dev/null
+++ b/invoices/forms.py
@@ -0,0 +1,60 @@
+from crispy_forms import helper
+from crispy_forms import layout
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+from . import models
+from subjects import models as subject_models
+
+
+class InvoiceItemForm(forms.ModelForm):
+ class Meta:
+ model = models.InvoiceItem
+ fields = ["amount", "amount_unit", "description", "price_for_amount"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.helper = helper.FormHelper()
+ self.helper.form_show_labels = False
+
+
+InvoiceItemFormSet = forms.inlineformset_factory(
+ models.Invoice, models.InvoiceItem, form=InvoiceItemForm,
+ fields=["description", "amount", "amount_unit", "price_for_amount"],
+ extra=3, can_delete=False, validate_min=True, min_num=1,
+)
+
+
+class CreateInvoiceForm(forms.ModelForm):
+ class Meta:
+ model = models.Invoice
+ fields = ['customer', 'supplier', 'due_date']
+ widgets = {
+ 'due_date': forms.DateInput(attrs={'type': 'date'}),
+ }
+
+ def __init__(self, *args, **kwargs):
+ self._user = kwargs.pop('current_user', None)
+
+ super().__init__(*args, **kwargs)
+
+ if self._user.supplier:
+ self.fields["supplier"].queryset = subject_models.Subject.objects.filter(
+ id=self._user.supplier.id,
+ )
+ else:
+ self.fields["supplier"].queryset = subject_models.Subject.objects.none()
+ self.fields["customer"].queryset = self._user.customers
+
+ self.helper = helper.FormHelper()
+ self.helper.form_method = 'post'
+ self.helper.add_input(layout.Submit('submit', _('Save')))
+
+ def save(self, commit=True):
+ invoice = super().save(commit=False)
+ invoice.user = self._user
+ invoice.customer_data = invoice.customer.get_latest_data()
+ invoice.supplier_data = invoice.supplier.get_latest_data()
+ if commit:
+ invoice.save()
+ return invoice
diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py
new file mode 100644
index 0000000..90fcb87
--- /dev/null
+++ b/invoices/migrations/0001_initial.py
@@ -0,0 +1,126 @@
+# Generated by Django 5.0.4 on 2024-06-14 20:08
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = [
+ ('subjects', '0004_remove_subject_city_remove_subject_city_part_and_more'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Invoice',
+ fields=[
+ (
+ 'id',
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name='ID',
+ ),
+ ),
+ ('invoice_date', models.DateField(verbose_name='Invoice date')),
+ ('due_date', models.DateField(verbose_name='Due date')),
+ (
+ 'customer',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='+',
+ to='subjects.subject',
+ ),
+ ),
+ (
+ 'customer_data',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='+',
+ to='subjects.subjectdata',
+ ),
+ ),
+ (
+ 'supplier',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='+',
+ to='subjects.subject',
+ ),
+ ),
+ (
+ 'supplier_data',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='+',
+ to='subjects.subjectdata',
+ ),
+ ),
+ (
+ 'user',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Invoice',
+ 'verbose_name_plural': 'Invoices',
+ },
+ ),
+ migrations.CreateModel(
+ name='InvoiceItem',
+ fields=[
+ (
+ 'id',
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name='ID',
+ ),
+ ),
+ (
+ 'amount',
+ models.DecimalField(
+ decimal_places=3, max_digits=8, verbose_name='Amount',
+ ),
+ ),
+ (
+ 'amount_unit',
+ models.CharField(
+ max_length=32, verbose_name='Amount unit',
+ ),
+ ),
+ (
+ 'description',
+ models.CharField(
+ max_length=64, verbose_name='Description',
+ ),
+ ),
+ (
+ 'price_for_amount',
+ models.DecimalField(
+ decimal_places=3, max_digits=8, verbose_name='Price for amount',
+ ),
+ ),
+ (
+ 'invoice',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='items',
+ to='invoices.invoice',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Invoice Item',
+ 'verbose_name_plural': 'Invoice Items',
+ },
+ ),
+ ]
diff --git a/invoices/migrations/0002_alter_invoice_invoice_date.py b/invoices/migrations/0002_alter_invoice_invoice_date.py
new file mode 100644
index 0000000..1a06509
--- /dev/null
+++ b/invoices/migrations/0002_alter_invoice_invoice_date.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.6 on 2024-07-04 21:59
+import django.utils.timezone
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("invoices", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="invoice",
+ name="invoice_date",
+ field=models.DateField(
+ default=django.utils.timezone.now, verbose_name="Invoice date",
+ ),
+ ),
+ ]
diff --git a/invoices/migrations/__init__.py b/invoices/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/invoices/models.py b/invoices/models.py
new file mode 100644
index 0000000..5ed4f3e
--- /dev/null
+++ b/invoices/models.py
@@ -0,0 +1,71 @@
+import decimal
+import typing
+
+from django.contrib.auth import get_user_model
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+
+if typing.TYPE_CHECKING:
+ from accounts.models import User
+
+from subjects import models as subject_models
+
+UserModel: 'User' = get_user_model()
+
+
+class Invoice(models.Model):
+ class Meta:
+ verbose_name = _('Invoice')
+ verbose_name_plural = _('Invoices')
+
+ user = models.ForeignKey(UserModel, models.CASCADE)
+ supplier = models.ForeignKey(
+ subject_models.Subject, on_delete=models.CASCADE, related_name='+', verbose_name=_('Supplier'),
+ )
+ supplier_data = models.ForeignKey(
+ subject_models.SubjectData, on_delete=models.CASCADE, related_name='+', verbose_name=_('Supplier data'),
+ )
+
+ customer = models.ForeignKey(
+ subject_models.Subject, on_delete=models.CASCADE, related_name='+', verbose_name=_('Customer'),
+ )
+ customer_data = models.ForeignKey(
+ subject_models.SubjectData, on_delete=models.CASCADE, related_name='+', verbose_name=_('Customer data'),
+ )
+ invoice_date = models.DateField(_('Invoice date'), default=timezone.now)
+ due_date = models.DateField(_('Due date'))
+
+ def __str__(self):
+ return f'{self.id} - {self.supplier_data.name} -> {self.customer_data.name}'
+
+ def total_price(self) -> decimal.Decimal:
+ total = decimal.Decimal(0)
+ for item in self.items.all():
+ total += item.total_price()
+ return total
+
+ def custom_id(self) -> str:
+ return f"{self.invoice_date.year}-{self.id:04}"
+
+
+class InvoiceItem(models.Model):
+ class Meta:
+ verbose_name = _('Invoice Item')
+ verbose_name_plural = _('Invoice Items')
+
+ invoice = models.ForeignKey(
+ Invoice, on_delete=models.CASCADE, related_name='items',
+ )
+ amount = models.DecimalField(_('Amount'), decimal_places=3, max_digits=8)
+ amount_unit = models.CharField(_('Amount unit'), max_length=32)
+ description = models.CharField(_('Description'), max_length=64)
+ price_for_amount = models.DecimalField(
+ _('Price for amount'), decimal_places=3, max_digits=8,
+ )
+
+ def total_price(self) -> decimal.Decimal:
+ return self.amount * self.price_for_amount
+
+ def __str__(self):
+ return f'{self.id} -> {self.invoice.id}'
diff --git a/invoices/static/css/invoice.css b/invoices/static/css/invoice.css
new file mode 100644
index 0000000..c802908
--- /dev/null
+++ b/invoices/static/css/invoice.css
@@ -0,0 +1,112 @@
+@import url("https://fonts.googleapis.com/css2?family=Ubuntu&display=swap");
+
+:root {
+ --font-family: "Ubuntu", sans-serif;
+ --main-color: #333;
+ --secondary-color: #888;
+ --border-color: #ddd;
+ --background-color: #fff;
+}
+
+header {
+ text-align: left;
+ margin-bottom: 20px;
+}
+
+.parties ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.parties li {
+ margin-top: 5px;
+}
+
+.parties h2 {
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 8px;
+ font-size: 1.2rem;
+}
+
+.invoice-id {
+ color: var(--secondary-color);
+}
+
+#invoice {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+#invoice th,
+#invoice td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.invoice-total {
+ margin-top: 20px;
+ text-align: right;
+}
+
+.parties-section {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+}
+
+.parties {
+ width: 48%;
+}
+
+#invoice tbody tr {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.small-col {
+ width: 5%;
+}
+
+.medium-col {
+ width: 15%;
+}
+
+.big-col {
+ width: 25%;
+}
+
+.right-align {
+ text-align: right;
+}
+
+footer {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ text-align: center;
+ left: 0;
+ right: 0;
+ padding: 0;
+ margin-top: 20px;
+ color: var(--main-color);
+ background-color: var(--background-color);
+}
+
+@media print {
+ @page {
+ size: auto;
+ margin: 0;
+ }
+
+ .navbar {
+ display: none !important;
+ }
+
+ body {
+ font-family: var(--font-family), sans-serif;
+ color: var(--main-color);
+ background-color: var(--background-color);
+ margin: 10mm 10mm;
+ }
+}
diff --git a/invoices/static/js/formset_mngr.js b/invoices/static/js/formset_mngr.js
new file mode 100644
index 0000000..76fc1cc
--- /dev/null
+++ b/invoices/static/js/formset_mngr.js
@@ -0,0 +1,55 @@
+document.addEventListener("DOMContentLoaded", (event) => {
+ const totalForms = document.getElementById("id_items-TOTAL_FORMS")
+ const initialForms = document.getElementById("id_items-INITIAL_FORMS")
+ const minForms = document.getElementById("id_items-MIN_NUM_FORMS")
+ const maxForms = document.getElementById("id_items-MAX_NUM_FORMS")
+ const formsetContainer = document.getElementById("formset-container")
+
+ console.log(totalForms.value, initialForms.value, minForms.value, maxForms.value, formsetContainer)
+
+ const renumberAll = () => {
+ let id = 0;
+ formsetContainer.querySelectorAll("tr").forEach(row => {
+ row.querySelectorAll("[name^='items-'], [id^='id_items-']").forEach(element => {
+ const namePattern = /items-(\d+)-/;
+ const idPattern = /id_items-(\d+)-/;
+ element.name = element.name.replace(namePattern, `items-${id}-`);
+ element.id = element.id.replace(idPattern, `id_items-${id}-`);
+ });
+ id += 1;
+ });
+ totalForms.value = id; // Update the total form count
+ };
+
+ const clearValues = (node) => {
+ node.querySelectorAll("input").forEach(element => {
+ if (element.type === "checkbox" || element.type === "radio") {
+ element.checked = false;
+ } else {
+ element.value = "";
+ }
+ })
+ }
+
+ document.getElementById("add-formset-item").addEventListener("click", () => {
+ if (parseInt(totalForms.value) >= parseInt(maxForms.value)) {
+ return;
+ }
+
+ const template = formsetContainer.firstElementChild;
+ const newForm = template.cloneNode(true)
+ formsetContainer.appendChild(newForm)
+ clearValues(newForm)
+
+ renumberAll();
+ });
+
+ formsetContainer.addEventListener("click", (event) => {
+ if (event.target.classList.contains("remove-formset-item") && (parseInt(totalForms.value) > parseInt(minForms.value))) {
+ const clickedButton = event.target
+ clickedButton.closest("tr").remove()
+ renumberAll();
+ }
+ });
+
+})
diff --git a/invoices/templates/invoices/index.html b/invoices/templates/invoices/index.html
new file mode 100644
index 0000000..b39a5a0
--- /dev/null
+++ b/invoices/templates/invoices/index.html
@@ -0,0 +1,78 @@
+{% extends "facturio/base.html" %}
+{% load crispy_forms_filters %}
+{% load static %}
+{% load i18n %}
+
+{% load crispy_forms_tags %}
+
+{% block title %}{% trans "Invoices" %}{% endblock %}
+
+{% block content %}
+ {% trans "Invoices" %}
+
+
{% trans "Create invoice" %}
+
+
+
+
+
{% trans "Existing invoices" %}
+
+
+
+ {% trans "Invoice date" %}
+ {% trans "Supplier" %}
+ {% trans "Customer" %}
+ {% trans "Due date" %}
+
+
+
+
+
+ {% for invoice in invoices %}
+
+ {{ invoice.invoice_date }}
+ {{ invoice.supplier_data.name }}
+ {{ invoice.customer_data.name }}
+ {{ invoice.due_date }}
+ {% trans "Show" %}
+ {% trans "Print" %}
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/invoices/templates/invoices/invoice.html b/invoices/templates/invoices/invoice.html
new file mode 100644
index 0000000..7020ef5
--- /dev/null
+++ b/invoices/templates/invoices/invoice.html
@@ -0,0 +1,202 @@
+{% load i18n %}
+
+
+
+
+
+ Faktura
+
+
+
+
+ {% trans "Invoice" %} {{ invoice.custom_id }}
+
+
+
+
+
{% trans "Supplier" %}
+
+ {% with invoice.supplier_data as sd %}
+ {{ sd.name }}
+ {{ sd.street }}
+ {{ sd.city }} - {{ sd.city_part }}, {{ sd.zip_code }}
+
+ {% trans "CIN" %}: {{ invoice.supplier.id }}
+ {% if invoice.supplier.vat_id %}
+ {% trans "VAT ID" %}: {{ invoice.supplier.vat_id }}
+ {% endif %}
+
+ Bankovní účet: TODO
+ {% endwith %}
+
+
+
+
+
{% trans "Customer" %}
+
+ {% with invoice.customer_data as cd %}
+ {{ cd.name }}
+ {{ cd.street }}
+ {{ cd.city }} - {{ cd.city_part }}, {{ cd.zip_code }}
+
+ {% trans "CIN" %}: {{ invoice.customer.id }}
+ {% if invoice.customer.vat_id %}
+ {% trans "VAT ID" %}: {{ invoice.customer.vat_id }}
+ {% endif %}
+
+ {% trans "Invoice date" %}: {{ invoice.invoice_date }}
+ {% trans "Due date" %}: {{ invoice.due_date }}
+ {% endwith %}
+
+
+
+
+
+ {% trans "Invoice details" %}
+
+
+
+ {% trans "Amount" %}
+ {% trans "Amount unit" %}
+ {% trans "Description" %}
+ {% trans "Price for amount" %}
+ {% trans "Total" %}
+
+
+
+ {% for invoice_item in invoice.items.all %}
+
+ {{ invoice_item.amount.normalize }}
+ {{ invoice_item.amount_unit }}
+ {{ invoice_item.description }}
+ {{ invoice_item.price_for_amount.normalize }} Kč
+ {{ invoice_item.total_price.normalize }} Kč
+
+ {% endfor %}
+
+
+
+
+
{% trans "Total" %}: {{ invoice.total_price.normalize }} Kč
+
+
+
+
+
+
diff --git a/invoices/templates/invoices/view.html b/invoices/templates/invoices/view.html
new file mode 100644
index 0000000..3acc7a7
--- /dev/null
+++ b/invoices/templates/invoices/view.html
@@ -0,0 +1,85 @@
+{% extends "facturio/base.html" %}
+{% load static %}
+{% load i18n %}
+{% block title %}{% trans "Invoice" %}{% endblock %}
+{% block head %}
+
+{% endblock %}
+{% block content %}
+
+ {% trans "Invoice" %} {{ invoice.custom_id }}
+
+
+
+
{% trans "Supplier" %}
+
+ {% with invoice.supplier_data as sd %}
+ {{ sd.name }}
+ {{ sd.street }}
+ {{ sd.city }} - {{ sd.city_part }}, {{ sd.zip_code }}
+
+ {% trans "CIN" %}: {{ invoice.supplier.id }}
+ {% if invoice.supplier.vat_id %}
+ {% trans "VAT ID" %}: {{ invoice.supplier.vat_id }}
+ {% endif %}
+
+ Bankovní účet: TODO
+ {% endwith %}
+
+
+
+
+
{% trans "Customer" %}
+
+ {% with invoice.customer_data as cd %}
+ {{ cd.name }}
+ {{ cd.street }}
+ {{ cd.city }} - {{ cd.city_part }}, {{ cd.zip_code }}
+
+ {% trans "CIN" %}: {{ invoice.customer.id }}
+ {% if invoice.customer.vat_id %}
+ {% trans "VAT ID" %}: {{ invoice.customer.vat_id }}
+ {% endif %}
+
+ {% trans "Invoice date" %}: {{ invoice.invoice_date }}
+ {% trans "Due date" %}: {{ invoice.due_date }}
+ {% endwith %}
+
+
+
+
+
+ {% trans "Invoice details" %}
+
+
+
+ {% trans "Amount" %}
+ {% trans "Amount unit" %}
+ {% trans "Description" %}
+ {% trans "Price for amount" %}
+ {% trans "Total" %}
+
+
+
+ {% for invoice_item in invoice.items.all %}
+
+ {{ invoice_item.amount.normalize }}
+ {{ invoice_item.amount_unit }}
+ {{ invoice_item.description }}
+ {{ invoice_item.price_for_amount.normalize }} Kč
+ {{ invoice_item.total_price.normalize }} Kč
+
+ {% endfor %}
+
+
+
+
+
{% trans "Total" %}: {{ invoice.total_price.normalize }} Kč
+
+
+
+
+{% endblock %}
diff --git a/invoices/tests.py b/invoices/tests.py
new file mode 100644
index 0000000..a39b155
--- /dev/null
+++ b/invoices/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/invoices/urls.py b/invoices/urls.py
new file mode 100644
index 0000000..6983ce2
--- /dev/null
+++ b/invoices/urls.py
@@ -0,0 +1,11 @@
+from django.urls import path
+
+from . import views
+
+app_name = 'invoices'
+
+urlpatterns = [
+ path('', views.home, name='index'),
+ path('', views.view_invoice, name='invoice'),
+ path('print/', views.print_invoice, name='print_invoice'),
+]
diff --git a/invoices/views.py b/invoices/views.py
new file mode 100644
index 0000000..6475748
--- /dev/null
+++ b/invoices/views.py
@@ -0,0 +1,52 @@
+from django.contrib.auth.decorators import login_required
+from django.db import transaction
+from django.http import HttpRequest
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from django.shortcuts import render
+from django.urls import reverse
+
+from . import forms
+from . import models
+
+
+@login_required
+def home(req: HttpRequest) -> HttpResponse:
+ user_invoices = models.Invoice.objects.filter(user=req.user).all()
+
+ if req.method == 'POST':
+ form = forms.CreateInvoiceForm(data=req.POST, current_user=req.user)
+ if form.is_valid():
+ with transaction.atomic():
+ invoice = form.save()
+ formset = forms.InvoiceItemFormSet(
+ data=req.POST, instance=invoice,
+ )
+ if formset.is_valid():
+ formset.save()
+ return redirect(reverse('invoices:invoice', kwargs=dict(invoice_id=invoice.id)))
+ else:
+ transaction.set_rollback(True)
+ else:
+ formset = forms.InvoiceItemFormSet(data=req.POST)
+
+ return render(req, 'invoices/index.html', dict(form=form, formset=formset, invoices=user_invoices))
+
+ elif req.method == 'GET':
+ form = forms.CreateInvoiceForm(current_user=req.user)
+ formset = forms.InvoiceItemFormSet()
+ return render(req, 'invoices/index.html', dict(form=form, formset=formset, invoices=user_invoices))
+
+ return HttpResponse(status=405)
+
+
+@login_required
+def view_invoice(req: HttpRequest, invoice_id: int) -> HttpResponse:
+ invoice = models.Invoice.objects.get(pk=invoice_id)
+ return render(req, 'invoices/view.html', dict(invoice=invoice))
+
+
+@login_required
+def print_invoice(req: HttpRequest, invoice_id: int):
+ invoice = models.Invoice.objects.get(pk=invoice_id)
+ return render(req, 'invoices/invoice.html', dict(invoice=invoice))
diff --git a/locale/cs/LC_MESSAGES/django.po b/locale/cs/LC_MESSAGES/django.po
index 23be407..c787e94 100644
--- a/locale/cs/LC_MESSAGES/django.po
+++ b/locale/cs/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-01 20:43+0000\n"
+"POT-Creation-Date: 2024-08-16 18:53+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -19,18 +19,18 @@ msgstr ""
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
-#: accounts/admin.py:12 subjects/models.py:9
-#: subjects/templates/subjects/index.html:4 templates/facturio/base.html:20
+#: accounts/admin.py:12 subjects/models.py:8
+#: subjects/templates/subjects/index.html:6 templates/facturio/base.html:21
msgid "Subjects"
msgstr "Subjekty"
-#: accounts/forms.py:17 accounts/templates/account/login.html:6
-#: templates/facturio/base.html:42
+#: accounts/forms.py:16 accounts/templates/account/login.html:6
+#: templates/facturio/base.html:41
msgid "Login"
msgstr "Přihlásit se"
#: accounts/forms.py:30 accounts/templates/account/register.html:6
-#: templates/facturio/base.html:45
+#: templates/facturio/base.html:44
msgid "Register"
msgstr "Registrovat se"
@@ -46,6 +46,14 @@ msgstr "přijmení"
msgid "email address"
msgstr "emailová adresa"
+#: accounts/models.py:16
+msgid "supplier"
+msgstr "dodavatel"
+
+#: accounts/models.py:19
+msgid "customers"
+msgstr "odběratelé"
+
#: accounts/templates/account/me.html:4 accounts/templates/account/me.html:7
msgid "About Me"
msgstr "O mně"
@@ -65,95 +73,252 @@ msgstr "Uživatelské jméno: %(username)s"
msgid "Email: %(email)s"
msgstr "Email: %(email)s"
-#: accounts/templates/account/me.html:24
-msgid "Linked subjects"
-msgstr "Propojené Subjekty"
+#: accounts/templates/account/me.html:24 subjects/forms.py:53
+msgid "Current supplier"
+msgstr "Aktuální dodavatel"
-#: facturio/settings.py:109
+#: accounts/templates/account/me.html:34
+#: subjects/templates/subjects/index.html:11
+#: subjects/templates/subjects/index.html:22
+#: subjects/templates/subjects/index.html:60
+msgid "Customers"
+msgstr "Zákazníci"
+
+#: facturio/settings/base.py:114
msgid "English"
msgstr "Angličtina"
-#: facturio/settings.py:110
+#: facturio/settings/base.py:114
msgid "Czech"
msgstr "Čeština"
-#: subjects/forms.py:14
-msgid "Your provided CIN is not correct."
-msgstr "Vaše poskytnuté IČO není správné."
+#: invoices/forms.py:51 subjects/forms.py:66
+msgid "Save"
+msgstr "Uložit"
-#: subjects/forms.py:15
-msgid "Subject with provided CIN already exists."
-msgstr "Subjekt s poskytnutým IČO již existuje."
+#: invoices/models.py:19 invoices/templates/invoices/invoice.html:125
+#: invoices/templates/invoices/view.html:4
+#: invoices/templates/invoices/view.html:10
+msgid "Invoice"
+msgstr "Faktura"
-#: subjects/forms.py:18 subjects/models.py:12
-#: subjects/templates/subjects/index.html:11
+#: invoices/models.py:20 invoices/templates/invoices/index.html:8
+#: invoices/templates/invoices/index.html:11 templates/facturio/base.html:24
+msgid "Invoices"
+msgstr "Faktury"
+
+#: invoices/models.py:24 invoices/templates/invoices/index.html:53
+#: invoices/templates/invoices/invoice.html:130
+#: invoices/templates/invoices/view.html:14
+msgid "Supplier"
+msgstr "Dodavatel"
+
+#: invoices/models.py:27
+msgid "Supplier data"
+msgstr "Informace o dodavateli"
+
+#: invoices/models.py:31 invoices/templates/invoices/index.html:54
+#: invoices/templates/invoices/invoice.html:148
+#: invoices/templates/invoices/view.html:32
+msgid "Customer"
+msgstr "Zákazník"
+
+#: invoices/models.py:34
+msgid "Customer data"
+msgstr "Informace o zákazníkovi"
+
+#: invoices/models.py:36 invoices/templates/invoices/index.html:52
+#: invoices/templates/invoices/invoice.html:160
+#: invoices/templates/invoices/view.html:44
+msgid "Invoice date"
+msgstr "Datum vystavení"
+
+#: invoices/models.py:37 invoices/templates/invoices/index.html:55
+#: invoices/templates/invoices/invoice.html:161
+#: invoices/templates/invoices/view.html:45
+msgid "Due date"
+msgstr "Datum splatnosti"
+
+#: invoices/models.py:53
+msgid "Invoice Item"
+msgstr "Prvek faktury"
+
+#: invoices/models.py:54
+msgid "Invoice Items"
+msgstr "Prvky Faktury"
+
+#: invoices/models.py:59 invoices/templates/invoices/invoice.html:172
+#: invoices/templates/invoices/view.html:56
+msgid "Amount"
+msgstr "Množství"
+
+#: invoices/models.py:60 invoices/templates/invoices/invoice.html:173
+#: invoices/templates/invoices/view.html:57
+msgid "Amount unit"
+msgstr "Množstevní jednotka"
+
+#: invoices/models.py:61 invoices/templates/invoices/invoice.html:174
+#: invoices/templates/invoices/view.html:58
+msgid "Description"
+msgstr "Popis"
+
+#: invoices/models.py:63 invoices/templates/invoices/invoice.html:175
+#: invoices/templates/invoices/view.html:59
+msgid "Price for amount"
+msgstr "Cena za MJ"
+
+#: invoices/templates/invoices/index.html:13
+msgid "Create invoice"
+msgstr "Vytvořit fakturu"
+
+#: invoices/templates/invoices/index.html:24
+msgid "Actions"
+msgstr "Akce"
+
+#: invoices/templates/invoices/index.html:35
+msgid "Remove"
+msgstr "Odebrat"
+
+#: invoices/templates/invoices/index.html:42
+msgid "Add More"
+msgstr "Přidat další"
+
+#: invoices/templates/invoices/index.html:43
+msgid "Create"
+msgstr "Vytvořit"
+
+#: invoices/templates/invoices/index.html:48
+msgid "Existing invoices"
+msgstr "Existující faktury"
+
+#: invoices/templates/invoices/index.html:68
+msgid "Show"
+msgstr "Zobrazit"
+
+#: invoices/templates/invoices/index.html:70
+msgid "Print"
+msgstr "Tisk"
+
+#: invoices/templates/invoices/invoice.html:137
+#: invoices/templates/invoices/invoice.html:155
+#: invoices/templates/invoices/view.html:21
+#: invoices/templates/invoices/view.html:39 subjects/forms.py:20
+#: subjects/models.py:10 subjects/templates/subjects/index.html:15
+#: subjects/templates/subjects/index.html:53
msgid "CIN"
msgstr "IČO"
-#: subjects/forms.py:20
-msgid "Enter the subject CIN, rest will be generated automatically"
-msgstr "Zadejte IČO subjektu, zbytek bude generován automaticky"
-
-#: subjects/forms.py:28
-msgid "Add"
-msgstr "Vytvořit"
-
-#: subjects/models.py:8
-msgid "Subject"
-msgstr "Subjekt"
-
-#: subjects/models.py:18 subjects/templates/subjects/index.html:12
-msgid "Name"
-msgstr "Jméno"
-
-#: subjects/models.py:23 subjects/templates/subjects/index.html:13
+#: invoices/templates/invoices/invoice.html:139
+#: invoices/templates/invoices/invoice.html:157
+#: invoices/templates/invoices/view.html:23
+#: invoices/templates/invoices/view.html:41 subjects/models.py:12
+#: subjects/templates/subjects/index.html:16
+#: subjects/templates/subjects/index.html:54
msgid "VAT ID"
msgstr "DIČ"
-#: subjects/models.py:30 subjects/templates/subjects/index.html:14
+#: invoices/templates/invoices/invoice.html:168
+#: invoices/templates/invoices/view.html:52
+msgid "Invoice details"
+msgstr "Detaily faktury"
+
+#: invoices/templates/invoices/invoice.html:176
+#: invoices/templates/invoices/invoice.html:193
+#: invoices/templates/invoices/view.html:60
+#: invoices/templates/invoices/view.html:77
+msgid "Total"
+msgstr "Celkem"
+
+#: subjects/forms.py:16
+msgid "Your provided CIN is not correct."
+msgstr "Vaše poskytnuté IČO není správné."
+
+#: subjects/forms.py:17
+msgid "Subject with provided CIN already exists."
+msgstr "Subjekt s poskytnutým IČO již existuje."
+
+#: subjects/forms.py:23
+msgid "Enter the subject CIN, rest will be generated automatically"
+msgstr "Zadejte IČO subjektu, zbytek bude generován automaticky"
+
+#: subjects/forms.py:32
+msgid "Add"
+msgstr "Vytvořit"
+
+#: subjects/models.py:7
+msgid "Subject"
+msgstr "Subjekt"
+
+#: subjects/models.py:26
+msgid "Subject Data"
+msgstr "Data Subjektu"
+
+#: subjects/models.py:27
+msgid "Subject Datas"
+msgstr "Data Subjektů"
+
+#: subjects/models.py:32 subjects/templates/subjects/index.html:17
+#: subjects/templates/subjects/index.html:55
+msgid "Name"
+msgstr "Jméno"
+
+#: subjects/models.py:33 subjects/templates/subjects/index.html:20
+#: subjects/templates/subjects/index.html:58
msgid "Street"
msgstr "Ulice"
-#: subjects/models.py:35 subjects/templates/subjects/index.html:15
+#: subjects/models.py:34 subjects/templates/subjects/index.html:21
+#: subjects/templates/subjects/index.html:59
msgid "Zip Code"
msgstr "PSČ"
-#: subjects/models.py:40 subjects/templates/subjects/index.html:16
+#: subjects/models.py:35 subjects/templates/subjects/index.html:18
+#: subjects/templates/subjects/index.html:56
msgid "City"
msgstr "Město"
-#: subjects/models.py:45 subjects/templates/subjects/index.html:17
+#: subjects/models.py:37 subjects/templates/subjects/index.html:19
+#: subjects/templates/subjects/index.html:57
msgid "City part"
msgstr "Část města"
+#: subjects/models.py:39
+msgid "Created date"
+msgstr "Datum vytvoření"
+
#: subjects/templates/subjects/create.html:6
msgid "Create Subjects"
msgstr "Vytvořit Subjekty"
-#: subjects/templates/subjects/index.html:7
+#: subjects/templates/subjects/index.html:31
+#: subjects/templates/subjects/index.html:69
+msgid "None"
+msgstr "Žádný"
+
+#: subjects/templates/subjects/index.html:40
+msgid "Cancel"
+msgstr "Zrušit"
+
+#: subjects/templates/subjects/index.html:48
+msgid "Others"
+msgstr "Ostatní"
+
+#: subjects/templates/subjects/index.html:49
msgid "Add new"
msgstr "Přidat nový"
-#: subjects/templates/subjects/index.html:18
-msgid "Connect"
-msgstr "Propojit"
-
-#: subjects/templates/subjects/index.html:35
-msgid "Link"
-msgstr "Propojit"
-
-#: subjects/templates/subjects/index.html:38
-msgid "Unlink"
-msgstr "Odpojit"
+#: subjects/templates/subjects/index.html:78
+msgid "Select"
+msgstr "Vybrat"
#: templates/facturio/base.html:8 templates/facturio/index.html:4
msgid "App"
msgstr "Aplikace"
-#: templates/facturio/base.html:34
+#: templates/facturio/base.html:33
msgid "Profile"
msgstr "Profil"
-#: templates/facturio/base.html:37
+#: templates/facturio/base.html:36
msgid "Logout"
msgstr "Odhlásit se"
diff --git a/manage.py b/manage.py
index ab5fbf9..50693d3 100755
--- a/manage.py
+++ b/manage.py
@@ -6,14 +6,17 @@ import sys
def main():
"""Run administrative tasks."""
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'facturio.settings')
+ os.environ.setdefault(
+ 'DJANGO_SETTINGS_MODULE',
+ 'facturio.settings.development',
+ )
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
'available on your PYTHONPATH environment variable? Did you '
- 'forget to activate a virtual environment?'
+ 'forget to activate a virtual environment?',
) from exc
execute_from_command_line(sys.argv)
diff --git a/poetry.lock b/poetry.lock
index 034c1dc..2c7495a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "ares-util"
@@ -16,13 +16,13 @@ requests = "*"
[[package]]
name = "asgiref"
-version = "3.7.2"
+version = "3.8.1"
description = "ASGI specs, helper code, and adapters"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
- {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
+ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
+ {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
]
[package.extras]
@@ -30,13 +30,13 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
name = "certifi"
-version = "2023.11.17"
+version = "2024.6.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
- {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
+ {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+ {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
]
[[package]]
@@ -158,13 +158,13 @@ test = ["pytest", "pytest-django"]
[[package]]
name = "django"
-version = "5.0.1"
+version = "5.0.6"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
files = [
- {file = "Django-5.0.1-py3-none-any.whl", hash = "sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1"},
- {file = "Django-5.0.1.tar.gz", hash = "sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854"},
+ {file = "Django-5.0.6-py3-none-any.whl", hash = "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905"},
+ {file = "Django-5.0.6.tar.gz", hash = "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f"},
]
[package.dependencies]
@@ -190,26 +190,42 @@ files = [
[package.dependencies]
django = ">=4.2"
+[[package]]
+name = "django-environ"
+version = "0.11.2"
+description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
+optional = false
+python-versions = ">=3.6,<4"
+files = [
+ {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"},
+ {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"},
+]
+
+[package.extras]
+develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
+docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
+testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"]
+
[[package]]
name = "idna"
-version = "3.6"
+version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
- {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
- {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "requests"
-version = "2.31.0"
+version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
- {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
@@ -224,48 +240,48 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "sqlparse"
-version = "0.4.4"
+version = "0.5.0"
description = "A non-validating SQL parser."
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.8"
files = [
- {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
- {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
+ {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
+ {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
]
[package.extras]
-dev = ["build", "flake8"]
+dev = ["build", "hatch"]
doc = ["sphinx"]
-test = ["pytest", "pytest-cov"]
[[package]]
name = "tzdata"
-version = "2023.4"
+version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
- {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
- {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
+ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[[package]]
name = "urllib3"
-version = "2.1.0"
+version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
- {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
- {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
+ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
+ {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "cf882708b341170fa02db4115bf2eeec4f2d0a97e468ab04136f4391ca1f9d20"
+content-hash = "fc91f96209fbdf439cf9da459e6b98e3897b9672d0250bc97a7622a190006ff4"
diff --git a/pyproject.toml b/pyproject.toml
index a363843..1d7040f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@ django = "^5.0"
crispy-bootstrap5 = "^2023.10"
django-crispy-forms = "^2.1"
ares-util = "^0.3.0"
+django-environ = "^0.11.2"
[build-system]
diff --git a/subjects/admin.py b/subjects/admin.py
index 7c6edbc..4e3dce7 100644
--- a/subjects/admin.py
+++ b/subjects/admin.py
@@ -1,8 +1,24 @@
from django.contrib import admin
-from . import models
+from .models import Subject
+from .models import SubjectData
-@admin.register(models.Subject)
+class SubjectDataInline(admin.TabularInline):
+ model = SubjectData
+ extra = 0 # is removes the extra empty forms
+ readonly_fields = [
+ 'name',
+ 'street',
+ 'zip_code',
+ 'city',
+ 'city_part',
+ 'created_date',
+ ]
+ can_delete = False # Optional: Prevent deletion in the inline
+
+
+@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
- list_display = ['id', 'name', 'city', 'city_part', 'zip_code']
+ list_display = ['id', 'vat_id']
+ inlines = [SubjectDataInline]
diff --git a/subjects/forms.py b/subjects/forms.py
index d564cfe..01f1741 100644
--- a/subjects/forms.py
+++ b/subjects/forms.py
@@ -8,6 +8,7 @@ from django.forms import fields
from django.utils.translation import gettext_lazy as _
from . import models
+from accounts.models import User
class CreateSubjectForm(forms.Form):
@@ -18,7 +19,9 @@ class CreateSubjectForm(forms.Form):
cin = fields.CharField(
label=_('CIN'),
max_length=8,
- help_text=_('Enter the subject CIN, rest will be generated automatically'),
+ help_text=_(
+ 'Enter the subject CIN, rest will be generated automatically',
+ ),
)
def __init__(self, *args, **kwargs):
@@ -41,3 +44,23 @@ class CreateSubjectForm(forms.Form):
return cin
raise ValidationError(self.error_messages['already_existing'])
+
+
+class SelectSubjectForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ('supplier',)
+ labels = {'supplier': _('Current supplier')}
+
+ def __init__(self, *args, **kwargs):
+ self._user = kwargs.pop('current_user', None)
+ super().__init__(*args, **kwargs)
+
+ self.fields["supplier"].queryset = models.Subject.objects.exclude(
+ id__in=self._user.get_customers(),
+ )
+
+ self.helper = helper.FormHelper()
+ self.helper.form_action = 'subjects:list'
+ self.helper.form_method = 'post'
+ self.helper.add_input(layout.Submit('submit', _('Save')))
diff --git a/subjects/migrations/0001_initial.py b/subjects/migrations/0001_initial.py
index cb5cb89..2f5271f 100644
--- a/subjects/migrations/0001_initial.py
+++ b/subjects/migrations/0001_initial.py
@@ -25,7 +25,7 @@ class Migration(migrations.Migration):
(
'vat_id',
models.CharField(
- blank=True, max_length=12, null=True, verbose_name='vat_id'
+ blank=True, max_length=12, null=True, verbose_name='vat_id',
),
),
('street', models.CharField(max_length=64, verbose_name='street')),
@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
(
'city_part',
models.CharField(
- blank=True, max_length=64, null=True, verbose_name='city_part'
+ blank=True, max_length=64, null=True, verbose_name='city_part',
),
),
],
diff --git a/subjects/migrations/0002_alter_subject_city_alter_subject_city_part_and_more.py b/subjects/migrations/0002_alter_subject_city_alter_subject_city_part_and_more.py
index a0a98a1..4e147e3 100644
--- a/subjects/migrations/0002_alter_subject_city_alter_subject_city_part_and_more.py
+++ b/subjects/migrations/0002_alter_subject_city_alter_subject_city_part_and_more.py
@@ -18,14 +18,14 @@ class Migration(migrations.Migration):
model_name='subject',
name='city_part',
field=models.CharField(
- blank=True, max_length=64, null=True, verbose_name='City part'
+ blank=True, max_length=64, null=True, verbose_name='City part',
),
),
migrations.AlterField(
model_name='subject',
name='id',
field=models.CharField(
- max_length=8, primary_key=True, serialize=False, verbose_name='CIN'
+ max_length=8, primary_key=True, serialize=False, verbose_name='CIN',
),
),
migrations.AlterField(
@@ -42,7 +42,7 @@ class Migration(migrations.Migration):
model_name='subject',
name='vat_id',
field=models.CharField(
- blank=True, max_length=12, null=True, verbose_name='VAT ID'
+ blank=True, max_length=12, null=True, verbose_name='VAT ID',
),
),
migrations.AlterField(
diff --git a/subjects/migrations/0003_alter_subject_options.py b/subjects/migrations/0003_alter_subject_options.py
index 43258b9..ca1d3ce 100644
--- a/subjects/migrations/0003_alter_subject_options.py
+++ b/subjects/migrations/0003_alter_subject_options.py
@@ -10,6 +10,9 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='subject',
- options={'verbose_name': 'Subject', 'verbose_name_plural': 'Subjects'},
+ options={
+ 'verbose_name': 'Subject',
+ 'verbose_name_plural': 'Subjects',
+ },
),
]
diff --git a/subjects/migrations/0004_remove_subject_city_remove_subject_city_part_and_more.py b/subjects/migrations/0004_remove_subject_city_remove_subject_city_part_and_more.py
new file mode 100644
index 0000000..b9491fa
--- /dev/null
+++ b/subjects/migrations/0004_remove_subject_city_remove_subject_city_part_and_more.py
@@ -0,0 +1,74 @@
+# Generated by Django 5.0.4 on 2024-06-14 20:08
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('subjects', '0003_alter_subject_options'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='subject',
+ name='city',
+ ),
+ migrations.RemoveField(
+ model_name='subject',
+ name='city_part',
+ ),
+ migrations.RemoveField(
+ model_name='subject',
+ name='name',
+ ),
+ migrations.RemoveField(
+ model_name='subject',
+ name='street',
+ ),
+ migrations.RemoveField(
+ model_name='subject',
+ name='zip_code',
+ ),
+ migrations.CreateModel(
+ name='SubjectData',
+ fields=[
+ (
+ 'id',
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name='ID',
+ ),
+ ),
+ ('name', models.CharField(max_length=128, verbose_name='Name')),
+ ('street', models.CharField(max_length=64, verbose_name='Street')),
+ ('zip_code', models.CharField(max_length=6, verbose_name='Zip Code')),
+ ('city', models.CharField(max_length=64, verbose_name='City')),
+ (
+ 'city_part',
+ models.CharField(
+ blank=True, max_length=64, null=True, verbose_name='City part',
+ ),
+ ),
+ (
+ 'created_date',
+ models.DateTimeField(
+ auto_now_add=True, verbose_name='Created date',
+ ),
+ ),
+ (
+ 'subject',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='subjects.subject',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Subject Data',
+ 'verbose_name_plural': 'Subject Datas',
+ },
+ ),
+ ]
diff --git a/subjects/migrations/0005_alter_subjectdata_subject.py b/subjects/migrations/0005_alter_subjectdata_subject.py
new file mode 100644
index 0000000..cad0919
--- /dev/null
+++ b/subjects/migrations/0005_alter_subjectdata_subject.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.6 on 2024-07-04 21:59
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("subjects", "0004_remove_subject_city_remove_subject_city_part_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="subjectdata",
+ name="subject",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="subject_data",
+ to="subjects.subject",
+ ),
+ ),
+ ]
diff --git a/subjects/models.py b/subjects/models.py
index ee44c16..854e08e 100644
--- a/subjects/models.py
+++ b/subjects/models.py
@@ -8,30 +8,35 @@ class Subject(models.Model):
verbose_name_plural = _('Subjects')
id = models.CharField(_('CIN'), max_length=8, primary_key=True)
-
- name = models.CharField(_('Name'), max_length=128)
-
- vat_id = models.CharField(_('VAT ID'), max_length=12, null=True, blank=True)
-
- street = models.CharField(
- _('Street'),
- max_length=64,
+ vat_id = models.CharField(
+ _('VAT ID'), max_length=12, null=True, blank=True,
)
- zip_code = models.CharField(
- _('Zip Code'),
- max_length=6,
- )
-
- city = models.CharField(
- _('City'),
- max_length=64,
- )
-
- city_part = models.CharField(_('City part'), max_length=64, null=True, blank=True)
+ def get_latest_data(self):
+ return self.subject_data.filter(subject=self).order_by(
+ '-created_date',
+ ).first()
def __str__(self):
- return self.name
+ return f'{self.id} - {self.get_latest_data().name}'
- def get_linked_users(self) -> list[int]:
- return list(self.user_set.values_list('id', flat=True))
+
+class SubjectData(models.Model):
+ class Meta:
+ verbose_name = _('Subject Data')
+ verbose_name_plural = _('Subject Datas')
+
+ subject = models.ForeignKey(
+ Subject, on_delete=models.CASCADE, related_name='subject_data',
+ )
+ name = models.CharField(_('Name'), max_length=128)
+ street = models.CharField(_('Street'), max_length=64)
+ zip_code = models.CharField(_('Zip Code'), max_length=6)
+ city = models.CharField(_('City'), max_length=64)
+ city_part = models.CharField(
+ _('City part'), max_length=64, null=True, blank=True,
+ )
+ created_date = models.DateTimeField(_('Created date'), auto_now_add=True)
+
+ def __str__(self):
+ return f'{self.subject.id} - {self.name} - {self.created_date}'
diff --git a/subjects/templates/subjects/index.html b/subjects/templates/subjects/index.html
index 6ff4b3f..6db8760 100644
--- a/subjects/templates/subjects/index.html
+++ b/subjects/templates/subjects/index.html
@@ -1,47 +1,87 @@
{% extends "facturio/base.html" %}
{% load i18n %}
+{% load crispy_forms_tags %}
+
{% block title %}{% trans "Subjects" %}{% endblock %}
{% block content %}
- {% trans "Add new" %}
-
-
-
- {% trans "CIN" %}
- {% trans "Name" %}
- {% trans "VAT ID" %}
- {% trans "Street" %}
- {% trans "Zip Code" %}
- {% trans "City" %}
- {% trans "City part" %}
- {% trans "Connect" %}
-
-
-
- {% for subject in subjects %}
- {# FIXME: in the future, or when this create problems, find a better way#}
- {% with linked_users=subject.get_linked_users %}
-
- {{ subject.id }}
- {{ subject.name }}
- {{ subject.vat_id|default:_("None")}}
- {{ subject.street }}
- {{ subject.zip_code }}
- {{ subject.city }}
- {{ subject.city_part }}
-
- {% if request.user.id not in linked_users %}
- {% trans "Link" %}
- {% else %}
- {% trans "Unlink" %}
- {% endif %}
-
-
- {% endwith %}
- {% endfor %}
-
-
+ {% with customers=request.user.get_customers %}
+ {% crispy form form.helper %}
+ {% trans "Customers" %}
+
+
+
+ {% trans "CIN" %}
+ {% trans "VAT ID" %}
+ {% trans "Name" %}
+ {% trans "City" %}
+ {% trans "City part" %}
+ {% trans "Street" %}
+ {% trans "Zip Code" %}
+ {% trans "Customers" %}
+
+
+
+ {% for subject in subjects %}
+ {% if subject.id in customers %}
+ {% with subject_data=subject.get_latest_data %}
+
+ {{ subject.id }}
+ {{ subject.vat_id|default:_("None") }}
+ {{ subject_data.name }}
+ {{ subject_data.city }}
+ {{ subject_data.city_part }}
+ {{ subject_data.street }}
+ {{ subject_data.zip_code }}
+
+ {% trans "Cancel" %}
+
+
+ {% endwith %}
+ {% endif %}
+ {% endfor %}
+
+
+ {% trans "Others" %}
+ {% trans "Add new" %}
+
+
+
+ {% trans "CIN" %}
+ {% trans "VAT ID" %}
+ {% trans "Name" %}
+ {% trans "City" %}
+ {% trans "City part" %}
+ {% trans "Street" %}
+ {% trans "Zip Code" %}
+ {% trans "Customers" %}
+
+
+
+ {% for subject in subjects %}
+ {% if subject.id not in customers %}
+ {% with subject_data=subject.get_latest_data %}
+
+ {{ subject.id }}
+ {{ subject.vat_id|default:_("None") }}
+ {{ subject_data.name }}
+ {{ subject_data.city }}
+ {{ subject_data.city_part }}
+ {{ subject_data.street }}
+ {{ subject_data.zip_code }}
+
+ {% trans "Select" %}
+
+
+ {% endwith %}
+ {% endif %}
+ {% endfor %}
+
+
+ {% endwith %}
{% endblock %}
diff --git a/subjects/views.py b/subjects/views.py
index e8f7f5c..2aef63e 100644
--- a/subjects/views.py
+++ b/subjects/views.py
@@ -16,8 +16,28 @@ def build_address(street: str, zip_code: int | str, city: str, city_part: str) -
@login_required
def main_page(req: HttpRequest) -> HttpResponse:
- subjects = models.Subject.objects.all()
- return render(req, 'subjects/index.html', dict(subjects=subjects))
+ if req.method == 'POST':
+ select_subject_form = forms.SelectSubjectForm(
+ data=req.POST, instance=req.user, current_user=req.user,
+ )
+ if select_subject_form.is_valid():
+ select_subject_form.save()
+ return redirect(reverse('subjects:list'))
+
+ elif req.method == 'GET':
+ subjects = models.Subject.objects.exclude(
+ id=req.user.supplier.id if req.user.supplier else None,
+ )
+ select_subject_form = forms.SelectSubjectForm(
+ instance=req.user, current_user=req.user,
+ )
+ return render(
+ req,
+ 'subjects/index.html',
+ dict(subjects=subjects, form=select_subject_form),
+ )
+
+ return HttpResponse(status=405)
@login_required
@@ -33,9 +53,11 @@ def create_subject(req: HttpRequest) -> HttpResponse:
ares_address_data = ares_data['address']
ares_legal_data = ares_data['legal']
- models.Subject.objects.create(
- id=ares_legal_data['company_id'],
- vat_id=ares_legal_data['company_vat_id'],
+ subject = models.Subject.objects.create(
+ id=ares_legal_data['company_id'], vat_id=ares_legal_data['company_vat_id'],
+ )
+ models.SubjectData.objects.create(
+ subject=subject,
name=ares_legal_data['company_name'],
street=ares_address_data['street'],
zip_code=ares_address_data['zip_code'],
diff --git a/templates/facturio/base.html b/templates/facturio/base.html
index 99dfe4b..a47513e 100644
--- a/templates/facturio/base.html
+++ b/templates/facturio/base.html
@@ -8,6 +8,7 @@
Facturio - {% block title %}{% trans "App" %}{% endblock %}
+ {% block head %}{% endblock %}
@@ -19,14 +20,12 @@
{% trans "Subjects" %}
- {##}
- {# Item 2 #}
- {# #}
+
+ {% trans "Invoices" %}
+
{% endif %}
-
-
{% if request.user.is_authenticated %}
diff --git a/templates/facturio/invoice.html b/templates/facturio/invoice.html
index 5fb13e4..68daeb1 100644
--- a/templates/facturio/invoice.html
+++ b/templates/facturio/invoice.html
@@ -16,7 +16,7 @@
}
body {
- font-family: var(--font-family);
+ font-family: var(--font-family), sans-serif;
margin: 20px;
color: var(--main-color);
background-color: var(--background-color);
@@ -160,7 +160,7 @@
- #
+ Počet
Jednotka
Popis položky
Cena za MJ