Skip to content
41 changes: 22 additions & 19 deletions backend/clients/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@
recurring_invoices_invoice_overdue_default_email_template,
recurring_invoices_invoice_cancelled_default_email_template,
)
from backend.core.models import OwnerBase, User, UserSettings, _private_storage
from backend.core.models import OwnerBase, User, UserSettings, get_private_storage
from backend.core.constants import MAX_LENGTH_STANDARD, MAX_LENGTH_NAME


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)
name = models.CharField(max_length=MAX_LENGTH_NAME, blank=False)
phone_number = models.CharField(max_length=MAX_LENGTH_STANDARD, 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)
company = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
contact_method = models.CharField(max_length=MAX_LENGTH_STANDARD, 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)
address = models.TextField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
city = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
country = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)

def __str__(self):
return self.name
Expand Down Expand Up @@ -61,17 +62,19 @@ class InvoiceDateType(models.TextChoices):
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 sender information
invoice_from_name = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_from_company = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_from_address = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_from_city = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_from_county = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_from_country = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_from_email = models.CharField(max_length=MAX_LENGTH_STANDARD, 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)
# Banking information
invoice_account_number = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_sort_code = models.CharField(max_length=MAX_LENGTH_STANDARD, null=True, blank=True)
invoice_account_holder_name = models.CharField(max_length=MAX_LENGTH_STANDARD, 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)
Expand Down Expand Up @@ -101,7 +104,7 @@ def get_issue_and_due_dates(self, issue_date: date | str | None = None) -> tuple

default_invoice_logo = models.ImageField(
upload_to="invoice_logos/",
storage=_private_storage,
storage=get_private_storage,
blank=True,
null=True,
)
2 changes: 1 addition & 1 deletion backend/core/api/public/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class APIAuthToken(OwnerBase, ExpiresBase):

hashed_key = models.CharField("Key", max_length=128, unique=True)

name = models.CharField("Key Name", max_length=64)
name = models.CharField("Key Name", max_length=64, blank=False)
description = models.TextField("Description", blank=True, null=True)
created = models.DateTimeField("Created", auto_now_add=True)
last_used = models.DateTimeField("Last Used", null=True, blank=True)
Expand Down
13 changes: 13 additions & 0 deletions backend/core/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Backend core cosntant.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in comment: "cosntant.py" should be "constants.py".

Proposed fix
-# Backend core cosntant.py
+# Backend core constants.py
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Backend core cosntant.py
# Backend core constants.py

"""
Constants used across the application for model field attributes.
"""

# Field length constants
MAX_LENGTH_STANDARD = 100 # Most commonly used max_length
MAX_LENGTH_NAME = 64 # Used for name fields
MAX_LENGTH_DESCRIPTION = 500 # Used for description fields

# Decimal field constants
DECIMAL_MAX_DIGITS = 15
DECIMAL_PLACES = 2
41 changes: 21 additions & 20 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,25 @@
from storages.backends.s3 import S3Storage


def _public_storage():
def get_public_storage():
return storages["public_media"]


def _private_storage() -> FileSystemStorage | S3Storage:
def get_private_storage() -> FileSystemStorage | S3Storage:
return storages["private_media"]


def RandomCode(length=6):
def generate_verification_code(length=6):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def generate_verification_code(length=6):
def generate_verification_code(length: int = 6):

return get_random_string(length=length).upper()


def RandomAPICode(length=89):
def generate_api_key(length=89):
return get_random_string(length=length).lower()


def upload_to_user_separate_folder(instance, filename, optional_actor=None) -> str:
def get_file_upload_path(instance, filename: str, optional_actor=None) -> str:
instance_name = instance._meta.verbose_name.replace(" ", "-")

print(instance, filename)

if optional_actor:
if isinstance(optional_actor, User):
return f"{instance_name}/users/{optional_actor.id}/{filename}"
Expand Down Expand Up @@ -81,7 +79,7 @@ class User(AbstractUser):
require_change_password = models.BooleanField(default=False) # does user need to change password upon next login

class Role(models.TextChoices):
# NAME DJANGO ADMIN NAME
# NAME DJANGO ADMIN NAME
DEV = "DEV", "Developer"
STAFF = "STAFF", "Staff"
USER = "USER", "User"
Expand Down Expand Up @@ -172,7 +170,7 @@ class ServiceTypes(models.TextChoices):
RESET_PASSWORD = "reset_password", "Reset Password"

uuid = models.UUIDField(default=uuid4, editable=False, unique=True) # This is the public identifier
token = models.TextField(default=RandomCode, editable=False) # This is the private token (should be hashed)
token = models.TextField(default=generate_verification_code, editable=False) # This is the private token (should be hashed)

user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -216,7 +214,7 @@ class CoreFeatures(models.TextChoices):
)
profile_picture = models.ImageField(
upload_to="profile_pictures/",
storage=_public_storage,
storage=get_public_storage,
blank=True,
null=True,
)
Expand Down Expand Up @@ -294,7 +292,7 @@ def set_expires(self):

