﻿<?xml version="1.0" encoding="utf-8"?>
<configset name="AuditTrailObjectHistory" author="MDA">
  <comment>2026-07-08, MDA: v1</comment>
  <requirements>
    <version-requirement>6.8.0.20</version-requirement>
  </requirements>
  <update>
    <after-import>
vtcapp.setpropertyvalue("MaintainFullChangeLog", True)
vtcapp.updatedatabase()
    </after-import>
  </update>
  <references />
  <objects>
    <object class="Script" alias="Script_Showdeletedobjects2">
      <member name="entryID">ScriptBrowseDeletedObjectsInAudittrail</member>
      <member name="designation">Browse deleted objects in Audit Trail</member>
      <member name="containerScript">false</member>
      <member name="scriptText"># coding: windows-1252
#
# Designation: Show deleted objects
# Classes: Container
# ObjectScript: No
# ContainerScript: No
# EventType:
# EventClass:
# EventMembers:
# ExtendedRights: No

# 2026-07-08, MDA: Created

import module_audit_trail_object_history

module_audit_trail_object_history.show_deleted_objects()
</member>
      <member name="eventType">None</member>
      <member name="objectScript">false</member>
      <member name="extendedRights">false</member>
      <member name="selectedObjectsScript">false</member>
      <member name="platform">Python3</member>
    </object>
    <object class="Script" alias="Script_module_AuditTrailObjectHistory0">
      <member name="entryID">ScriptModuleAuditTrailObjectHistory</member>
      <member name="designation">module_audit_trail_object_history</member>
      <member name="classes">BusinessClassesRoot</member>
      <member name="containerScript">false</member>
      <member name="scriptText"># coding: windows-1252
#
# Designation: module_audit_trail_object_history
# Classes:
# ObjectScript: No
# ContainerScript: No
# EventType:
# EventClass:
# EventMembers:
# ExtendedRights: No

# AuditTrail functions: object history and deleted objects dialogs.

# 2026-07-08, MDA: Created

import vtcmodel
import datetime


