From 28637e704f6eb89480fc8cb48950be94b4b0ebf2 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Wed, 1 Jan 2025 17:57:49 +0000 Subject: [PATCH] Updated to use core/ templates, attempted to fix migrations (broke them more though) Signed-off-by: Trey <73353716+TreyWW@users.noreply.github.com> --- backend/admin.py | 6 +- backend/api/emails/fetch.py | 2 +- backend/api/emails/send.py | 16 +- backend/api/emails/status.py | 10 +- .../api/public/endpoints/invoices/create.py | 2 +- backend/api/settings/defaults.py | 10 +- backend/api/settings/email_templates.py | 8 +- backend/auth_backends.py | 22 - backend/clients/api/delete.py | 4 +- backend/clients/api/fetch.py | 4 +- backend/clients/clients.py | 8 +- backend/clients/models.py | 168 ++-- backend/clients/views/create.py | 2 +- backend/clients/views/dashboard.py | 2 +- backend/clients/views/detail.py | 4 +- backend/context_processors.py | 70 +- backend/decorators.py | 272 ------ backend/finance/api/invoices/delete.py | 8 +- backend/finance/api/invoices/edit.py | 10 +- backend/finance/api/invoices/fetch.py | 2 +- backend/finance/api/invoices/manage.py | 2 +- .../finance/api/invoices/recurring/delete.py | 8 +- .../finance/api/invoices/recurring/edit.py | 10 +- .../finance/api/invoices/recurring/fetch.py | 2 +- .../recurring/generate_next_invoice_now.py | 8 +- .../finance/api/invoices/recurring/poll.py | 2 +- .../api/invoices/recurring/update_status.py | 4 +- .../finance/api/invoices/reminders/create.py | 12 +- .../finance/api/invoices/reminders/delete.py | 8 +- .../finance/api/invoices/reminders/fetch.py | 8 +- backend/finance/api/products/create.py | 2 +- backend/finance/api/products/fetch.py | 2 +- backend/finance/api/receipts/delete.py | 2 +- backend/finance/api/receipts/download.py | 2 +- backend/finance/api/receipts/edit.py | 4 +- backend/finance/api/receipts/fetch.py | 2 +- backend/finance/api/receipts/new.py | 2 +- backend/finance/api/reports/generate.py | 6 +- backend/finance/models.py | 84 +- backend/finance/service/clients/create.py | 2 +- backend/finance/service/defaults/get.py | 10 +- backend/finance/service/defaults/update.py | 7 +- .../service/invoices/common/create/create.py | 4 +- .../invoices/common/create/get_page.py | 6 +- .../recurring/generation/next_invoice.py | 6 +- .../service/invoices/single/create/create.py | 4 +- .../views/invoices/recurring/create.py | 2 +- .../views/invoices/recurring/dashboard.py | 2 +- .../finance/views/invoices/recurring/edit.py | 4 +- .../views/invoices/recurring/overview.py | 2 +- .../finance/views/invoices/single/create.py | 2 +- .../views/invoices/single/dashboard.py | 2 +- backend/finance/views/invoices/single/edit.py | 7 +- .../views/invoices/single/manage_access.py | 4 +- .../finance/views/invoices/single/overview.py | 2 +- .../finance/views/invoices/single/schedule.py | 2 +- backend/finance/views/invoices/single/view.py | 2 +- backend/finance/views/receipts/dashboard.py | 2 +- backend/middleware.py | 74 -- backend/migrations/0001_initial.py | 820 +++++++----------- backend/migrations/0002_initial.py | 420 --------- backend/modals.py | 10 +- backend/models.py | 5 +- backend/storage/api/delete.py | 4 +- backend/storage/api/fetch.py | 2 +- frontend/templates/base/htmx.html | 7 - frontend/templates/core/auth/auth.html | 2 +- .../{ => core}/base/+left_drawer.html | 0 frontend/templates/{ => core}/base/_head.html | 0 frontend/templates/{ => core}/base/base.html | 4 +- .../{ => core}/base/breadcrumbs.html | 2 +- .../{ => core}/base/breadcrumbs_ul.html | 0 frontend/templates/core/base/htmx.html | 7 + frontend/templates/{ => core}/base/toast.html | 0 .../templates/{ => core}/base/toasts.html | 0 .../base/topbar/+icon_dropdown.html | 2 +- .../base/topbar/_notification_count.html | 0 .../topbar/_notification_dropdown_items.html | 2 +- .../base/topbar/_organizations_list.html | 0 .../{ => core}/base/topbar/_topbar.html | 0 .../base/topbar/team_selector/selector.html | 0 .../pages/clients/create/create.html | 14 +- .../pages/clients/dashboard/dashboard.html | 2 +- .../pages/clients/detail/dashboard.html | 12 +- frontend/templates/pages/create_account.html | 156 ++-- frontend/templates/pages/dashboard.html | 2 +- .../templates/pages/emails/dashboard.html | 16 +- .../pages/file_storage/dashboard.html | 17 +- .../templates/pages/file_storage/upload.html | 21 +- frontend/templates/pages/forgot_password.html | 93 +- .../pages/invoices/dashboard/core/main.html | 2 +- .../recurring/dashboard/core/main.html | 2 +- .../pages/invoices/single/edit/edit.html | 2 +- .../single/manage_access/manage_access.html | 24 +- .../pages/invoices/single/schedules/view.html | 4 +- .../invoices/single/view/invoice_page.html | 334 +++---- .../templates/pages/landing/landing_base.html | 80 +- frontend/templates/pages/login.html | 215 ++--- .../templates/pages/quotas/dashboard.html | 2 +- frontend/templates/pages/quotas/list.html | 18 +- .../templates/pages/quotas/view_requests.html | 70 +- .../templates/pages/receipts/dashboard.html | 2 +- .../templates/pages/reports/dashboard.html | 2 +- .../pages/reports/monthly_report_base.html | 188 ++-- frontend/templates/pages/reset_password.html | 123 +-- frontend/templates/pages/settings/main.html | 2 +- .../templates/pages/settings/teams/leave.html | 2 +- .../pages/settings/teams/login_to_team.html | 9 +- .../templates/pages/settings/teams/main.html | 90 +- .../pages/settings/teams/permissions.html | 50 +- poetry.lock | 9 +- pyproject.toml | 3 +- settings/local_settings.py | 4 +- settings/settings.py | 20 +- tests/api/test_clients.py | 12 +- tests/views/test_clients.py | 2 +- 116 files changed, 1438 insertions(+), 2399 deletions(-) delete mode 100644 backend/auth_backends.py delete mode 100644 backend/decorators.py delete mode 100644 backend/middleware.py delete mode 100644 backend/migrations/0002_initial.py delete mode 100644 frontend/templates/base/htmx.html rename frontend/templates/{ => core}/base/+left_drawer.html (100%) rename frontend/templates/{ => core}/base/_head.html (100%) rename frontend/templates/{ => core}/base/base.html (53%) rename frontend/templates/{ => core}/base/breadcrumbs.html (90%) rename frontend/templates/{ => core}/base/breadcrumbs_ul.html (100%) create mode 100644 frontend/templates/core/base/htmx.html rename frontend/templates/{ => core}/base/toast.html (100%) rename frontend/templates/{ => core}/base/toasts.html (100%) rename frontend/templates/{ => core}/base/topbar/+icon_dropdown.html (96%) rename frontend/templates/{ => core}/base/topbar/_notification_count.html (100%) rename frontend/templates/{ => core}/base/topbar/_notification_dropdown_items.html (98%) rename frontend/templates/{ => core}/base/topbar/_organizations_list.html (100%) rename frontend/templates/{ => core}/base/topbar/_topbar.html (100%) rename frontend/templates/{ => core}/base/topbar/team_selector/selector.html (100%) diff --git a/backend/admin.py b/backend/admin.py index 9b348839c..cd6793d8a 100644 --- a/backend/admin.py +++ b/backend/admin.py @@ -9,15 +9,13 @@ InvoiceProduct, Receipt, ReceiptDownloadToken, + FinanceDefaultValues, ) -from backend.clients.models import Client, DefaultValues - # from django.contrib.auth.models imp/ort User # admin.register(Invoice) admin.site.register( [ - Client, Invoice, InvoiceURL, InvoiceItem, @@ -26,7 +24,7 @@ ReceiptDownloadToken, InvoiceReminder, InvoiceRecurringProfile, - DefaultValues, + FinanceDefaultValues, ] ) diff --git a/backend/api/emails/fetch.py b/backend/api/emails/fetch.py index fa1cf9cb1..dfc12b391 100644 --- a/backend/api/emails/fetch.py +++ b/backend/api/emails/fetch.py @@ -4,7 +4,7 @@ from django.shortcuts import render, redirect from django_ratelimit.core import is_ratelimited -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.models import EmailSendStatus from core.types.htmx import HtmxHttpRequest diff --git a/backend/api/emails/send.py b/backend/api/emails/send.py index 7c6bd86e5..968d3e578 100644 --- a/backend/api/emails/send.py +++ b/backend/api/emails/send.py @@ -16,8 +16,8 @@ from django.views.decorators.http import require_POST from mypy_boto3_sesv2.type_defs import BulkEmailEntryResultTypeDef -from backend.decorators import feature_flag_check, web_require_scopes -from backend.decorators import htmx_only +from core.decorators import feature_flag_check, web_require_scopes +from core.decorators import htmx_only from backend.models import Client from backend.models import EmailSendStatus @@ -82,7 +82,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse: if validated_bulk: messages.error(request, validated_bulk) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") message += email_footer() message_single_line_html = message.replace("\r\n", "
").replace("\n", "
") @@ -124,7 +124,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse: } ) messages.success(request, f"Successfully emailed {len(email_list)} people.") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") EMAIL_SENT = send_templated_bulk_email( email_list=email_list, @@ -138,7 +138,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse: if EMAIL_SENT.failed: messages.error(request, EMAIL_SENT.error) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") # todo - fix @@ -188,7 +188,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse: # except QuotaLimit.DoesNotExist: # ... - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") def _send_single_email_view(request: WebRequest) -> HttpResponse: @@ -205,7 +205,7 @@ def _send_single_email_view(request: WebRequest) -> HttpResponse: if validated_single: messages.error(request, validated_single) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") message += email_footer() message_single_line_html = message.replace("\r\n", "
").replace("\n", "
") @@ -249,7 +249,7 @@ def _send_single_email_view(request: WebRequest) -> HttpResponse: # QuotaUsage.create_str(request.user, "emails-single-count", status_object.id) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") def validate_bulk_inputs(*, request, emails, clients, message, subject) -> str | None: diff --git a/backend/api/emails/status.py b/backend/api/emails/status.py index 218522db5..6df99aa32 100644 --- a/backend/api/emails/status.py +++ b/backend/api/emails/status.py @@ -8,7 +8,7 @@ from django_ratelimit.core import is_ratelimited from mypy_boto3_sesv2.type_defs import GetMessageInsightsResponseTypeDef, InsightsEventTypeDef -from backend.decorators import htmx_only, feature_flag_check, web_require_scopes +from core.decorators import htmx_only, feature_flag_check, web_require_scopes from backend.models import EmailSendStatus from core.types.htmx import HtmxHttpRequest from settings.helpers import EMAIL_CLIENT @@ -26,13 +26,13 @@ def get_status_view(request: HtmxHttpRequest, status_id: str) -> HttpResponse: EMAIL_STATUS = EmailSendStatus.objects.get(user=request.user, id=status_id) except EmailSendStatus.DoesNotExist: messages.error(request, "Status not found") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") message_insight = get_message_insights(message_id=EMAIL_STATUS.aws_message_id) # type: ignore[arg-type] if isinstance(message_insight, str): messages.error(request, message_insight) - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) important_info = get_important_info_from_response(message_insight) @@ -41,7 +41,7 @@ def get_status_view(request: HtmxHttpRequest, status_id: str) -> HttpResponse: EMAIL_STATUS.save() messages.success(request, f"Status updated to {important_info['status']}") - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) @require_POST @@ -52,7 +52,7 @@ def refresh_all_statuses_view(request: HtmxHttpRequest) -> HttpResponse: request, group="email-refresh_all_statuses", key="user", rate="1/m", increment=True ): messages.error(request, "Woah, slow down! Refreshing the statuses takes a while, give us a break!") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") if request.user.logged_in_as_team: ALL_STATUSES = EmailSendStatus.objects.filter(organization=request.user.logged_in_as_team) else: diff --git a/backend/api/public/endpoints/invoices/create.py b/backend/api/public/endpoints/invoices/create.py index 0cb04a737..a00f28e44 100644 --- a/backend/api/public/endpoints/invoices/create.py +++ b/backend/api/public/endpoints/invoices/create.py @@ -4,7 +4,7 @@ from rest_framework.decorators import api_view from rest_framework.response import Response -from backend.clients.models import Client +from backend.models import Client from core.api.public.decorators import require_scopes from core.api.public.helpers.response import APIResponse from backend.api.public.serializers.invoices import InvoiceSerializer diff --git a/backend/api/settings/defaults.py b/backend/api/settings/defaults.py index df235bb22..12f186703 100644 --- a/backend/api/settings/defaults.py +++ b/backend/api/settings/defaults.py @@ -4,7 +4,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.clients.models import Client +from backend.models import Client from backend.finance.service.clients.validate import validate_client from backend.finance.service.defaults.get import get_account_defaults from backend.finance.service.defaults.update import change_client_defaults @@ -62,10 +62,10 @@ def change_client_defaults_endpoint(request: WebRequest, client_id: int | None = if response.failed: messages.error(request, response.error) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") messages.success(request, "Successfully updated client defaults") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") @require_http_methods(["DELETE"]) @@ -85,11 +85,11 @@ def remove_client_default_logo_endpoint(request: WebRequest, client_id: int | No if not defaults.default_invoice_logo: messages.error(request, "No default logo to remove") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") defaults.default_invoice_logo.delete() messages.success(request, "Successfully updated client defaults") - resp = render(request, "base/toast.html") + resp = render(request, "core/base/toast.html") resp["HX-Refresh"] = "true" return resp diff --git a/backend/api/settings/email_templates.py b/backend/api/settings/email_templates.py index a54fe39be..77fc0ff9b 100644 --- a/backend/api/settings/email_templates.py +++ b/backend/api/settings/email_templates.py @@ -3,7 +3,7 @@ from django.views.decorators.http import require_POST from backend.finance.service.defaults.get import get_account_defaults -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from core.types.requests import WebRequest @@ -15,11 +15,11 @@ def save_email_template(request: WebRequest, template: str): if template not in ["invoice_created", "invoice_overdue", "invoice_cancelled"]: messages.error(request, f"Invalid template: {template}") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") if content is None: messages.error(request, f"Missing content for template: {template}") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") acc_defaults = get_account_defaults(request.actor) @@ -29,4 +29,4 @@ def save_email_template(request: WebRequest, template: str): messages.success(request, f"Email template '{template}' saved successfully") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") diff --git a/backend/auth_backends.py b/backend/auth_backends.py deleted file mode 100644 index e9f1e6fa9..000000000 --- a/backend/auth_backends.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend - - -class EmailInsteadOfUsernameBackend(ModelBackend): - def authenticate(self, request, username=None, password=None, **kwargs): - UserModel = get_user_model() - - if username is None or password is None: - return - - try: - user = UserModel.objects.get(email=username) - except UserModel.DoesNotExist: - # Run the default password hasher once to reduce the timing - # difference between an existing and a nonexistent user (#20760). - UserModel().set_password(password) - return None - else: - if user.check_password(password): - return user - return None diff --git a/backend/clients/api/delete.py b/backend/clients/api/delete.py index a0416f220..8cb9d4a8c 100644 --- a/backend/clients/api/delete.py +++ b/backend/clients/api/delete.py @@ -2,7 +2,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.service.clients.delete import delete_client, DeleteClientServiceResponse from core.types.requests import WebRequest @@ -16,4 +16,4 @@ def client_delete(request: WebRequest, id: int): messages.error(request, response.error) else: messages.success(request, f"Successfully deleted client #{id}") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") diff --git a/backend/clients/api/fetch.py b/backend/clients/api/fetch.py index bbbac23ed..d5ebef01a 100644 --- a/backend/clients/api/fetch.py +++ b/backend/clients/api/fetch.py @@ -1,8 +1,8 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes -from backend.clients.models import Client +from core.decorators import web_require_scopes +from backend.models import Client from backend.finance.service.clients.get import fetch_clients, FetchClientServiceResponse from core.types.htmx import HtmxHttpRequest from core.types.requests import WebRequest diff --git a/backend/clients/clients.py b/backend/clients/clients.py index adf7cbb68..231981f46 100644 --- a/backend/clients/clients.py +++ b/backend/clients/clients.py @@ -3,7 +3,7 @@ from django.dispatch import receiver from django.db.models.signals import post_save -from backend.clients.models import Client, DefaultValues +from backend.models import Client, FinanceDefaultValues logger = logging.getLogger(__name__) @@ -16,11 +16,11 @@ def create_client_defaults(sender: Type[Client], instance: Client, created, **kw logger.info(f"Creating client defaults for client #{instance.id}") if instance.user: - account_defaults, _ = DefaultValues.objects.get_or_create(user=instance.owner, client=None) + account_defaults, _ = FinanceDefaultValues.objects.get_or_create(user=instance.owner, client=None) else: - account_defaults, _ = DefaultValues.objects.get_or_create(organization=instance.owner, client=None) + account_defaults, _ = FinanceDefaultValues.objects.get_or_create(organization=instance.owner, client=None) - defaults = DefaultValues.objects.create(client=instance, owner=instance.owner) # type: ignore[misc] + defaults = FinanceDefaultValues.objects.create(client=instance, owner=instance.owner) # type: ignore[misc] defaults.invoice_date_value = account_defaults.invoice_date_value defaults.invoice_date_type = account_defaults.invoice_date_type diff --git a/backend/clients/models.py b/backend/clients/models.py index 19e5dabbd..391471bce 100644 --- a/backend/clients/models.py +++ b/backend/clients/models.py @@ -7,101 +7,73 @@ recurring_invoices_invoice_overdue_default_email_template, recurring_invoices_invoice_cancelled_default_email_template, ) -from backend.models import OwnerBase, User, UserSettings, _private_storage - - -class Client(OwnerBase): - active = models.BooleanField(default=True) - name = models.CharField(max_length=64) - phone_number = models.CharField(max_length=100, blank=True, null=True) - email = models.EmailField(blank=True, null=True) - email_verified = models.BooleanField(default=False) - company = models.CharField(max_length=100, blank=True, null=True) - contact_method = models.CharField(max_length=100, blank=True, null=True) - is_representative = models.BooleanField(default=False) - - address = models.TextField(max_length=100, blank=True, null=True) - city = models.CharField(max_length=100, blank=True, null=True) - country = models.CharField(max_length=100, blank=True, null=True) - - def __str__(self): - return self.name - - def has_access(self, user: User) -> bool: - if not user.is_authenticated: - return False - - if user.logged_in_as_team: - return self.organization == user.logged_in_as_team - else: - return self.user == user - - -class DefaultValues(OwnerBase): - class InvoiceDueDateType(models.TextChoices): - days_after = "days_after" # days after issue - date_following = "date_following" # date of following month - date_current = "date_current" # date of current month - - class InvoiceDateType(models.TextChoices): - day_of_month = "day_of_month" - days_after = "days_after" - - client = models.OneToOneField(Client, on_delete=models.CASCADE, related_name="default_values", null=True, blank=True) - - currency = models.CharField( - max_length=3, - default="GBP", - choices=[(code, info["name"]) for code, info in UserSettings.CURRENCIES.items()], - ) - - invoice_due_date_value = models.PositiveSmallIntegerField(default=7, null=False, blank=False) - invoice_due_date_type = models.CharField(max_length=20, choices=InvoiceDueDateType.choices, default=InvoiceDueDateType.days_after) - - invoice_date_value = models.PositiveSmallIntegerField(default=15, null=False, blank=False) - invoice_date_type = models.CharField(max_length=20, choices=InvoiceDateType.choices, default=InvoiceDateType.day_of_month) - - invoice_from_name = models.CharField(max_length=100, null=True, blank=True) - invoice_from_company = models.CharField(max_length=100, null=True, blank=True) - invoice_from_address = models.CharField(max_length=100, null=True, blank=True) - invoice_from_city = models.CharField(max_length=100, null=True, blank=True) - invoice_from_county = models.CharField(max_length=100, null=True, blank=True) - invoice_from_country = models.CharField(max_length=100, null=True, blank=True) - invoice_from_email = models.CharField(max_length=100, null=True, blank=True) - - invoice_account_number = models.CharField(max_length=100, null=True, blank=True) - invoice_sort_code = models.CharField(max_length=100, null=True, blank=True) - invoice_account_holder_name = models.CharField(max_length=100, null=True, blank=True) - - email_template_recurring_invoices_invoice_created = models.TextField(default=recurring_invoices_invoice_created_default_email_template) - email_template_recurring_invoices_invoice_overdue = models.TextField(default=recurring_invoices_invoice_overdue_default_email_template) - email_template_recurring_invoices_invoice_cancelled = models.TextField( - default=recurring_invoices_invoice_cancelled_default_email_template - ) - - def get_issue_and_due_dates(self, issue_date: date | str | None = None) -> tuple[str, str]: - due: date - issue: date - - if isinstance(issue_date, str): - issue = date.fromisoformat(issue_date) or date.today() - else: - issue = issue_date or date.today() - - match self.invoice_due_date_type: - case self.InvoiceDueDateType.days_after: - due = issue + timedelta(days=self.invoice_due_date_value) - case self.InvoiceDueDateType.date_following: - due = date(issue.year, issue.month + 1, self.invoice_due_date_value) - case self.InvoiceDueDateType.date_current: - due = date(issue.year, issue.month, self.invoice_due_date_value) - case _: - raise ValueError("Invalid invoice due date type") - return date.isoformat(issue), date.isoformat(due) - - default_invoice_logo = models.ImageField( - upload_to="invoice_logos/", - storage=_private_storage, - blank=True, - null=True, - ) +from backend.models import _private_storage, DefaultValuesBase + +# class FinanceDefaultValues(DefaultValuesBase): +# class InvoiceDueDateType(models.TextChoices): +# days_after = "days_after" +# date_following = "date_following" +# date_current = "date_current" +# +# class InvoiceDateType(models.TextChoices): +# day_of_month = "day_of_month" +# days_after = "days_after" +# +# invoice_due_date_value = models.PositiveSmallIntegerField(default=7, null=False, blank=False) +# invoice_due_date_type = models.CharField( +# max_length=20, +# choices=InvoiceDueDateType.choices, +# default=InvoiceDueDateType.days_after, +# ) +# +# invoice_date_value = models.PositiveSmallIntegerField(default=15, null=False, blank=False) +# invoice_date_type = models.CharField( +# max_length=20, +# choices=InvoiceDateType.choices, +# default=InvoiceDateType.day_of_month, +# ) +# +# invoice_from_name = models.CharField(max_length=100, null=True, blank=True) +# invoice_from_company = models.CharField(max_length=100, null=True, blank=True) +# invoice_from_address = models.CharField(max_length=100, null=True, blank=True) +# invoice_from_city = models.CharField(max_length=100, null=True, blank=True) +# invoice_from_county = models.CharField(max_length=100, null=True, blank=True) +# invoice_from_country = models.CharField(max_length=100, null=True, blank=True) +# invoice_from_email = models.CharField(max_length=100, null=True, blank=True) +# +# invoice_account_number = models.CharField(max_length=100, null=True, blank=True) +# invoice_sort_code = models.CharField(max_length=100, null=True, blank=True) +# invoice_account_holder_name = models.CharField(max_length=100, null=True, blank=True) +# +# email_template_recurring_invoices_invoice_created = models.TextField(default=recurring_invoices_invoice_created_default_email_template) +# email_template_recurring_invoices_invoice_overdue = models.TextField(default=recurring_invoices_invoice_overdue_default_email_template) +# email_template_recurring_invoices_invoice_cancelled = models.TextField( +# default=recurring_invoices_invoice_cancelled_default_email_template +# ) +# +# default_invoice_logo = models.ImageField( +# upload_to="invoice_logos/", +# storage=_private_storage, +# blank=True, +# null=True, +# ) +# +# def get_issue_and_due_dates(self, issue_date: date | str | None = None) -> tuple[str, str]: +# due: date +# issue: date +# +# if isinstance(issue_date, str): +# issue = date.fromisoformat(issue_date) or date.today() +# else: +# issue = issue_date or date.today() +# +# match self.invoice_due_date_type: +# case self.InvoiceDueDateType.days_after: +# due = issue + timedelta(days=self.invoice_due_date_value) +# case self.InvoiceDueDateType.date_following: +# due = date(issue.year, issue.month + 1, self.invoice_due_date_value) +# case self.InvoiceDueDateType.date_current: +# due = date(issue.year, issue.month, self.invoice_due_date_value) +# case _: +# raise ValueError("Invalid invoice due date type") +# return date.isoformat(issue), date.isoformat(due) diff --git a/backend/clients/views/create.py b/backend/clients/views/create.py index b9caf81e9..8a2dec93b 100644 --- a/backend/clients/views/create.py +++ b/backend/clients/views/create.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.shortcuts import render, redirect -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.service.clients.create import create_client, CreateClientServiceResponse from core.types.requests import WebRequest diff --git a/backend/clients/views/dashboard.py b/backend/clients/views/dashboard.py index eddaaa869..ab1ffd18b 100644 --- a/backend/clients/views/dashboard.py +++ b/backend/clients/views/dashboard.py @@ -1,6 +1,6 @@ from django.shortcuts import render -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from core.types.htmx import HtmxHttpRequest diff --git a/backend/clients/views/detail.py b/backend/clients/views/detail.py index 70e62fafd..574bce923 100644 --- a/backend/clients/views/detail.py +++ b/backend/clients/views/detail.py @@ -4,11 +4,11 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.service.clients.delete import delete_client, DeleteClientServiceResponse from backend.finance.service.clients.validate import validate_client from core.types.requests import WebRequest -from backend.clients.models import Client +from backend.models import Client @require_http_methods(["GET"]) diff --git a/backend/context_processors.py b/backend/context_processors.py index 4dbac686c..5922925aa 100644 --- a/backend/context_processors.py +++ b/backend/context_processors.py @@ -1,74 +1,12 @@ -from typing import List, Optional, Dict, Any - -from core.service.base.breadcrumbs import get_breadcrumbs -from django.http import HttpRequest -from django.urls import reverse - -import calendar - -from settings.helpers import get_var, BASE_DIR +from typing import Dict, Any +from core.types.requests import WebRequest from backend import __version__ -from settings.settings import DEBUG - - -## Context processors need to be put in SETTINGS TEMPLATES to be recognized -def navbar(request): - # cached_navbar_items = cache.get("navbar_items") - # if cached_navbar_items is None: - # navbar_items = load_navbar_items() - # - # # Cache the sidebar items for a certain time (e.g., 3600 seconds = 1 hr) - # cache.set("navbar_items", navbar_items, 60 * 60 * 3) # 3 hrs - # else: - # navbar_items = cached_navbar_items - # context = {"navbar_items": navbar_items} - return {} - -def extras(request: HttpRequest): - # import_method can be one of: "webpack", "public_cdn", "custom_cdn" +def extras(request: WebRequest): data: Dict[str, Any] = {} - import pathlib - - def get_git_revision(base_path): - if not DEBUG: - return "prod" - - git_dir = pathlib.Path(base_path) / ".git" - - # check file exists - - if not git_dir.exists() or not git_dir.is_dir() or not (git_dir / "HEAD").exists(): - return "commit not found" - - with (git_dir / "HEAD").open("r") as head: - ref = head.readline().split(" ")[-1].strip() - - if not (git_dir / ref).exists(): - return "commit not found" - - with (git_dir / ref).open("r") as git_hash: - return git_hash.readline().strip() - - data["version"] = __version__ - data["git_branch"] = get_var("BRANCH") - data["git_version"] = get_git_revision(BASE_DIR) - data["import_method"] = get_var("IMPORT_METHOD", default="webpack") - data["analytics"] = get_var("ANALYTICS_STRING") - data["calendar_util"] = calendar - data["day_names_sunday_first"] = [calendar.day_name[(i + 6) % 7] for i in range(7)] - data["day_names_monday_first"] = [day for day in calendar.day_name] - - if hasattr(request, "htmx") and request.htmx.boosted: - data["base"] = "base/htmx.html" - else: - data["base"] = "base/base.html" + data["finances_version"] = __version__ return data - - -def breadcrumbs(request: HttpRequest): - return get_breadcrumbs(request=request) diff --git a/backend/decorators.py b/backend/decorators.py deleted file mode 100644 index ec65eb6a2..000000000 --- a/backend/decorators.py +++ /dev/null @@ -1,272 +0,0 @@ -from __future__ import annotations - -import logging -from functools import wraps -from typing import TypedDict - -from django.contrib import messages -from django.http import HttpResponse -from django.http import HttpResponseRedirect -from django.shortcuts import redirect -from django.shortcuts import render -from django.urls import reverse - -from backend.models import TeamMemberPermission -from core.types.requests import WebRequest -from core.utils.feature_flags import get_feature_status - -logger = logging.getLogger(__name__) - - -def not_authenticated(view_func): - def wrapper_func(request, *args, **kwargs): - if request.user.is_authenticated: - return redirect("dashboard") - else: - return view_func(request, *args, **kwargs) - - return wrapper_func - - -def staff_only(view_func): - def wrapper_func(request, *args, **kwargs): - if request.user.is_staff and request.user.is_authenticated: - return view_func(request, *args, **kwargs) - else: - messages.error(request, "You don't have permission to view this page.") - return redirect("dashboard") - - return wrapper_func - - -def superuser_only(view_func): - def wrapper_func(request, *args, **kwargs): - if request.user.is_authenticated and request.user.is_superuser: - return view_func(request, *args, **kwargs) - else: - messages.error(request, "You don't have permission to view this page.") - return redirect("dashboard") - - return wrapper_func - - -def htmx_only(viewname: str = "dashboard"): - def decorator(view_func): - def wrapper_func(request, *args, **kwargs): - if request.htmx: - return view_func(request, *args, **kwargs) - else: - return redirect(viewname) - - return wrapper_func - - return decorator - - -def hx_boost(view): - """ - Decorator for HTMX requests. - - used by wrapping FBV in @hx_boost and adding **kwargs to param - then you can use context = kwargs.get("context", {}) to continue and then it will handle HTMX boosts - """ - - @wraps(view) - def wrapper(request, *args, **kwargs): - if request.htmx.boosted: - kwargs["context"] = kwargs.get("context", {}) | {"base": "base/htmx.html"} - return view(request, *args, **kwargs) - - return wrapper - - -def feature_flag_check(flag, status=True, api=False, htmx=False): - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - feat_status = get_feature_status(flag) - - if feat_status == status: - return view_func(request, *args, **kwargs) - - if api and htmx: - messages.error(request, "This feature is currently disabled.") - return render(request, "base/toasts.html") - elif api: - return HttpResponse(status=403, content="This feature is currently disabled.") - messages.error(request, "This feature is currently disabled.") - try: - last_visited_url = request.session["last_visited"] - current_url = request.build_absolute_uri() - if last_visited_url != current_url: - return HttpResponseRedirect(last_visited_url) - except KeyError: - pass - return HttpResponseRedirect(reverse("dashboard")) - - return wrapper - - return decorator - - -class FlagItem(TypedDict): - name: str - desired: bool - - -def feature_flag_check_multi(flag_list: list[FlagItem], api=False, htmx=False): - """ - Checks if at least one of the flags in the list is the desired status - """ - - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - if not any(get_feature_status(flag["name"]) == flag["desired"] for flag in flag_list): - if api and htmx: - messages.error(request, "This feature is currently disabled.") - return render(request, "base/toasts.html") - elif api: - return HttpResponse(status=403, content="This feature is currently disabled.") - messages.error(request, "This feature is currently disabled.") - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - - return view_func(request, *args, **kwargs) - - return wrapper - - return decorator - - -# def quota_usage_check(limit: str | QuotaLimit, extra_data: str | int | None = None, api=False, htmx=False): -# def decorator(view_func): -# @wraps(view_func) -# def wrapper(request, *args, **kwargs): -# try: -# quota_limit = QuotaLimit.objects.get(slug=limit) if isinstance(limit, str) else limit -# except QuotaLimit.DoesNotExist: -# return view_func(request, *args, **kwargs) -# -# if not quota_limit.strict_goes_above_limit(request.user, extra=extra_data): -# return view_func(request, *args, **kwargs) -# -# if api and htmx: -# messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") -# return render(request, "partials/messages_list.html", {"autohide": False}) -# elif api: -# return HttpResponse(status=403, content=f"You have reached the quota limit for this service '{quota_limit.slug}'") -# messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") -# try: -# last_visited_url = request.session["last_visited"] -# current_url = request.build_absolute_uri() -# if last_visited_url != current_url: -# return HttpResponseRedirect(last_visited_url) -# except KeyError: -# pass -# return HttpResponseRedirect(reverse("dashboard")) -# -# return wrapper -# -# return decorator - - -not_logged_in = not_authenticated -logged_out = not_authenticated - - -def web_require_scopes(scopes: str | list[str], htmx=False, api=False, redirect_url=None): - """ - Only to be used by WebRequests (htmx or html) NOT PUBLIC API - """ - - def decorator(view_func): - @wraps(view_func) - def _wrapped_view(request: WebRequest, *args, **kwargs): - if request.team_id and not request.team: - return return_error(request, "Team not found") - - if request.team: - # Check for team permissions based on team_id and scopes - if not request.team.is_owner(request.user): - team_permissions = TeamMemberPermission.objects.filter(team=request.team, user=request.user).first() - - if not team_permissions: - return return_error(request, "You do not have permission to perform this action (no permissions for team)") - - # single scope - if isinstance(scopes, str) and scopes not in team_permissions.scopes: - return return_error(request, f"You do not have permission to perform this action ({scopes})") - - # scope list - if isinstance(scopes, list): - for scope in scopes: - if scope not in team_permissions.scopes: - return return_error(request, f"You do not have permission to perform this action ({scope})") - return view_func(request, *args, **kwargs) - - _wrapped_view.required_scopes = scopes - return _wrapped_view - - def return_error(request: WebRequest, msg: str): - logging.info(f"User does not have permission to perform this action (User ID: {request.user.id}, Scopes: {scopes})") - if api and htmx: - messages.error(request, msg) - return render(request, "base/toast.html", {"autohide": False}) - elif api: - return HttpResponse(status=403, content=msg) - elif request.htmx: - messages.error(request, msg) - resp = HttpResponse(status=200) - - try: - last_visited_url = request.session["last_visited"] - current_url = request.build_absolute_uri() - if last_visited_url != current_url: - resp["HX-Replace-Url"] = last_visited_url - except KeyError: - ... - resp["HX-Refresh"] = "true" - return resp - - messages.error(request, msg) - - try: - last_visited_url = request.session["last_visited"] - current_url = request.build_absolute_uri() - if last_visited_url != current_url: - return HttpResponseRedirect(last_visited_url) - except KeyError: - pass - - if not redirect_url: - return HttpResponseRedirect(reverse("dashboard")) - - try: - return HttpResponseRedirect(reverse(redirect_url)) - except KeyError: - return HttpResponseRedirect(reverse("dashboard")) - - return decorator - - -# wrapper around billing has_entitlements only load - -from django.conf import settings - - -def has_entitlements(entitlements: list[str] | str, htmx_api: bool = False): - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - if settings.BILLING_ENABLED: - from billing.decorators import has_entitlements_called_from_backend_handler - - wrapped_view_func = has_entitlements_called_from_backend_handler( - entitlements if isinstance(entitlements, list) else [entitlements], htmx_api - )(view_func) - return wrapped_view_func(request, *args, **kwargs) - return view_func(request, *args, **kwargs) - - return wrapper - - return decorator diff --git a/backend/finance/api/invoices/delete.py b/backend/finance/api/invoices/delete.py index 9f18fb340..2dd33e720 100644 --- a/backend/finance/api/invoices/delete.py +++ b/backend/finance/api/invoices/delete.py @@ -5,7 +5,7 @@ from django.urls.exceptions import Resolver404 from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.models import Invoice from core.types.htmx import HtmxHttpRequest @@ -21,11 +21,11 @@ def delete_invoice(request: HtmxHttpRequest): invoice = Invoice.objects.get(id=delete_items.get("invoice", "")) except Invoice.DoesNotExist: messages.error(request, "Invoice Not Found") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") if not invoice.has_access(request.user): messages.error(request, "You do not have permission to delete this invoice") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") QuotaLimit.delete_quota_usage("invoices-count", request.user, invoice.id, invoice.date_created) @@ -34,7 +34,7 @@ def delete_invoice(request: HtmxHttpRequest): if request.htmx: if not redirect: messages.success(request, "Invoice deleted") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") try: resolve(redirect) diff --git a/backend/finance/api/invoices/edit.py b/backend/finance/api/invoices/edit.py index c99bc08d7..4b2ff180c 100644 --- a/backend/finance/api/invoices/edit.py +++ b/backend/finance/api/invoices/edit.py @@ -6,7 +6,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods, require_POST -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice from core.types.htmx import HtmxHttpRequest @@ -61,14 +61,14 @@ def edit_invoice(request: HtmxHttpRequest): new_value = datetime.strptime(new_value, "%Y-%m-%d").date() # type: ignore[assignment] except ValueError: messages.error(request, "Invalid date format for date_due") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") setattr(invoice, column_name, new_value) invoice.save() if request.htmx: messages.success(request, "Invoice edited") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") return JsonResponse({"message": "Invoice successfully edited"}, status=200) @@ -141,14 +141,14 @@ def edit_discount(request: HtmxHttpRequest, invoice_id: str): messages.success(request, "Discount was applied successfully") - response = render(request, "base/toasts.html") + response = render(request, "core/base/toasts.html") response["HX-Trigger"] = "update_invoice" return response def return_message(request: HttpRequest, message: str, success: bool = True) -> HttpResponse: send_message(request, message, success) - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") def send_message(request: HttpRequest, message: str, success: bool = False) -> None: diff --git a/backend/finance/api/invoices/fetch.py b/backend/finance/api/invoices/fetch.py index ccbdb0bb7..1dda518d9 100644 --- a/backend/finance/api/invoices/fetch.py +++ b/backend/finance/api/invoices/fetch.py @@ -1,7 +1,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice from core.types.htmx import HtmxHttpRequest from backend.finance.service.invoices.common.fetch import get_context diff --git a/backend/finance/api/invoices/manage.py b/backend/finance/api/invoices/manage.py index e917df75d..450a76dcf 100644 --- a/backend/finance/api/invoices/manage.py +++ b/backend/finance/api/invoices/manage.py @@ -6,7 +6,7 @@ from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice from core.types.htmx import HtmxHttpRequest diff --git a/backend/finance/api/invoices/recurring/delete.py b/backend/finance/api/invoices/recurring/delete.py index a5c857da8..7d9a6dbd1 100644 --- a/backend/finance/api/invoices/recurring/delete.py +++ b/backend/finance/api/invoices/recurring/delete.py @@ -5,7 +5,7 @@ from django.urls.exceptions import Resolver404 from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import InvoiceRecurringProfile from backend.boto3.async_tasks.tasks import Task from backend.boto3.scheduler.delete_schedule import delete_boto_schedule @@ -23,11 +23,11 @@ def delete_invoice_recurring_profile_endpoint(request: WebRequest): invoice_profile = InvoiceRecurringProfile.objects.get(id=delete_items.get("invoice_profile", "")) except InvoiceRecurringProfile.DoesNotExist: messages.error(request, "Invoice recurring profile Not Found") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") if not invoice_profile.has_access(request.user): messages.error(request, "You do not have permission to delete this Invoice recurring profile") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") # QuotaLimit.delete_quota_usage("invoices-count", request.user, invoice.id, invoice.date_created) @@ -39,7 +39,7 @@ def delete_invoice_recurring_profile_endpoint(request: WebRequest): if request.htmx: if not redirect: messages.success(request, "Invoice profile deleted") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") try: resolve(redirect) diff --git a/backend/finance/api/invoices/recurring/edit.py b/backend/finance/api/invoices/recurring/edit.py index 228e0aeb2..bb75c23b6 100644 --- a/backend/finance/api/invoices/recurring/edit.py +++ b/backend/finance/api/invoices/recurring/edit.py @@ -5,7 +5,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes, has_entitlements +from core.decorators import web_require_scopes, has_entitlements from backend.finance.models import InvoiceRecurringProfile from backend.finance.service.invoices.recurring.get import get_invoice_profile from backend.finance.service.invoices.recurring.validate.frequencies import validate_and_update_frequency @@ -20,7 +20,7 @@ def edit_invoice_recurring_profile_endpoint(request: WebRequest, invoice_profile if invoice_profile_response.failed: messages.error(request, invoice_profile_response.error) - return render(request, "base/toasts.html", {"autohide": False}) + return render(request, "core/base/toasts.html", {"autohide": False}) invoice_profile: InvoiceRecurringProfile = invoice_profile_response.response @@ -34,7 +34,7 @@ def edit_invoice_recurring_profile_endpoint(request: WebRequest, invoice_profile if frequency_update_response.failed: messages.error(request, frequency_update_response.error) - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") attributes_to_update = { "date_due": request.POST.get("date_due"), @@ -67,13 +67,13 @@ def edit_invoice_recurring_profile_endpoint(request: WebRequest, invoice_profile new_value = datetime.strptime(new_value, "%Y-%m-%d").date() # type: ignore[assignment] except ValueError: messages.error(request, "Invalid date format for date_due") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") setattr(invoice_profile, column_name, new_value) invoice_profile.save() if request.htmx: messages.success(request, "Successfully saved profile!") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") return JsonResponse({"message": "Invoice successfully edited"}, status=200) diff --git a/backend/finance/api/invoices/recurring/fetch.py b/backend/finance/api/invoices/recurring/fetch.py index ec5cc5619..e669694ba 100644 --- a/backend/finance/api/invoices/recurring/fetch.py +++ b/backend/finance/api/invoices/recurring/fetch.py @@ -2,7 +2,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import InvoiceRecurringProfile from backend.finance.service.invoices.common.fetch import get_context from core.types.requests import WebRequest diff --git a/backend/finance/api/invoices/recurring/generate_next_invoice_now.py b/backend/finance/api/invoices/recurring/generate_next_invoice_now.py index 4c85ed88c..cb03aff1a 100644 --- a/backend/finance/api/invoices/recurring/generate_next_invoice_now.py +++ b/backend/finance/api/invoices/recurring/generate_next_invoice_now.py @@ -2,7 +2,7 @@ from django.shortcuts import render from django.views.decorators.http import require_POST -from backend.decorators import web_require_scopes, htmx_only +from core.decorators import web_require_scopes, htmx_only from backend.finance.models import InvoiceRecurringProfile from backend.finance.service.defaults.get import get_account_defaults from backend.finance.service.invoices.recurring.generation.next_invoice import safe_generate_next_invoice_service @@ -24,7 +24,7 @@ def generate_next_invoice_now_endpoint(request: WebRequest, invoice_profile_id): if not invoice_recurring_profile: messages.error(request, "Failed to fetch next invoice; cannot find Invoice recurring profile.") - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) if invoice_recurring_profile.client_to: account_defaults = get_account_defaults(invoice_recurring_profile.owner, invoice_recurring_profile.client_to) @@ -33,7 +33,7 @@ def generate_next_invoice_now_endpoint(request: WebRequest, invoice_profile_id): if not invoice_recurring_profile.has_access(request.user): messages.error(request, "You do not have permission to modify this invoice recurring profile.") - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) next_invoice_issue_date = invoice_recurring_profile.next_invoice_issue_date() @@ -59,4 +59,4 @@ def generate_next_invoice_now_endpoint(request: WebRequest, invoice_profile_id): else: logger.info(svc_resp.error) messages.error(request, f"Failed to fetch next invoice; {svc_resp.error}") - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) diff --git a/backend/finance/api/invoices/recurring/poll.py b/backend/finance/api/invoices/recurring/poll.py index 00e3bcc56..a668dbd97 100644 --- a/backend/finance/api/invoices/recurring/poll.py +++ b/backend/finance/api/invoices/recurring/poll.py @@ -5,7 +5,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes, htmx_only +from core.decorators import web_require_scopes, htmx_only from backend.finance.models import InvoiceRecurringProfile from backend.boto3.async_tasks.tasks import Task from backend.boto3.scheduler.create_schedule import create_boto_schedule diff --git a/backend/finance/api/invoices/recurring/update_status.py b/backend/finance/api/invoices/recurring/update_status.py index 3c41c7da7..5e691c4de 100644 --- a/backend/finance/api/invoices/recurring/update_status.py +++ b/backend/finance/api/invoices/recurring/update_status.py @@ -4,7 +4,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_POST -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.boto3.async_tasks.tasks import Task from backend.boto3.scheduler.create_schedule import create_boto_schedule from backend.boto3.scheduler.get import get_boto_schedule @@ -90,7 +90,7 @@ def recurring_profile_change_status_endpoint(request: WebRequest, invoice_profil def return_message(request: HttpRequest, message: str, success: bool = True) -> HttpResponse: send_message(request, message, success) - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") def send_message(request: HttpRequest, message: str, success: bool = False) -> None: diff --git a/backend/finance/api/invoices/reminders/create.py b/backend/finance/api/invoices/reminders/create.py index 4ef2a309e..a724a318b 100644 --- a/backend/finance/api/invoices/reminders/create.py +++ b/backend/finance/api/invoices/reminders/create.py @@ -6,7 +6,7 @@ # from django.shortcuts import render, redirect # from django.utils import timezone # -# from backend.decorators import web_require_scopes +# from core.decorators import web_require_scopes # from backend.finance.models import Invoice, InvoiceReminder, QuotaUsage # from backend.utils.quota_limit_ops import quota_usage_check_under # from infrastructure.aws.schedules.create_reminder import CreateReminderInputData, create_reminder_schedule @@ -47,17 +47,17 @@ # invoice = Invoice.objects.get(id=invoice_id) # except Invoice.DoesNotExist: # messages.error(request, "Invoice not found") -# return render(request, "base/toast.html", {"autohide": False}) +# return render(request, "core/base/toast.html", {"autohide": False}) # # # Check user permission # if not invoice.has_access(user=request.user): # messages.error(request, "You do not have permission to create schedules for this invoice") -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") # # # Check reminder type # if reminder_type not in InvoiceReminder.ReminderTypes.values: # messages.error(request, "Invalid reminder type") -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") # # # Ensure days is set for non-overdue reminders # if reminder_type == "on_overdue": @@ -70,7 +70,7 @@ # raise ValueError # except ValueError: # messages.error(request, "Invalid days value. Make sure it's an integer from 1-31") -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") # # # Create reminder object # reminder = InvoiceReminder(invoice=invoice, reminder_type=reminder_type) @@ -96,4 +96,4 @@ # return render(request, "pages/invoices/single/schedules/reminders/_table_row.html", {"reminder": REMINDER.reminder}) # else: # messages.error(request, REMINDER.message) -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") diff --git a/backend/finance/api/invoices/reminders/delete.py b/backend/finance/api/invoices/reminders/delete.py index 8935b1c77..89b0178bc 100644 --- a/backend/finance/api/invoices/reminders/delete.py +++ b/backend/finance/api/invoices/reminders/delete.py @@ -4,7 +4,7 @@ # from django.shortcuts import render # from django.views.decorators.http import require_http_methods # -# from backend.decorators import feature_flag_check, web_require_scopes +# from core.decorators import feature_flag_check, web_require_scopes # from backend.finance.models import InvoiceReminder # # from backend.types.htmx import HtmxHttpRequest @@ -20,11 +20,11 @@ # reminder = InvoiceReminder.objects.get(id=reminder_id) # except InvoiceReminder.DoesNotExist: # messages.error(request, "Schedule not found!") -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") # # if not reminder.invoice.has_access(request.user): # messages.error(request, "You do not have access to this invoice.") -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") # # original_status = reminder.status # reminder.set_status("deleting") @@ -40,7 +40,7 @@ # else: # reminder.set_status(original_status) # messages.error(request, f"Failed to delete schedule: {delete_status.message}") -# return render(request, "base/toasts.html") +# return render(request, "core/base/toasts.html") # # reminder.set_status("cancelled") # diff --git a/backend/finance/api/invoices/reminders/fetch.py b/backend/finance/api/invoices/reminders/fetch.py index 49047d630..396f6703b 100644 --- a/backend/finance/api/invoices/reminders/fetch.py +++ b/backend/finance/api/invoices/reminders/fetch.py @@ -4,7 +4,7 @@ from django.views.decorators.http import require_GET from django_ratelimit.core import is_ratelimited -from backend.decorators import feature_flag_check, web_require_scopes +from core.decorators import feature_flag_check, web_require_scopes from backend.finance.models import Invoice from core.types.htmx import HtmxHttpRequest @@ -16,17 +16,17 @@ def fetch_reminders(request: HtmxHttpRequest, invoice_id: str): ratelimit = is_ratelimited(request, group="fetch_reminders", key="user", rate="20/30s", increment=True) if ratelimit: messages.error(request, "Too many requests") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") try: invoice = Invoice.objects.prefetch_related("invoice_reminders").get(id=invoice_id) except Invoice.DoesNotExist: messages.error(request, "Invoice not found") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") if not invoice.has_access(request.user): messages.error(request, "You do not have permission to view this invoice") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") context: dict = {} diff --git a/backend/finance/api/products/create.py b/backend/finance/api/products/create.py index f8ee5321d..92f2fc063 100644 --- a/backend/finance/api/products/create.py +++ b/backend/finance/api/products/create.py @@ -1,7 +1,7 @@ from django.contrib import messages from backend.finance.api.products.fetch import fetch_products -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import InvoiceProduct from core.types.htmx import HtmxHttpRequest diff --git a/backend/finance/api/products/fetch.py b/backend/finance/api/products/fetch.py index 4586d8d49..78fa16eb9 100644 --- a/backend/finance/api/products/fetch.py +++ b/backend/finance/api/products/fetch.py @@ -1,7 +1,7 @@ from django.db.models import Q, QuerySet from django.shortcuts import render -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import InvoiceProduct from core.types.htmx import HtmxHttpRequest diff --git a/backend/finance/api/receipts/delete.py b/backend/finance/api/receipts/delete.py index d60bc963b..3ab2ed00a 100644 --- a/backend/finance/api/receipts/delete.py +++ b/backend/finance/api/receipts/delete.py @@ -4,7 +4,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.models import Receipt from core.types.requests import WebRequest diff --git a/backend/finance/api/receipts/download.py b/backend/finance/api/receipts/download.py index 70e199c25..525f0c320 100644 --- a/backend/finance/api/receipts/download.py +++ b/backend/finance/api/receipts/download.py @@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.models import Receipt, ReceiptDownloadToken diff --git a/backend/finance/api/receipts/edit.py b/backend/finance/api/receipts/edit.py index c3ccf74eb..6771ce86f 100644 --- a/backend/finance/api/receipts/edit.py +++ b/backend/finance/api/receipts/edit.py @@ -7,7 +7,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods, require_POST -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.models import Receipt @@ -23,7 +23,7 @@ def edit_receipt(request, receipt_id): raise Receipt.DoesNotExist except Receipt.DoesNotExist: messages.error(request, "Receipt not found") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") file: InMemoryUploadedFile | None = request.FILES.get("receipt_image") date = request.POST.get("receipt_date") diff --git a/backend/finance/api/receipts/fetch.py b/backend/finance/api/receipts/fetch.py index ca5e7d370..238d7dd4e 100644 --- a/backend/finance/api/receipts/fetch.py +++ b/backend/finance/api/receipts/fetch.py @@ -1,7 +1,7 @@ from django.db.models import Q, QuerySet from django.shortcuts import render, redirect -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.models import Receipt from core.types.htmx import HtmxHttpRequest diff --git a/backend/finance/api/receipts/new.py b/backend/finance/api/receipts/new.py index aa9076a7f..9775962d2 100644 --- a/backend/finance/api/receipts/new.py +++ b/backend/finance/api/receipts/new.py @@ -4,7 +4,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes, has_entitlements +from core.decorators import web_require_scopes, has_entitlements from backend.models import Receipt from core.types.requests import WebRequest diff --git a/backend/finance/api/reports/generate.py b/backend/finance/api/reports/generate.py index c87e7c2c7..cdb750dac 100644 --- a/backend/finance/api/reports/generate.py +++ b/backend/finance/api/reports/generate.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.shortcuts import render -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.service.reports.generate import generate_report from core.types.requests import WebRequest @@ -16,10 +16,10 @@ def generate_report_endpoint(request: WebRequest): if generated_report.failed: messages.error(request, generated_report.error) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") messages.success(request, f"Successfully generated report ({str(generated_report.response.uuid)[:4]})") - resp = render(request, "base/toast.html") + resp = render(request, "core/base/toast.html") resp["HX-Trigger"] = "refresh_reports_table" return resp diff --git a/backend/finance/models.py b/backend/finance/models.py index 464010443..3e2c01695 100644 --- a/backend/finance/models.py +++ b/backend/finance/models.py @@ -3,12 +3,19 @@ from decimal import Decimal from typing import Literal from uuid import uuid4 + +from core.models import DefaultValuesBase from django.core.validators import MaxValueValidator from django.db import models from django.utils import timezone from shortuuid.django_fields import ShortUUIDField -from backend.clients.models import Client, DefaultValues +from backend.data.default_email_templates import ( + recurring_invoices_invoice_created_default_email_template, + recurring_invoices_invoice_overdue_default_email_template, + recurring_invoices_invoice_cancelled_default_email_template, +) +from backend.models import Client from backend.managers import InvoiceRecurringProfile_WithItemsManager from backend.models import OwnerBase, UserSettings, _private_storage, USER_OR_ORGANIZATION_CONSTRAINT, User, ExpiresBase, Organization @@ -288,7 +295,7 @@ def next_invoice_issue_date(self) -> date: case _: return datetime.now().date() - def next_invoice_due_date(self, account_defaults: DefaultValues, from_date: date = datetime.now().date()) -> date: + def next_invoice_due_date(self, account_defaults: FinanceDefaultValues, from_date: date = datetime.now().date()) -> date: match account_defaults.invoice_due_date_type: case account_defaults.InvoiceDueDateType.days_after: return from_date + timedelta(days=account_defaults.invoice_due_date_value) @@ -405,3 +412,76 @@ class ReceiptDownloadToken(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) file = models.ForeignKey(Receipt, on_delete=models.CASCADE) token = models.UUIDField(default=uuid4, editable=False, unique=True) + + +class FinanceDefaultValues(DefaultValuesBase): + class InvoiceDueDateType(models.TextChoices): + days_after = "days_after" + date_following = "date_following" + date_current = "date_current" + + class InvoiceDateType(models.TextChoices): + day_of_month = "day_of_month" + days_after = "days_after" + + invoice_due_date_value = models.PositiveSmallIntegerField(default=7, null=False, blank=False) + invoice_due_date_type = models.CharField( + max_length=20, + choices=InvoiceDueDateType.choices, + default=InvoiceDueDateType.days_after, + ) + + invoice_date_value = models.PositiveSmallIntegerField(default=15, null=False, blank=False) + invoice_date_type = models.CharField( + max_length=20, + choices=InvoiceDateType.choices, + default=InvoiceDateType.day_of_month, + ) + + invoice_from_name = models.CharField(max_length=100, null=True, blank=True) + invoice_from_company = models.CharField(max_length=100, null=True, blank=True) + invoice_from_address = models.CharField(max_length=100, null=True, blank=True) + invoice_from_city = models.CharField(max_length=100, null=True, blank=True) + invoice_from_county = models.CharField(max_length=100, null=True, blank=True) + invoice_from_country = models.CharField(max_length=100, null=True, blank=True) + invoice_from_email = models.CharField(max_length=100, null=True, blank=True) + + invoice_account_number = models.CharField(max_length=100, null=True, blank=True) + invoice_sort_code = models.CharField(max_length=100, null=True, blank=True) + invoice_account_holder_name = models.CharField(max_length=100, null=True, blank=True) + + email_template_recurring_invoices_invoice_created = models.TextField(default=recurring_invoices_invoice_created_default_email_template) + email_template_recurring_invoices_invoice_overdue = models.TextField(default=recurring_invoices_invoice_overdue_default_email_template) + email_template_recurring_invoices_invoice_cancelled = models.TextField( + default=recurring_invoices_invoice_cancelled_default_email_template + ) + + default_invoice_logo = models.ImageField( + upload_to="invoice_logos/", + storage=_private_storage, + blank=True, + null=True, + ) + + def get_issue_and_due_dates(self, issue_date: date | str | None = None) -> tuple[str, str]: + due: date + issue: date + + if isinstance(issue_date, str): + issue = date.fromisoformat(issue_date) or date.today() + else: + issue = issue_date or date.today() + + match self.invoice_due_date_type: + case self.InvoiceDueDateType.days_after: + due = issue + timedelta(days=self.invoice_due_date_value) + case self.InvoiceDueDateType.date_following: + due = date(issue.year, issue.month + 1, self.invoice_due_date_value) + case self.InvoiceDueDateType.date_current: + due = date(issue.year, issue.month, self.invoice_due_date_value) + case _: + raise ValueError("Invalid invoice due date type") + return date.isoformat(issue), date.isoformat(due) + + class Meta: + constraints: list = [USER_OR_ORGANIZATION_CONSTRAINT()] diff --git a/backend/finance/service/clients/create.py b/backend/finance/service/clients/create.py index 341aeb180..2a37fc104 100644 --- a/backend/finance/service/clients/create.py +++ b/backend/finance/service/clients/create.py @@ -1,4 +1,4 @@ -from backend.clients.models import Client +from backend.models import Client from backend.finance.service.clients.validate import validate_client_create from core.utils.dataclasses import BaseServiceResponse diff --git a/backend/finance/service/defaults/get.py b/backend/finance/service/defaults/get.py index 813c47675..7cde191c1 100644 --- a/backend/finance/service/defaults/get.py +++ b/backend/finance/service/defaults/get.py @@ -1,12 +1,12 @@ from backend.models import User, Organization -from backend.clients.models import DefaultValues, Client +from backend.models import FinanceDefaultValues, Client -def get_account_defaults(actor: User | Organization, client: Client | None = None) -> DefaultValues: +def get_account_defaults(actor: User | Organization, client: Client | None = None) -> FinanceDefaultValues: if not client: - account_defaults = DefaultValues.filter_by_owner(owner=actor).filter(client__isnull=True).first() + account_defaults = FinanceDefaultValues.filter_by_owner(owner=actor).filter(client__isnull=True).first() if account_defaults: return account_defaults - return DefaultValues.objects.create(owner=actor, client=None) # type: ignore[misc] - return DefaultValues.filter_by_owner(owner=actor).get(client=client) + return FinanceDefaultValues.objects.create(owner=actor, client=None) # type: ignore[misc] + return FinanceDefaultValues.filter_by_owner(owner=actor).get(client=client) diff --git a/backend/finance/service/defaults/update.py b/backend/finance/service/defaults/update.py index 56cc304ae..4428c839c 100644 --- a/backend/finance/service/defaults/update.py +++ b/backend/finance/service/defaults/update.py @@ -1,15 +1,14 @@ from PIL import Image -from backend.models import DefaultValues +from backend.models import FinanceDefaultValues from core.types.requests import WebRequest from core.utils.dataclasses import BaseServiceResponse -class ClientDefaultsServiceResponse(BaseServiceResponse[DefaultValues]): ... +class ClientDefaultsServiceResponse(BaseServiceResponse[FinanceDefaultValues]): ... -def change_client_defaults(request: WebRequest, defaults: DefaultValues) -> ClientDefaultsServiceResponse: - +def change_client_defaults(request: WebRequest, defaults: FinanceDefaultValues) -> ClientDefaultsServiceResponse: # put = QueryDict(request.body) invoice_due_date_option = request.POST.get("invoice_due_date_option", "") invoice_due_date_value = request.POST.get("invoice_due_date_value", "") diff --git a/backend/finance/service/invoices/common/create/create.py b/backend/finance/service/invoices/common/create/create.py index 546655604..e45884423 100644 --- a/backend/finance/service/invoices/common/create/create.py +++ b/backend/finance/service/invoices/common/create/create.py @@ -1,6 +1,6 @@ from django.contrib import messages -from backend.models import Invoice, InvoiceRecurringProfile, InvoiceItem, Client, DefaultValues +from backend.models import Invoice, InvoiceRecurringProfile, InvoiceItem, Client, FinanceDefaultValues from backend.finance.service.defaults.get import get_account_defaults from core.types.requests import WebRequest @@ -55,7 +55,7 @@ def save_invoice_common(request: WebRequest, invoice_items, invoice: Invoice | I if invoice.client_to is not None and invoice.client_to.default_values.default_invoice_logo: invoice.logo = invoice.client_to.default_values.default_invoice_logo else: - defaults: DefaultValues = get_account_defaults(request.actor) + defaults: FinanceDefaultValues = get_account_defaults(request.actor) if defaults: invoice.logo = defaults.default_invoice_logo invoice.sort_code = request.POST.get("sort_code") diff --git a/backend/finance/service/invoices/common/create/get_page.py b/backend/finance/service/invoices/common/create/get_page.py index b2d60b475..0d0034d58 100644 --- a/backend/finance/service/invoices/common/create/get_page.py +++ b/backend/finance/service/invoices/common/create/get_page.py @@ -2,7 +2,7 @@ from typing import NamedTuple from django.core.exceptions import PermissionDenied, ValidationError -from backend.models import Client, InvoiceProduct, DefaultValues +from backend.models import Client, InvoiceProduct, FinanceDefaultValues from backend.finance.service.clients.validate import validate_client from backend.finance.service.defaults.get import get_account_defaults from core.types.requests import WebRequest @@ -10,7 +10,7 @@ class CreateInvoiceContextTuple(NamedTuple): - defaults: DefaultValues + defaults: FinanceDefaultValues context: dict @@ -23,7 +23,7 @@ def global_get_invoice_context(request: WebRequest) -> CreateInvoiceContextServi "existing_products": InvoiceProduct.objects.filter(user=request.user), } - defaults: DefaultValues + defaults: FinanceDefaultValues if client_id := request.GET.get("client"): try: diff --git a/backend/finance/service/invoices/recurring/generation/next_invoice.py b/backend/finance/service/invoices/recurring/generation/next_invoice.py index de94c7115..56fb3a2aa 100644 --- a/backend/finance/service/invoices/recurring/generation/next_invoice.py +++ b/backend/finance/service/invoices/recurring/generation/next_invoice.py @@ -2,7 +2,7 @@ from django.db import transaction, IntegrityError -from backend.models import Invoice, InvoiceRecurringProfile, DefaultValues, AuditLog +from backend.models import Invoice, InvoiceRecurringProfile, FinanceDefaultValues, AuditLog from backend.finance.service.defaults.get import get_account_defaults from backend.finance.service.invoices.common.emails.on_create import on_create_invoice_email_service from core.utils.dataclasses import BaseServiceResponse @@ -19,7 +19,7 @@ class GenerateNextInvoiceServiceResponse(BaseServiceResponse[Invoice]): ... def generate_next_invoice_service( invoice_recurring_profile: InvoiceRecurringProfile, issue_date: date = date.today(), - account_defaults: DefaultValues | None = None, + account_defaults: FinanceDefaultValues | None = None, ) -> GenerateNextInvoiceServiceResponse: """ This will generate the next single invoice based on the invoice recurring profile @@ -114,7 +114,7 @@ def handle_invoice_generation_failure(invoice_recurring_profile, error_message): def safe_generate_next_invoice_service( invoice_recurring_profile: InvoiceRecurringProfile, issue_date: date = date.today(), - account_defaults: DefaultValues | None = None, + account_defaults: FinanceDefaultValues | None = None, ) -> GenerateNextInvoiceServiceResponse: """ Safe wrapper to generate the next invoice with transaction rollback and error logging. diff --git a/backend/finance/service/invoices/single/create/create.py b/backend/finance/service/invoices/single/create/create.py index fd6e178a8..e16be54dc 100644 --- a/backend/finance/service/invoices/single/create/create.py +++ b/backend/finance/service/invoices/single/create/create.py @@ -3,7 +3,7 @@ from django.contrib import messages from django.core.exceptions import PermissionDenied, ValidationError -from backend.finance.models import Invoice, InvoiceItem, Client, InvoiceProduct, DefaultValues +from backend.models import Invoice, InvoiceItem, Client, InvoiceProduct, FinanceDefaultValues # from backend.models import QuotaUsage from backend.finance.service.clients.validate import validate_client @@ -18,7 +18,7 @@ def get_invoice_context(request: WebRequest) -> dict: "existing_products": InvoiceProduct.objects.filter(user=request.user), } - defaults: DefaultValues + defaults: FinanceDefaultValues if client_id := request.GET.get("client"): try: diff --git a/backend/finance/views/invoices/recurring/create.py b/backend/finance/views/invoices/recurring/create.py index c0e215362..d6bcdfdef 100644 --- a/backend/finance/views/invoices/recurring/create.py +++ b/backend/finance/views/invoices/recurring/create.py @@ -3,7 +3,7 @@ from django.views.decorators.http import require_http_methods from backend.finance.models import InvoiceRecurringProfile -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.boto3.handler import BOTO3_HANDLER from backend.boto3.async_tasks.tasks import Task from backend.boto3.scheduler.create_schedule import create_boto_schedule diff --git a/backend/finance/views/invoices/recurring/dashboard.py b/backend/finance/views/invoices/recurring/dashboard.py index 04bf31d2f..564c8ae89 100644 --- a/backend/finance/views/invoices/recurring/dashboard.py +++ b/backend/finance/views/invoices/recurring/dashboard.py @@ -1,6 +1,6 @@ from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes, has_entitlements +from core.decorators import web_require_scopes, has_entitlements from core.types.requests import WebRequest from backend.finance.views.invoices.handler import invoices_core_handler diff --git a/backend/finance/views/invoices/recurring/edit.py b/backend/finance/views/invoices/recurring/edit.py index 026fcb8e8..496239b63 100644 --- a/backend/finance/views/invoices/recurring/edit.py +++ b/backend/finance/views/invoices/recurring/edit.py @@ -2,7 +2,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes, has_entitlements +from core.decorators import web_require_scopes, has_entitlements from backend.finance.models import InvoiceRecurringProfile from backend.finance.service.invoices.recurring.get import get_invoice_profile from backend.finance.views.invoices.handler import invoices_core_handler @@ -71,7 +71,7 @@ def invoice_edit_page_endpoint(request, invoice_profile_id): if get_response.failed: messages.error(request, get_response.error_message) - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") invoice_profile: InvoiceRecurringProfile = get_response.response diff --git a/backend/finance/views/invoices/recurring/overview.py b/backend/finance/views/invoices/recurring/overview.py index ec99f9ea0..ff5e77785 100644 --- a/backend/finance/views/invoices/recurring/overview.py +++ b/backend/finance/views/invoices/recurring/overview.py @@ -1,4 +1,4 @@ -from backend.decorators import * +from core.decorators import * from backend.models import * from backend.finance.service.defaults.get import get_account_defaults from backend.finance.views.invoices.handler import invoices_core_handler diff --git a/backend/finance/views/invoices/single/create.py b/backend/finance/views/invoices/single/create.py index 631bf675d..abe5d5ad0 100644 --- a/backend/finance/views/invoices/single/create.py +++ b/backend/finance/views/invoices/single/create.py @@ -1,7 +1,7 @@ from django.shortcuts import redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes, has_entitlements +from core.decorators import web_require_scopes, has_entitlements from backend.finance.service.invoices.single.create.create import create_invoice_items, save_invoice from backend.finance.service.invoices.single.create.get_page import get_invoice_context from core.types.requests import WebRequest diff --git a/backend/finance/views/invoices/single/dashboard.py b/backend/finance/views/invoices/single/dashboard.py index 27a08887d..7f1badbc7 100644 --- a/backend/finance/views/invoices/single/dashboard.py +++ b/backend/finance/views/invoices/single/dashboard.py @@ -2,7 +2,7 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice from core.types.requests import WebRequest from backend.finance.views.invoices.handler import invoices_core_handler diff --git a/backend/finance/views/invoices/single/edit.py b/backend/finance/views/invoices/single/edit.py index f6d702d88..80d6790f9 100644 --- a/backend/finance/views/invoices/single/edit.py +++ b/backend/finance/views/invoices/single/edit.py @@ -6,7 +6,7 @@ from django.views.decorators.http import require_http_methods from core.types.requests import WebRequest -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice, Client, InvoiceItem from core.types.htmx import HtmxHttpRequest @@ -140,7 +140,8 @@ def edit_invoice(request: WebRequest, invoice_id): "client_city": request.POST.get("to_city"), "client_county": request.POST.get("to_county"), "client_country": request.POST.get("to_country"), - "client_is_representative": True if request.POST.get("is_representative") == "on" else False, # type: ignore[dict-item] + "client_is_representative": True if request.POST.get("is_representative") == "on" else False, + # type: ignore[dict-item] "client_to": None, } ) @@ -166,7 +167,7 @@ def edit_invoice(request: WebRequest, invoice_id): messages.success(request, "Invoice edited") if request.htmx: - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") return invoice_edit_page_get(request, invoice_id) diff --git a/backend/finance/views/invoices/single/manage_access.py b/backend/finance/views/invoices/single/manage_access.py index a6e40af77..dc1d70fe4 100644 --- a/backend/finance/views/invoices/single/manage_access.py +++ b/backend/finance/views/invoices/single/manage_access.py @@ -2,7 +2,7 @@ from django.http import HttpResponse from django.shortcuts import redirect, render -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice, InvoiceURL from backend.finance.service.invoices.single.get_invoice import get_invoice_by_actor from core.types.htmx import HtmxHttpRequest @@ -65,7 +65,7 @@ def delete_code(request: HtmxHttpRequest, code): raise InvoiceURL.DoesNotExist except (Invoice.DoesNotExist, InvoiceURL.DoesNotExist): messages.error(request, "Invalid URL") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") # QuotaLimit.delete_quota_usage("invoices-access_codes", request.user, invoice.id, code_obj.created_on) diff --git a/backend/finance/views/invoices/single/overview.py b/backend/finance/views/invoices/single/overview.py index f7ca164ac..90b509df1 100644 --- a/backend/finance/views/invoices/single/overview.py +++ b/backend/finance/views/invoices/single/overview.py @@ -1,6 +1,6 @@ from urllib.parse import urlencode -from backend.decorators import * +from core.decorators import * from backend.models import * from backend.finance.views.invoices.handler import invoices_core_handler diff --git a/backend/finance/views/invoices/single/schedule.py b/backend/finance/views/invoices/single/schedule.py index 39317965c..e4777d67c 100644 --- a/backend/finance/views/invoices/single/schedule.py +++ b/backend/finance/views/invoices/single/schedule.py @@ -2,7 +2,7 @@ # from django.http import HttpResponse # from django.shortcuts import render, redirect # -# from backend.decorators import feature_flag_check, web_require_scopes +# from core.decorators import feature_flag_check, web_require_scopes # from backend.finance.models import Invoice, QuotaLimit # from backend.types.htmx import HtmxHttpRequest # diff --git a/backend/finance/views/invoices/single/view.py b/backend/finance/views/invoices/single/view.py index db49d9a68..3f9eec81f 100644 --- a/backend/finance/views/invoices/single/view.py +++ b/backend/finance/views/invoices/single/view.py @@ -7,7 +7,7 @@ from login_required import login_not_required -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from backend.finance.models import Invoice, InvoiceURL from core.types.htmx import HtmxHttpRequest diff --git a/backend/finance/views/receipts/dashboard.py b/backend/finance/views/receipts/dashboard.py index 23cf1510b..47109b46d 100644 --- a/backend/finance/views/receipts/dashboard.py +++ b/backend/finance/views/receipts/dashboard.py @@ -1,7 +1,7 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import render -from backend.decorators import web_require_scopes +from core.decorators import web_require_scopes from core.types.htmx import HtmxHttpRequest diff --git a/backend/middleware.py b/backend/middleware.py deleted file mode 100644 index 43bb6158c..000000000 --- a/backend/middleware.py +++ /dev/null @@ -1,74 +0,0 @@ -from django.contrib.auth.models import AnonymousUser -from django.utils.deprecation import MiddlewareMixin -from django.contrib.auth import get_user -from django.db import connection, OperationalError -from django.http import HttpResponse - -from backend.models import User -from core.types.htmx import HtmxAnyHttpRequest -from core.types.requests import WebRequest - - -class HealthCheckMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - if request.path == "/api/hc/healthcheck/": - try: - status = connection.ensure_connection() - except OperationalError: - status = "error" - - if not status: # good - return HttpResponse(status=200, content="All operations are up and running!") - return HttpResponse(status=503, content="Service Unavailable") - return self.get_response(request) - - -class HTMXPartialLoadMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request: HtmxAnyHttpRequest): - response: HttpResponse = self.get_response(request) - - if hasattr(response, "retarget"): - response.headers["HX-Retarget"] = response.retarget - elif request.htmx.boosted and not response.headers.get("HX-Retarget") and not hasattr(response, "no_retarget"): - response.headers["HX-Retarget"] = "#main_content" - response.headers["HX-Reswap"] = "innerHTML" - # if 'data-layout="breadcrumbs"' not in str(response.content): - response.headers["HX-Trigger"] = "update_breadcrumbs" - return response - - -class LastVisitedMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - if request.method == "GET" and "text/html" in request.headers.get("Accept", ""): - try: - request.session["last_visited"] = request.session["currently_visiting"] - except KeyError: - pass - current_url = request.build_absolute_uri() - request.session["currently_visiting"] = current_url - return self.get_response(request) - - -class CustomUserMiddleware(MiddlewareMixin): - def process_request(self, request: WebRequest): - user = get_user(request) - - # Replace request.user with CustomUser instance if authenticated - if user.is_authenticated: - request.user = User.objects.get(pk=user.pk) - request.team = request.user.logged_in_as_team or None - request.team_id = request.team.id if request.team else None - request.actor = request.team or request.user - else: - # If user is not authenticated, set request.user to AnonymousUser - request.user = AnonymousUser() # type: ignore[assignment] - request.actor = request.user diff --git a/backend/migrations/0001_initial.py b/backend/migrations/0001_initial.py index a7107828c..02e7fd0e9 100644 --- a/backend/migrations/0001_initial.py +++ b/backend/migrations/0001_initial.py @@ -1,11 +1,13 @@ -# Generated by Django 5.1.4 on 2024-12-21 22:26 +# Generated by Django 5.1.4 on 2025-01-01 17:51 import backend.data.default_email_templates import core.models import django.core.validators +import django.db.models.deletion import django.db.models.manager import shortuuid.django_fields import uuid +from django.conf import settings from django.db import migrations, models @@ -13,55 +15,60 @@ class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [ + ("core", "0002_client"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] operations = [ migrations.CreateModel( - name="Client", + name="InvoiceItem", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=50)), + ("description", models.CharField(max_length=100)), + ("is_service", models.BooleanField(default=True)), + ("hours", models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), + ("price_per_hour", models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), + ("price", models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), + ], + ), + migrations.CreateModel( + name="FileStorageFile", fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("file", models.FileField(storage=core.models._private_storage, upload_to=core.models.upload_to_user_separate_folder)), + ("file_uri_path", models.CharField(max_length=500)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", + "last_edited_by", + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="files_edited", + to=settings.AUTH_USER_MODEL, ), ), - ("active", models.BooleanField(default=True)), - ("name", models.CharField(max_length=64)), ( - "phone_number", - models.CharField(blank=True, max_length=100, null=True), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), - ("email", models.EmailField(blank=True, max_length=254, null=True)), - ("email_verified", models.BooleanField(default=False)), - ("company", models.CharField(blank=True, max_length=100, null=True)), ( - "contact_method", - models.CharField(blank=True, max_length=100, null=True), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), - ("is_representative", models.BooleanField(default=False)), - ("address", models.TextField(blank=True, max_length=100, null=True)), - ("city", models.CharField(blank=True, max_length=100, null=True)), - ("country", models.CharField(blank=True, max_length=100, null=True)), ], options={ "abstract": False, }, ), migrations.CreateModel( - name="DefaultValues", + name="FinanceDefaultValues", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ( "currency", models.CharField( @@ -82,11 +89,7 @@ class Migration(migrations.Migration): ( "invoice_due_date_type", models.CharField( - choices=[ - ("days_after", "Days After"), - ("date_following", "Date Following"), - ("date_current", "Date Current"), - ], + choices=[("days_after", "Days After"), ("date_following", "Date Following"), ("date_current", "Date Current")], default="days_after", max_length=20, ), @@ -95,54 +98,19 @@ class Migration(migrations.Migration): ( "invoice_date_type", models.CharField( - choices=[ - ("day_of_month", "Day Of Month"), - ("days_after", "Days After"), - ], - default="day_of_month", - max_length=20, + choices=[("day_of_month", "Day Of Month"), ("days_after", "Days After")], default="day_of_month", max_length=20 ), ), - ( - "invoice_from_name", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_from_company", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_from_address", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_from_city", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_from_county", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_from_country", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_from_email", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_account_number", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_sort_code", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "invoice_account_holder_name", - models.CharField(blank=True, max_length=100, null=True), - ), + ("invoice_from_name", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_from_company", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_from_address", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_from_city", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_from_county", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_from_country", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_from_email", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_account_number", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_sort_code", models.CharField(blank=True, max_length=100, null=True)), + ("invoice_account_holder_name", models.CharField(blank=True, max_length=100, null=True)), ( "email_template_recurring_invoices_invoice_created", models.TextField( @@ -163,123 +131,87 @@ class Migration(migrations.Migration): ), ( "default_invoice_logo", - models.ImageField( - blank=True, - null=True, - storage=core.models._private_storage, - upload_to="invoice_logos/", + models.ImageField(blank=True, null=True, storage=core.models._private_storage, upload_to="invoice_logos/"), + ), + ( + "client", + models.OneToOneField( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="default_values", to="core.client" ), ), + ( + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), + ), + ( + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), ], - options={ - "abstract": False, - }, ), migrations.CreateModel( - name="FileStorageFile", + name="InvoiceProduct", fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=50)), + ("description", models.CharField(max_length=100)), + ("quantity", models.IntegerField()), + ("rate", models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), ( - "file", - models.FileField( - storage=core.models._private_storage, - upload_to=core.models.upload_to_user_separate_folder, - ), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), - ("file_uri_path", models.CharField(max_length=500)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), ], options={ "abstract": False, }, ), migrations.CreateModel( - name="Invoice", + name="InvoiceRecurringProfile", fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("boto_schedule_arn", models.CharField(blank=True, max_length=2048, null=True)), + ("boto_schedule_uuid", models.UUIDField(blank=True, default=None, null=True)), + ("boto_last_updated", models.DateTimeField(auto_now=True)), + ("received", models.BooleanField(default=False)), ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", + "boto_schedule_status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("creating", "Creating"), + ("completed", "Completed"), + ("failed", "Failed"), + ("deleting", "Deleting"), + ("cancelled", "Cancelled"), + ], + default="pending", + max_length=100, ), ), - ( - "client_name", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_email", - models.EmailField(blank=True, max_length=254, null=True), - ), - ( - "client_company", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_address", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_city", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_county", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_country", - models.CharField(blank=True, max_length=100, null=True), - ), + ("client_name", models.CharField(blank=True, max_length=100, null=True)), + ("client_email", models.EmailField(blank=True, max_length=254, null=True)), + ("client_company", models.CharField(blank=True, max_length=100, null=True)), + ("client_address", models.CharField(blank=True, max_length=100, null=True)), + ("client_city", models.CharField(blank=True, max_length=100, null=True)), + ("client_county", models.CharField(blank=True, max_length=100, null=True)), + ("client_country", models.CharField(blank=True, max_length=100, null=True)), ("client_is_representative", models.BooleanField(default=False)), ("self_name", models.CharField(blank=True, max_length=100, null=True)), - ( - "self_company", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "self_address", - models.CharField(blank=True, max_length=100, null=True), - ), + ("self_company", models.CharField(blank=True, max_length=100, null=True)), + ("self_address", models.CharField(blank=True, max_length=100, null=True)), ("self_city", models.CharField(blank=True, max_length=100, null=True)), - ( - "self_county", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "self_country", - models.CharField(blank=True, max_length=100, null=True), - ), + ("self_county", models.CharField(blank=True, max_length=100, null=True)), + ("self_country", models.CharField(blank=True, max_length=100, null=True)), ("sort_code", models.CharField(blank=True, max_length=8, null=True)), - ( - "account_holder_name", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "account_number", - models.CharField(blank=True, max_length=100, null=True), - ), + ("account_holder_name", models.CharField(blank=True, max_length=100, null=True)), + ("account_number", models.CharField(blank=True, max_length=100, null=True)), ("vat_number", models.CharField(blank=True, max_length=100, null=True)), - ( - "logo", - models.ImageField( - blank=True, - null=True, - storage=core.models._private_storage, - upload_to="invoice_logos", - ), - ), + ("logo", models.ImageField(blank=True, null=True, storage=core.models._private_storage, upload_to="invoice_logos")), ("notes", models.TextField(blank=True, null=True)), ( "currency", @@ -299,197 +231,74 @@ class Migration(migrations.Migration): ), ("date_created", models.DateTimeField(auto_now_add=True)), ("date_issued", models.DateField(blank=True, null=True)), - ( - "discount_amount", - models.DecimalField(decimal_places=2, default=0, max_digits=15), - ), + ("discount_amount", models.DecimalField(decimal_places=2, default=0, max_digits=15)), ( "discount_percentage", models.DecimalField( - decimal_places=2, - default=0, - max_digits=5, - validators=[django.core.validators.MaxValueValidator(100)], + decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MaxValueValidator(100)] ), ), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("reference", models.CharField(blank=True, max_length=16, null=True)), - ("date_due", models.DateField()), + ("active", models.BooleanField(default=True)), ( "status", models.CharField( - choices=[ - ("draft", "Draft"), - ("pending", "Pending"), - ("paid", "Paid"), - ], - default="draft", - max_length=10, + choices=[("ongoing", "Ongoing"), ("paused", "paused"), ("cancelled", "cancelled")], default="paused", max_length=10 ), ), - ("status_updated_at", models.DateTimeField(auto_now_add=True)), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="InvoiceItem", - fields=[ ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", + "frequency", + models.CharField( + choices=[("weekly", "Weekly"), ("monthly", "Monthly"), ("yearly", "Yearly")], default="monthly", max_length=20 ), ), - ("name", models.CharField(max_length=50)), - ("description", models.CharField(max_length=100)), - ("is_service", models.BooleanField(default=True)), - ( - "hours", - models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True), - ), - ( - "price_per_hour", - models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True), - ), - ( - "price", - models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True), - ), - ], - ), - migrations.CreateModel( - name="InvoiceProduct", - fields=[ + ("end_date", models.DateField(blank=True, null=True)), + ("due_after_days", models.PositiveSmallIntegerField(default=7)), + ("day_of_week", models.PositiveSmallIntegerField(blank=True, null=True)), + ("day_of_month", models.PositiveSmallIntegerField(blank=True, null=True)), + ("month_of_year", models.PositiveSmallIntegerField(blank=True, null=True)), + ("client_to", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="core.client")), + ("items", models.ManyToManyField(blank=True, to="backend.invoiceitem")), ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), - ("name", models.CharField(max_length=50)), - ("description", models.CharField(max_length=100)), - ("quantity", models.IntegerField()), ( - "rate", - models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), ], options={ "abstract": False, }, + managers=[ + ("with_items", django.db.models.manager.Manager()), + ], ), migrations.CreateModel( - name="InvoiceRecurringProfile", + name="Invoice", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "boto_schedule_arn", - models.CharField(blank=True, max_length=2048, null=True), - ), - ( - "boto_schedule_uuid", - models.UUIDField(blank=True, default=None, null=True), - ), - ("boto_last_updated", models.DateTimeField(auto_now=True)), - ("received", models.BooleanField(default=False)), - ( - "boto_schedule_status", - models.CharField( - choices=[ - ("pending", "Pending"), - ("creating", "Creating"), - ("completed", "Completed"), - ("failed", "Failed"), - ("deleting", "Deleting"), - ("cancelled", "Cancelled"), - ], - default="pending", - max_length=100, - ), - ), - ( - "client_name", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_email", - models.EmailField(blank=True, max_length=254, null=True), - ), - ( - "client_company", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_address", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_city", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_county", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "client_country", - models.CharField(blank=True, max_length=100, null=True), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("client_name", models.CharField(blank=True, max_length=100, null=True)), + ("client_email", models.EmailField(blank=True, max_length=254, null=True)), + ("client_company", models.CharField(blank=True, max_length=100, null=True)), + ("client_address", models.CharField(blank=True, max_length=100, null=True)), + ("client_city", models.CharField(blank=True, max_length=100, null=True)), + ("client_county", models.CharField(blank=True, max_length=100, null=True)), + ("client_country", models.CharField(blank=True, max_length=100, null=True)), ("client_is_representative", models.BooleanField(default=False)), ("self_name", models.CharField(blank=True, max_length=100, null=True)), - ( - "self_company", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "self_address", - models.CharField(blank=True, max_length=100, null=True), - ), + ("self_company", models.CharField(blank=True, max_length=100, null=True)), + ("self_address", models.CharField(blank=True, max_length=100, null=True)), ("self_city", models.CharField(blank=True, max_length=100, null=True)), - ( - "self_county", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "self_country", - models.CharField(blank=True, max_length=100, null=True), - ), + ("self_county", models.CharField(blank=True, max_length=100, null=True)), + ("self_country", models.CharField(blank=True, max_length=100, null=True)), ("sort_code", models.CharField(blank=True, max_length=8, null=True)), - ( - "account_holder_name", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "account_number", - models.CharField(blank=True, max_length=100, null=True), - ), + ("account_holder_name", models.CharField(blank=True, max_length=100, null=True)), + ("account_number", models.CharField(blank=True, max_length=100, null=True)), ("vat_number", models.CharField(blank=True, max_length=100, null=True)), - ( - "logo", - models.ImageField( - blank=True, - null=True, - storage=core.models._private_storage, - upload_to="invoice_logos", - ), - ), + ("logo", models.ImageField(blank=True, null=True, storage=core.models._private_storage, upload_to="invoice_logos")), ("notes", models.TextField(blank=True, null=True)), ( "currency", @@ -509,89 +318,56 @@ class Migration(migrations.Migration): ), ("date_created", models.DateTimeField(auto_now_add=True)), ("date_issued", models.DateField(blank=True, null=True)), - ( - "discount_amount", - models.DecimalField(decimal_places=2, default=0, max_digits=15), - ), + ("discount_amount", models.DecimalField(decimal_places=2, default=0, max_digits=15)), ( "discount_percentage", models.DecimalField( - decimal_places=2, - default=0, - max_digits=5, - validators=[django.core.validators.MaxValueValidator(100)], + decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MaxValueValidator(100)] ), ), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("active", models.BooleanField(default=True)), + ("reference", models.CharField(blank=True, max_length=16, null=True)), + ("date_due", models.DateField()), ( "status", models.CharField( - choices=[ - ("ongoing", "Ongoing"), - ("paused", "paused"), - ("cancelled", "cancelled"), - ], - default="paused", - max_length=10, + choices=[("draft", "Draft"), ("pending", "Pending"), ("paid", "Paid")], default="draft", max_length=10 ), ), + ("status_updated_at", models.DateTimeField(auto_now_add=True)), + ("client_to", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="core.client")), ( - "frequency", - models.CharField( - choices=[ - ("weekly", "Weekly"), - ("monthly", "Monthly"), - ("yearly", "Yearly"), - ], - default="monthly", - max_length=20, - ), - ), - ("end_date", models.DateField(blank=True, null=True)), - ("due_after_days", models.PositiveSmallIntegerField(default=7)), - ( - "day_of_week", - models.PositiveSmallIntegerField(blank=True, null=True), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), ( - "day_of_month", - models.PositiveSmallIntegerField(blank=True, null=True), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), + ("items", models.ManyToManyField(blank=True, to="backend.invoiceitem")), ( - "month_of_year", - models.PositiveSmallIntegerField(blank=True, null=True), + "invoice_recurring_profile", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="generated_invoices", + to="backend.invoicerecurringprofile", + ), ), ], options={ "abstract": False, }, - managers=[ - ("with_items", django.db.models.manager.Manager()), - ], ), migrations.CreateModel( name="InvoiceReminder", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "boto_schedule_arn", - models.CharField(blank=True, max_length=2048, null=True), - ), - ( - "boto_schedule_uuid", - models.UUIDField(blank=True, default=None, null=True), - ), + ("boto_schedule_arn", models.CharField(blank=True, max_length=2048, null=True)), + ("boto_schedule_uuid", models.UUIDField(blank=True, default=None, null=True)), ("boto_last_updated", models.DateTimeField(auto_now=True)), ("received", models.BooleanField(default=False)), ( @@ -613,15 +389,15 @@ class Migration(migrations.Migration): ( "reminder_type", models.CharField( - choices=[ - ("before_due", "Before Due"), - ("after_due", "After Due"), - ("on_overdue", "On Overdue"), - ], + choices=[("before_due", "Before Due"), ("after_due", "After Due"), ("on_overdue", "On Overdue")], default="before_due", max_length=100, ), ), + ( + "invoice", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="invoice_reminders", to="backend.invoice"), + ), ], options={ "verbose_name": "Invoice Reminder", @@ -631,68 +407,56 @@ class Migration(migrations.Migration): migrations.CreateModel( name="InvoiceURL", fields=[ - ( - "expires", - models.DateTimeField( - blank=True, - help_text="When the item will expire", - null=True, - verbose_name="Expires", - ), - ), + ("expires", models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires")), ("active", models.BooleanField(default=True)), ( "uuid", shortuuid.django_fields.ShortUUIDField( - alphabet=None, - length=8, - max_length=8, - prefix="", - primary_key=True, - serialize=False, + alphabet=None, length=8, max_length=8, prefix="", primary_key=True, serialize=False ), ), ("system_created", models.BooleanField(default=False)), ("created_on", models.DateTimeField(auto_now_add=True)), + ( + "created_by", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ( + "invoice", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="invoice_urls", to="backend.invoice"), + ), ], options={ "verbose_name": "Invoice URL", "verbose_name_plural": "Invoice URLs", }, ), + migrations.CreateModel( + name="MonthlyReportRow", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("date", models.DateField()), + ("reference_number", models.CharField(max_length=100)), + ("item_type", models.CharField(max_length=100)), + ("client_name", models.CharField(blank=True, max_length=64, null=True)), + ("paid_in", models.DecimalField(decimal_places=2, default=0, max_digits=15)), + ("paid_out", models.DecimalField(decimal_places=2, default=0, max_digits=15)), + ("client", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.client")), + ], + ), migrations.CreateModel( name="MonthlyReport", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "uuid", - models.UUIDField(default=uuid.uuid4, editable=False, unique=True), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), ("name", models.CharField(blank=True, max_length=100, null=True)), - ( - "profit", - models.DecimalField(decimal_places=2, default=0, max_digits=15), - ), + ("profit", models.DecimalField(decimal_places=2, default=0, max_digits=15)), ("invoices_sent", models.PositiveIntegerField(default=0)), ("start_date", models.DateField()), ("end_date", models.DateField()), ("recurring_customers", models.PositiveIntegerField(default=0)), - ( - "payments_in", - models.DecimalField(decimal_places=2, default=0, max_digits=15), - ), - ( - "payments_out", - models.DecimalField(decimal_places=2, default=0, max_digits=15), - ), + ("payments_in", models.DecimalField(decimal_places=2, default=0, max_digits=15)), + ("payments_out", models.DecimalField(decimal_places=2, default=0, max_digits=15)), ( "currency", models.CharField( @@ -709,58 +473,36 @@ class Migration(migrations.Migration): max_length=3, ), ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="MonthlyReportRow", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("date", models.DateField()), - ("reference_number", models.CharField(max_length=100)), - ("item_type", models.CharField(max_length=100)), - ("client_name", models.CharField(blank=True, max_length=64, null=True)), ( - "paid_in", - models.DecimalField(decimal_places=2, default=0, max_digits=15), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), ( - "paid_out", - models.DecimalField(decimal_places=2, default=0, max_digits=15), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), + ("items", models.ManyToManyField(blank=True, to="backend.monthlyreportrow")), ], + options={ + "abstract": False, + }, ), migrations.CreateModel( name="MultiFileUpload", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("started_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), + ("finished_at", models.DateTimeField(blank=True, editable=False, null=True)), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ("files", models.ManyToManyField(related_name="multi_file_uploads", to="backend.filestoragefile")), ( - "finished_at", - models.DateTimeField(blank=True, editable=False, null=True), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), ( - "uuid", - models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), ], options={ @@ -770,31 +512,22 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Receipt", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("name", models.CharField(max_length=100)), - ( - "image", - models.ImageField(storage=core.models._private_storage, upload_to="receipts"), - ), + ("image", models.ImageField(storage=core.models._private_storage, upload_to="receipts")), ("total_price", models.FloatField(blank=True, null=True)), ("date", models.DateField(blank=True, null=True)), ("date_uploaded", models.DateTimeField(auto_now_add=True)), ("receipt_parsed", models.JSONField(blank=True, null=True)), + ("merchant_store", models.CharField(blank=True, max_length=255, null=True)), + ("purchase_category", models.CharField(blank=True, max_length=200, null=True)), ( - "merchant_store", - models.CharField(blank=True, max_length=255, null=True), + "organization", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="core.organization"), ), ( - "purchase_category", - models.CharField(blank=True, max_length=200, null=True), + "user", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), ], options={ @@ -804,19 +537,98 @@ class Migration(migrations.Migration): migrations.CreateModel( name="ReceiptDownloadToken", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "token", - models.UUIDField(default=uuid.uuid4, editable=False, unique=True), - ), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("token", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ("file", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="backend.receipt")), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), + migrations.AddConstraint( + model_name="filestoragefile", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_filestoragefile_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="financedefaultvalues", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_financedefaultvalues_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="invoiceproduct", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_invoiceproduct_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="invoicerecurringprofile", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_invoicerecurringprofile_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="invoice", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_invoice_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="monthlyreport", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_monthlyreport_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="multifileupload", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_multifileupload_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="receipt", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="backend_receipt_check_user_or_organization", + ), + ), ] diff --git a/backend/migrations/0002_initial.py b/backend/migrations/0002_initial.py deleted file mode 100644 index f121c1a32..000000000 --- a/backend/migrations/0002_initial.py +++ /dev/null @@ -1,420 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-21 22:26 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ("backend", "0001_initial"), - ("core", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name="client", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="client", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="defaultvalues", - name="client", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="default_values", - to="backend.client", - ), - ), - migrations.AddField( - model_name="defaultvalues", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="defaultvalues", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="filestoragefile", - name="last_edited_by", - field=models.ForeignKey( - blank=True, - editable=False, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="files_edited", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="filestoragefile", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="filestoragefile", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="invoice", - name="client_to", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="backend.client", - ), - ), - migrations.AddField( - model_name="invoice", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="invoice", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="invoice", - name="items", - field=models.ManyToManyField(blank=True, to="backend.invoiceitem"), - ), - migrations.AddField( - model_name="invoiceproduct", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="invoiceproduct", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="invoicerecurringprofile", - name="client_to", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="backend.client", - ), - ), - migrations.AddField( - model_name="invoicerecurringprofile", - name="items", - field=models.ManyToManyField(blank=True, to="backend.invoiceitem"), - ), - migrations.AddField( - model_name="invoicerecurringprofile", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="invoicerecurringprofile", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="invoice", - name="invoice_recurring_profile", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="generated_invoices", - to="backend.invoicerecurringprofile", - ), - ), - migrations.AddField( - model_name="invoicereminder", - name="invoice", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="invoice_reminders", - to="backend.invoice", - ), - ), - migrations.AddField( - model_name="invoiceurl", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="invoiceurl", - name="invoice", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="invoice_urls", - to="backend.invoice", - ), - ), - migrations.AddField( - model_name="monthlyreport", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="monthlyreport", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="monthlyreportrow", - name="client", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="backend.client", - ), - ), - migrations.AddField( - model_name="monthlyreport", - name="items", - field=models.ManyToManyField(blank=True, to="backend.monthlyreportrow"), - ), - migrations.AddField( - model_name="multifileupload", - name="files", - field=models.ManyToManyField(related_name="multi_file_uploads", to="backend.filestoragefile"), - ), - migrations.AddField( - model_name="multifileupload", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="multifileupload", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="receipt", - name="organization", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="core.organization", - ), - ), - migrations.AddField( - model_name="receipt", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="receiptdownloadtoken", - name="file", - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="backend.receipt"), - ), - migrations.AddField( - model_name="receiptdownloadtoken", - name="user", - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddConstraint( - model_name="client", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_client_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="defaultvalues", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_defaultvalues_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="filestoragefile", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_filestoragefile_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="invoiceproduct", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_invoiceproduct_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="invoicerecurringprofile", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_invoicerecurringprofile_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="invoice", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_invoice_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="monthlyreport", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_monthlyreport_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="multifileupload", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_multifileupload_check_user_or_organization", - ), - ), - migrations.AddConstraint( - model_name="receipt", - constraint=models.CheckConstraint( - condition=models.Q( - models.Q(("organization__isnull", False), ("user__isnull", True)), - models.Q(("organization__isnull", True), ("user__isnull", False)), - _connector="OR", - ), - name="backend_receipt_check_user_or_organization", - ), - ), - ] diff --git a/backend/modals.py b/backend/modals.py index 53c56a8e6..1b469f30d 100644 --- a/backend/modals.py +++ b/backend/modals.py @@ -36,7 +36,7 @@ class SendSingleEmailModal(Modal, EmailContext): def get(self, request: WebRequest): if not get_feature_status("areUserEmailsAllowed"): messages.error(request, "Emails are disabled") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") context = self.get_context(request) @@ -45,7 +45,7 @@ def get(self, request: WebRequest): if not invoice_url or not invoice_url.invoice.has_access(request.user): messages.error(request, "You don't have access to this invoice") - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) context["invoice"] = invoice_url.invoice context["selected_clients"] = [ @@ -224,7 +224,7 @@ def get(self, request: WebRequest): context["invoice"] = invoice else: messages.error(request, "You don't have access to this invoice") - return render(request, "base/toasts.html") + return render(request, "core/base/toasts.html") except Invoice.DoesNotExist: return self.Response(request, context) @@ -235,7 +235,7 @@ class SendEmailContext: def get_context(self, request: WebRequest) -> dict: if not get_feature_status("areUserEmailsAllowed"): messages.error(request, "Emails are disabled") - return render(request, "base/toast.html") + return render(request, "core/base/toast.html") context = {} @@ -259,7 +259,7 @@ def get(self, request: WebRequest): if not invoice_url or not invoice_url.invoice.has_access(request.user): messages.error(request, "You don't have access to this invoice") - return render(request, "base/toast.html", {"autohide": False}) + return render(request, "core/base/toast.html", {"autohide": False}) context["invoice"] = invoice_url.invoice context["selected_clients"] = [ diff --git a/backend/models.py b/backend/models.py index fcc2196aa..233555ea8 100644 --- a/backend/models.py +++ b/backend/models.py @@ -23,6 +23,8 @@ _public_storage, upload_to_user_separate_folder, RandomAPICode, + Client, + DefaultValuesBase, ) from backend.finance.models import ( @@ -36,8 +38,7 @@ ReceiptDownloadToken, MonthlyReport, MonthlyReportRow, + FinanceDefaultValues, ) -from backend.clients.models import Client, DefaultValues - from backend.storage.models import FileStorageFile, MultiFileUpload diff --git a/backend/storage/api/delete.py b/backend/storage/api/delete.py index 822a562ec..262be1105 100644 --- a/backend/storage/api/delete.py +++ b/backend/storage/api/delete.py @@ -4,7 +4,7 @@ from django.shortcuts import render from django.views.decorators.http import require_http_methods -from backend.decorators import htmx_only +from core.decorators import htmx_only from backend.models import FileStorageFile from core.types.requests import WebRequest @@ -31,7 +31,7 @@ def recursive_file_delete_endpoint(request: WebRequest) -> HttpResponse: if failed_files: messages.error(request, f"Failed to delete: {', '.join([file for file in failed_files])}") - resp = render(request, "base/toast.html") + resp = render(request, "core/base/toast.html") else: resp = HttpResponse(status=200) diff --git a/backend/storage/api/fetch.py b/backend/storage/api/fetch.py index efdf23125..d6f6915e4 100644 --- a/backend/storage/api/fetch.py +++ b/backend/storage/api/fetch.py @@ -1,7 +1,7 @@ from django.utils.html import escape from django.views.decorators.http import require_GET -from backend.decorators import htmx_only +from core.decorators import htmx_only from backend.models import FileStorageFile # from backend.core.service.billing.calculate.test import generate_monthly_billing_summary diff --git a/frontend/templates/base/htmx.html b/frontend/templates/base/htmx.html deleted file mode 100644 index 5d4b7d1a3..000000000 --- a/frontend/templates/base/htmx.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "core/base/htmx.html" %} -{##} -{% block left_drawer %} - {% include "base/+left_drawer.html" with swap=True %} -{% endblock %} -{#{% include "base/breadcrumbs.html" with swap=True %}#} -{#{% include "base/toasts.html" %}#} diff --git a/frontend/templates/core/auth/auth.html b/frontend/templates/core/auth/auth.html index 1fda5b9d3..170e420ff 100644 --- a/frontend/templates/core/auth/auth.html +++ b/frontend/templates/core/auth/auth.html @@ -1,6 +1,6 @@ {% extends "core/auth/auth.html" %} {% block head %} - {% include 'base/_head.html' %} + {% include 'core/base/_head.html' %} {% endblock %} {% block details %}