def save(self, *args, **kwargs):
if not self.code:
self.code = RandomCode(10)
self.code = generate_verification_code(10)
self.set_expires()
super().save()

Expand Down Expand Up @@ -509,14 +507,17 @@ def get_quota_limit(self, user: User, quota_limit: QuotaLimit | None = None):
return self.value

def get_period_usage(self, user: User):
if self.limit_type == "forever":
return self.quota_usage.filter(user=user, quota_limit=self).count()
elif self.limit_type == "per_month":
return self.quota_usage.filter(user=user, quota_limit=self, created_at__month=datetime.now().month).count()
elif self.limit_type == "per_day":
return self.quota_usage.filter(user=user, quota_limit=self, created_at__day=datetime.now().day).count()
else:
return "Not available"
base_query = self.quota_usage.filter(user=user, quota_limit=self)

period_filters = {
"forever": {},
"per_month": {"created_at__month": timezone.now().month},
"per_day": {"created_at__day": timezone.now().day},
}

if filters := period_filters.get(self.limit_type):
return base_query.filter(**filters).count()
return "Not available"

def strict_goes_above_limit(self, user: User, extra: str | int | None = None, add: int = 0) -> bool:
current: Union[int, None, QuerySet[QuotaUsage], Literal["Not Available"]]
Expand Down Expand Up @@ -697,7 +698,7 @@ class Meta:


class FileStorageFile(OwnerBase):
file = models.FileField(upload_to=upload_to_user_separate_folder, storage=_private_storage)
file = models.FileField(upload_to=get_file_upload_path, storage=get_private_storage)
file_uri_path = models.CharField(max_length=500) # relative path not including user folder/media
last_edited_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, editable=False, related_name="files_edited")
created_at = models.DateTimeField(auto_now_add=True)
Expand Down
6 changes: 3 additions & 3 deletions backend/core/views/auth/passwords/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils.http import url_has_allowed_host_and_scheme

from backend.models import User, PasswordSecret
from backend.core.models import RandomCode
from backend.core.models import generate_verification_code
from backend.core.types.htmx import HtmxHttpRequest
from settings import settings

Expand Down Expand Up @@ -38,7 +38,7 @@ def set_password_generate(request: HtmxHttpRequest):
if not USER_OBJ:
messages.error(request, f"User not found")
return redirect("dashboard")
CODE = RandomCode(40)
CODE = generate_verification_code(40)
HASHED_CODE = make_password(CODE, salt=settings.SECRET_KEY)

PWD_SECRET, created = PasswordSecret.objects.update_or_create(
Expand Down Expand Up @@ -85,7 +85,7 @@ def password_reset(request: HtmxHttpRequest):

PasswordSecret.objects.filter(user=USER).all().delete()

CODE = RandomCode(40)
CODE = generate_verification_code(40)
HASHED_CODE = make_password(CODE)
expires_date = date.today() + timedelta(days=3)
expires_datetime = timezone.make_aware(datetime.combine(expires_date, datetime.min.time()))
Expand Down
64 changes: 38 additions & 26 deletions backend/finance/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,20 @@

from backend.clients.models import Client, DefaultValues
from backend.managers import InvoiceRecurringProfile_WithItemsManager

from backend.core.models import OwnerBase, UserSettings, _private_storage, USER_OR_ORGANIZATION_CONSTRAINT, User, ExpiresBase, Organization
from backend.core.models import (
OwnerBase,
UserSettings,
get_private_storage,
USER_OR_ORGANIZATION_CONSTRAINT,
User,
ExpiresBase,
Organization,
)
from backend.core.constants import (
MAX_LENGTH_STANDARD,
DECIMAL_MAX_DIGITS,
DECIMAL_PLACES,
)


class BotoSchedule(models.Model):
Expand Down Expand Up @@ -49,17 +61,17 @@ def set_received(self, status: bool = True, save=True):


class InvoiceProduct(OwnerBase):
name = models.CharField(max_length=50)
description = models.CharField(max_length=100)
name = models.CharField(max_length=50, blank=False)
description = models.CharField(max_length=100, blank=False)
quantity = models.IntegerField()
rate = models.DecimalField(max_digits=15, decimal_places=2, blank=True, null=True)


class InvoiceItem(models.Model):
# objects = InvoiceItemManager()

name = models.CharField(max_length=50)
description = models.CharField(max_length=100)
name = models.CharField(max_length=50, blank=False)
description = models.CharField(max_length=100, blank=False)
is_service = models.BooleanField(default=True)
# from
# if service
Expand All @@ -78,29 +90,29 @@ def __str__(self):
class InvoiceBase(OwnerBase):
client_to = models.ForeignKey(Client, on_delete=models.SET_NULL, blank=True, null=True)

client_name = models.CharField(max_length=100, blank=True, null=True)
client_name = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
client_email = models.EmailField(blank=True, null=True)
client_company = models.CharField(max_length=100, blank=True, null=True)
client_address = models.CharField(max_length=100, blank=True, null=True)
client_city = models.CharField(max_length=100, blank=True, null=True)
client_county = models.CharField(max_length=100, blank=True, null=True)
client_country = models.CharField(max_length=100, blank=True, null=True)
client_company = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
client_address = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
client_city = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
client_county = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
client_country = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
client_is_representative = models.BooleanField(default=False)

self_name = models.CharField(max_length=100, blank=True, null=True)
self_company = models.CharField(max_length=100, blank=True, null=True)
self_address = models.CharField(max_length=100, blank=True, null=True)
self_city = models.CharField(max_length=100, blank=True, null=True)
self_county = models.CharField(max_length=100, blank=True, null=True)
self_country = models.CharField(max_length=100, blank=True, null=True)

sort_code = models.CharField(max_length=8, blank=True, null=True) # 12-34-56
account_holder_name = models.CharField(max_length=100, blank=True, null=True)
account_number = models.CharField(max_length=100, blank=True, null=True)
vat_number = models.CharField(max_length=100, blank=True, null=True)
self_name = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
self_company = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
self_address = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
self_city = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
self_county = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
self_country = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)

