Source code for translator_directory.models.ticket

from __future__ import annotations

from functools import cached_property
from markupsafe import Markup, escape
from onegov.translator_directory.models.time_report import TranslatorTimeReport
from onegov.core.elements import Link, LinkGroup
from onegov.core.templates import render_macro
from onegov.core.utils import linkify
from onegov.org import _
from onegov.org.models.ticket import OrgTicketMixin
from onegov.ticket import Handler
from onegov.ticket import handlers
from onegov.ticket import Ticket
from onegov.core.elements import Intercooler
from onegov.translator_directory.collections.documents import (
    TranslatorDocumentCollection)
from onegov.translator_directory.constants import (
    TIME_REPORT_INTERPRETING_TYPES,
    ASSIGNMENT_LOCATIONS,
    FINANZSTELLE
)
from onegov.translator_directory.layout import AccreditationLayout
from onegov.translator_directory.layout import TranslatorLayout
from onegov.translator_directory.models.accreditation import Accreditation
from onegov.translator_directory.models.mutation import TranslatorMutation
from onegov.translator_directory.models.translator import Translator
from onegov.translator_directory.constants import TRANSLATOR_FA_ICON
from onegov.translator_directory.utils import get_custom_text

from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:
    from onegov.org.request import OrgRequest
    from onegov.translator_directory.request import TranslatorAppRequest