MAX_DELETED_LIST_LENGTH = 150
# complete mapping of Vertec BOLD_TYPE ids to class names
BOLD_TYPE_NAMES = {
    0: 'Mailaddress', 1: 'Address', 3: 'CommsChannel', 4: 'WrapperLinkType', 5: 'WrapperLinkRole',
    6: 'LinkContainer', 7: 'Genericlink', 8: 'WrapperLinkContainer', 9: 'GenericLinkRole',
    10: 'LinkRole', 11: 'GenericLinkContainer', 12: 'Folder', 13: 'FolderEntryLink',
    15: 'PublicFolder', 16: 'SingletonFolder', 17: 'RatesTable', 18: 'Project', 19: 'Service',
    20: 'RateLevel', 21: 'Expense', 22: 'Servicetype', 23: 'Expensetype', 24: 'UserProjectLink',
    25: 'UserLevel', 28: 'ProjectUserLinktype', 29: 'AbstractEntry', 30: 'Entry', 32: 'Session',
    33: 'User', 35: 'BusinessClassesRoot', 36: 'GenericLinkType', 37: 'Linktype', 
    38: 'LinkContainerServicesOnInvoice', 39: 'MailaddressLayouttemplate', 40: 'Invoice',
    41: 'CurrencyProperty', 42: 'IntegerProperty', 43: 'Property', 44: 'StringProperty',
    45: 'DateTimeProperty', 46: 'LinkContainerInvoices', 51: 'Downpayment',
    52: 'LinkContainerDownpayments', 54: 'LinkContainerExpensesOnInvoice', 55: 'ZipCodeItem',
    60: 'Document', 61: 'Note', 67: 'OpenService', 70: 'ChargedService', 71: 'FileDoc',
    72: 'PaperDoc', 77: 'OpenExpense', 80: 'ChargedExpense', 81: 'Posting', 82: 'PostingDetailItem',
    84: 'Account', 85: 'Contact', 88: 'Person', 89: 'BooleanProperty', 90: 'ObjectProperty',
    91: 'VATType', 92: 'ProjectCostItem', 93: 'ProjectCostitemType', 94: 'Mailaddresslayout',
    95: 'NumberGenerator', 96: 'UsedPrefix', 97: 'Container', 98: 'Language', 100: 'ExchangeRate',
    101: 'Currency', 102: 'SimpleAddress', 103: 'AbstractUser', 104: 'UserRight',
    105: 'AbstractFolder', 106: 'ExpressionFolder', 107: 'UserGroup', 108: 'UserGroupUserLink',
    109: 'StandardUserGroup', 111: 'SuperGroup', 114: 'RateItem', 115: 'LegacyReport',
    116: 'SelectionProperty', 120: 'SqlFolder', 126: 'RatesTableFolder', 127: 'CustomField',
    128: 'CustomFieldItem', 129: 'DateFilterContainer', 130: 'SqlLinkContainer', 
    131: 'AbstractAddress', 132: 'ProjectCustomerLinktype', 134: 'InvoiceTotal',
    135: 'InvoiceDownpaymentLink', 136: 'AbstractReport', 138: 'EntryDocumentLink',
    139: 'CommsChannelType', 140: 'DBVersion', 144: 'ReportTemplateItem', 
    145: 'ChargedCostItemsLinkRole', 146: 'ProjectInvoiceLinkRole', 147: 'BudgetEntry',
    165: 'ClassRight', 166: 'ObjectRight', 167: 'RightType', 169: 'UserGroupRootFolderLink',
    170: 'RootdataFolder', 171: 'SettingsFolder', 176: 'Phase', 186: 'ProjectCounterpartyLinktype',
    188: 'ProjectCounterpartyLink', 190: 'GridColDef', 196: 'UserPhaseLink',
    206: 'EnteringProjectsRole', 207: 'QueryFolder', 208: 'QuerySource', 209: 'CrossTableRow',
    210: 'Script', 212: 'ClassConstraint', 218: 'CommentaryLink', 219: 'PendingTask',
    220: 'PendingTasksContainer', 221: 'BlobProperty', 222: 'AbstractCustomClass',
    223: 'CustomClass0', 224: 'CustomClass1', 225: 'CustomClass2', 226: 'CustomClass3',
    227: 'CustomClass4', 228: 'CustomClass5', 229: 'CustomClass6', 230: 'CustomClass7',
    231: 'CustomClass8', 232: 'CustomClass9', 233: 'Extension', 235: 'ServicetypePhaseLink',
    237: 'ExpensetypePhaseLink', 241: 'PhaseTotal', 275: 'LinkEntry', 278: 'AbstractExpenditure',
    279: 'Outlay', 280: 'AbstractExpendituretype', 281: 'Outlaytype', 282: 'OpenOutlay',
    283: 'ChargedOutlay', 284: 'AbstractExpendituretypePhaseLink', 285: 'OutlaytypePhaseLink',
    286: 'Creditor', 288: 'IdGenerator', 292: 'ServiceSum', 293: 'Skill', 295: 'Payment',
    296: 'Paymenttype', 297: 'Timer', 298: 'PresenceEntry', 299: 'ServiceContainer',
    300: 'PersonalFolder', 301: 'PseudoContainer', 302: 'CrosstableContainer', 304: 'Activity',
    305: 'Activitytype', 306: 'ActivitiesContainer', 307: 'EntryActivitiesLink',
    308: 'ActivitiesContainerPending', 309: 'GridDef', 310: 'ResourceLink', 314: 'Absence',
    318: 'ActivitiesContainerPendingNotNew', 319: 'MinutesProperty', 320: 'WDayHelper',
    321: 'Textblock', 322: 'Projecttype', 323: 'ProjecttypeUserLink',
    324: 'ProjecttypeServicetypeLink', 325: 'ProjecttypeExpensetypeLink',
    326: 'ProjecttypeOutlaytypeLink', 327: 'CustomClass10', 328: 'CustomClass11',
    329: 'CustomClass12', 330: 'CustomClass13', 331: 'CustomClass14', 332: 'CustomClass15',
    333: 'CustomClass16', 334: 'CustomClass17', 335: 'CustomClass18', 336: 'CustomClass19',
    337: 'BudgetChange', 338: 'OutlayDetailitem', 339: 'Terms', 340: 'AccRecTerms',
    341: 'AccPayTerms', 342: 'UserSetting', 344: 'ClassSettings', 345: 'Couple',
    346: 'ProjectCostitemtypePhaseLink', 347: 'CustomLinkType', 348: 'CustomlinkRole',
    349: 'CustomLink', 351: 'TableContainer', 352: 'TableRow', 353: 'WeekServiceContainer',
    354: 'WeekServiceRow', 355: 'WeekPresenceContainer', 356: 'WeekPresenceRow', 357: 'Approval',
    358: 'Absencetype', 359: 'Upgrade', 360: 'CustomClass20', 361: 'CustomClass21',
    362: 'CustomClass22', 363: 'CustomClass23', 364: 'CustomClass24', 365: 'CustomClass25',
    366: 'CustomClass26', 367: 'CustomClass27', 368: 'CustomClass28', 369: 'CustomClass29',
    370: 'AbstractGridColDef', 371: 'TransientGridColDef', 372: 'WeekVacationRow',
    373: 'PropertyFolder', 374: 'AbstractGridDef', 375: 'TransientGridDef',
    377: 'ActivitiesDateFilterContainer', 378: 'Offer', 379: 'Configset', 380: 'ConfigsetObjectLink',
    382: 'Report', 383: 'BIMeasure', 384: 'ScheduledTask', 385: 'BIViewDefinition',
    386: 'Opportunity', 387: 'OpportunityStatus', 388: 'ExpensesContainer', 389: 'OutlaysContainer',
    390: 'TranslationItem', 392: 'AbsencesContainer', 394: 'Notification',
    395: 'RscUtilizationDimension', 396: 'RscViewDefinition', 397: 'AbstractWorker',
    398: 'PlanningWorker', 399: 'CustomClass30', 400: 'CustomClass31', 401: 'CustomClass32',
    402: 'CustomClass33', 403: 'CustomClass34', 404: 'CustomClass35', 405: 'CustomClass36',
    406: 'CustomClass37', 407: 'CustomClass38', 408: 'CustomClass39', 409: 'CustomClass40',
    410: 'CustomClass41', 411: 'CustomClass42', 412: 'CustomClass43', 413: 'CustomClass44',
    414: 'CustomClass45', 415: 'CustomClass46', 416: 'CustomClass47', 417: 'CustomClass48',
    418: 'CustomClass49', 419: 'ConfigSetCustomClass00', 420: 'ConfigSetCustomClass01',
    421: 'ConfigSetCustomClass02', 422: 'ConfigSetCustomClass03', 423: 'ConfigSetCustomClass04',
    424: 'ConfigSetCustomClass05', 425: 'ConfigSetCustomClass06', 426: 'ConfigSetCustomClass07',
    427: 'ConfigSetCustomClass08', 428: 'ConfigSetCustomClass09', 429: 'ConfigSetCustomClass10',
    430: 'ConfigSetCustomClass11', 431: 'ConfigSetCustomClass12', 432: 'ConfigSetCustomClass13',
    433: 'ConfigSetCustomClass14', 434: 'ConfigSetCustomClass15', 435: 'ConfigSetCustomClass16',
    436: 'ConfigSetCustomClass17', 437: 'ConfigSetCustomClass18', 438: 'ConfigSetCustomClass19',
    439: 'ConfigSetCustomClass20', 440: 'ConfigSetCustomClass21', 441: 'ConfigSetCustomClass22',
    442: 'ConfigSetCustomClass23', 443: 'ConfigSetCustomClass24', 444: 'ConfigSetCustomClass25',
    445: 'ConfigSetCustomClass26', 446: 'ConfigSetCustomClass27', 447: 'ConfigSetCustomClass28',
    448: 'ConfigSetCustomClass29', 449: 'ConfigSetCustomClass30', 450: 'ConfigSetCustomClass31',
    451: 'ConfigSetCustomClass32', 452: 'ConfigSetCustomClass33', 453: 'ConfigSetCustomClass34',
    454: 'ConfigSetCustomClass35', 455: 'ConfigSetCustomClass36', 456: 'ConfigSetCustomClass37',
    457: 'ConfigSetCustomClass38', 458: 'ConfigSetCustomClass39', 459: 'ConfigSetCustomClass40',
    460: 'ConfigSetCustomClass41', 461: 'ConfigSetCustomClass42', 462: 'ConfigSetCustomClass43',
    463: 'ConfigSetCustomClass44', 464: 'ConfigSetCustomClass45', 465: 'ConfigSetCustomClass46',
    466: 'ConfigSetCustomClass47', 467: 'ConfigSetCustomClass48', 468: 'ConfigSetCustomClass49',
    469: 'DocumentData', 470: 'VATTypeDetailItem', 471: 'NewlyCreatedLink', 472: 'ActivityInUseLink',
    473: 'DimensionDisplayMeasuresLink', 474: 'IndexFieldDefinition',
    475: 'DimensionDisplayAbstractFolderLink', 476: 'DimensionDisplayLinkRoleLink',
    477: 'WebServiceDefinition', 478: 'StreetAddressInfo',
}


