"""Per-tenant emiflow configuration models.
:class:`Settings` is a singleton (created lazily on first read) that holds
references to storage directories and the user-defined emissions catalog;
:class:`TransportChainDefaults` keeps default transport-chain attributes
per vehicle type; :class:`BusinessCentralSettings` holds OAuth credentials
for the Dynamics 365 Business Central integration.
"""
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ferrosoft.apps.dataimport.convert import create_or_get_model
from ferrosoft.apps.emiflow.models.catalog import EmissionsCatalog, VehicleType
from ferrosoft.apps.emiflow.models.transport import DistanceType, TransportOperation
from ferrosoft.apps.ferrobase.models import UUIDModel, Directory
[docs]
class Settings(UUIDModel):
"""User configurable settings for Emiflow."""
class Meta:
verbose_name = _("Emiflow settings")
report_directory = models.ForeignKey(
Directory,
on_delete=models.SET_NULL,
related_name="+",
null=True,
blank=True,
verbose_name=_("Storage directory for emission report documents"),
)
company_logo_directory = models.ForeignKey(
Directory,
on_delete=models.SET_NULL,
related_name="+",
null=True,
blank=True,
verbose_name=_("Storage directory for company logos"),
)
user_catalog = models.ForeignKey(
EmissionsCatalog,
on_delete=models.SET_NULL,
related_name="+",
null=True,
blank=True,
verbose_name=_("Emissions catalog for user defined values"),
)
[docs]
@classmethod
def get_instance(cls) -> "Settings":
"""Return the singleton ``Settings`` row, creating it on first call.
Prefetches the related :class:`TransportChainDefaults` rows so
subsequent ``get_transport_chain_defaults`` lookups don't hit the
database.
"""
it = cls.objects.prefetch_related("transport_chain_defaults").first()
if it is None:
return cls.objects.create()
return it
[docs]
def get_company_logo_directory(self) -> Directory:
"""Resolve the company-logo directory, creating it on first call.
Falls back to creating (or fetching) a directory named *Company
Logos* and persisting the FK when the field was unset.
"""
if self.company_logo_directory is not None:
return self.company_logo_directory
directory = create_or_get_model(
Directory, lambda: Directory.objects.create(name=_("Company Logos"))
)
if self.company_logo_directory is None:
self.company_logo_directory = directory
self.save(update_fields=["company_logo_directory"])
return directory
[docs]
def get_transport_chain_defaults(
self, vehicle_type: VehicleType
) -> "TransportChainDefaults":
"""Return the defaults row for ``vehicle_type``, inserting an empty one if absent."""
defaults = self.transport_chain_defaults.filter(
vehicle_type=vehicle_type.value
).first()
if defaults is not None:
return defaults
return TransportChainDefaults.objects.create(
settings=self, vehicle_type=vehicle_type
)
[docs]
class TransportChainDefaults(UUIDModel):
"""Per-vehicle-type defaults applied when creating a new transport chain."""
class Meta:
verbose_name = _("Defaults for transport chains scoped by vehicle type")
constraints = [
models.UniqueConstraint(
name="emiflow_uniq_tc_defaults",
fields=[
"settings",
"vehicle_type",
],
)
]
settings = models.ForeignKey(
Settings,
on_delete=models.CASCADE,
related_name="transport_chain_defaults",
)
vehicle_type = models.PositiveIntegerField(
verbose_name=_("Vehicle type"),
choices=VehicleType.choices,
)
distance_type = models.PositiveIntegerField(
verbose_name=_("Distance type"),
choices=DistanceType.choices,
null=True,
blank=True,
)
transport_operation = models.ForeignKey(
TransportOperation,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name=_("Transport Operation"),
)
@property
def are_defaults_set(self) -> bool:
"""Return ``True`` when at least a distance type has been configured."""
return self.distance_type is not None
[docs]
class BusinessCentralSettings(UUIDModel):
"""Microsoft Dynamics 365 Business Central connection settings (per tenant).
Stores the BC tenant ID, environment, API base URL, and OAuth
client-credentials parameters used by the BC integration in
:mod:`ferrosoft.apps.emiflow.services.businesscentral`.
"""
class Meta:
verbose_name = _("Business Central Settings")
verbose_name_plural = _("Business Central Settings")
tenant_id = models.UUIDField(
null=True,
verbose_name=_("Business Central Tenant ID"),
)
environment = models.CharField(
default="",
verbose_name=_("Business Central Environment Name"),
)
api_base_url = models.CharField(
default="https://api.businesscentral.dynamics.com",
verbose_name=_("Business Central API Base URL"),
)
oauth_auth_url = models.CharField(
default="https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token",
verbose_name=_("OAuth Authorization URL"),
help_text=_(
"URL for Client Credentials grant type. Optional placeholder {{TENANT_ID}} is replaced with Business Central tenant ID."
),
)
oauth_client_id = models.CharField(
default="",
verbose_name=_("OAuth Client ID"),
)
oauth_client_secret = models.CharField(
default="",
verbose_name=_("OAuth Client Secret"),
)
oauth_scope = models.CharField(
default="https://api.businesscentral.dynamics.com/.default",
verbose_name=_("OAuth Scope"),
)
[docs]
@classmethod
def get_instance(cls):
"""Return the singleton BC settings row, creating it on first call."""
it = cls.objects.first()
if it is None:
return cls.objects.create()
return it
[docs]
def get_absolute_url(self):
return reverse("emiflow:businesscentralsettings_update")