[docs] class TranslatorMutationTicket(OrgTicketMixin, Ticket):
[docs] __mapper_args__ = {'polymorphic_identity': 'TRN'} # type:ignore
if TYPE_CHECKING:
[docs] handler: TranslatorMutationHandler
[docs] def reference_group(self, request: OrgRequest) -> str: return self.title
@handlers.registered_handler('TRN')
[docs] class TranslatorMutationHandler(Handler):
[docs] handler_title = _('Translator')
[docs] code_title = _('Translators')
@cached_property
[docs] def translator(self) -> Translator | None: return self.session.query(Translator).filter_by( id=self.data['handler_data'].get('id') ).first()
@cached_property
[docs] def mutation(self) -> TranslatorMutation | None: if self.translator: return TranslatorMutation( self.session, self.translator.id, self.ticket.id ) return None
@property
[docs] def deleted(self) -> bool: return self.translator is None
@property
[docs] def ticket_deletable(self) -> bool: # For now we don't support this because lots of functionality # depends on data in translator tickets if self.deleted: return True return False
@cached_property
[docs] def email(self) -> str: return self.data['handler_data'].get('submitter_email', '')
@cached_property
[docs] def message(self) -> str: return self.data['handler_data'].get('submitter_message', '')
@cached_property
[docs] def proposed_changes(self) -> dict[str, Any]: return self.data['handler_data'].get('proposed_changes', {})
@cached_property
[docs] def state(self) -> str | None: return self.data.get('state')
@cached_property
[docs] def uploaded_files(self) -> list[Any]: from onegov.file import File file_ids = self.data['handler_data'].get('file_ids', []) if not file_ids: return [] return [ self.session.query(File).filter_by(id=fid).first() for fid in file_ids ]
@property
[docs] def title(self) -> str: return self.translator.title if self.translator else '<Deleted>'
@property
[docs] def subtitle(self) -> str: return _('Mutation')
@cached_property
[docs] def group(self) -> str: return _('Translator')
[docs] def get_summary( self, request: TranslatorAppRequest # type:ignore[override] ) -> Markup: assert self.mutation is not None assert self.translator is not None layout = TranslatorLayout(self.translator, request) changes = self.mutation.translated(request, self.proposed_changes) return render_macro( layout.macros['display_translator_mutation'], request, { 'translator': self.translator, 'message': linkify(self.message).replace('\n', Markup('<br>')), 'changes': changes, 'layout': layout, 'uploaded_files': self.uploaded_files, }, )
[docs] class TimeReportTicket(OrgTicketMixin, Ticket):
[docs] __mapper_args__ = {'polymorphic_identity': 'TRP'} # type:ignore
[docs] es_type_name = 'translator_time_reports'
[docs] def reference_group(self, request: OrgRequest) -> str: return self.title
@handlers.registered_handler('TRP')
[docs] class TimeReportHandler(Handler):
[docs] handler_title = _('Time Report')
[docs] code_title = _('Time Reports')
@cached_property
[docs] def translator(self) -> Translator | None: return ( self.session.query(Translator) .filter_by(id=self.data['handler_data'].get('translator_id')) .first() )
@cached_property
[docs] def time_report(self) -> TranslatorTimeReport | None: time_report_id = self.data['handler_data'].get('time_report_id') if not time_report_id: return None return ( self.session.query(TranslatorTimeReport) .filter_by(id=time_report_id) .first() )
@property
[docs] def deleted(self) -> bool: return self.translator is None
@property
[docs] def ticket_deletable(self) -> bool: if self.deleted: return True if self.time_report and self.time_report.status == 'confirmed': return True return False
@cached_property
[docs] def email(self) -> str: return self.data['handler_data'].get('submitter_email', '')
@cached_property
[docs] def state(self) -> str | None: return self.data.get('state')
@property
[docs] def title(self) -> str: return self.translator.title if self.translator else '<Deleted>'
@property
[docs] def subtitle(self) -> str: return _('Time Report')
@cached_property
[docs] def group(self) -> str: return _('Time Report')
[docs] def get_summary( self, request: TranslatorAppRequest # type:ignore[override] ) -> Markup: if not self.translator or not self.time_report: return Markup('') layout = TranslatorLayout(self.translator, request) report = self.time_report # Status badge at the top if report.status == 'confirmed': status_class = 'success' status_text = request.translate(_('Confirmed')) else: status_class = 'warning' status_text = request.translate(_('Pending')) status_badge = ( f'<div class="alert-box callout {status_class}" ' f'style="margin-bottom: 1rem;">' f'<strong>{request.translate(_("Status"))}: </strong>' f'{status_text}' f'</div>' ) translator_link = ( f'<a href="{request.link(self.translator)}">' f'{escape(self.translator.title)}</a>' ) start_time = escape(layout.format_date(report.start, 'datetime')) end_time = escape(layout.format_date(report.end, 'datetime')) time_range = f'{start_time} - {end_time}' assignment_type_key = report.assignment_type assignment_type_translated = '-' if assignment_type_key: assignment_type_translated = request.translate( TIME_REPORT_INTERPRETING_TYPES[assignment_type_key] ) assignment_date_formatted = escape( layout.format_date(report.assignment_date, 'date') ) summary_parts = [ status_badge, "<dl class='field-display'>", f"<dt>{request.translate(_('Translator'))}</dt>", f'<dd>{translator_link}</dd>', f"<dt>{request.translate(_('Time'))}</dt>", f'<dd>{time_range}</dd>', f"<dt>{request.translate(_('Assignment Date'))}</dt>", f'<dd>{assignment_date_formatted}</dd>', ] # Display assignment location if available if report.assignment_location: location_name, address = ASSIGNMENT_LOCATIONS.get( report.assignment_location, (report.assignment_location, '') ) location_display = f'{escape(location_name)}' if address: location_display += f'<br>{escape(address)}' summary_parts.extend( [ f"<dt>{request.translate(_('Assignment Location'))}</dt>", f'<dd>{location_display}</dd>', ] ) summary_parts.extend( [ f"<dt>{request.translate(_('Type'))}</dt>", f'<dd>{escape(assignment_type_translated)}</dd>', ] ) if report.finanzstelle: finanzstelle_name = FINANZSTELLE[report.finanzstelle].name summary_parts.extend( [ f"<dt>{request.translate(_('Finanzstelle'))}</dt>", f'<dd>{escape(finanzstelle_name)}</dd>', ] ) if report.case_number: summary_parts.extend( [ f"<dt>{request.translate(_('Case Number'))}</dt>", f'<dd>{escape(report.case_number)}</dd>', ] ) if report.notes: notes_html = linkify(report.notes).replace('\n', Markup('<br>')) summary_parts.extend( [ f"<dt>{request.translate(_('Notes'))}</dt>", f'<dd>{notes_html}</dd>', ] ) summary_parts.extend( [ f"<dt>{request.translate(_('Hourly Rate'))}</dt>", f'<dd>{layout.format_currency(report.hourly_rate)}</dd>', f"<dt>{request.translate(_('Duration'))}</dt>", f'<dd>{report.duration_hours} h</dd>', ] ) # Use centralized calculation from model breakdown = report.calculate_compensation_breakdown() # Show day/night breakdown if there are night hours if report.night_minutes > 0: day_hours = report.day_hours_decimal night_hours = report.night_hours_decimal summary_parts.extend( [ f"<dt>{request.translate(_('Day hours'))} " f"({layout.format_currency(report.hourly_rate)} × " f"{day_hours} h)</dt>", f'<dd>{layout.format_currency(breakdown["day_pay"])}</dd>', f"<dt>{request.translate(_('Night hours 20-06'))} " f"({layout.format_currency(report.night_hourly_rate)} × " f"{night_hours} h, +50%)</dt>", f'<dd>{layout.format_currency(breakdown["night_pay"])}</dd>', ] ) else: # No night hours - show simple base pay summary_parts.extend( [ f"<dt>{request.translate(_('Base pay'))} " f"({layout.format_currency(report.hourly_rate)} × " f"{report.duration_hours} h)</dt>", f'<dd>{layout.format_currency(breakdown["day_pay"])}</dd>', ] ) # Show surcharge amounts if any if breakdown['weekend_surcharge'] > 0: # Calculate actual weekend hours that get the surcharge (non-night) weekend_holiday_hours = report.weekend_holiday_hours_decimal night_hours = report.night_hours_decimal weekend_non_night_hours = weekend_holiday_hours - min( weekend_holiday_hours, night_hours ) label = ( f"{request.translate(_('Weekend surcharge amount'))} " f"({layout.format_currency(report.hourly_rate)} × " f"{weekend_non_night_hours} h, +25%)" ) amount = layout.format_currency(breakdown['weekend_surcharge']) summary_parts.extend( [ f'<dt>{label}</dt>', f'<dd>{amount}</dd>', ] ) if breakdown['urgent_surcharge'] > 0: # Calculate the base amount for urgent surcharge actual_work_pay = ( breakdown['day_pay'] + breakdown['night_pay'] + breakdown['weekend_surcharge'] ) rate = report.SURCHARGE_RATES['urgent'] label = ( f"{request.translate(_('Urgent surcharge'))} " f"({layout.format_currency(actual_work_pay)} × {rate}%, " f"+{rate}%)" ) amount = layout.format_currency(breakdown['urgent_surcharge']) summary_parts.extend( [ f'<dt>{label}</dt>', f'<dd>{amount}</dd>', ] ) # Show break time deduction if applicable if breakdown['break_deduction'] > 0: break_hours = report.break_time_hours summary_parts.extend( [ f"<dt>{request.translate(_('Break time'))} " f"({layout.format_currency(report.hourly_rate)} × " f"-{break_hours} h)</dt>", f'<dd>-{layout.format_currency(breakdown["break_deduction"])}</dd>', ] ) summary_parts.extend( [ f"<dt><strong>" f"{request.translate(_('Subtotal (work compensation)'))} " f"</strong></dt>", f'<dd><strong>' f'{layout.format_currency(breakdown["adjusted_subtotal"])}' f'</strong></dd>', ] ) travel_label = request.translate(_('Travel')) # Build travel details showing from-to addresses if report.assignment_location: location_name, address = ASSIGNMENT_LOCATIONS.get( report.assignment_location, (report.assignment_location, '') ) translator_address = ( f'{report.translator.address}, ' f'{report.translator.zip_code} {report.translator.city}' ) if report.travel_distance: travel_label = ( f"{request.translate(_('Travel'))} " f"({request.translate(_('from'))} " f"{escape(translator_address)} " f"{request.translate(_('to'))} {escape(location_name)}, " f"{report.travel_distance} km \u00d7 2)" ) else: travel_label = ( f"{request.translate(_('Travel'))} " f"({request.translate(_('from'))} " f"{escape(translator_address)} " f"{request.translate(_('to'))} {escape(location_name)})" ) elif report.translator.drive_distance: translator_address = ( f'{report.translator.address}, ' f'{report.translator.zip_code} {report.translator.city}' ) travel_label = ( f"{request.translate(_('Travel'))} " f"({request.translate(_('from'))} " f"{escape(translator_address)}, " f"{report.translator.drive_distance} km \u00d7 2)" ) summary_parts.extend( [ f'<dt>{travel_label}</dt>', f'<dd>{layout.format_currency(report.travel_compensation)}' f'</dd>', ] ) if breakdown['meal'] > 0: meal_label = request.translate(_('Meal Allowance (6+ hours)')) summary_parts.extend( [ f'<dt>{meal_label}</dt>', f'<dd>{layout.format_currency(breakdown["meal"])}</dd>', ] ) # Build total calculation formula calculation_parts = [ layout.format_currency(breakdown['adjusted_subtotal']) ] if breakdown['travel'] > 0: calculation_parts.append( layout.format_currency(breakdown['travel']) ) if breakdown['meal'] > 0: calculation_parts.append( layout.format_currency(breakdown['meal']) ) calculation_formula = ' + '.join(calculation_parts) summary_parts.extend( [ f"<dt><strong>{request.translate(_('Total'))}</strong> " f"({calculation_formula})</dt>", f'<dd><strong>' f'{layout.format_currency(breakdown["total"])}' f'</strong></dd>', '</dl>', ] ) return Markup(''.join(summary_parts)) # nosec: B704
[docs] class AccreditationTicket(OrgTicketMixin, Ticket):
[docs] __mapper_args__ = {'polymorphic_identity': 'AKK'} # type:ignore
if TYPE_CHECKING:
[docs] handler: AccreditationHandler
[docs] def reference_group(self, request: OrgRequest) -> str: return self.title
@handlers.registered_handler('AKK')
[docs] class AccreditationHandler(Handler):
[docs] handler_title = _('Accreditation')
[docs] code_title = _('Accreditations')
@cached_property
[docs] def translator(self) -> Translator | None: return self.session.query(Translator).filter_by( id=self.data['handler_data'].get('id') ).first()
@cached_property
[docs] def accreditation(self) -> Accreditation | None: if self.translator is None: return None return Accreditation(self.session, self.translator.id, self.ticket.id)
@property
[docs] def deleted(self) -> bool: return self.translator is None
@property
[docs] def ticket_deletable(self) -> bool: # For now we don't support this because lot's of functionality # depends on data in translator tickets if self.deleted: return True return False
@cached_property
[docs] def email(self) -> str: return self.data['handler_data'].get('submitter_email', '')
@cached_property
[docs] def state(self) -> str | None: return self.data.get('state')
@property
[docs] def title(self) -> str: return self.translator.title if self.translator else '<Deleted>'
@property
[docs] def subtitle(self) -> str: return _('Request Accreditation')
@cached_property
[docs] def group(self) -> str: return _('Accreditation')
[docs] def get_summary( self, request: TranslatorAppRequest # type:ignore[override] ) -> Markup: layout = AccreditationLayout(self.translator, request) locale = request.locale.split('_')[0] if request.locale else None locale = 'de' if locale == 'de' else 'en' agreement_text = get_custom_text( request, f'({locale}) Custom admission course agreement') return render_macro( layout.macros['display_accreditation'], request, { 'translator': self.translator, 'ticket_data': self.data['handler_data'], 'layout': layout, 'agreement_text': agreement_text } )