def _resolve_user(user_id):
    """
    Resolves an AuditTrail UserId to the user's display name.
    Returns str(user.objid) if missing.
    """
    try:
        found_object = vtcapp.getobjectbyid(user_id)
    except Exception:
        return str(user_id)
    if found_object.evalocl("not oclistypeof(User)"):
        return str(user_id)
    return found_object.evalocl("asString")


def _format_timestamp(timestamp):
    """
    Formats an AuditTrail timestamp as [date_in_current_format]+' HH:MM:SS', or '' if None.
    """
    if not timestamp:
        return ''
    return "{} {}".format(vtcapp.datetostr(timestamp.date()), timestamp.strftime('%H:%M:%S'))


def _resolve_class(bold_type):
    """
    Returns a human-readable class name for a BoldType ID, or the raw ID if unknown.
    """
    return BOLD_TYPE_NAMES.get(bold_type) or str(bold_type)


def _show_history_dialog(class_name, object_name_id, entries, show_back_button=False):
    """
    Shows a scrollable column-based dialog with the AuditTrail entries for one object.
    """
    all_entries = entries
    member_filter = None
    date_from = None
    date_to = None

    all_members = []
    for entry in all_entries:
        member = entry.get('Member', '')
        if member and member not in all_members:
            all_members.append(member)
    all_members.sort()

    show_filter = (len(all_members) &gt; 1)
    note_type_only_label = vtcapp.translate("Note")
    combo_items_string = ','.join([note_type_only_label] + all_members)
    all_members_label = vtcapp.translate("All")
    combo_items_string = "{},{}".format(all_members_label, combo_items_string)

    note_member_label = "{}/{}".format(note_type_only_label, vtcapp.translate("Member"))

    date_to = vtcapp.currentdate()
    date_from = vtcapp.incday(date_to, -7)

    dlg_definition = """
&lt;Dialog Title="{Translate 'Object history'}" Width="900" &gt;
    &lt;Group Orientation="Vertical" &gt;
        &lt;Group Orientation="Horizontal" &gt;
            &lt;Group Orientation="Vertical" &gt;
                &lt;TextBlock Name="TextBlockObjectNameId" /&gt;
                &lt;TextBlock Name="TextBlockObjectClass" /&gt;
            &lt;/Group &gt;
            {% if show_filter %}
            &lt;Group Orientation="Vertical" &gt;
                &lt;StringComboBox Name="ComboMember" Label="{{note_member_label}}" FlexWidth="2" /&gt;
                &lt;Group Orientation="Horizontal" &gt;
                    &lt;DatePicker Name="DateFrom" Label="{Translate 'Date from'}" /&gt;
                    &lt;DatePicker Name="DateTo" Label="{Translate 'Date to'}" /&gt;
                &lt;/Group &gt;
                &lt;Group Orientation="Horizontal" &gt;
                    &lt;Spacer /&gt;
                    &lt;Button Text="{Translate 'Filter'}" IsAccept="True"
                        Command="{Binding OkCommand}" /&gt;
                &lt;/Group&gt;
            &lt;/Group&gt;
            {% endif %}
        &lt;/Group&gt;
        &lt;Spacer Height="10" /&gt;
        &lt;Table FillLastColumn="True" ColumnSpacing="10" &gt;
            &lt;Table.Columns&gt;
                &lt;TableColumn Width="130" &gt;
                    &lt;TextBlock ShowLabel="False" Text="{Translate 'Date'}" Appearance="Info" /&gt;
                &lt;/TableColumn&gt;
                &lt;TableColumn Width="120" &gt;
                    &lt;TextBlock ShowLabel="False" Text="{Translate 'User'}" Appearance="Info" /&gt;
                &lt;/TableColumn&gt;
                &lt;TableColumn Width="80" &gt;
                    &lt;TextBlock ShowLabel="False" Text="{Translate 'Type'}" Appearance="Info" /&gt;
                &lt;/TableColumn&gt;
                &lt;TableColumn Width="180" &gt;
                    &lt;TextBlock ShowLabel="False" Text="{Translate 'Member'}" Appearance="Info" /&gt;
                &lt;/TableColumn&gt;  
                &lt;TableColumn Width="300" &gt;
                    &lt;TextBlock ShowLabel="False" Text="{Translate 'New value'}" Appearance="Info" /&gt;
                &lt;/TableColumn&gt; 
                &lt;TableColumn /&gt;
            &lt;/Table.Columns&gt;
            &lt;TableRow Visible="False" /&gt;
        &lt;/Table&gt;
        &lt;ScrollableGroup MaxGroupHeight="400" Orientation="Vertical" ShowVerticalScrollbar="True"&gt;
            &lt;Table FillLastColumn="True" ShowHeader="False" &gt;
                &lt;Table.Columns &gt;
                    &lt;TableColumn Width="130" /&gt;
                    &lt;TableColumn Width="120" /&gt;
                    &lt;TableColumn Width="80" /&gt;
                    &lt;TableColumn Width="180" /&gt;
                    &lt;TableColumn Width="300" /&gt;
                    &lt;TableColumn /&gt;
                &lt;/Table.Columns&gt;
            {% for row in entry_rows %}
                &lt;TableRow&gt;
                    &lt;TableCell&gt;
                        &lt;TextBlock Name="TextTimestamp_{{ loop.index0 }}" ShowLabel="False" Text="" Appearance="Info" /&gt;
                    &lt;/TableCell&gt;
                    &lt;TableCell&gt;
                        &lt;TextBlock Name="TextUser_{{ loop.index0 }}" ShowLabel="False" Text="" Appearance="Info" /&gt;
                    &lt;/TableCell&gt;
                    &lt;TableCell&gt;
                        &lt;TextBlock Name="TextAuditType_{{ loop.index0 }}" ShowLabel="False" Text="" Appearance="Info" /&gt;
                    &lt;/TableCell&gt;
                    &lt;TableCell&gt;
                        &lt;TextBlock Name="TextMember_{{ loop.index0 }}" ShowLabel="False" Text="" Appearance="Info" /&gt;
                    &lt;/TableCell&gt;
                    &lt;TableCell&gt;
                        &lt;TextBlock Name="TextNewValue_{{ loop.index0 }}" ShowLabel="False" Text="" Width="300" Appearance="Info" /&gt;
                    &lt;/TableCell&gt;
                    &lt;TableCell /&gt;
                &lt;/TableRow&gt;
            {% endfor %}
            &lt;/Table &gt;
            &lt;Spacer Height="10" /&gt;
        &lt;/ScrollableGroup&gt;
    &lt;/Group&gt;
    &lt;Dialog.Buttons&gt;
        {% if show_back_button %}
        &lt;Button Name="BtnBackToList" Text="{Translate 'Back to list'}" Command="{Binding CloseCommand}" /&gt;
        {% endif %}
        &lt;Button Text="{Translate 'Close'}" IsCancel="True" Command="{Binding CancelCommand}" /&gt;
    &lt;/Dialog.Buttons&gt;
&lt;/Dialog&gt;
"""

    # The loop re-renders only entry_rows on each iteration - the template and member list are fixed.
    # Re-entry occurs when the user applies a member filter (OkCommand)
    # or returns from history ("Back to list").
    while True:
        if member_filter or date_from or date_to:
            display_entries = []
            for entry in all_entries:
                if member_filter:
                    # filtering Notes only
                    if member_filter == vtcapp.translate("Note"):
                        if entry.get("AuditType") != "Note":
                            continue
                    # filtering on members
                    elif entry.get('Member') != member_filter:
                        continue
                if date_from or date_to:
                    timestamp = entry.get('TimeStamp')
                    entry_date = timestamp.date() if timestamp and hasattr(timestamp, 'hour') else timestamp
                    if date_from and entry_date and entry_date &lt; date_from:
                        continue
                    if date_to and entry_date and entry_date &gt; date_to:
                        continue
                display_entries.append(entry)
        else:
            display_entries = all_entries

        entry_rows = []
        for entry in display_entries:
            entry_rows.append({
                'timestamp': _format_timestamp(entry.get('TimeStamp')),
                'user': _resolve_user(entry.get('UserId')),
                'audit_type': vtcapp.translate(entry.get('AuditType', '')),
                'member': entry.get('Member', ''),
                'new_value': str(entry.get('NewValue', '') or ''),
            })

        renderer_context = {
            'entry_rows': entry_rows,
            'show_filter': show_filter,
            'show_back_button': show_back_button,
            'note_member_label': note_member_label,
        }

        dlg_rendered = vtcapp.rendertemplate(dlg_definition, renderer_context)

        init_values = {}
        init_values["TextBlockObjectNameId.Text"] = object_name_id
        init_values["TextBlockObjectClass.Text"] = "{}: {}".format(vtcapp.translate("Class name"), class_name)
        init_values["DateFrom"] = date_from
        init_values["DateTo"] = date_to
        for i, row in enumerate(entry_rows):
            init_values['TextTimestamp_{}.Text'.format(i)] = row['timestamp']
            init_values['TextUser_{}.Text'.format(i)] = row['user']
            init_values['TextAuditType_{}.Text'.format(i)] = row['audit_type']
            init_values['TextMember_{}.Text'.format(i)] = row['member']
            init_values['TextNewValue_{}.Text'.format(i)] = row['new_value']
        if show_filter:
            init_values["ComboMember.ListItemsString"] = combo_items_string
            if member_filter:
                init_values["ComboMember.SelectedValue"] = member_filter
            else:
                init_values["ComboMember.SelectedValue"] = all_members_label
            if date_from:
                init_values["DateFrom"] = date_from
            if date_to:
                init_values["DateTo"] = date_to

        ok, values = vtcapp.showcustomdialog(dlg_rendered, init_values)

        if not ok:
            return False
        if values.get('BtnBackToList'):
            return True

        selected_value = values.get('ComboMember') or ''
        if not selected_value or selected_value == all_members_label:
            member_filter = None
        else:
            member_filter = selected_value

        date_from_raw = values.get('DateFrom')
        date_from = date_from_raw.date() if date_from_raw and hasattr(date_from_raw, 'hour') else (date_from_raw or None)
        date_to_raw = values.get('DateTo')
        date_to = date_to_raw.date() if date_to_raw and hasattr(date_to_raw, 'hour') else (date_to_raw or None)


