# Copyright (c) 2025 Ferrosoft GmbH. All rights reserved.
from django.http import QueryDict
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ferrosoft.apps.date_time import now_utc
from ferrosoft.apps.emiflow.filter import TransportChainFilter, TransportOperationFilter
from ferrosoft.apps.emiflow.forms.transport import (
ReportGeneratorForm,
TransportOperationForm,
TransportChainForm,
FreightItemForm,
TransportChainElementForm,
)
from ferrosoft.apps.emiflow.models import (
EmitterEpitype,
TransportChain,
EmissionBookingRecord,
PredefinedLCC,
TransportChainType,
IntensityCapableEntity,
Emitter,
FreightItem,
TransportChainElement,
OperationEpitype,
BookingStatus,
BookingError,
)
from ferrosoft.apps.emiflow.models.operation import TransportOperation
from ferrosoft.apps.emiflow.tables import (
TransportChainTable,
FreightItemTable,
TransportChainElementTable,
TransportOperationTable,
TransportChainElementActionTable,
TransportLCCValueTable,
)
from ferrosoft.apps.emiflow.views import intensity_create_url, TransportLCCValueCreate
from ferrosoft.apps.ferrobase.auth.roles import Permissions, Verb
from ferrosoft.apps.ferrobase.forms.copy import copy_form_factory
from ferrosoft.apps.ferrobase.models import IDSequence, Country
from ferrosoft.apps.ferrobase.services.idgenerator import (
IdGeneratorPostgres,
IdGenerator,
)
from ferrosoft.apps.ferrobase.tables import Action
from ferrosoft.apps.ferrobase.tenant.utils import tenant_db_transaction
from ferrosoft.apps.ferrobase.views.generic import (
DetailView,
TableView,
CreateView,
UpdateView,
DeleteView,
model_crud_routes,
DependentCreateView,
CopyView,
)
from ferrosoft.strparse import parse_int_noexcept
[docs]
class FreightItemCreate(DependentCreateView):
form_class = FreightItemForm
help_topic = "freight_items"
model = FreightItem
parent_model = TransportChain
[docs]
class FreightItemUpdate(UpdateView):
form_class = FreightItemForm
help_topic = "freight_items"
model = FreightItem
related_links_default = False
[docs]
def get_success_url(self):
return self.object.chain.get_absolute_url()
[docs]
class FreightItemDelete(DeleteView):
help_topic = "freight_items"
model = FreightItem
[docs]
def get_success_url(self):
return self.object.chain.get_absolute_url()
freight_item_urls = model_crud_routes(
FreightItem,
create_view=FreightItemCreate,
update_view=FreightItemUpdate,
delete_view=FreightItemDelete,
)
[docs]
class TransportChainElementCreate(DependentCreateView):
epitype_query = "e"
form_class = TransportChainElementForm
help_topic = "transport_chain_elements"
model = TransportChainElement
parent_model = TransportChain.prefetched_objects
enable_missing_parent = True
[docs]
def cancel_view(self):
# Sometimes returning None is alright, because cancel button is rendered
# only once when parent_object is not None. Other calls are from HTMX
# re-rendering form controls.
return (
None
if self.parent_object is None
else self.parent_object.get_absolute_url()
)
[docs]
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"country_mapping": Country.id_mapping(),
"partner_address": {
"consignor": None
if self.parent_object is None
else self.parent_object.consignor,
"consignee": None
if self.parent_object is None
else self.parent_object.consignee,
},
}
[docs]
def get_template_names(self):
# Form gets re-rendered with HTMX.
if self.request.headers.get("HX-Request") == "true":
return "emiflow/transport/tce/form.html"
else:
return "emiflow/transport/tce/create.html"
[docs]
class TransportChainElementUpdate(UpdateView):
form_class = TransportChainElementForm
help_topic = "transport_chain_elements"
model = TransportChainElement
related_links_default = False
[docs]
def cancel_view(self):
return self.object.chain.get_absolute_url()
[docs]
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"country_mapping": Country.id_mapping(),
"partner_address": {
"consignor": self.object.chain.consignor,
"consignee": self.object.chain.consignee,
},
}
[docs]
def get_success_url(self):
return self.object.chain.get_absolute_url()
[docs]
def get_template_names(self):
# Form gets re-rendered with HTMX.
if self.request.headers.get("HX-Request") == "true":
return "emiflow/transport/tce/form.html"
else:
return "emiflow/transport/tce/update.html"
[docs]
class TransportChainElementDelete(DeleteView):
help_topic = "transport_chain_elements"
model = TransportChainElement
[docs]
def get_success_url(self):
return self.object.chain.get_absolute_url()
tce_urls = model_crud_routes(
TransportChainElement,
create_view=TransportChainElementCreate,
update_view=TransportChainElementUpdate,
delete_view=TransportChainElementDelete,
)
[docs]
class TransportIndexView(TableView):
model = TransportChain
filterset_class = TransportChainFilter
help_topic = "transports"
ordering = "-reference"
queryset = TransportChain.prefetched_objects
table_class = TransportChainTable
template_name = "emiflow/transport/index.html"
[docs]
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"report_generator_form": ReportGeneratorForm(),
}
[docs]
class TransportCreate(CreateView):
form_class = TransportChainForm
form_template = "emiflow/transport/form.html"
help_topic = "new_transport"
model = TransportChain
template_name = "emiflow/transport/create.html"
[docs]
@staticmethod
def cancel_view():
return reverse("emiflow:transportchain_index")
[docs]
class TransportUpdate(UpdateView):
[docs]
class TransportDetailView(DetailView):
help_topic = "transports"
model = TransportChain
template_name = "emiflow/transport/detail.html"
[docs]
def get(self, request, *args, **kwargs):
# Parent class get() is skipped, because it would call get_object(),
# but this isn't needed in this class, as the object is set via
# __init__ (see get() of TransportUpdate).
return self.render_to_response(self.get_context_data())
[docs]
def get_queryset(self):
return TransportChain.prefetched_objects.all()
[docs]
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"freight_item_table": FreightItemTable(
self.object.freight_items_ordered()
),
"tce_table": TransportChainElementTable(self.object.elements_ordered()),
"emission": EmissionBookingRecord.objects.find_first(
PredefinedLCC.value_from(
TransportChainType(self.object.transport_type)
),
self.object.pk,
),
"related_links": [
(reverse("emiflow:report_index"), _("Create Report")),
(reverse("emiflow:transportchain_create"), _("Create Transport")),
(reverse("emiflow:transportchain_index"), _("Show Transports")),
],
}
form_class = TransportChainForm
form_template = "emiflow/transport/form.html"
help_topic = "new_transport"
model = TransportChain
template_name = "emiflow/transport/update.html"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._posting_error = None
[docs]
def get(self, request, *args, **kwargs):
self.object: TransportChain = self.get_object()
if self.object.is_status(BookingStatus.POSTED):
detail_view = self.TransportDetailView(object=self.object, request=request)
return detail_view.get(request, *args, **kwargs)
return self.render_to_response(self.get_context_data())
[docs]
def get_queryset(self):
return TransportChain.prefetched_objects.all()
[docs]
def get_context_data(self, **kwargs):
is_posted = self.object.is_status(BookingStatus.POSTED)
return super().get_context_data(**kwargs) | {
"disable_submit": is_posted,
"enable_posting": not is_posted,
"freight_item_table": FreightItemTable(
self.object.freight_items_ordered(),
create_url=reverse(
"emiflow:freightitem_create",
query={FreightItemCreate.parent_query: str(self.object.pk)},
),
disable_update=is_posted,
),
"lcc_values_table": TransportLCCValueTable(
self.object.lcc_values.all(),
create_url=reverse(
"emiflow:transportlccvalue_create",
query={
TransportLCCValueCreate.parent_query: str(self.object.pk),
},
),
disable_update=is_posted,
),
"posting_error": self._posting_error,
"tce_table": TransportChainElementActionTable(
self.object.elements_ordered(),
actions=[
Action(
url=reverse(
"emiflow:transportchainelement_create",
query={
TransportChainElementCreate.parent_query: str(
self.object.pk
),
TransportChainElementCreate.epitype_query: OperationEpitype.TRANSPORT.value,
},
),
label=_("New Transport Element"),
),
Action(
url=reverse(
"emiflow:transportchainelement_create",
query={
TransportChainElementCreate.parent_query: str(
self.object.pk
),
TransportChainElementCreate.epitype_query: OperationEpitype.HUB.value,
},
),
label=_("New Hub Element"),
),
],
disable_update=is_posted,
),
}
[docs]
def post(self, request, *args, **kwargs):
# Beside normal Django form submission, there is also this special form
# instructing to post the transport.
if request.POST.get("post_transport", None) == "1":
self.object = self.get_object()
try:
with tenant_db_transaction():
self.object.post_emission()
except BookingError as e:
self._posting_error = e
# Parent class has to be fooled, so request isn't processed as a
# normal form submission.
request.method = "GET"
request.POST = QueryDict()
return self.render_to_response(self.get_context_data())
return redirect(self.object.get_absolute_url())
return super().post(request, *args, **kwargs)
[docs]
class TransportDelete(DeleteView):
help_topic = "transports"
model = TransportChain
transport_chain_urls = model_crud_routes(
TransportChain,
create_view=TransportCreate,
read_view=TransportIndexView,
update_view=TransportUpdate,
delete_view=TransportDelete,
)
[docs]
class TransportOperationCopy(CopyView):
[docs]
@staticmethod
def prepare_copy(source: TransportOperation, target: TransportOperation, **kwargs):
def copy_emission_intensity():
# Copy emission intensity as-is and let user adjust it in update view.
source_intensity = getattr(source, "emission_intensity", None)
if source_intensity is not None:
target_intensity = source_intensity.copy_with_owner(
IntensityCapableEntity.TRANSPORT_OPERATION, target.pk
)
target.emission_intensity = target_intensity
target.save(update_fields=["emission_intensity"])
target.reference = IdGenerator.get_instance(
IDSequence.TRANSPORT_OPERATION
).next_id()
return [copy_emission_intensity]
form_class = copy_form_factory(
TransportOperation,
fields=[
"name",
"activity_type",
"emitter",
"energy_carrier",
],
preparator=prepare_copy,
)
model = TransportOperation
[docs]
class TransportOperationCreate(CreateView):
form_class = TransportOperationForm
help_topic = "new_transport_operation"
model = TransportOperation
[docs]
class TransportOperationRead(TableView):
filterset_class = TransportOperationFilter
help_topic = "transport_operations"
model = TransportOperation
ordering = "-reference"
table_class = TransportOperationTable
[docs]
class TransportOperationUpdate(UpdateView):
add_copy_action = True
form_class = TransportOperationForm
help_topic = "new_transport_operation"
model = TransportOperation
template_name = "emiflow/transportoperation/update.html"
[docs]
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"intensity_create_url": intensity_create_url(
IntensityCapableEntity.TRANSPORT_OPERATION,
self.object.pk,
self.object.energy_carrier_id,
EmitterEpitype(self.object.emitter.epitype),
),
}
[docs]
class TransportOperationDelete(DeleteView):
help_topic = "transport_operations"
model = TransportOperation
transport_operation_urls = model_crud_routes(
TransportOperation,
copy_view=TransportOperationCopy,
create_view=TransportOperationCreate,
read_view=TransportOperationRead,
update_view=TransportOperationUpdate,
delete_view=TransportOperationDelete,
)