What do you get to manage?

diff --git a/frontend/templates/base/+left_drawer.html b/frontend/templates/core/base/+left_drawer.html similarity index 100% rename from frontend/templates/base/+left_drawer.html rename to frontend/templates/core/base/+left_drawer.html diff --git a/frontend/templates/base/_head.html b/frontend/templates/core/base/_head.html similarity index 100% rename from frontend/templates/base/_head.html rename to frontend/templates/core/base/_head.html diff --git a/frontend/templates/base/base.html b/frontend/templates/core/base/base.html similarity index 53% rename from frontend/templates/base/base.html rename to frontend/templates/core/base/base.html index 62456e1af..7a46e8c7a 100644 --- a/frontend/templates/base/base.html +++ b/frontend/templates/core/base/base.html @@ -1,7 +1,7 @@ {% extends "core/base/base.html" %} {% block topbar %} - {% include 'base/topbar/_topbar.html' %} + {% include 'core/base/topbar/_topbar.html' %} {% endblock %} {% block drawer %} - {% include "base/+left_drawer.html" %} + {% include "core/base/+left_drawer.html" %} {% endblock drawer %} diff --git a/frontend/templates/base/breadcrumbs.html b/frontend/templates/core/base/breadcrumbs.html similarity index 90% rename from frontend/templates/base/breadcrumbs.html rename to frontend/templates/core/base/breadcrumbs.html index f35fa4ab7..e8c55a02d 100644 --- a/frontend/templates/base/breadcrumbs.html +++ b/frontend/templates/core/base/breadcrumbs.html @@ -8,7 +8,7 @@ hx-swap="innerHTML" {% if swap %}hx-swap-oob='outerHTML:div[data-layout="breadcrumbs"]'{% endif %}> {% if breadcrumb %} - {% include "base/breadcrumbs_ul.html" %} + {% include "core/base/breadcrumbs_ul.html" %} {% endif %} {% endif %} diff --git a/frontend/templates/base/breadcrumbs_ul.html b/frontend/templates/core/base/breadcrumbs_ul.html similarity index 100% rename from frontend/templates/base/breadcrumbs_ul.html rename to frontend/templates/core/base/breadcrumbs_ul.html diff --git a/frontend/templates/core/base/htmx.html b/frontend/templates/core/base/htmx.html new file mode 100644 index 000000000..15c065300 --- /dev/null +++ b/frontend/templates/core/base/htmx.html @@ -0,0 +1,7 @@ +{% extends "core/base/htmx.html" %} +{##} +{% block left_drawer %} + {% include "core/base/+left_drawer.html" with swap=True %} +{% endblock %} +{#{% include "core/base/breadcrumbs.html" with swap=True %}#} +{#{% include "core/base/toasts.html" %}#} diff --git a/frontend/templates/base/toast.html b/frontend/templates/core/base/toast.html similarity index 100% rename from frontend/templates/base/toast.html rename to frontend/templates/core/base/toast.html diff --git a/frontend/templates/base/toasts.html b/frontend/templates/core/base/toasts.html similarity index 100% rename from frontend/templates/base/toasts.html rename to frontend/templates/core/base/toasts.html diff --git a/frontend/templates/base/topbar/+icon_dropdown.html b/frontend/templates/core/base/topbar/+icon_dropdown.html similarity index 96% rename from frontend/templates/base/topbar/+icon_dropdown.html rename to frontend/templates/core/base/topbar/+icon_dropdown.html index cc726821d..542588256 100644 --- a/frontend/templates/base/topbar/+icon_dropdown.html +++ b/frontend/templates/core/base/topbar/+icon_dropdown.html @@ -37,7 +37,7 @@ {% endif %}
  • - Current Version: {{ version }} + Current Version: {{ finances_version }}
  • {% if git_version and git_version != "prod" %}
  • diff --git a/frontend/templates/base/topbar/_notification_count.html b/frontend/templates/core/base/topbar/_notification_count.html similarity index 100% rename from frontend/templates/base/topbar/_notification_count.html rename to frontend/templates/core/base/topbar/_notification_count.html diff --git a/frontend/templates/base/topbar/_notification_dropdown_items.html b/frontend/templates/core/base/topbar/_notification_dropdown_items.html similarity index 98% rename from frontend/templates/base/topbar/_notification_dropdown_items.html rename to frontend/templates/core/base/topbar/_notification_dropdown_items.html index 9e20b20da..6f1b4ce77 100644 --- a/frontend/templates/base/topbar/_notification_dropdown_items.html +++ b/frontend/templates/core/base/topbar/_notification_dropdown_items.html @@ -85,5 +85,5 @@
  • {% endif %} {% if not initial_load %} - {% include "base/topbar/_notification_count.html" %} + {% include "core/base/topbar/_notification_count.html" %} {% endif %} diff --git a/frontend/templates/base/topbar/_organizations_list.html b/frontend/templates/core/base/topbar/_organizations_list.html similarity index 100% rename from frontend/templates/base/topbar/_organizations_list.html rename to frontend/templates/core/base/topbar/_organizations_list.html diff --git a/frontend/templates/base/topbar/_topbar.html b/frontend/templates/core/base/topbar/_topbar.html similarity index 100% rename from frontend/templates/base/topbar/_topbar.html rename to frontend/templates/core/base/topbar/_topbar.html diff --git a/frontend/templates/base/topbar/team_selector/selector.html b/frontend/templates/core/base/topbar/team_selector/selector.html similarity index 100% rename from frontend/templates/base/topbar/team_selector/selector.html rename to frontend/templates/core/base/topbar/team_selector/selector.html diff --git a/frontend/templates/pages/clients/create/create.html b/frontend/templates/pages/clients/create/create.html index 05711b8ce..eb0de8c7e 100644 --- a/frontend/templates/pages/clients/create/create.html +++ b/frontend/templates/pages/clients/create/create.html @@ -1,4 +1,4 @@ -{% extends base|default:"base/base.html" %} +{% extends base|default:"core/base/base.html" %} {% load static %} {% block content %}
    @@ -10,7 +10,7 @@ Single + class="toggle toggle-primary"/> Representative
    @@ -29,7 +29,7 @@ minlength="3" type="text" class="peer input input-bordered w-full" - placeholder="Bob Smith" /> + placeholder="Bob Smith"/> @@ -49,7 +49,7 @@ minlength="4" type="email" class="peer input input-bordered w-full" - placeholder="bsmith@example.com" /> + placeholder="bsmith@example.com"/> @@ -71,7 +71,7 @@ minlength="3" type="text" class="peer input input-bordered w-full" - placeholder="Google" /> + placeholder="Google"/> @@ -113,7 +113,7 @@ + placeholder="07000000000"/> @@ -130,7 +130,7 @@ + placeholder="Enter email, phone number, etc."/> diff --git a/frontend/templates/pages/clients/dashboard/dashboard.html b/frontend/templates/pages/clients/dashboard/dashboard.html index 5adee025b..2711e29df 100644 --- a/frontend/templates/pages/clients/dashboard/dashboard.html +++ b/frontend/templates/pages/clients/dashboard/dashboard.html @@ -1,4 +1,4 @@ -{% extends base|default:"base/base.html" %} +{% extends base|default:"core/base/base.html" %} {% block content %}

    Clients

    diff --git a/frontend/templates/pages/clients/detail/dashboard.html b/frontend/templates/pages/clients/detail/dashboard.html index 853b95676..0a46e4987 100644 --- a/frontend/templates/pages/clients/detail/dashboard.html +++ b/frontend/templates/pages/clients/detail/dashboard.html @@ -1,4 +1,4 @@ -{% extends base|default:"base/base.html" %} +{% extends base|default:"core/base/base.html" %} {% block content %}
    @@ -33,11 +33,11 @@

    {{ client.name }}

    - + hx-trigger="load" {# click once #} + hx-target='find div[data-htmx="details_container"]' + hx-swap="innerHTML" + hx-get="{% url 'api:settings:client_defaults' client_id=client.id %}"> +
    Customer Defaults
    diff --git a/frontend/templates/pages/create_account.html b/frontend/templates/pages/create_account.html index 0f3de62f1..85ac36295 100644 --- a/frontend/templates/pages/create_account.html +++ b/frontend/templates/pages/create_account.html @@ -1,97 +1,99 @@ {% load static %} - {% include 'partials/base/_head.html' %} - -
    -
    -
    -
    - - -
    -
    -
    - {% include 'base/toasts.html' %} -

    Create account

    -
    - {% csrf_token %} -
    + diff --git a/frontend/templates/pages/dashboard.html b/frontend/templates/pages/dashboard.html index f3dd59102..31ccd60b5 100644 --- a/frontend/templates/pages/dashboard.html +++ b/frontend/templates/pages/dashboard.html @@ -1,4 +1,4 @@ -{% extends base|default:"base/base.html" %} +{% extends base|default:"core/base/base.html" %} {% block content %}
    diff --git a/frontend/templates/pages/emails/dashboard.html b/frontend/templates/pages/emails/dashboard.html index 0b388d526..884d015f3 100644 --- a/frontend/templates/pages/emails/dashboard.html +++ b/frontend/templates/pages/emails/dashboard.html @@ -1,4 +1,4 @@ -{% extends base|default:"base/base.html" %} +{% extends base|default:"core/base/base.html" %} {% block content %}
    @@ -58,18 +58,18 @@
    - - - - - - + + + + + + - {% include 'components/table/skeleton_rows.html' with rows=3 cols=5 %} + {% include 'components/table/skeleton_rows.html' with rows=3 cols=5 %} {#
    IDSent DateRecipientStatus
    IDSent DateRecipientStatus