def show_object_history(obj):
    """
    Shows a dialog with the full AuditTrail history of the given object.
    """
    class_name = vtcmodel.gettypeinfo(obj).defaultname
    entries = vtcapp.getchangelogforobject(obj.objid, None)
    _show_history_dialog(class_name, obj.evalocl("asstring"), entries)


def _is_container_subclass(class_name):
    """
    Checks whether the given class name is a subclass of Container (folder/link container hierarchy).
    Returns True if Container appears anywhere in the class's ancestor chain, False otherwise.
    """
    typeinfo = vtcmodel.gettypeinfo(class_name)
    while typeinfo is not None:
        if typeinfo.defaultname == 'Container':
            return True
        typeinfo = typeinfo.parent
    return False


def show_deleted_objects():
    """
    Shows a period picker, then a list of deleted objects, then the selected object's history.
    """
    dlg_definition = """
&lt;Dialog Title="{Translate 'Deleted objects'}" Width="500"&gt;
    &lt;DatePicker Name="DateFrom" LabelWidth="150" Label="{Translate 'Date from'}" /&gt;
    &lt;DatePicker Name="DateTo" LabelWidth="150" Label="{Translate 'Date to'}" /&gt;
    &lt;ClassOptionsBox Name="ComboClasses" LabelWidth="150" Label="{Translate 'Class filter (optional)'}" SelectionMode="Multi" /&gt;
    &lt;Dialog.Buttons&gt;
        &lt;Button Text="{Translate 'OK'}" IsAccept="True" Command="{Binding OkCommand}" /&gt;
        &lt;Button Text="{Translate 'Cancel'}" IsCancel="True" Command="{Binding CancelCommand}" /&gt;
    &lt;/Dialog.Buttons&gt;
&lt;/Dialog&gt;
"""
    init_values = {}
    init_values["DateFrom"] = vtcapp.currentdate()
    init_values["DateTo"] = vtcapp.currentdate()
    ok, values = vtcapp.showcustomdialog(dlg_definition, init_values)
    if not ok:
        return

    selected_date_from = values["DateFrom"]
    selected_date_to = values["DateTo"]
    class_filter_str = (values["ComboClasses"] or "").strip()
    if class_filter_str:
        class_filter = []
        for part in class_filter_str.split(","):
            part = part.strip()
            if part:
                class_filter.append(part)
    else:
        class_filter = None

    changelog_entries = vtcapp.getchangelogsince(selected_date_from, class_filter)
    deleted_entries = []

    for entry in changelog_entries:
        if entry.get('AuditType') != 'Deleted':
            continue
        if (entry.get('TimeStamp')).date() &gt; selected_date_to.date():
            continue
        deleted_entries.append(entry)

    if not deleted_entries:
        vtcapp.msgbox(vtcapp.translate("No deleted objects found in selected period."), 64, "AuditTrail")
        return
    
    too_many_entries_text = ""
    too_many_entries = len(deleted_entries) &gt; MAX_DELETED_LIST_LENGTH
    if too_many_entries:
        too_many_entries_text = "{} {}".format(vtcapp.translate("Found {} deleted objects.").format(len(deleted_entries)),
                                                vtcapp.translate("Displayed only first {}.").format(MAX_DELETED_LIST_LENGTH))
        deleted_entries = deleted_entries[:MAX_DELETED_LIST_LENGTH]

    dlg_definition = """
&lt;Dialog Title="{Translate 'Deleted objects'}" Width="650"&gt;
    {% if too_many_entries %}
    &lt;TextBlock Text="{{ too_many_entries_text }}" 
        Appearance="Error" ShowLabel="False" /&gt;
        &lt;Spacer Height="10" /&gt;
    {%endif%}
    &lt;Table FillLastColumn="True" ColumnSpacing="10" &gt;
        &lt;Table.Columns&gt;
            &lt;TableColumn Width="130" &gt;
                &lt;TextBlock ShowLabel="False" Text="{Translate 'Date'}" Appearance="Info" /&gt;
            &lt;/TableColumn&gt;
            &lt;TableColumn Width="140" &gt;
                &lt;TextBlock ShowLabel="False" Text="{Translate 'User'}" Appearance="Info" /&gt;
            &lt;/TableColumn&gt;
            &lt;TableColumn Width="140" &gt;
                &lt;TextBlock ShowLabel="False" Text="{Translate 'Class'}" Appearance="Info" /&gt;
            &lt;/TableColumn&gt;
            &lt;TableColumn Width="80" &gt;
                &lt;TextBlock ShowLabel="False" Text="{Translate 'ID'}" Appearance="Info" /&gt;
            &lt;/TableColumn&gt;  
            &lt;TableColumn Width="100" &gt;
                &lt;TextBlock ShowLabel="False" Text="" Appearance="Info" /&gt;
            &lt;/TableColumn&gt; 
            &lt;TableColumn /&gt;
        &lt;/Table.Columns&gt;
        &lt;TableRow Visible="False" /&gt;
    &lt;/Table&gt;
    &lt;ScrollableGroup MaxGroupHeight="400" Orientation="Vertical" ShowVerticalScrollbar="True"&gt;
        &lt;Table FillLastColumn="True" ShowHeader="False" &gt;
            &lt;Table.Columns &gt;
                &lt;TableColumn Width="130" /&gt;
                &lt;TableColumn Width="140" /&gt;
                &lt;TableColumn Width="140" /&gt;
                &lt;TableColumn Width="80" /&gt;
                &lt;TableColumn Width="100" /&gt;
                &lt;TableColumn /&gt;
            &lt;/Table.Columns&gt;
            {% for row in deleted_rows %}
            &lt;TableRow&gt;
                &lt;TableCell&gt;
                    &lt;TextBlock ShowLabel="False" Text="{{ row.timestamp }}" Appearance="Info" /&gt;
                &lt;/TableCell&gt;
                &lt;TableCell&gt;
                    &lt;TextBlock ShowLabel="False" Text="{{ row.user }}" Appearance="Info" /&gt;
                &lt;/TableCell&gt;
                &lt;TableCell&gt;
                    &lt;TextBlock ShowLabel="False" Text="{{ row.class_name }}" Appearance="Info" /&gt;
                &lt;/TableCell&gt;
                &lt;TableCell&gt;
                    &lt;TextBlock ShowLabel="False" Text="{{ row.bold_id }}" Appearance="Info" /&gt;
                &lt;/TableCell&gt;
                &lt;TableCell&gt;
                    &lt;Button Name="BtnHistory_{{ loop.index0 }}" Text="{Translate 'History'}" Command="{Binding CloseCommand}" Appearance="Input" /&gt;
                &lt;/TableCell&gt;
                &lt;TableCell /&gt;
            &lt;/TableRow&gt;
            {% endfor %}
        &lt;/Table &gt;
        &lt;Spacer Height="10" /&gt;
    &lt;/ScrollableGroup&gt;
    &lt;Dialog.Buttons&gt;
        &lt;Button Text="{Translate 'Close'}" IsCancel="True" Command="{Binding CancelCommand}" /&gt;
    &lt;/Dialog.Buttons&gt;
&lt;/Dialog&gt;
"""
    deleted_rows = []
    for entry in deleted_entries:
        deleted_rows.append({
            'timestamp': _format_timestamp(entry.get('TimeStamp')),
            'user': _resolve_user(entry.get('UserId')),
            'class_name': _resolve_class(entry.get('BoldType')),
            'bold_id': str(entry.get('BoldId', '')),
        })

    init_values = {}
    init_values["too_many_entries"] = too_many_entries
    init_values["too_many_entries_text"] = too_many_entries_text
    init_values["deleted_rows"] = deleted_rows
    # Rendered once - the list and init_values do not change between iterations.
    dlg_rendered = vtcapp.rendertemplate(dlg_definition, init_values)

    init_values_deleted = {}

    # The loop re-shows the dialog after returning from the history view ("Back to list").
    while True:
        ok, values = vtcapp.showcustomdialog(dlg_rendered, init_values_deleted)
        if not ok:
            return

        clicked_index = None
        for i in range(len(deleted_rows)):
            if values.get('BtnHistory_{}'.format(i)):
                clicked_index = i
                break

        # to distinguish from clicked index of 0
        if clicked_index is None:
            return

        selected_entry = deleted_entries[clicked_index]
        object_id = selected_entry.get('BoldId')
        if not object_id:
            vtcapp.msgbox(vtcapp.translate("Object ID not available for this entry."), 48, "AuditTrail")
            continue
        class_name = _resolve_class(selected_entry.get('BoldType'))

        history = vtcapp.getchangelogforobject(object_id, None)
        if not history:
            vtcapp.msgbox(vtcapp.translate("No history found for this object."), 64, "AuditTrail")
            continue
        went_back = _show_history_dialog(class_name, str(object_id), history, show_back_button=True)
        if not went_back:
            return
