Source code for ferrosoft.apps.emiflow.views.transport

#  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] def get_form_kwargs(self): return super().get_form_kwargs() | { "initial": { "chain": self.parent_object, } }
[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 form_valid(self, form): # Do not save the form when HTMX is the originator (form is just # re-rendered due to option change). if self.request.headers.get("HX-Request") == "true": return self.render_to_response(self.get_context_data()) return super().form_valid(form)
[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_form_kwargs(self): return super().get_form_kwargs() | { "initial": { "chain": self.parent_object, "epitype": parse_int_noexcept( self.request.GET.get(self.epitype_query, None) ), }, }
[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 form_valid(self, form): # Do not save the form when HTMX is the originator (form is just # re-rendered due to option change). if self.request.headers.get("HX-Request") == "true": return self.render_to_response(self.get_context_data()) return super().form_valid(form)
[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] def get_form_kwargs(self): gen = IdGeneratorPostgres.create(IDSequence.TRANSPORT_CHAIN) return super().get_form_kwargs() | { "initial": { "booking_date": now_utc(), "reference": gen.next_id(), } }
[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
[docs] def form_valid(self, form): # Only open transports should be deletable. if not self.object.is_status(BookingStatus.OPEN): return redirect(self.object.get_absolute_url()) return super().form_valid(form)
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] def get_form_kwargs(self): gen = IdGeneratorPostgres.create(IDSequence.TRANSPORT_OPERATION) return super().get_form_kwargs() | { "initial": { "reference": gen.next_id(), } }
[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, )