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 %}
ID | -Sent Date | -Recipient | -Status | -
---|---|---|---|
ID | +Sent Date | +Recipient | +Status | +