</member>
      <member name="eventType">None</member>
      <member name="objectScript">false</member>
      <member name="extendedRights">false</member>
      <member name="selectedObjectsScript">false</member>
      <member name="platform">Python3</member>
    </object>
    <object class="Script" alias="Script_Showfolderhistory3">
      <member name="entryID">ScriptShowFolderHistory</member>
      <member name="designation">Show object history</member>
      <member name="classes">Container</member>
      <member name="containerScript">false</member>
      <member name="condExpression">(self-&gt;size=1) and (self-&gt;select(objid&gt;-1)-&gt;size&gt;0) and (Timsession.allInstances-&gt;first.login.hasRight('admin'))</member>
      <member name="scriptText"># coding: windows-1252
#
# Designation: Show object history
# Classes: Container
# CondExpression: (self-&gt;size=1) and (Timsession.allInstances-&gt;first.login.hasRight('admin'))
# ObjectScript: No
# ContainerScript: No
# EventType:
# EventClass:
# EventMembers:
# ExtendedRights: No

# Description: shows container history

# 2026-07-08, MDA: Created

import module_audit_trail_object_history

folder = argobject
module_audit_trail_object_history.show_object_history(folder)
</member>
      <member name="eventType">None</member>
      <member name="objectScript">false</member>
      <member name="extendedRights">false</member>
      <member name="selectedObjectsScript">true</member>
      <member name="platform">Python3</member>
    </object>
    <object class="Script" alias="Script_Showobjecthistory1">
      <member name="entryID">ScriptShowObjectHistory</member>
      <member name="designation">Show object history</member>
      <member name="classes">AbstractEntry</member>
      <member name="containerScript">false</member>
      <member name="condExpression">(self-&gt;size=1) and (Timsession.allInstances-&gt;first.login.hasRight('admin'))</member>
      <member name="scriptText"># coding: windows-1252