sort_code = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True) # 12-34-56
account_holder_name = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
account_number = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
vat_number = models.CharField(max_length=MAX_LENGTH_STANDARD, blank=True, null=True)
logo = models.ImageField(
upload_to="invoice_logos",
storage=_private_storage,
storage=get_private_storage,
blank=True,
null=True,
)
Expand Down Expand Up @@ -385,8 +397,8 @@ def get_currency_symbol(self):


class Receipt(OwnerBase):
name = models.CharField(max_length=100)
image = models.ImageField(upload_to="receipts", storage=_private_storage)
name = models.CharField(max_length=100, blank=False)
image = models.ImageField(upload_to="receipts", storage=get_private_storage)
total_price = models.FloatField(null=True, blank=True)
date = models.DateField(null=True, blank=True)
date_uploaded = models.DateTimeField(auto_now_add=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="verificationcodes",
name="token",
field=models.TextField(default=backend.core.models.RandomCode, editable=False),
field=models.TextField(default=backend.core.models.generate_verification_code, editable=False),
),
]
2 changes: 1 addition & 1 deletion backend/migrations/0023_apikey_invoiceonetimeschedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Migration(migrations.Migration):
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("service", models.CharField(choices=[("aws_api_destination", "Aws Api Destination")], max_length=20, null=True)),
("key", models.CharField(default=backend.core.models.RandomAPICode, max_length=100)),
("key", models.CharField(default=backend.core.models.generate_api_key, max_length=100)),
("last_used", models.DateTimeField(auto_now_add=True)),
],
options={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="invoice",
name="logo",
field=models.ImageField(blank=True, null=True, storage=backend.core.models._private_storage, upload_to="invoice_logos"),
field=models.ImageField(blank=True, null=True, storage=backend.core.models.get_private_storage, upload_to="invoice_logos"),
),
migrations.AlterField(
model_name="receipt",
name="image",
field=models.ImageField(storage=backend.core.models._private_storage, upload_to="receipts"),
field=models.ImageField(storage=backend.core.models.get_private_storage, upload_to="receipts"),
),
migrations.AlterField(
model_name="teammemberpermission",
Expand All @@ -63,7 +63,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="usersettings",
name="profile_picture",
field=models.ImageField(blank=True, null=True, storage=backend.core.models._public_storage, upload_to="profile_pictures/"),
field=models.ImageField(blank=True, null=True, storage=backend.core.models.get_public_storage, upload_to="profile_pictures/"),
),
migrations.CreateModel(
name="InvoiceRecurringProfile",
Expand Down Expand Up @@ -109,7 +109,10 @@ class Migration(migrations.Migration):
("reference", models.CharField(blank=True, max_length=100, null=True)),
("invoice_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=backend.core.models._private_storage, upload_to="invoice_logos")),
(
"logo",
models.ImageField(blank=True, null=True, storage=backend.core.models.get_private_storage, upload_to="invoice_logos"),
),
("notes", models.TextField(blank=True, null=True)),
(
"currency",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import backend.models
from django.db import migrations, models

from backend.core.models import _private_storage
from backend.core.models import get_private_storage


class Migration(migrations.Migration):
Expand All @@ -16,6 +16,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="defaultvalues",
name="default_invoice_logo",
field=models.ImageField(blank=True, null=True, storage=_private_storage, upload_to="invoice_logos/"),
field=models.ImageField(blank=True, null=True, storage=get_private_storage, upload_to="invoice_logos/"),
),
]
Loading