From 681f85b1b8331eee8f14d3403ed39c1958edf054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Krop=C3=A1=C4=8Dek?= Date: Fri, 16 Aug 2024 20:12:49 +0000 Subject: [PATCH] Implement basic invoicing --- .gitignore | 3 + .pre-commit-config.yaml | 37 +- README.md | 3 + TODO.md | 21 - accounts/admin.py | 6 +- accounts/forms.py | 6 +- accounts/migrations/0001_initial.py | 20 +- ...er_email_alter_user_first_name_and_more.py | 4 +- ..._subjects_user_customers_user_suppliers.py | 31 + ...e_user_suppliers_user_supplier_and_more.py | 36 ++ accounts/models.py | 13 +- accounts/templates/account/me.html | 21 +- accounts/views.py | 1 - db_schema.drawio | 556 ++++++++++++++++++ db_schema_rework.drawio | 523 ++++++++++++++++ facturio/asgi.py | 5 +- facturio/settings/__init__.py | 0 facturio/{settings.py => settings/base.py} | 14 +- facturio/settings/development.py | 1 + facturio/settings/production.py | 1 + facturio/urls.py | 1 + facturio/wsgi.py | 5 +- invoices/__init__.py | 0 invoices/admin.py | 21 + invoices/apps.py | 6 + invoices/forms.py | 60 ++ invoices/migrations/0001_initial.py | 126 ++++ .../0002_alter_invoice_invoice_date.py | 21 + invoices/migrations/__init__.py | 0 invoices/models.py | 71 +++ invoices/static/css/invoice.css | 112 ++++ invoices/static/js/formset_mngr.js | 55 ++ invoices/templates/invoices/index.html | 78 +++ invoices/templates/invoices/invoice.html | 202 +++++++ invoices/templates/invoices/view.html | 85 +++ invoices/tests.py | 1 + invoices/urls.py | 11 + invoices/views.py | 52 ++ locale/cs/LC_MESSAGES/django.po | 273 +++++++-- manage.py | 7 +- poetry.lock | 78 ++- pyproject.toml | 1 + subjects/admin.py | 22 +- subjects/forms.py | 25 +- subjects/migrations/0001_initial.py | 4 +- ...t_city_alter_subject_city_part_and_more.py | 6 +- .../migrations/0003_alter_subject_options.py | 5 +- ..._city_remove_subject_city_part_and_more.py | 74 +++ .../0005_alter_subjectdata_subject.py | 23 + subjects/models.py | 49 +- subjects/templates/subjects/index.html | 120 ++-- subjects/views.py | 32 +- templates/facturio/base.html | 9 +- templates/facturio/invoice.html | 4 +- 54 files changed, 2709 insertions(+), 232 deletions(-) create mode 100644 README.md delete mode 100644 TODO.md create mode 100644 accounts/migrations/0005_remove_user_subjects_user_customers_user_suppliers.py create mode 100644 accounts/migrations/0006_remove_user_suppliers_user_supplier_and_more.py create mode 100644 db_schema.drawio create mode 100644 db_schema_rework.drawio create mode 100644 facturio/settings/__init__.py rename facturio/{settings.py => settings/base.py} (92%) create mode 100644 facturio/settings/development.py create mode 100644 facturio/settings/production.py create mode 100644 invoices/__init__.py create mode 100644 invoices/admin.py create mode 100644 invoices/apps.py create mode 100644 invoices/forms.py create mode 100644 invoices/migrations/0001_initial.py create mode 100644 invoices/migrations/0002_alter_invoice_invoice_date.py create mode 100644 invoices/migrations/__init__.py create mode 100644 invoices/models.py create mode 100644 invoices/static/css/invoice.css create mode 100644 invoices/static/js/formset_mngr.js create mode 100644 invoices/templates/invoices/index.html create mode 100644 invoices/templates/invoices/invoice.html create mode 100644 invoices/templates/invoices/view.html create mode 100644 invoices/tests.py create mode 100644 invoices/urls.py create mode 100644 invoices/views.py create mode 100644 subjects/migrations/0004_remove_subject_city_remove_subject_city_part_and_more.py create mode 100644 subjects/migrations/0005_alter_subjectdata_subject.py 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 %}
-
{% trans "Linked subjects" %}
+
{% trans "Current supplier" %}
+
+ {% endif %} + {% if request.user.customers.exists %} +
+
{% trans "Customers" %}
+
{% 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" %}

+
+ {% csrf_token %} + {{ form|crispy }} + {{ formset.management_form }} + + + + {% for field in formset.empty_form.visible_fields %} + + {% endfor %} + + + + + {% for formset_form in formset %} + + {% for field in formset_form.visible_fields %} + + {% endfor %} + + + {% endfor %} + +
{{ field.label }}{% trans "Actions" %}
{{ field|as_crispy_field }} + +
+ + + +
+
+ +
+

{% trans "Existing invoices" %}

+ + + + + + + + + + + + + {% for invoice in invoices %} + + + + + + + + + {% endfor %} + +
{% trans "Invoice date" %}{% trans "Supplier" %}{% trans "Customer" %}{% trans "Due date" %}
{{ invoice.invoice_date }}{{ invoice.supplier_data.name }}{{ invoice.customer_data.name }}{{ invoice.due_date }}{% trans "Show" %}{% trans "Print" %}
+ +
+ +{% 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" %}

+ + + + + + + + + + + + {% for invoice_item in invoice.items.all %} + + + + + + + + {% endfor %} + +
{% trans "Amount" %}{% trans "Amount unit" %}{% trans "Description" %}{% trans "Price for amount" %}{% trans "Total" %}
{{ invoice_item.amount.normalize }}{{ invoice_item.amount_unit }}{{ invoice_item.description }}{{ invoice_item.price_for_amount.normalize }} Kč{{ invoice_item.total_price.normalize }} Kč
+ +
+

{% 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" %}

+ + + + + + + + + + + + {% for invoice_item in invoice.items.all %} + + + + + + + + {% endfor %} + +
{% trans "Amount" %}{% trans "Amount unit" %}{% trans "Description" %}{% trans "Price for amount" %}{% trans "Total" %}
{{ invoice_item.amount.normalize }}{{ invoice_item.amount_unit }}{{ invoice_item.description }}{{ invoice_item.price_for_amount.normalize }} Kč{{ invoice_item.total_price.normalize }} Kč
+ +
+

{% 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" %} - - - - - - - - - - - - - - - {% for subject in subjects %} - {# FIXME: in the future, or when this create problems, find a better way#} - {% with linked_users=subject.get_linked_users %} - - - - - - - - - - - {% endwith %} - {% endfor %} - -
{% trans "CIN" %}{% trans "Name" %}{% trans "VAT ID" %}{% trans "Street" %}{% trans "Zip Code" %}{% trans "City" %}{% trans "City part" %}{% trans "Connect" %}
{{ 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 %} -
+ {% with customers=request.user.get_customers %} + {% crispy form form.helper %} +

{% trans "Customers" %}

+ + + + + + + + + + + + + + + {% for subject in subjects %} + {% if subject.id in customers %} + {% with subject_data=subject.get_latest_data %} + + + + + + + + + + + {% endwith %} + {% endif %} + {% endfor %} + +
{% trans "CIN" %}{% trans "VAT ID" %}{% trans "Name" %}{% trans "City" %}{% trans "City part" %}{% trans "Street" %}{% trans "Zip Code" %}{% trans "Customers" %}
{{ 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" %} +
+

{% trans "Others" %}

+ {% trans "Add new" %} + + + + + + + + + + + + + + + {% for subject in subjects %} + {% if subject.id not in customers %} + {% with subject_data=subject.get_latest_data %} + + + + + + + + + + + {% endwith %} + {% endif %} + {% endfor %} + +
{% trans "CIN" %}{% trans "VAT ID" %}{% trans "Name" %}{% trans "City" %}{% trans "City part" %}{% trans "Street" %}{% trans "Zip Code" %}{% trans "Customers" %}
{{ 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 %} {% 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 %}