#
# Designation: Show object history
# Classes: AbstractEntry
# CondExpression: (self-&gt;size=1) and (Timsession.allInstances-&gt;first.login.hasRight('admin'))
# ObjectScript: No
# ContainerScript: No
# EventType:
# EventClass:
# EventMembers:
# ExtendedRights: No

# Description: shows object history

# 2026-07-08, MDA: Created

import module_audit_trail_object_history

obj = argobject
module_audit_trail_object_history.show_object_history(obj)
</member>
      <member name="eventType">None</member>
      <member name="objectScript">false</member>
      <member name="extendedRights">false</member>
      <member name="selectedObjectsScript">true</member>
      <member name="platform">Python3</member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Backtolist5">
      <member name="entryID">TranslationItemBackToList</member>
      <member name="text">
        <mltext language="DE">Zurück zur Liste</mltext>
        <mltext language="EN">Back to list</mltext>
        <mltext language="FR">Retour à la liste</mltext>
        <mltext language="IT">Torna all'elenco</mltext>
        <mltext language="DD">Zurück zur Liste</mltext>
        <mltext language="NL">Terug naar de lijst</mltext>
        <mltext language="NV">Back to list</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:BrowsedeletedobjectsinAuditTrail14">
      <member name="entryID">TranslationitemBrowseDeletedObjectsInAuditTrail</member>
      <member name="text">
        <mltext language="DE">Gelöschte Objekte in AuditTrail durchsuchen</mltext>
        <mltext language="EN">Browse deleted objects in AuditTrail</mltext>
        <mltext language="FR">Parcourir les objets supprimés dans l'AuditTrail</mltext>
        <mltext language="IT">Sfoglia gli oggetti eliminati nell'AuditTrail</mltext>
        <mltext language="DD">Gelöschte Objekte in AuditTrail durchsuchen</mltext>
        <mltext language="NL">Verwijderde objecten bekijken in AuditTrail</mltext>
        <mltext language="NV">Browse deleted objects in Audit Trail</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Classfilter(optional)8">
      <member name="entryID">TranslationItemClassFilterOptional</member>
      <member name="text">
        <mltext language="DE">Klassenfilter (optional)</mltext>
        <mltext language="EN">Class filter (optional)</mltext>
        <mltext language="FR">Filtre de classe (optionnel)</mltext>
        <mltext language="IT">Filtro classe (opzionale)</mltext>
        <mltext language="DD">Klassenfilter (optional)</mltext>
        <mltext language="NL">Klassefilter (optioneel)</mltext>
        <mltext language="NV">Class filter (optional)</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Deleted15">
      <member name="entryID">TranslationitemDeleted</member>
      <member name="text">
        <mltext language="DE">Gelöscht</mltext>
        <mltext language="EN">Deleted</mltext>
        <mltext language="FR">Supprimé</mltext>
        <mltext language="IT">Eliminato</mltext>
        <mltext language="DD">Gelöscht</mltext>
        <mltext language="NL">Verwijderd</mltext>
        <mltext language="NV">Deleted</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Deletedobjects7">
      <member name="entryID">TranslationItemDeletedObjects</member>
      <member name="text">
        <mltext language="DE">Gelöschte Objekte</mltext>
        <mltext language="EN">Deleted objects</mltext>
        <mltext language="FR">Objets supprimés</mltext>
        <mltext language="IT">Oggetti eliminati</mltext>
        <mltext language="DD">Gelöschte Objekte</mltext>
        <mltext language="NL">Verwijderde objecten</mltext>
        <mltext language="NV">Deleted objects</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Displayedonlyfirst{}.16">
      <member name="entryID">TranslationitemDisplayedOnlyFirst</member>
      <member name="text">
        <mltext language="DE">Nur die ersten {} werden angezeigt.</mltext>
        <mltext language="EN">Displayed only first {}.</mltext>
        <mltext language="FR">Seuls les {} premiers sont affichés.</mltext>
        <mltext language="IT">Visualizzati solo i primi {}.</mltext>
        <mltext language="DD">Nur die ersten {} werden angezeigt.</mltext>
        <mltext language="NL">Alleen de eerste {} worden weergegeven.</mltext>
        <mltext language="NV">Displayed only first {}.</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Filteronmember6">
      <member name="entryID">TranslationItemFilter</member>
      <member name="text">
        <mltext language="DE">Filtern</mltext>
        <mltext language="EN">Filter</mltext>
        <mltext language="FR">Filtrer</mltext>
        <mltext language="IT">Filtra</mltext>
        <mltext language="DD">Filtern</mltext>
        <mltext language="NL">Filteren</mltext>
        <mltext language="NV">Filter</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Found{}deletedobjects.17">
      <member name="entryID">TranslationitemFoundDeletedObjects</member>
      <member name="text">
        <mltext language="DE">{} gelöschte Objekte gefunden.</mltext>
        <mltext language="EN">Found {} deleted objects.</mltext>
        <mltext language="FR">{} objets supprimés trouvés.</mltext>
        <mltext language="IT">Trovati {} oggetti eliminati.</mltext>
        <mltext language="DD">{} gelöschte Objekte gefunden.</mltext>
        <mltext language="NL">{} verwijderde objecten gevon.</mltext>
        <mltext language="NV">Found {} deleted objects.</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Member15">
      <member name="entryID">TranslationItemMember</member>
      <member name="text">
        <mltext language="DE">Member</mltext>
        <mltext language="EN">Member</mltext>
        <mltext language="FR">Membre</mltext>
        <mltext language="IT">Membro</mltext>
        <mltext language="DD">Member</mltext>
        <mltext language="NL">Member</mltext>
        <mltext language="NV">Member</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Nodeletedobjectsfoundinselectedperiod.9">
      <member name="entryID">TranslationItemNoDeletedObjectsFoundInSelectedPeriod</member>
      <member name="text">
        <mltext language="DE">Keine gelöschten Objekte im gewählten Zeitraum gefunden.</mltext>
        <mltext language="EN">No deleted objects found in selected period.</mltext>
        <mltext language="FR">Aucun objet supprimé trouvé dans la période sélectionnée.</mltext>
        <mltext language="IT">Nessun oggetto eliminato trovato nel periodo selezionato.</mltext>
        <mltext language="DD">Keine gelöschten Objekte im gewählten Zeitraum gefunden.</mltext>
        <mltext language="NL">Geen verwijderde objecten gevonden in de geselecteerde periode.</mltext>
        <mltext language="NV">No deleted objects found in selected period.</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Nohistoryfoundforthisobject.11">
      <member name="entryID">TranslationItemNoHistoryFoundForThisObject</member>
      <member name="text">
        <mltext language="DE">Kein Verlauf für dieses Objekt gefunden.</mltext>
        <mltext language="EN">No history found for this object.</mltext>
        <mltext language="FR">Aucun historique trouvé pour cet objet.</mltext>
        <mltext language="IT">Nessuna cronologia trovata per questo oggetto.</mltext>
        <mltext language="DD">Kein Verlauf für dieses Objekt gefunden.</mltext>
        <mltext language="NL">Geen geschiedenis gevonden voor dit object.</mltext>
        <mltext language="NV">No history found for this object.</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Objecthistory4">
      <member name="entryID">TranslationItemObjectHistory</member>
      <member name="text">
        <mltext language="DE">Objektverlauf</mltext>
        <mltext language="EN">Object history</mltext>
        <mltext language="FR">Historique de l'objet</mltext>
        <mltext language="IT">Cronologia dell'oggetto</mltext>
        <mltext language="DD">Objektverlauf</mltext>
        <mltext language="NL">Objectgeschiedenis</mltext>
        <mltext language="NV">Object history</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:ObjectIDnotavailableforthisentry.10">
      <member name="entryID">TranslationItemObjectIdNotAvailableForThisEntry</member>
      <member name="text">
        <mltext language="DE">Objekt-ID für diesen Eintrag nicht verfügbar.</mltext>
        <mltext language="EN">Object ID not available for this entry.</mltext>
        <mltext language="FR">ID d'objet non disponible pour cette entrée.</mltext>
        <mltext language="IT">ID oggetto non disponibile per questa voce.</mltext>
        <mltext language="DD">Objekt-ID für diesen Eintrag nicht verfügbar.</mltext>
        <mltext language="NL">Object-ID niet beschikbaar voor dit item.</mltext>
        <mltext language="NV">Object ID not available for this entry.</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Showobjecthistory12">
      <member name="entryID">TranslationItemShowObjectHistory</member>
      <member name="text">
        <mltext language="DE">Objektverlauf anzeigen</mltext>
        <mltext language="EN">Show object history</mltext>
        <mltext language="FR">Afficher l'historique de l'objet</mltext>
        <mltext language="IT">Mostra cronologia oggetto</mltext>
        <mltext language="DD">Objektverlauf anzeigen</mltext>
        <mltext language="NL">Objectgeschiedenis weergeven</mltext>
        <mltext language="NV">Show object history</mltext>
      </member>
    </object>
  </objects>
  <settings />
</configset>