Copyright
Dieses Dokument unterliegt dem Copyright © 2014. Die Beitragenden sind unten aufgeführt.
Sie dürfen dieses Dokument unter den Bedingungen der GNU General Public License (http://
www.gnu.org/licenses/gpl.html), Version 3 oder höher, oder der Creative Commons Attribution
License (http://creativecommons.org/licenses/by/3.0/), Version 3.0 oder höher, verändern und/oder weitergeben.
Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt.
Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben,
die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt.
Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird
das Symbol (R) in diesem Buch nicht verwendet.
Mitwirkende/Autoren
Robert Großkopf
Titelblatt: Robert Großkopf

basierend auf dem Design von Bayu Rizaldhan Rayes und dem
Gestaltungsentwurf von Drew Jensen

Rückmeldung (Feedback)Kommentare oder Vorschläge zu diesem Dokument können Sie in deutscher Sprache an die
Adresse discuss@de.libreoffice.org senden.

Vorsicht
Alles, was an eine Mailingliste geschickt wird, inklusive der E-Mail-Adresse und anderer
persönlicher Daten, die die E-Mail enthält, wird öffentlich archiviert und kann nicht
gelöscht werden. Also, schreiben Sie mit Bedacht!

Datum der Veröffentlichung und Softwareversion
Veröffentlicht am 01.08.2023. Basierend auf der Version LibreOffice 7.6.

Inhaltsverzeichnis
Vorwort..............................................................................................................................13
Zeitmessung......................................................................................................................15
Einführung.............................................................................................................................................. 16
Tabellen...................................................................................................................................................16
Start...................................................................................................................................................16
Zeitspeicher.......................................................................................................................................17
Abfragen................................................................................................................................................. 18
Abfrage_Start.....................................................................................................................................18
Abfrage_Unterwegs............................................................................................................................19
Abfrage_Ziel.......................................................................................................................................19
Formulare................................................................................................................................................20
Einzelmessung_System......................................................................................................................20
Einzelmessung_Zeit...........................................................................................................................21
Meldung_Liste_Teilnehmer.................................................................................................................22
Meldung_Zeit_Teilnehmer..................................................................................................................23
Nur_Start............................................................................................................................................24
Nur_Ziel..............................................................................................................................................25
Start_Ziel_Platz.................................................................................................................................. 27
Start_Ziel_Gruppe..............................................................................................................................28
Berichte.................................................................................................................................................. 29
Ergebnisliste...................................................................................................................................... 29
Ergebnisliste_Zeit...............................................................................................................................30
Urkunden........................................................................................................................................... 31
Makros.................................................................................................................................................... 32
Starten und Stoppen der Zeit............................................................................................................33
Zeitmessung_Start_System.........................................................................................................33
Zeitmessung_Ende_System........................................................................................................33
Zeitmessung_Start......................................................................................................................34
Zeitmessung_Ende......................................................................................................................35
Sekunden_2_SQLTime..................................................................................................................36
Formulare mit Zeitmessung verknüpfen............................................................................................37
Zeitmessung_Start_Einzel...........................................................................................................37
Zeitmessung_Ende_Einzel...........................................................................................................38
Zeitmessung_Start_Gruppe.........................................................................................................38
Zeitmessung_Ende_Gruppe........................................................................................................38
Zeitmessung_Start_Ziel...............................................................................................................39
Zeitmessung_Ende_Listenfeld.....................................................................................................39
Aktuelle Standardwerte für Datum und Zeit........................................................................40
Einführung.............................................................................................................................................. 41
Tabellen...................................................................................................................................................41
Datum_Aenderdatum.........................................................................................................................41
Zeitstempel_Aenderstempel..............................................................................................................42
Ansichten................................................................................................................................................ 43
Abfragen................................................................................................................................................. 43
DatumZeitvorgabe.............................................................................................................................43
Zeitdifferenzen...................................................................................................................................44
Zeitdifferenzen_Formularvorlage.......................................................................................................45
Formulare................................................................................................................................................46
Defaultdatum_Subform_neue_Datensaetze.......................................................................................46
Defaultdatum_Zeit_Subform_neue_Datensaetze...............................................................................48
Defaultdatum_Aenderung..................................................................................................................48
Defaultdatum_Makro_SQL..................................................................................................................49
Defaultdatum_Zeit_Makro_SQL..........................................................................................................49

Defaultdatum_Makro..........................................................................................................................50
Defaultdatum_Zeit_Makro..................................................................................................................51
Defaultdatum_Makro_Standarddatum...............................................................................................51
Defaultdatum_Makro_Standarddatum_verlegt...................................................................................52
Defaultdatum_Makro_Standardzeitstempel.......................................................................................52
Defaultdatum_Zeitdifferenz_Makro....................................................................................................53
Berichte.................................................................................................................................................. 53
Bericht_Zeitdifferenzen......................................................................................................................54
Bericht_Zeitdifferenzen_Formularvorlage..........................................................................................56
Makros.................................................................................................................................................... 56
Update............................................................................................................................................... 56
Datum_aktuell....................................................................................................................................57
Datum_Zeit_aktuell............................................................................................................................58
Standarddatum..................................................................................................................................59
Standarddatum_verlegt.....................................................................................................................59
StandardZeitstempel.........................................................................................................................60
UpdateTimestamp..............................................................................................................................60
Dateien einbinden..............................................................................................................62
Einführung.............................................................................................................................................. 63
Tabellen...................................................................................................................................................64
Dateien.............................................................................................................................................. 64
Dateien_intern................................................................................................................................... 64
Ansichten................................................................................................................................................ 65
Abfragen................................................................................................................................................. 65
Formulare................................................................................................................................................66
Pfadeingabe.......................................................................................................................................66
Pfadeingabe_Tabellenkontrollfeld.......................................................................................................67
Dateiaufnahme..................................................................................................................................68
Dateiaufnahme_Name.......................................................................................................................68
Berichte.................................................................................................................................................. 69
Makros.................................................................................................................................................... 71
Betrachten......................................................................................................................................... 71
DateiAuslesen....................................................................................................................................71
DateiAuslesen_mitName....................................................................................................................72
DateiName.........................................................................................................................................73
Serienbrief direkt...............................................................................................................74
Einführung.............................................................................................................................................. 75
Tabellen...................................................................................................................................................75
Anschrift.............................................................................................................................................75
Rechnung...........................................................................................................................................76
Rechnungsinhalt................................................................................................................................ 76
Verkauf...............................................................................................................................................77
Waren.................................................................................................................................................77
Abfragen................................................................................................................................................. 78
Adresseingabe................................................................................................................................... 78
Etiketten............................................................................................................................................ 79
Rechnung_einzeilig............................................................................................................................79
Rechnung_nicht_gedruckt..................................................................................................................80
Rechnung_Textfelder..........................................................................................................................80
Formulare................................................................................................................................................81
Anschrift.............................................................................................................................................82
Anschrift_Textfelder............................................................................................................................84
Etiketten_Serienbrief_im_Formular....................................................................................................85
Rechnung...........................................................................................................................................89
Rechnung_Textfelder..........................................................................................................................91

Rechnung_Textfelder_Uebertrag........................................................................................................94
Rechnung_Textfelder_Uebertrag_Seriendruck....................................................................................96
Waren.................................................................................................................................................97
Berichte.................................................................................................................................................. 97
Makros.................................................................................................................................................... 97
Rechnung...........................................................................................................................................97
Rechnungsinhalt_zusammenstellen...................................................................................................98
Serienbrief......................................................................................................................................... 99
Output_MailMerge..............................................................................................................................99
Textfelder_Fuellen............................................................................................................................100
Textfelder_Fuellen_ID.......................................................................................................................101
Tabellen_fuellen...............................................................................................................................102
Rechnungsinhalt_zusammenstellen_Tabelle....................................................................................103
Tabellen_fuellen_Uebertrag..............................................................................................................104
Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag...................................................................106
Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck...............................................106
PDF_Export.......................................................................................................................................107
Direktdruck_Start.............................................................................................................................108
Terminübersicht Ferienhäuser...........................................................................................110
Einführung............................................................................................................................................ 111
Tabellen.................................................................................................................................................111
Mieter...............................................................................................................................................111
Haus.................................................................................................................................................112
Reservierung....................................................................................................................................112
Filter.................................................................................................................................................113
NrBis31............................................................................................................................................ 113
Ansichten..............................................................................................................................................114
Ansicht_Datum.................................................................................................................................114
Ansicht_Datum_lfdNr....................................................................................................................... 115
Abfragen...............................................................................................................................................115
Belegung..........................................................................................................................................115
Bericht.............................................................................................................................................117
Formulare..............................................................................................................................................118
Berichte................................................................................................................................................ 121
Makros.................................................................................................................................................. 121
Startdatum.......................................................................................................................................121
Enddatum........................................................................................................................................ 122
BuchungsAenderung........................................................................................................................124
NeueBuchung.................................................................................................................................. 124
DatumsAenderung...........................................................................................................................125
Mailaufruf........................................................................................................................127
Einführung............................................................................................................................................ 128
Tabellen.................................................................................................................................................128
Kontakte...........................................................................................................................................128
Mails.................................................................................................................................................129
Mail_CC............................................................................................................................................ 129
Anhaenge.........................................................................................................................................130
Ansichten..............................................................................................................................................131
Abfragen...............................................................................................................................................131
Formulare..............................................................................................................................................131
Kontakte_Mail_Web..........................................................................................................................131
Mail_mit_Anhang..............................................................................................................................133
Berichte................................................................................................................................................ 134
Makros.................................................................................................................................................. 134
Mauszeiger.......................................................................................................................................134

FormSave.........................................................................................................................................134
Website_Aufruf.................................................................................................................................135
Mail_Aufruf.......................................................................................................................................136
Attachment......................................................................................................................................138
Mail_Attachment..............................................................................................................................139
Suchen und Filtern...........................................................................................................142
Einführung............................................................................................................................................ 143
Tabellen.................................................................................................................................................143
Filter.................................................................................................................................................144
Kategorien....................................................................................................................................... 145
Medien............................................................................................................................................. 145
Medienart.........................................................................................................................................146
Ort....................................................................................................................................................146
rel_Medien_Verfasser.......................................................................................................................146
Verfasser..........................................................................................................................................147
Ansichten..............................................................................................................................................147
Ansicht_Suche..................................................................................................................................147
Ansicht_Filter_Start..........................................................................................................................148
Ansicht_Standardfilter......................................................................................................................148
Standardfilter_Firebird......................................................................................................................149
Abfragen...............................................................................................................................................149
Filter.................................................................................................................................................149
Filter_Form................................................................................................................................ 149
Filter_Form_Subform..................................................................................................................150
Filter_Form_Subform_3Filter......................................................................................................151
Filter_Ansicht.............................................................................................................................151
Filter_Form_Nullwerte................................................................................................................152
Filter_Leerfeld............................................................................................................................152
Suche...............................................................................................................................................152
Suche_Form...............................................................................................................................152
Suche_Form_Subform................................................................................................................153
Parametersuche_Medien...........................................................................................................154
Filter und Suche kombiniert.............................................................................................................155
Suche_Filter_Form_Subform_3Filter...........................................................................................155
Filter_Such_Ansicht...................................................................................................................156
Listenfeldabfragen...........................................................................................................................157
Listenfeld_Kategorie_ohne_Eintrag...........................................................................................157
Listenfeld_Kategorie_bedingt....................................................................................................158
Listenfeld_Kategorie_bedingt_Datensaetze..............................................................................159
Listenfeld_Kategorie_bedingt_Suche_Filter...............................................................................159
Listenfeld_Kategorie_bedingt_Suche_Filter_Datensaetze..........................................................160
Listenfeld_Medienart_bedingt...................................................................................................160
Listenfeld_Medienart_bedingt_Datensaetze..............................................................................160
Listenfeld_Medienart_bedingt_Suche_Filter...............................................................................161
Listenfeld_Medienart_bedingt_Suche_Filter_Datensaetze.........................................................161
Listenfeld_Verfasser_bedingt....................................................................................................161
Listenfeld_Verfasser_bedingt_Datensaetze_langsam................................................................162
Listenfeld_Verfasser_bedingt_Datensaetze_optimiert...............................................................162
Listenfeld_Verfasser_bedingt_Suche_Filter................................................................................163
Listenfeld_Verfasser_bedingt_Suche_Filter_Datensaetze..........................................................163
Formulare..............................................................................................................................................164
Filter_ohne_Makros...........................................................................................................................164
Filter_Formular..........................................................................................................................164
Filter_Formular_Nullwerte..........................................................................................................166
Filter_Formular_Subformular.....................................................................................................166
Filter_Formular_Subformular_3Filter..........................................................................................167
Standardfilter_Formular_Subformular.......................................................................................168
Standardfilter_Formular_Subformular_Datensatzkorrektur.......................................................169

Filter_mit_Makros.............................................................................................................................169
Filter_Formular..........................................................................................................................170
Filter_Formular_direkt_Nullwerte...............................................................................................171
Filter_Formular_Listfeldbegrenzung...........................................................................................172
Filter_Formular_Subformular.....................................................................................................173
Filter_Formular_Subformular_bedingende_Filter........................................................................174
Filter_Formular_Subformular_bedingende_Filter_allround.........................................................175
Filter_Formular_Subformular_bedingende_Filter_Datensaetze..................................................175
Suche_ohne_Makros.........................................................................................................................176
Suche_Formular........................................................................................................................ 176
Suche_Formular_Subformular...................................................................................................177
Parameter_Suche_Formular_Subformular.................................................................................178
Suche_mit_Makros...........................................................................................................................178
Suche_Formular........................................................................................................................ 179
Suche_Formular_Subformular...................................................................................................180
Suche_Filter_Formular_Subformular_ohne_Makros...........................................................................181
Suche_Filter_hierarchisch_mit_Makros.............................................................................................182
Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros.........................................................183
Berichte................................................................................................................................................ 184
Makros.................................................................................................................................................. 184
Filter.................................................................................................................................................184
Filter_Formular_direkt.......................................................................................................................185
Filter_bedingt...................................................................................................................................186
Filter_bedingt_allround.....................................................................................................................186
Filter_Suche......................................................................................................................................187
Filter_Suche_hierarchisch.................................................................................................................188
Filter_Suche_hierarchisch_Start.......................................................................................................189
Filter_Suche_hierarchisch_allround..................................................................................................190
ListFilter........................................................................................................................................... 191
Haushaltsbuch..................................................................................................................193
Einführung............................................................................................................................................ 194
Tabellen.................................................................................................................................................194
Kasse............................................................................................................................................... 194
Adressat...........................................................................................................................................195
Konto................................................................................................................................................196
Kategorie......................................................................................................................................... 196
rel_Kasse_Kategorie.........................................................................................................................197
Filter.................................................................................................................................................198
Ansichten..............................................................................................................................................198
Ansicht_Kasse_Kategorie.................................................................................................................198
Ansicht_Kasse_mit_Umbuchungen...................................................................................................200
Ansicht_Filter....................................................................................................................................201
Ansicht_Kategorie_Diagramm..........................................................................................................202
Ansicht_Konto_Diagramm................................................................................................................202
Ansicht_Bericht_Kategorie...............................................................................................................203
Abfragen...............................................................................................................................................204
Kategorieverlauf..............................................................................................................................204
Kasse_Saldo_Konto_Kategorie..........................................................................................................204
Kasse_Saldo_Konto_Kategorie_Umbuch...........................................................................................205
Kontoverlauf.....................................................................................................................................206
Saldo_Kategorie...............................................................................................................................207
Saldo_Kategorie_ungesplittet...........................................................................................................208
Saldo_Konto.....................................................................................................................................209
Formulare..............................................................................................................................................210
Formulare ohne Makros....................................................................................................................210
Kasse_oM..................................................................................................................................210
Konto_oM...................................................................................................................................210

Konto_Salden_oM......................................................................................................................211
Konto_Salden_separat_oM.........................................................................................................212
Formulare mit einfachem Aktualisierungsmakro..............................................................................213
Konto_Salden_komplett_eM.......................................................................................................213
Konto_Salden_komplett_Umbuchung_eM..................................................................................214
Formulare mit mehrfachem Makroeinsatz.......................................................................................215
Kasse.........................................................................................................................................215
Kasse_Umbuchung....................................................................................................................216
Konto.........................................................................................................................................217
Konto_Salden............................................................................................................................ 218
Konto_Salden_komplett.............................................................................................................219
Konto_Salden_komplett_Umbuchung........................................................................................220
Buchung_Umbuchung_Salden...................................................................................................221
Hsqldb:..............................................................................................................................223
Firebird:.............................................................................................................................223
Kategorie_Konto_Auswertung....................................................................................................224
Berichte................................................................................................................................................ 225
Makros.................................................................................................................................................. 228
Aktualisieren_einfach.......................................................................................................................228
Aktualisieren....................................................................................................................................228
ZeileZwischenspeichern..................................................................................................................229
SplitRest...........................................................................................................................................230
periodische_Buchung.......................................................................................................................232
Adressat_neu................................................................................................................................... 234
Monat_aktuell...................................................................................................................................236
Formular_starten..............................................................................................................................236
Aktuelles_Buchungsdatum...............................................................................................................236
Kontofilter_Formstart.......................................................................................................................237
Kontofilter_Feldstart.........................................................................................................................238
Kontoliste_filtern..............................................................................................................................238
ChangeData.....................................................................................................................................239
Termine............................................................................................................................242
Einführung............................................................................................................................................ 243
Tabellen.................................................................................................................................................244
Tabellen zur Dateneingabe über Formulare oder direkt...................................................................244
Termine.....................................................................................................................................244
Terminart...................................................................................................................................245
Zielgruppe.................................................................................................................................246
Ferien........................................................................................................................................246
Filtertabelle......................................................................................................................................247
Durch Makros gefüllt Tabellen..........................................................................................................248
tmp_Halbjahreskalender...........................................................................................................248
tmp_Gruppe_multi.....................................................................................................................249
tmp_Kalender............................................................................................................................249
tmp_Monat................................................................................................................................249
tmp_Terminuebersicht...............................................................................................................250
tmp_Wochenkalender................................................................................................................250
tmp_Wochenkalender_halb.......................................................................................................251
Ansichten..............................................................................................................................................251
Ansicht_Gruppe_multi......................................................................................................................251
Ansicht_Kalender_Export.................................................................................................................253
Ansicht_Kalender_ohne_Ferien_Export.............................................................................................255
Ansicht_Termine_Monat_Jahre..........................................................................................................256
Ansicht_Wochen_einzeln..................................................................................................................256
Ansicht_Wochentage........................................................................................................................257
Abfragen...............................................................................................................................................258
Liste_Gruppe_Multi...........................................................................................................................258

Termine_Gruppe_multi.....................................................................................................................258
Terminuebersicht............................................................................................................................. 259
Wochenkalender_Mo_Fr....................................................................................................................259
Formulare..............................................................................................................................................260
Terminart_Termin..............................................................................................................................260
Termine_Kalenderübersicht..............................................................................................................261
Termine_Monat_Terminart................................................................................................................263
Termine_Tabelleneingabe.................................................................................................................264
Berichte................................................................................................................................................ 265
Halbjahreskalender_A4_quer...........................................................................................................265
Liste_Gruppe_multi..........................................................................................................................266
Liste_Gruppe_multi_1HJ............................................................................................................266
Liste_Gruppe_multi_2HJ............................................................................................................267
Monatskalender_Mo_Fr.....................................................................................................................267
Monatskalender_Mo_So....................................................................................................................268
Wochenkalender_halb......................................................................................................................268
Wochentage_Mensaabo...................................................................................................................270
Zeitplan............................................................................................................................................270
Makros.................................................................................................................................................. 270
AA_Variablen....................................................................................................................................270
Datenbankstart.........................................................................................................................271
OfficeVersion.............................................................................................................................271
Datumswert..............................................................................................................................272
Listenfeld_Text.......................................................................................................................... 272
ID_Ermittlung............................................................................................................................273
Abfragen..........................................................................................................................................273
Kalender....................................................................................................................................273
Monatskalender.........................................................................................................................276
Wochenkalender_halb...............................................................................................................279
Halbjahreskalender...................................................................................................................281
Zeitplan.....................................................................................................................................283
GruppeMultiUebersicht.............................................................................................................286
Monat........................................................................................................................................289
Monatsansicht...........................................................................................................................290
Date_2_SQLDate.......................................................................................................................291
Date_2_Datum..........................................................................................................................291
SQLDate_2_Datum....................................................................................................................292
Date_2_Monat...........................................................................................................................292
Datum_Ostersonntag................................................................................................................293
FeiertageArray.......................................................................................................................... 294
BerichtZeilenhoeheAuto............................................................................................................294
Bericht_Start.............................................................................................................................295
Backup.............................................................................................................................................298
Datenbankbackup.....................................................................................................................298
Backup_sofort...........................................................................................................................299
Eingabe............................................................................................................................................299
DatenSpeichern........................................................................................................................300
DatenAktualisieren_Termine......................................................................................................300
DatenAktualisieren_Terminort_Termine.....................................................................................300
LetzterDatensatz.......................................................................................................................301
LetzterDatensatz_Termine_Kalender.........................................................................................301
Anfangswert_Datum..................................................................................................................302
Monatsansicht_aktualisieren.....................................................................................................302
Monat_aktualisieren..................................................................................................................304
Termindaten_uebertragen.........................................................................................................304
GruppeMultiSpeichern...............................................................................................................306
DialogStart................................................................................................................................306
DialogEnde................................................................................................................................307
Export.............................................................................................................................................. 307

Ical_Export................................................................................................................................307
Export_Start..............................................................................................................................309
Import.............................................................................................................................................. 310
Ical_Import................................................................................................................................310
Ical_Timestamp.........................................................................................................................313
Import_Start..............................................................................................................................313
LetzterSonntag..........................................................................................................................314
Filter.................................................................................................................................................315
Wartung...........................................................................................................................................315
Tabellenindex_runter.................................................................................................................315
Tabellenstart............................................................................................................................. 316
Dialog Datumseingabe.....................................................................................................................316
Vereinsverwaltung............................................................................................................318
Einführung............................................................................................................................................ 319
Tabellen.................................................................................................................................................319
Zentrale Mitgliedstabelle.................................................................................................................320
Adresse............................................................................................................................................323
tbl_Adresse................................................................................................................................323
tbl_Ort.......................................................................................................................................324
Konto................................................................................................................................................326
tbl_Konto...................................................................................................................................326
tbl_Bank....................................................................................................................................327
Vorstand...........................................................................................................................................328
tbl_Mitglied_VorstandPosten.....................................................................................................328
tbl_VorstandPosten....................................................................................................................329
Schlüssel..........................................................................................................................................330
tbl_MitgliedSchluessel...............................................................................................................331
tbl_Schluessel...........................................................................................................................332
tbl_SchluesselAnzahl.................................................................................................................332
Abteilung..........................................................................................................................................333
tbl_Mitglied_Abteilung...............................................................................................................334
tbl_Abteilung.............................................................................................................................334
Beitrag............................................................................................................................................. 335
tbl_Gruppe_Beitrag...................................................................................................................336
tbl_Beitrag.................................................................................................................................336
tbl_Beitrag_Staffel.....................................................................................................................337
Boot................................................................................................................................................. 338
tbl_Boot.....................................................................................................................................339
tbl_BootArt................................................................................................................................341
tbl_BootStaender......................................................................................................................341
tbl_BootHalle.............................................................................................................................342
Tabellen ohne Relationen.................................................................................................................343
tbl_Altergruppen.......................................................................................................................343
tbl_Einstellungen.......................................................................................................................343
tbl_Filter....................................................................................................................................344
Ansichten..............................................................................................................................................346
viw_Abteilungen_Mitglied.................................................................................................................346
viw_Adresse_Num_Namen...............................................................................................................347
viw_Anschrift....................................................................................................................................348
viw_Beitrag...................................................................................................................................... 350
viw_Beitrag_Gruppe_kalkuliert.........................................................................................................355
viw_Boote_Mitglied..........................................................................................................................357
viw_Filter_Mitglieder_komplett_AusEin.............................................................................................358
viw_Gruppe......................................................................................................................................360
viw_Mitglieder_komplett..................................................................................................................362
viw_Reports......................................................................................................................................364
viw_Schluessel_Mitglied...................................................................................................................366
viw_ServVar..................................................................................................................................... 367

viw_Statistik_Alter_Detail.................................................................................................................368
Abfragen...............................................................................................................................................369
qry_Adresse_Gruppe........................................................................................................................370
qry_Anschriften_2spaltig..................................................................................................................370
qry_Anschriften_3spaltig..................................................................................................................371
qry_Beitraege...................................................................................................................................373
qry_Bootsstaender...........................................................................................................................374
qry_Filter..........................................................................................................................................375
qry_Filter_Austritt.............................................................................................................................376
qry_Filter_Gruppe.............................................................................................................................378
qry_Filter_Mitglied............................................................................................................................378
qry_Filter_Vorstand...........................................................................................................................379
qry_Forms........................................................................................................................................ 380
qry_Jubilaeum..................................................................................................................................380
qry_Mitglied_Konto...........................................................................................................................382
qry_Mitgliedsdauer.......................................................................................................................... 383
qry_Schluessel_Uebersicht...............................................................................................................384
qry_Vorstand_aktuell........................................................................................................................385
qry_Vorstand_aktuell_lt_Satzung......................................................................................................386
Formulare..............................................................................................................................................387
frm_Beitrag......................................................................................................................................387
frm_Berichte.................................................................................................................................... 388
frm_Boot.......................................................................................................................................... 390
frm_Einstellungen............................................................................................................................392
frm_Mitglied.....................................................................................................................................394
frm_Schluessel.................................................................................................................................399
frm_Vorstand....................................................................................................................................400
Berichte................................................................................................................................................ 401
rpt_Anschriften_Tabelle....................................................................................................................401
rpt_Austritt.......................................................................................................................................402
rpt_Beitraege...................................................................................................................................404
rpt_Beitraege_Konto.........................................................................................................................406
rpt_Bootstaender_Belegung.............................................................................................................407
rpt_Etiketten_2_Spalten...................................................................................................................408
rpt_Etiketten_3_Spalten...................................................................................................................408
rpt_Jubilaeum_Ehrenurkunde...........................................................................................................409
rpt_Mitglieder_AusEin......................................................................................................................411
rpt_Mitglieder_AusEin_Konto............................................................................................................413
rpt_Mitglieder_komplett...................................................................................................................415
rpt_Mitglieder_komplett_Konto.........................................................................................................417
rpt_Mitgliedsdauer...........................................................................................................................418
rpt_Schluesseluebersicht.................................................................................................................419
rpt_Statistik......................................................................................................................................420
rpt_Vorstand_aktuell........................................................................................................................421
rpt_Vorstand_aktuell_lt_Satzung......................................................................................................422
Makros.................................................................................................................................................. 423
Backup.............................................................................................................................................423
DatabaseBackup.......................................................................................................................423
DatabaseStart...........................................................................................................................424
FilterIDSet.................................................................................................................................424
Preferences...............................................................................................................................425
Design..............................................................................................................................................426
PreferencesHide........................................................................................................................426
BoolChange...............................................................................................................................427
BoolChangeKey.........................................................................................................................427
BoolStart...................................................................................................................................428
AfterRecordChange...................................................................................................................428
Group........................................................................................................................................429

Druck...............................................................................................................................................431
ReportStart................................................................................................................................431
Eingabekontrolle..............................................................................................................................432
NewBoatAllowed.......................................................................................................................432
SelectBoat.................................................................................................................................433
SelectBoatRack.........................................................................................................................434
SelectSQL..................................................................................................................................434
SelectGroup..............................................................................................................................435
BoardFilterSelect.......................................................................................................................436
SelectBoard...............................................................................................................................437
SelectKey.................................................................................................................................. 438
IBAN_Test..................................................................................................................................438
IBAN_Length_Change................................................................................................................439
Filter.................................................................................................................................................440
Filtern........................................................................................................................................440
Navigation........................................................................................................................................441
FormStart..................................................................................................................................441
Navigation.................................................................................................................................441
Speichern.........................................................................................................................................443
ListboxContentChange..............................................................................................................443
NewGroup.................................................................................................................................443
ShowTableControl......................................................................................................................444
SubscriptionChanged................................................................................................................444
SubscriptionSave......................................................................................................................444
Wartung...........................................................................................................................................445
Tabellenindex_runter.................................................................................................................445
Tabellenstart............................................................................................................................. 446
Tabellenbereinigung..................................................................................................................446
Tabellenbereinigung_Ort...........................................................................................................447
Tabellenbereinigung_Bank........................................................................................................447
ViewsErstellen...........................................................................................................................447

Vorwort

Vorwort13

Diese Sammlung verschiedener Datenbanken wird laufend erweitert. Sie ist aufgrund vonNachfragen in Foren und Mailinglisten entstanden.
Nahezu alle Datenbanken existieren sowohl in einer Fassung für die interne HSQLDB als auch für
die interne FIREBIRD Datenbank. Die internen HSQLDB Datenbanken wurden vom Code her soweit angepasst, dass bei einer Migration nach FIREBIRD möglichst wenig Probleme auftreten. So
taucht z.B. die Funktion IFNULL nicht mehr auf, weil sowohl in der HSQLDB als auch in FIREBIRD
die Funktion COALESCE genau zum gleichen Ergebnis führt, IFNULL aber für FIREBIRD unbekannt
ist. Eine Liste der Funktionen, die bei der Migration von der einen zu der anderen Datenbank
berücksichtigt werden müssen, ist im Handbuch aufgeführt.
Gerade im Umgang mit Datums- und Zeitwerten gäbe es in FIREBIRD im Verhältnis zur alten
HSQLDB, die in Base integriert ist, große Vorteile. Gegebenenfalls sind dann Anmerkungen zu
alternativen Lösungen in der Beschreibung vorhanden.
Zur besseren Übersicht ist in dieser Beschreibung die Datenbankbezeichnung immer grün
gekennzeichnet.
Folgende Kennzeichnungen sind in diesem Text zu finden:
HSQLDB oder FIREBIRD

Datenbanknamen werden mit Kapitälchen und in grüner
Farbe geschrieben

HSQLDB oder FIREBIRDDie Funktion ist für die entsprechende Datenbank nicht verfügbar. Eine einfache Codeänderung reicht nicht aus, um die
Funktion zu ermöglichen.

"Tabellenname" und "Feldname"Wie beim SQL-Code werden Tabellennamen und Feldnamen
mit doppelten Anführungszeichen maskiert'Robert' oder '1'

Werte aus Abfragen werden mit einfache Anführungszeichen
maskiert.

«Zeitmessung»

Andere Anführungszeichen dienen der allgemeinen Hervorhebung

Vorwort14

Zeitmessung

Zeitmessung

15

Einführung
Zeitmessungen könnten z. B. für die Durchführung von Sportveranstaltungen genutzt werden.
Gedacht ist hier nicht an große Veranstaltungen, wo so etwas sowieso elektronisch erfolgt. Vielmehr könnte es um Veranstaltungen im Schulbereich gehen.
Der PC ersetzt hier die Stoppuhr und das Niederschreiben der Ergebnisse. Er kann außerdem
auch die Auswertung erstellen sowie Urkunden drucken. Im Vordergrund soll hier die Funktion
der Stoppuhr stehen.
Um mit der Datenbank «Zeitmessung»1 auch Zeitmessungen durchführen zu können werden in
geringem Umfang Makrokenntnisse gebraucht.

Tabellen
Die Datenbank besteht im sparsamsten Aufbau aus zwei Tabellen.

Start
Datenziel
Formular: Einzelmessung_System, Einzelmessung_Zeit, Meldung_Liste_Teilnehmer,
Meldung_Zeit_Teilnehmer, Nur_Start, Nur_Ziel, Start_Ziel_Platz
Makro:

Zeitmessung_Ende_System, Zeitmessung_Ende

Abfrage:

Abfrage_Unterwegs, Abfrage_Ziel

Ansicht, Bericht:

1keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Startzeit

Datum/Zeit

Fremdschlüssel für "Zeitspeicher"."Startzeit". Hier wird Tag
und Uhrzeit des vorgesehenen Startes festgehalten. Personen, die zusammen starten, haben die gleiche Startzeit.
Eingabe erforderlich: Nein

Name

Text

Der Name der Person, die startet. Bei Schulwettkämpfen
wären hier Zusätze wie Klasse, Schule oder gegebenenfalls
auch Geburtsdaten notwendig.
Eingabe erforderlich: Ja

Dauer
HSQLDB

Integer

Die Dauer wird in Millisekunden gemessen. Es handelt sichdabei um einen Zahlenwert ohne Nachkommastellen, keinen
Wert wie bei einer Uhrzeit.
Eingabe erforderlich: Nein

Zeit
HSQLDB

Datum/Zeit

Die Zeitdauer wird hier als Zeit im Format
Stunden:Minuten:Sekunden,Hundertstelsekunden aus dem
Formular «Einzelmessung_Zeitfeld» übernommen. Der
Datumsteil wird durch ein Makro hinzugefügt.
Eingabe erforderlich: Nein

Zeit
FIREBIRDZeit

Die Zeitdauer wird hier mit Hundertstelsekunden abgespeichert. Dies ist in der HSQLDB nicht möglich.
Eingabe erforderlich: Nein

Beispieldatenbank Beispiel_Zeitmessung.odb

Zeitmessung

16

Zeitspeicher
Datenziel
Formular: Einzelmessung_System, Einzelmessung_Zeit, Meldung_Liste_Teilnehmer,
Meldung_Zeit_Teilnehmer, Nur_Ziel, Start_Ziel_Platz
Makro:

Zeitmessung_Start_System, Zeitmessung_Ende_System, Zeitmessung_Start,
Zeitmessung_Ende

Abfrage:

Abfrage_Start, Abfrage_Unterwegs, Abfrage_Ziel

Ansicht, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

Startzeit

Datum/Zeit

Die Startzeit ist Primärschlüssel dieser Tabelle. Sie ist gleichzeitig Fremdschlüssel der Tabelle "Start". Damit wird für alle
Personen, die die gleiche Startzeit haben, auch bei derDurchführung der einheitliche Start festgelegt.

Zeitspeicher
HSQLDB

Integer

Hier wird ein interner Basic-Wert für die tatsächliche Startzeit
abgespeichert. Der Wert wird erst dann eingefügt, wenn der
Button Start betätigt wurde.
Eingabe erforderlich: Nein

Timestamp
HSQLDB

Datum/Zeit

Hier wird die tatsächliche Startzeit zusammen mit Datum
abgespeichert. Wird statt "Zeitspeicher" genutzt, wenn direkteine Zeit gemessen wird.
Eingabe erforderlich: Nein

Zeitspeicher
FIREBIRD

Zeit

Direkte Eingabe der Startzeit. In der HSQLDB nicht möglich.
Eingabe erforderlich: NeinDie Datum/Zeit-Felder in der HSQLDB können, sofern sie durch die GUI erstellt wurden, keine
Bruchteile einer Sekunde darstellen. Sie müssen erst mit dem Befehl
001 ALTER TABLE "Start" ALTER COLUMN "Zeit" TIMESTAMP(6)001 ALTER TABLE "Zeitspeicher" ALTER COLUMN "Timestamp" TIMESTAMP(6)für die Aufnahme von 1/1000 Sekunden vorbereitet werden.
Beide Tabellen werden miteinander in den Beziehungen verbunden. Es muss also eine Startzeit
festgelegt werden, bevor eine Person in die Startliste übertragen werden kann.

Zeitmessung

17Die Beziehung zwischen den Tabellen wird mit einem rechten Mausklick auf die Verbindungslinie genauer definiert. Wird im "Zeitspeicher" eine "Startzeit" geändert, so werden alle "Startzeit"-Einträge in der Tabelle "Start" entsprechend angepasst. Dies ist über die Update Optionen→ Kask. Update gewährleistet. Wird eine "Startzeit" aus der Tabelle "Zeitspeicher" gelöscht, so
sollen Einträge, die sich aus der Tabelle "Start" genau auf die gelöschte "Startzeit" beziehen,geleert werden. Dies wird über die Löschoptionen → Null setzen erreicht.
Die tatsächlichen Startzeiten müssen nicht mit der eingetragenen Startzeit übereinstimmen.
Auch wenn eine eingetragene Startzeit z. B. 10:00 Uhr ist, kann tatsächlich um 15:13 Uhrgestartet und trotzdem ein genaues Messergebnis erwartet werden.

AbfragenAbfrage_Start
Datenquelle
Tabelle:Zeitspeicher

Datenziel
Formular: Nur_Start, Start_Ziel_Platz, Start_Ziel_Gruppe

Zeitmessung

18

HSQLDB
001 SELECT * FROM "Zeitspeicher" WHERE "Timestamp" IS NULLFIREBIRD
001 SELECT * FROM "Zeitspeicher" WHERE "Zeitspeicher" IS NULL
Starten darf natürlich nur die Gruppe, die nicht bereits unterwegs ist.
Mit dem Start wird ein Wert in das Feld "Timestamp" (HSQLDB) bzw. "Zeitspeicher" (FIREBIRD)
geschrieben. Nur die Startzeit kann noch genutzt werden, bei der dieses Feld leer ist.
Die HSQLDB hat daneben noch das Feld "Zeitspeicher", das aber nur einen Integer-Wert für die
Zeitmessung über Makro mit Systemticks speichert.

Abfrage_Unterwegs
Datenquelle
Tabelle:Zeitspeicher, Start

Datenziel
Formular: Start_Ziel_Platz, Start_Ziel_Gruppe

001 SELECT "Start".*002 FROM "Start", "Zeitspeicher"
003 WHERE "Start"."Startzeit" = "Zeitspeicher"."Startzeit"
004 AND "Start"."Zeit" IS NULL
005 AND "Zeitspeicher"."Timestamp" IS NOT NULL
Diese Abfrage unterscheidet sich bei FIREBIRD nur dadurch, dass statt des "Timestamp"-Feldes
in Zeile 5 das "Zeitspeicher"-Feld angesprochen wird.
Diese Abfrage listet alle die Personen auf, die bereits gestartet (Zeile 5), aber noch unterwegs
sind (Zeile 4). Ihre Zielzeit wurde noch nicht gemessen.

Abfrage_Ziel
Datenquelle
Tabelle:Zeitspeicher, Start

Datenziel
Formular: Nur_Ziel, Start_Ziel_Platz, Start_Ziel_Gruppe
Bericht:

Ergebnisliste, Ergebnisliste_Zeit, Urkunden

Variante für HSQLDB mit dem Feld "Dauer" und der Zusammenstellung eines Zeitformates:
001 SELECT "a".*,002 ( SELECT "Timestamp" FROM "Zeitspeicher" WHERE "Startzeit" ="a"."Startzeit" ) AS "Zeitspeicher",003 ( SELECT COUNT( "ID" ) FROM "Start" WHERE "Zeit" <= "a"."Zeit" AND"Startzeit" = "a"."Startzeit" ) AS "Platz"
004 FROM "Start" AS "a"005 WHERE ( SELECT "Timestamp" FROM "Zeitspeicher" WHERE "Startzeit" ="a"."Startzeit" ) IS NOT NULLÄnderung für FIREBIRD:
001 SELECT "a".*,002 ( SELECT "Zeitspeicher" FROM "Zeitspeicher" WHERE "Startzeit" ="a"."Startzeit" ) AS "Zeitspeicher",
Zeitmessung

19003 ( SELECT COUNT( "ID" ) FROM "Start" WHERE "Zeit" <= "a"."Zeit" AND"Startzeit" = "a"."Startzeit" ) AS "Platz",
004 CAST( "a"."Zeit" AS TIMESTAMP ) AS "RBZeit
005 FROM "Start" AS "a"006 WHERE ( SELECT "Zeitspeicher" FROM "Zeitspeicher" WHERE "Startzeit" ="a"."Startzeit" ) IS NOT NULL
Hier werden alle Felder der Tabelle "Start" angezeigt (Zeile 1). Dabei wird der Tabelle "Start" ein
Alias "a" zugewiesen (Zeile 4 bzw. 5), damit von den Unterabfragen auf die äußere Abfrage
zugegriffen werden kann. Angezeigt werden sollen nur die Datensätze, die einen Eintrag in derTabelle "Zeitspeicher" im Feld "Timestamp" (HSQLDB) bzw. "Zeitspeicher" (FIREBIRD) haben (Zeile
2 und 5 bzw. 6). Dieses Feld ist über eine korrelierende Unterabfrage eingebunden, weil für die
Ermittlung des Platzes sowieso eine korrelierende Unterabfrage erstellt werden musste und die
Abfrage sonst auch nicht mehr über ein Formular beschrieben werden könnte.
Der Platz wird so ermittelt, dass alle die Datensätze gezählt werden, die einen kleineren Eintrag
bei dem Feld "Zeit" haben und gleichzeitig die gleiche "Startzeit" aufweisen können wie deraktuelle Datensatz (Zeile 3). Die Beziehung zum aktuellen Datensatz stellt hier der Alias "a"her.
Für die Darstellung von Zeiten mit Nachkommastellen im Report-Builder ist bei FIREBIRD die
Zeile 4 notwendig. Hier wird nur für diesen Zweck der Datentyp von TIME zu TIMESTAMP konvertiert. Der Report-Builder zeigt bis einschließlich Version LO 6.4 leider nur Felder des Typs
TIMESTAMP mit Zeiten im Bereich von unter einer Sekunde an. Der Bug ist bisher vermutlich
nicht aufgefallen, weil die interne HSQLDB eben Zeitfelder nicht mit kleineren Einheiten als einer
Sekunde abspeichert.

Formulare
Die Messung erfolgt innerhalb von Formularen. Ein Formular zeigt nur die Messung eines Wertes. In den folgenden Formularen werden dann auch Mitbewerber angezeigt. Schließlich wird
eine komplette Zeitmessung für ein Rennen ausgestellt, bei dem gleichzeitig gestartete Teilnehmer in eine Rangliste eingeordnet werden. Hier zuerst einmal die einfachste Variante eines
Formulars mit einfachen Feldern.

Einzelmessung_System

1 Formular (Tabelle: Start)
1 Listenfeld Startzeit (Tabelle: Zeitspeicher)

Zeitmessung

20Makros in Feldeigenschaften1

Schaltfläche Start (Aktion ausführen)

Module1.Zeitmessung_Start_System

1

Schaltfläche Stop (Aktion ausführen)

Module1.Zeitmessung_Ende_System

Das Formular sieht erst einmal recht karg aus. Die ursprünglich geplante Startzeit ist zusammen mit dem Datum als Zeitstempel abgespeichert. Da für die Datenbank über die oben
genannten 2 Tabellen auch andere Funktionen gewährleistet werden sollen, kann hier nur danneine Startzeit/Startdatum angegeben werden, wenn die entsprechende Zeit bereits in der anderen Tabelle vorhanden ist. Deshalb ist für die Startzeit ein Listenfeld vorgesehen. Das Listenfeld
liest die "Startzeit" aus der Tabelle "Zeitspeicher" und trägt genau diesen Wert in die Tabelle"Start" als "Startzeit" ein. Für die Messung ist diese Zeit wegen der Abspeicherung der Startzeit
in der zweiten Tabelle zwingend notwendig.
Start startet die Zeitmessung. Mit Stop wird die Zeitmessung beendet und die gemessene Zeit
in dem Feld «Dauer» abgespeichert.
Die Zeitmessung erfolgt hier rein über ein Makro, das als Messhilfe die Systemticks des Rechners nutzt. Dies erlaubt eine Angabe im Millisekundenbereich. Der abgespeicherte Wert ist
schließlich eine Integer-Ganzzahl, also kein Zeitwert im Sinne der Datenbank.
Diese Formular ist so nur in der HSQLDB realisiert, da in FIREBIRD der Feldtyp TIME bereits die
Abspeicherung von 1/1000 Sekunden erlaubt.

Einzelmessung_Zeit
Dieses Formular unterscheidet sich von dem Formular «Einzelmessung_System» äußerlich nur
gering. Statt der Angabe einer Zeit in Millisekunden erfolgt hier die Zeitangabe in der Schreibweise Minuten:Sekunden,Hundertstelsekunden.

1 Formular (Tabelle: Start)
1 Listenfeld Startzeit (Tabelle: Zeitspeicher)
Makros in Feldeigenschaften1

Schaltfläche Start (Aktion ausführen)

Module1.Zeitmessung_Start_Einzel

1.1

Schaltfläche Stop (Aktion ausführen)

Module1.Zeitmessung_Ende_Einzel

Das Feld "Zeit" in der Tabelle "Start" ist in der HSQLDB als «Datum/Zeit [TIMESTAMP]» - Feld definiert. Die reinen Zeitfelder sind in der HSQLDB nicht in der Lage, Zeiten im Bereich unterhalb
einer Sekunde zu speichern. Leider sind aber auch die TIMESTAMP-Felder von der Standardeinstellung in Base nicht dafür vorgesehen. Hier muss entsprechend über SQL nachgeholfen werden. In Extras → SQL ist einzugeben:
001 ALTER TABLE "Start" ALTER COLUMN "Zeit" TIMESTAMP(6)Jetzt ist das Feld in der Lage, auch im Millisekundenbereich Zeiten aufzunehmen.
Zeitmessung

21Die FIREBIRD-Version dieser Datenbank greift dagegen direkt auf ein Feld des Typs TIME zu, dasvon vornherein Zeiten im 1/1000-Sekunden-Bereich speichern kann.
Das nächste Problem für ein Formular besteht darin, dass so ein Feld sowohl ein Datum als
auch eine Zeit abspeichert. Deshalb erstellt Base auch im Formularassistenten oder bei dem
Weg über das direkte Hinzufügen von Feldern für ein Datum/Zeit – Feld zwei Eingabemasken.
Eine dient zur Eingabe des Datums, eine andere zur Eingabe der Zeit. Diese ist für eine einfache Zeitangabe mit Nachkommastellen nicht brauchbar. Stattdessen muss ein Feld her, dassbeides darstellen könnte und außerdem in der Lage ist, auch noch die Hundertstel Sekunden
mit aufzunehmen. Deshalb wird das Feld als «Formatiertes Feld» erstellt. Dieses Feld wird hiernur zur Anzeige benutzt und deswegen in Eigenschaften → Allgemein → Nur Lesen auf «Ja»
gestellt. Zur Eingabe von Werten ist es so auf dem direkten Wege nicht geeignet, da neben der
Zeiteingabe eine Datumseingabe erwartet wird, das Feld aber bei der vorliegenden Formatierung diese Datumseingabe nicht mitliefert.
Die Ermittlung der korrekten Zeit muss wieder über ein Makro geleistet werden.

Meldung_Liste_Teilnehmer

1 Teilnehmer (Tabelle: Start)
1 Listenfeld Startzeit (Tabelle: Zeitspeicher)
In diesem Formular werden die Teilnehmer eingegeben. Die Startnummer ist gleich dem Primärschlüsselfeld. Der Name wird einfach komplett eingetragen. Für Sportevents noch nicht brauchbar – aber das ist schnell durch zusätzlich Felder in der Tabelle zu beheben. Wichtig ist lediglich,dass den startenden Personen eine Startzeit zugewiesen wird. Hier ist das Feld, auf dem
Screenshot so nicht erkennbar, als Listenfeld ausgelegt. Es liest direkt die Schlüsselwerte aus
der Tabelle "Zeitspeicher". So können in diesem Formular fehlerfrei und schnell die Startzeiten
den Personen zugeordnet werden.

Zeitmessung

22Meldung_Zeit_Teilnehmer

1 Formular (Tabelle: Zeitspeicher)

1.1 Teilnehmer (Tabelle: Start)

Fehlt eine Startzeit, so ist das vorhergehenden Formular im Nachteil. Die Startzeit muss vorgegeben sein, damit ein Teilnehmer auch starten kann. Sie kann aber nicht zugewiesen werden,
wenn sie im Listenfeld nicht auftaucht.
Hier hilft die einfache Formular – Unterformular – Konstruktion. Im Hauptformular wird die Startzeit eingegeben, im Unterformular dazu tauchen alle die Personen auf, die zu dieser Zeit starten wollen.
Der Formularsprung von Hauptformular zu Unterformular muss hier mit einem kleinen Trickbewerkstelligt werden, da das Hauptformular nur ein Feld hat. Bewegt sich der Nutzer mit dem
Tabulator normal durch ein Formular, so geht der Cursor nach dem letzten Eingabefeld zum ersten Eingabefeld des nächsten Datensatzes im Formular, nicht aber zum Unterformular. Um zum
Unterformular zu kommen müssen 3 Bedingungen erfüllt sein:
1. Das Hauptformular muss mit allen Feldern im Formularnavigator vor dem Unterformularerscheinen.
2. Das Hauptformular braucht mindestens 2 Felder, die für den Tabulatorsprung sortiert
werden können.
3. Die Sortierung der Felder erfolgt über Aktivierungsreihenfolge → Automatische Sortierung.
In der Tabelle "Zeitspeicher" befinden sich die Felder "Startzeit" und "Zeitspeicher". Nur das
Feld "Startzeit" ist oben im Formular zu sehen. Zum Erstellen des Formulars wurde aber auchdas Feld "Zeitspeicher" vorübergehend in das Formular mit aufgenommen. Danach wurde das
Unterformular erstellt und die Automatische Sortierung aufgerufen. Jetzt ist das Unterformularso eingebunden, dass der Tabulatorsprung vom Hauptformular zum Unterformular geht. Das
Feld "Zeitspeicher" kann entfernt werden und der Sprung zum Unterformular funktioniert auchweiter.
Aus dem Unterformular heraus geht es übrigens wieder mit Strg + Tab . Nur muss dann dochanschließend beim Wechsel der Startzeit einmal die Navigationsleiste aufgesucht werden,
damit ein nächster Datensatz im Hauptformular eingegeben werden kann.

Zeitmessung

23

Nur_Start
Bei den bisherigen Einzelmessungen kann es ohne weiteres passieren, dass ein Start mehrmals
hintereinander eingegeben, also die Zeit nicht korrekt gemessen wird. Dies lässt sich vermeiden, wenn nach dem erfolgten Start ein erneutes Beschreiben des Start-Wertes unmöglich
gemacht wird.
Aus dem folgenden Formular verschwinden nach dem Start die Datensätze der Personen, die
zur gleichen Startzeit unterwegs sind.

1 Zeitspeicher (Abfrage: Abfrage_Start)

1.1 Formular (Tabelle: Start)

Makros in Feldeigenschaften1

Schaltfläche Start (Aktion ausführen)

Module1.Zeitmessung_Start

In dem Unterformular werden die Personen angezeigt, die zu der obigen Startzeit starten wollen. Es wäre auch möglich, hier zu der Gruppe noch schnell ein paar Personen neu hinzuzufügen, die sich nicht rechtzeitig gemeldet haben, jetzt aber doch noch an den Start gehen wollen.
In dem Hauptformular wird der Start für alle untenstehenden Personen durch Start ausgelöst.
Das Hauptformular beruht auf einer Abfrage, die dafür sorgt, dass hier nur die Gruppen zur Verfügung stehen, die noch keinen Ersteintrag bei "Zeitspeicher"."Timestamp" (HSQLDB) oder
"Zeitspeicher"."Zeitspeicher" (FIREBIRD) haben, also noch nicht gestartet sind.
Der Startwert wird dann in die Tabelle "Zeitspeicher" als Datum/Zeit (HSQLDB) oder als Zeit
(FIREBIRD) per Makro übertragen.
Diese recht einfache Abfrage hätte auch direkt im Formular «Zeitspeicher» als Filterwert eingegeben werden können:
rechte Maustaste über dem Formularnamen "Zeitspeicher" → Eigenschaften → Daten → Filter
In das freie Feld kann eingetragen werden: ("Zeitspeicher" IS NULL)...nur hat das natürlich den Nachteil, dass dieser Filter auch ausgeschaltet werden kann undprompt Rennen wieder verfügbar wären, die eigentlich schon gestartet wurde.

Zeitmessung24Nur_Ziel
Dieses Formular muss wesentlich mehr Funktionen erfüllen als die vorhergehenden Formulare.
Es soll dazu dienen, nur nach Auswahl der Startnummer den Zieldurchgang zu regeln. Dabei
soll es anzeigen, welche Zeit die gerade das Ziel passierte Person benötigt hat. Außerdem sollnoch die Einordnung in Form einer Rangliste erfolgen.

1 Ziel (Abfrage: Abfrage_Ziel)

1.1 Platz (Abfrage: Abfrage_Ziel)

1 Listenfeld Startzeit (Tabelle: Zeitspeicher, Start)
Makros in Feldeigenschaften
1.1

Listenfeld «Startnummern» (Modifiziert)

Module1.Zeitmessung_Ende_Listenfeld

Hier ist gerade Uwe Maier als 2. Person ins Ziel gekommen. Die Startnummer ("ID") von Uwe
Maier wurde im Listenfeld links oben beim Zieldurchlauf angeklickt. Dadurch wurde ein Makro
ausgelöst, der Name zu der Startnummer und die Zeit wurde angezeigt, gleichzeitig die Positionierung in dem Feld der Personen, die zur gleichen Zeit gestartet waren. Das Listenfeld zeigt
jetzt nur noch die Startnummer der Personen, die noch unterwegs sind, an.
Das Listenfeld «Startnummer» zeigt den gleichen Wert an, den es auch an die darunterliegende
Tabelle weitergibt. Die Datenquelle für das Listenfeld ist auf SQL eingestellt.
001 SELECT "Start"."ID"002
FROM "Start", "Zeitspeicher"
003
WHERE "Start"."Startzeit" = "Zeitspeicher"."Startzeit"
004
AND "Zeitspeicher"."Zeitspeicher" IS NOT NULL
005
AND "Start"."Zeit" IS NULL
Diese Abfrage stellt einmal das Feld "Start"."ID" dar. Die Einstellung Eigenschaften des Listenfeldes → Daten → Gebundenes Feld = 0 bewirkt, dass genau dieses Feld Daten an die darunterliegenden Tabelle überträgt. Bei älteren Versionen von LO muss mit der Einstellung Gebundenes Feld = 1 und einer doppelten Abfrage des Feldes "Start"."ID" für die Weitergabe des Wertes
gesorgt werden.
Die Abfrage stellt zuerst einmal die Beziehung zwischen den Tabellen "Start" und "Zeitspeicher"her (Zeile 3). Dann muss die Bedingung erfüllt sein, dass "Zeitspeicher"."Zeitspeicher" nicht
leer ist (Zeile 4). Das bedeutet, dass eben ein Start bereits erfolgt ist. Und schließlich sollen nur
die Datensätze erscheinen, bei denen in "Start"."Zeit" bisher kein Eintrag existiert (Zeile 5).
Datensätze von Personen, deren Zeit schon gemessen wurde, fallen also auch heraus.
Zeitmessung

25Das Formular "Ziel" hat als Datengrundlage eine Abfrage, und zwar "Abfrage_Ziel". Diese
Abfrage wird zusätzlich über Formular-Eigenschaften → Daten → Filter eingeschränkt, so dassnur der aktuelle Datensatz angezeigt wird, der dem Eintrag in der Tabelle "Filter" entspricht:
001 ("a"."ID" = (SELECT "Start_ID" FROM "Filter" WHERE "ID" = TRUE))
Das Unterformular «Platz» greift auf die gleiche Abfrage zu. Hier ist die Filterung dann allerdings anders angelegt. Unter Formular-Eigenschaften → Daten → Filter steht:
001 ( "a"."Dauer" IS NOT NULL )Es werden nur die Werte aus der Abfrage angezeigt, die einen Eintrag im Feld "a"."Dauer"haben. Es werden also nur die Personen angezeigt, die auch tatsächlich mit einer entsprechendvermerkten Zeit das Ziel erreicht haben.
Unter Formular-Eigenschaften → Daten → Sortierung steht:
001 "Platz" ASCDies bedeutet, dass Datensätze nach der erreichten Platzierung, vermerkt im Feld "Platz",
angezeigt werden sollen.

Zeitmessung

26

Start_Ziel_Platz

1 Zeitspeicher (Abfrage: Abfrage_Start)

1.1 Formular (Tabelle: Start)

2 Ziel (Abfrage: Abfrage_Ziel)

2.1 Platz (Abfrage: Abfrage_Ziel)

2 Listenfeld Startzeit (Tabelle: Zeitspeicher, Start)
Makros in Feldeigenschaften1

Schaltfläche Start (Aktion ausführen)

Module1.Zeitmessung_Start_Ziel

2

Listenfeld «Startnummern» (Modifiziert)

Module1.Zeitmessung_Ende_Listenfeld

Dieses Formular ist lediglich eine Kombination der beiden Formulare "Nur_Start" und "Nur_Ziel".
Die Übersicht zeigt schon, dass diese beiden Formulare nebeneinander auf der Formularfläche
liegen. Es kann jetzt also im gleichen Formular gestartet und gestoppt werden.

Zeitmessung27Start_Ziel_Gruppe

1 Zeitspeicher (Abfrage Abfrage_Start)
2 Ziel (Abfrage Abfrage_Unterwegs)
3 Platz (Abfrage Abfrage_Ziel)
Makros in Feldeigenschaften1

Schaltfläche Start (Aktion ausführen)

Module1.Zeitmessung_Start_Gruppe

2

Schaltfläche Stop (Aktion ausführen)

Module1.Zeitmessung_Ende_Gruppe

Im Formular «Zeitspeicher» kann hier gegebenenfalls noch die Startzeit angepasst werden.
Ansonsten ist das komplette Formular aber vor weiteren Eingaben geschützt, wie ein Blick auf
die Tabellenkontrollfelder ohne zusätzliche Eingabezeile anzeigen.
Über Start wird an die Tabelle "Zeitspeicher" die Startzeit übergeben. Im Formular "Ziel" werden dann direkt alle gestarteten Personen mit den Startnummern "ID" angezeigt.
Kommt eine Person auf das Ziel zu, so wird der Startsatz markiert. Beim Zieldurchlauf wird
Stop gedrückt. Dadurch wird ein Makro ausgelöst, das die Startzeit von der Zielzeit abzieht und
als "Zeit" in die Tabelle "Start" überträgt. Anschließend wird Das Formular "Ziel" und das Formular "Platz" neu eingelesen. Die Person, die gerade das Ziel durchlaufen hat, verschwindet
aus dem oberen Tabellenkontrollfeld und taucht mit Zeitangabe und Platzangabe direkt als erster Datensatz im unteren Tabellenkontrollfeld auf.
Das untere Formular «Platz» benötigt die folgenden zusätzlichen Einstellungen:

Zeitmessung

28Formulareigenschaften → Daten → Filter
001 ( ( SELECT COUNT( "ID" ) FROM "Start" WHERE "Zeit" <= "a"."Zeit" AND"Startzeit" = "a"."Startzeit" ) > 0 )Formulareigenschaften → Daten → Sortierung
001 "Startzeit" DESC, "Zeit" DESC
Der Filter sorgt dafür, dass nur die Datensätze angezeigt werden, bei denen der Wert für "Platz"
größer als '0' ist. Hier schafft es das Filtermodul nicht, mit dem entsprechenden Alias zu arbeiten. Es benötigt stattdessen den Originalcode aus der Abfrage, der den Platz berechnet. Mitdiesem Code hat jede Person zum Start den Platz '0'.
Die Sortierung zweigt die Einläufe nach der Startzeit (höchster Wert zuerst) und nach der Zeit(höchster Wert zuerst) sortiert an. Die Sortierung auch nach der Startzeit ist notwendig, weil in
dem Tabellenkontrollfeld für das Formular «Platz» die zu unterschiedlichen Zeiten startenden
Personen gemischt aufgelistet werden.

Berichte
Ergebnisliste

Datenquelle
Abfrage:

Abfrage_Ziel

Eine direkte Auswertung der Abfrage "Abfrage_Ziel" stellt die Ergebnisliste dar. Die Felder sindbereits in der Abfrage errechnet, so dass nur noch eine Gruppierung nach der Startzeit undnach der Platzierung notwendig ist. Dieser Bericht wurde nur für das Formular«Einzelmessung_System» erstellt, das eine Abspeicherung der Zeiten als Integer-Zahlen beinhaltet. Deswegen ist der Bericht auch nur in der HSQLBD-Beispieldatenbank vertreten.
Im Entwurf sieht dieser Bericht so aus:

Zeitmessung

29Damit die Bezeichnungen «Platz», «Name» und «Dauer» nur pro Rennen einmal erscheinensind sie in dem Gruppenkopf für die Startzeit verankert worden.
Der Gruppenkopf für das Feld «Dauer» dient nur der Sortierung nach der Zeit. Hier könnte also
genauso gut das Feld «Platz» zur Sortierung genutzt werden. Dieser Gruppenkopf wird in dem
Bericht nicht angezeigt (Eigenschaften Gruppenkopf → Allgemein → Sichtbar: Nein)
Auf jeder Seite erscheint eine Seitennummerierung in der Form Seite … von … .

Ergebnisliste_Zeit

Datenquelle
Abfrage:

Abfrage_Ziel

Dieser Bericht unterscheidet sich von dem vorangehenden Bericht nur dadurch, dass jetzt ausder Datenquelle das Feld "Zeit" statt des Feldes "Dauer" eingebunden wurde. Dadurch kann die
Zeit dann ansprechender so dargestellt werden, dass die Darstellung einem üblichen Format
entspricht.
Da der Report-Builder reine Felder des Typs TIME nicht korrekt mit Nachkommastellen im
Zeitbereich versieht, musste hier bei dem TIME-Feld von FIREBIRD eine Umwandlung zum TIMESTAMP-Feld in der Abfrage erfolgen. Das Feld für FIREBIRD heißt deswegen gesondert "RBZeit".

Zeitmessung

30

Urkunden

Datenquelle
Abfrage:

Abfrage_Ziel

Auch ein Urkundendruck ist problemlos möglich. Sinnvoll ist hier allerdings, den Druckjob möglichst zu vereinfachen. Die einfachste Lösung wäre, nur die veränderlichen Felder im Bericht zu
positionieren und auf einen schön gestalteten Urkundenvordruck zu drucken. Je einfacher der
Bericht gehalten wird, desto schneller baut sich schließlich auch das Fenster für den Druck auf.
Und je weniger grafische Elemente eingefügt sind, desto kürzer ist auch die Druckzeit, die während irgendeiner Veranstaltung sowieso recht knapp ist. Der Aufruf des Urkundendrucks für alle
TeilnehmerInnen ist hier kein Problem. Da geht es eher um die Schnelligkeit, mit der der Report
Builder die Seiten erstellt und der Drucker die Seiten verarbeitet.
Die Blattübersicht ist hier stark verkleinert. Schließlich ist der Druck «Urkunde» im Original auf
eine Schriftart von 110 Punkten gesetzt worden.

Zeitmessung31

In der verkleinerten Ansicht des Editors erscheint das Feld «Name» nicht richtig zentriert. Der
Ausdruck zeigt aber eine entsprechenden saubere Zentrierung.
Für die Ausgabe der Platzierung ist eine kleine Formel erforderlich, da sonst die Zentrierung
Probleme bereitet. Mit [Platz] wird das entsprechende Feld aus der Datenquelle angesprochen,
mit &". Platz" wird an die Platzierung aus dem Datenfeld ein Punkt, ein Leerzeichen und derBegriff «Platz» angefügt.
Damit nur eine Urkunde pro Seite gedruckt wird ist bei Eigenschaften Detail → Allgemein → Seitenumbruch erzwingen: Nach Bereich ausgewählt.

MakrosZum Starten und Stoppen der Zeiten ist jeweils eine Prozedur notwendig. In dieser Datenbank
sind nur deshalb mehrere Prozeduren vorhanden, weil zum einen unterschiedliche Formen der
Zeitmessung genutzt werden, zum anderen aber auch unterschiedliche Formulare vertretensind, die jeweils auf ihre besondere Art und Weise den Zusatzkommandos zum Start oder zum
Abspeichern des Zieldurchlaufs benötigen.

Zeitmessung32Starten und Stoppen der Zeit
Zeitmessung_Start_System
Aufruf aus
Formular: Einzelmessung_System

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016

Zeitspeicher.Zeitspeicher

nur HSQLDBSUB Zeitmessung_Start_System(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECT
DIM stStartzeit AS STRING
DIM stSql AS STRING
DIM loZeit AS LONG
oForm = oEvent.Source.Model.Parent
stStartzeit = Left(oForm.getString(oForm.findColumn("Startzeit")),19)
loZeit = getSystemTicks() ' Gibt die Systemzeit in Millisekunden an
oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
stSql = "UPDATE ""Zeitspeicher"" SET ""Zeitspeicher"" = '"+loZeit+"' WHERE""Startzeit"" = '"+stStartzeit+"'"oSQL_Statement.executeUpdate(stSql)
END SUB

Über das auslösende Ereignis, die Betätigung von Start , wird das Formular ermittelt (Zeile 9).
Aus dem Feld "Startzeit" soll der Zeitstempel für die Startzeit ausgelesen werden (Zeile 10).
Dieser kann auch Nachkommastellen enthalten. Deshalb wird der Wert als String ausgelesen
und berücksichtigt nur die ersten 19 Zeichen (10 Zeichen für das Datum, ein Leerzeichen, 8
Zeichen für die Zeit).
Die momentane Zeit wird über die SystemTicks ermittelt (Zeile 11). Diese geben einen Ganzzahlwert wieder, der beständig durchläuft und jede Millisekunde um einen Wert erhöht wird.
Die Verbindung zur Datenbank wird aus dem Formular ausgelesen und das SQL-Statement
vorbereitet. Die Formulierung für das Statement wird in der Variablen stSql erfasst (Zeile 14).
Hier wird in der Tabelle "Zeitspeicher" das Feld "Zeitspeicher" mit dem Wert der SystemTicks
beschrieben. Dabei wird aber nur das Feld beschrieben, das zu der Startzeit passt. In Zeile 15
wird diese SQL-Formulierung abgeschickt.

Zeitmessung_Ende_System
Aufruf aus
Formular: Einzelmessung_System

Benötigt
Tabelle:
001
002
003
004
005
006
007
008

Zeitspeicher.Zeitspeicher, Start.Dauer

nur HSQLDBSUB Zeitmessung_Ende_System(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECT
DIM inRow AS INTEGER
DIM stID AS STRING
DIM stSql AS STRING
DIM loZeit AS LONG

Zeitmessung

33

009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025

oForm = oEvent.Source.Model.Parent
stStartzeit = Left(oForm.getString(oForm.findColumn("Startzeit")),19)
stID = oForm.getString(oForm.findColumn("ID"))
loZeit = getSystemTicks() ' Gibt die Systemzeit in Millisekunden an
oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
stSql = "SELECT "+loZeit+" - ""Zeitspeicher""FROM ""Zeitspeicher"" WHERE""Startzeit"" = '"+stStartzeit+"'"oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
loZeit = oResult.getLong(1)WEND
stSql = "UPDATE ""Start"" SET ""Dauer"" = '"+loZeit+"' WHERE""ID"" = '"+stID+"'"oSQL_Statement.executeUpdate(stSql)
inRow = oForm.getRow()oForm.reload()
oForm.absolute(inRow)
END SUB

Wie in der vorhergehenden Prozedur werden Formular und Startzeit ausgelesen. Hinzugekommen ist das Auslesen des Feldes "ID" aus der Tabelle "Start" (Zeile 11). Dies ist ja der Primärschlüssel (und gleichzeitig die Startnummer), hinter der die Person steht, für die die Zielzeit
eingetragen werden soll.
Die aktuellen SystemTicks werden in Zeile 12 ausgelesen. Über eine Abfrage wird von diesem
Wert der vorher eingetragene Wert in der Tabelle "Zeitspeicher" abgezogen (Zeile 15). Der Wert
wird in die Variable loZeit in Zeile 18 eingelesen.
Jetzt muss diese Zeit als Zeit für die entsprechende Person noch in die Tabelle "Start" übertragen werden. Dies erfolgt wieder über einen Update-Befehl (Zeile 20).
Zum Schluss wird die Datensatznummer des Formulars bestimmt, das Formular zur Übernahme
der Daten neu geladen und auf die vorher ausgelesene Datensatznummer wieder eingestellt.
Ohne das Auslesen der Datensatznummer würde stattdessen immer wieder nur der erste
Datensatz erscheinen und nach jedem Zieldurchlauf müsste dann wieder gescrollt werden.

Zeitmessung_Start
Aufruf aus
Formular: Nur_Start
Makro:

Zeitmessung_Start_Einzel, Zeitmessung_Start_Gruppe, Fehler: Verweis nicht gefunden

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015

Zeitspeicher.Timestamp

SUB Zeitmessung_Start(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECT
DIM stStartzeit AS STRING
DIM stSql AS STRINGoForm = oEvent.Source.Model.Parent
IF oForm.RowCount > 0 THEN
stStartzeit = Left(oForm.getString(oForm.findColumn("Startzeit")),19)
oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
stSql = "UPDATE ""Zeitspeicher"" SET ""Timestamp"" = NOW() WHERE""Startzeit"" = '"+stStartzeit+"'"oSQL_Statement.executeUpdate(stSql)
oForm.reload()
END IF

Zeitmessung

34

016

END SUB

Nur in den einfachen Formularen ist es möglich, einzelne Personen doppelt starten zu lassen.
Einige Formular bieten hingegen irgendwann niemanden mehr an, der noch an den Start gehen
kann. Hier kann es also passieren, dass bei Drücken von Start , gar kein Datensatz mehr vorhanden ist. Dies würde bei der obigen Prozedur dann zu einer Fehlermeldung führen. Deswegen
ist hier die Nachfrage nach der angezeigten Datensatzanzahl wichtig. Nur wenn sie über 0 ist(Zeile 8) läuft der Rest der Prozedur ab.
Diesmal werden keine SystemTicks eingelesen sondern nur das Feld "Timestamp" mit dem
aktuellen Wert beschrieben (Zeile 12). Es ist also nachher auch in der Tabelle "Zeitspeicher"möglich, die ganz genaue Startzeit zu erkennen.
In Zeile 12 unterscheidet sich der Code bei FIREBIRD nur geringfügig von dem obigen Code:""Zeitspeicher"" = TIME 'NOW' ersetzt hier ""Timestamp"" = NOW(). Im Feld "Zeitspeicher"wird in FIREBIRD direkt die Zeit als Datentyp TIME gespeichert.
Zum Schluss wird das Formular wieder neu geladen, da die einmal gestarteten Personen nicht
mehr zur Auswahl stehen sollen.

Zeitmessung_Ende
Aufruf aus
Makro:

Zeitmessung_Ende_Einzel, Zeitmessung_Ende_Gruppe, Zeitmessung_Ende_Listenfeld

Benötigt
Tabelle:Zeitspeicher.Timestamp, Start.Zeit

Makro:

Sekunden_2_SQLTime

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027

SUB Zeitmessung_Ende(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECTDIM oResult AS OBJECT
DIM stID AS STRING
DIM stStartzeit AS STRING
DIM stSql AS STRING
DIM stZeit AS STRING
DIM loZeit AS LONG
oForm = oEvent.Source.Model.Parent
IF oForm.RowCount > 0 THEN
stID = oForm.getString(oForm.findColumn("ID"))
stStartzeit = Left(oForm.getString(oForm.findColumn("Startzeit")),19)
oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
stSql = "SELECT DATEDIFF('ms', ""Timestamp"", NOW()) FROM ""Zeitspeicher""
WHERE ""Startzeit"" = '"+stStartzeit+"'"oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
loZeit = oResult.getLong(1) ' Zeitangabe in Millisekunden
WEND
stZeit = Sekunden_2_SQLTime(loZeit)
stSql = "UPDATE ""Start"" SET ""Zeit"" = '"+stZeit+"' WHERE ""ID"" ='"+stID+"'"oSQL_Statement.executeUpdate(stSql)
oForm.reload()
END IF
END SUB

Wer einmal durch den Zieleinlauf gegangen ist soll hier nicht noch einmal registriert werden.
Da die Formulare so angelegt sind, dass eben die Anzahl der Datensätze von den Personen, die
unterwegs sind, beständig abnimmt, kann es passieren, dass gar kein Datensatz mehr da ist

Zeitmessung

35und trotzdem Stop gesetzt würde. Wie beim Start wird deshalb auch hier überprüft, ob das Formular überhaupt noch Daten enthält (Zeile 12).
Mit der Abfrage in Zeile 17 wird der Unterschied zwischen dem Feld "Timestamp" und der aktuellen Zeit NOW() in der Maßeinheit Millisekunden ermittelt. Dieser Wert wird in Zeile 20 ausgelesen und in Zeile 22 an die Funktion «Sekunden_2_SQLTime» weiter gegeben. Diese Funktion
macht daraus einen Timestamp, der anschließend in Zeile 23 in den SQL-Code eingefügt undals Zeit für die Person abgespeichert werden kann.
Zum Schluss wird das Formular wieder neu geladen, damit die Anzeige aktualisiert wird.
Der Code dieser Prozedur unterscheidet sich bei FIREBIRD ab Zeile 17:
017

stSql = "SELECT TIME 'NOW' - ""Zeitspeicher"" FROM ""Zeitspeicher"" WHERE""Startzeit"" = '"+stStartzeit+"'"oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
doZeit = oResult.getDouble(1)WEND
stZeit = Sekunden_2_SQLTime(doZeit)
stSql = "UPDATE ""Start"" SET ""Zeit"" = '"+stZeit+"' WHERE ""ID"" ='"+stID+"'"

018
019
020
021
022
023

Die Zeit aus dem Zeitspeicher wird direkt von der momentanen Zeit subtrahiert. Das Ergebnis
ist eine Zahl mit Nachkommastellen, die sich nach der Maßeinheit Sekunde richtet. Deswegen
wird hier die Variable als Double ausgelesen. Die Funktion, mit der die Zeit daraus für SQL-Zwecke ermittelt werden soll, hat zwar in FIREBIRD den gleichen Namen wie in HSQLDB. Der Inhalt ist
aber nicht komplett identisch, da die Funktion hier ja einen anderen Wert geliefert bekommt
und auch einen anderen Datentyp für die Weitergabe in die Tabelle "Start" liefern muss.
Der Zeitwert wird schließlich an die Tabelle Start für den entsprechenden Teilnehmer weitergegeben.

Sekunden_2_SQLTime
Aufruf aus
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

Zeitmessung_Ende

FUNCTION Sekunden_2_SQLTime(loHSek AS LONG) AS STRING
DIM stSek AS STRING
DIM doHSek AS DOUBLE
DIM inSek AS INTEGER
DIM inMin AS INTEGER
DIM inStd AS INTEGER
DIM stHSek AS STRING
doSek = loHSek/1000
stSek = CStr(doSek)
stHSek = Mid(stSek, InStr(stSek,",")+1)
doHSek = doSek - Fix(doSek)
inSek = (doSek - doHSek) Mod 60
inMin = ( (doSek - inSek - doHSek) / 60 ) Mod 60
inStd = ( doSek - inSek - doHSek - 60 * inMin) / 3600
IF inStd < 0 THEN
inStd = inStd + 24
END IF
Sekunden_2_SQLTime = Year(Now) & "-" & Right( "0" & Month(Now) , 2) & "-" &Right( "0" & Day(Now) , 2) & " " & Right( "0" & inStd , 2) & ":" &Right( "0" & inMin , 2) & ":" & Right( "0" & inSek , 2) & "." & stHSek
END FUNCTION

Diese Funktion ist ausgelagert aus der Prozedur «Zeitmessung_Ende», die die erreichte Zeit in
der Datenbank speichern soll. Sie ist zuerst für Firebird erstellt und hier nur für HSQLDB etwas
abgewandelt worden.

Zeitmessung

36Die Zeitangabe erfolgt in Millisekunden. Für die Weitergabe als Timestamp wird aber die Zeit in
Sekunden, Minuten und Stunden benötigt. Zuerst wird deshalb die eingehende Angabe durch
1000 dividiert (Zeile 8). Die Nachkommastellen dieser Variablen stellen jetzt die Millisekundendar. Sie werden in Zeile 10 als Text über den Dezimaltrenner ausgelesen. Basic richtet sich hiernach dem landesüblichen Trenner, hier also das Komma.
In den weiteren Schritten wird jeweils durch Restberechnung und Subtraktion der bereits herausgenommenen Reste zuerst die Anzahl der Sekunden, dann die der Minuten und schließlich
die der Sekunden ermittelt.
In FIREBIRD kann es passieren, dass die Stundenanzahl schließlich negativ ist, wenn der Start am
Vortag gewesen ist und der Zieldurchlauf am folgenden Tag. Schließlich wird ja nur die Zeit
gespeichert, und die geht bis maximal 24 Stunden. Deswegen wird in Zeile 16 in diesem Falleinfach 24 Stunden zu der Zeitangabe hinzugefügt.
Zum Schluss wird schließlich bei der HSQLDB der Timestamp mit dem aktuellen Datum und der
berechneten Zeit erstellt.
In FIREBIRD ist die Variable, die an die Funktion weitergegeben wird, bereits eine Variable mit
Nachkommastellen, die dann die Tausendstelsekunden darstellt. In sofern entfällt am Anfang
die Umwandlung.
Zum Schluss unterscheidet sich der Code auch noch, weil hier nicht ein Timestamp, sondern
lediglich eine Zeit weitergegeben wird:
020

Sekunden_2_SQLTime = inStd & ":" & Right( "0" & inMin , 2) & ":" &Right( "0" & inSek , 2) & "." & stHSek

Die Umwandlung ist hier deutlich kürzer, weil eben das gesamte Datum fehlt. Wie bei der
Umwandlung zum Timestamp wird auch hier dabei darauf geachtet, dass einstellige Zahlen fürdie Minuten und Sekunden durch eine zusätzliche 'o' zu zweistelligen Einträgen werden. Mit
Right("0" & inMin,2) werden die 2 Zeichen von rechts ausgelesen. Bei zweistelligen Einträgen wird also die '0' nicht übernommen.

Formulare mit Zeitmessung verknüpfen
Jede der folgenden Prozeduren stellt nur die Verbindung der Formulare mit den Prozeduren zum
Starten und zum Beenden her. In unterschiedlichen Formularen werden unterschiedliche
zusätzliche Funktionen benötigt, die vor oder nach dem Abspeichern ablaufen müssen. Diese
Prozeduren sind in HSQLDB und FIREBIRD gleich.

Zeitmessung_Start_Einzel
Aufruf aus
Formular: Einzelmessung_Zeit

Benötigt
Makro:
001
002
003
004
005
006
007
008

Zeitmessung_Start

SUB Zeitmessung_Start_Einzel(oEvent AS OBJECT)
DIM oForm AS OBJECT
DIM inRow AS INTEGER
oForm = oEvent.Source.Model.Parent
inRow = oForm.getRow()
Zeitmessung_Start(oEvent)oForm.absolute(inRow)
END SUB

Bevor der Start erfolgt wird hier die Zeile des aktuellen Datensatzes des Formulars ausgelesen.
Nach Ablauf der Start-Prozedur (Zeile 6) wird das Formular wieder auf diese Zeile eingestellt.

Zeitmessung

37

Zeitmessung_Ende_Einzel
Aufruf aus
Formular: Einzelmessung_Zeit

Benötigt
Makro:
001
002
003
004
005
006
007
008

Zeitmessung_Ende

SUB Zeitmessung_Ende_Einzel(oEvent AS OBJECT)
DIM oForm AS OBJECT
DIM inRow AS INTEGER
oForm = oEvent.Source.Model.Parent
inRow = oForm.getRow()
Zeitmessung_Ende(oEvent)oForm.absolute(inRow)
END SUB

Wie «Zeitmessung_Start_Einzel», dieses Mal nur für das Ende der Zeitmessung (Zeile 6).

Zeitmessung_Start_Gruppe
Aufruf aus
Formular: Start_Ziel_Gruppe

Benötigt
Makro:
001
002
003
004
005
006
007

Zeitmessung_Start

SUB Zeitmessung_Start_Gruppe(oEvent AS OBJECT)
DIM oForm AS OBJECT
Zeitmessung_Start(oEvent)oForm = oEvent.Source.Model.Parent
oFormZiel = oForm.Parent.getByName("Ziel")oFormZiel.reload()
END SUBDie Prozedur «Zeitmessung_Start» läuft in einem anderen Formular ab. Das Formular «Ziel»
wird danach aufgesucht und neu geladen, damit dort der Start registriert wird.

Zeitmessung_Ende_Gruppe
Aufruf aus
Formular: Start_Ziel_Platz, Start_Ziel_Gruppe

Benötigt
Makro:
001
002
003
004
005
006
007

Zeitmessung_Ende

SUB Zeitmessung_Ende_Gruppe(oEvent AS OBJECT)
DIM oForm AS OBJECT
Zeitmessung_Ende(oEvent)oForm = oEvent.Source.Model.Parent
oFormPlatz = oForm.Parent.getByName("Platz")oFormPlatz.reload()
END SUBDie Prozedur «Zeitmessung_Ende» läuft in einem anderen Formular ab. Das Formular «Platz»
wird danach aufgesucht und neu geladen, damit dort der Start registriert wird.

Zeitmessung

38

Zeitmessung_Start_Ziel
001
002
003
004
005
006
007
008
009

SUB Zeitmessung_Start_Ziel(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECT
Zeitmessung_Start(oEvent)oForm = oEvent.Source.Model.Parent
oFormZiel = oForm.Parent.getByName("Ziel")oFeld = oFormZiel.getByName("Startnummern")oFeld.refresh()
END SUBDie Prozedur «Zeitmessung_Start» läuft in einem anderen Formular ab. Das Formular «Ziel»
wird danach aufgesucht. Das Listenfeld «Startnummern» wird anschließend neu eingelesen(Zeile 8).

Zeitmessung_Ende_Listenfeld
Aufruf aus
Formular: Nur_Ziel, Start_Ziel_Platz

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015

Zeitmessung_Ende

SUB Zeitmessung_Ende_Listenfeld(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECT
DIM stID AS STRING
oFeld = oEvent.Source.ModelstID = oFeld.CurrentValue
IF stID <> "" THEN
oForm = oFeld.Parent
oForm.Filter = """ID"" = '"+stID+"'"oForm.ApplyFilter = TRUE
oForm.reload()
Zeitmessung_Ende(oEvent)oFeld.refresh()
END IF
END SUB

Diese Prozedur wird von dem Listenfeld ausgelöst. In Zeile 6 wird der aktuelle Wert des Listenfeldes ausgelesen. Ist die Wert nicht leer (Zeile 7), so wird zuerst das Formular mit einem Filter
versehen und neu geladen. In dem Formular ist jetzt der Datensatz zu sehen, der abgespeichert werden soll. Danach läuft die Prozedur «Zeitmessung_Ende» ab. Anschließend wird das
Listenfeld neu eingelesen, da jetzt ja eine "ID" weniger zur Verfügung steht. Die Person ist
durchs Ziel gekommen.

Zeitmessung

39

Aktuelle Standardwerte für Datum und
Zeit

Aktuelle Standardwerte für Datum und Zeit40

Einführung
In Formularen gibt es für Formularfelder die Möglichkeit, Standardwerte zu setzen. Damit werden über die grafische Benutzeroberfläche Wertvorgaben für bestimmte Felder gemacht, dieabgespeichert werden, wenn sie nicht überschrieben werden.
Auch bei der Erstellung von Tabellen ist in den Feldeigenschaften ein Feld für den «Defaultwert»
vorgesehen. Ein Eintrag an dieser Stelle erzeugt ebenfalls einen festen Vorgabewert für die Eingabe in ein Feld – jetzt aber bei Tabellen. Er hat im übrigen nichts mit dem Default-Wert dereigentlichen Datenbank zu tun. Der Default-Wert, definiert in einer Datenbank, wird nur danngeschrieben, wenn ein Feld als leeres Feld abgespeichert werden soll.
Alle Standardwerte der grafischen Benutzeroberfläche haben den Nachteil, dass sie nicht flexibel sind. Es kann nicht das beim Öffnen des Formulars aktuelle Datum oder die aktuelle Zeiteingegeben werden.
Die folgende Datenbank2 zeigt an Beispielen, wie es möglich ist, ein aktuelles Datum bzw. eineaktuelle Datum-Zeit-Kombination beim Erstellen von Datensätzen zu erzeugen. Es zeigt außerdem, welche Möglichkeiten es gibt, ein Datum hinterher mit möglichst geringem Aufwandändern zu lassen. Typische Beispiel wären hier eine Zeitmessung in Form von Arbeitsbeginn
und Arbeitsende oder ein Vermerk, wann ein Datensatz erstellt und wann er zuletzt geändert
worden ist.

Tabellen
Die Tabellen stellen nur einfache Beispieltabellen dar. Sie sollen nur zeigen, wie aktuelle Standardwerte für ein Datum bzw. ein Datum mit Zeitangabe gesetzt werden können.

Datum_Aenderdatum
Datenziel
Formular: Defaultdatum_Aenderung, Defaultdatum_Makro, Defaultdatum_Makro_Standarddatum, Defaultdatum_Makro_Standarddatum_verlegt,
Defaultdatum_Subform_neue_Datensaetze
Abfrage:

DatumZeitvorgabe

Bericht, Makro: keine direkt

2

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann auch als Auto-Wert-Feld gesetzt
werden.

Name

Text

Der Name einer Person oder irgendeine Bezeichnung. Steht
hier nur als Beispieleintrag ohne weitere Bedeutung.
Eingabe erforderlich: Nein

Datum

Datum

Hier soll ein Startdatum gespeichert werden, das nachher
nicht mehr verändert wird.
Eingabe erforderlich: Nein

AenderDatum

Datum

Hier soll beim Erstellen des Datensatzes das gleiche Datum
wie im Feld "Datum" abgespeichert werden. Dieses Feld wird
beim erneuten Aufrufen des Datensatzes aber gegebenenfalls überschrieben.
Eingabe erforderlich: Nein

Beispieldatenbank Beispiel_Default_Datum_Zeit.odb

Aktuelle Standardwerte für Datum und Zeit

41

Die Tabelle «Datum_SQLDefault_Aenderdatum» ist von den Feldern her völlig gleich aufgebaut.
Datenziel
Formular: Defaultdatum_Makro_SQL
Abfrage, Bericht, Makro: keine direkt

Hier ist im Unterschied zur Tabelle «Datum_Aenderdatum» allerdings über Extras → SQL hinterher ein SQL-Defaultwert eingestellt worden:
001 ALTER TABLE "Datum_SQLDefault_Aenderdatum"
002 ALTER COLUMN "Datum"
003 SET DEFAULT CURRENT_DATE;
001 ALTER TABLE "Datum_SQLDefault_Aenderdatum"
002 ALTER COLUMN "AenderDatum"
003 SET DEFAULT CURRENT_DATE;
Damit werden in dieser Tabelle beim Abspeichern eines neuen Datensatzes die Felder "Datum"und "AenderDatum" automatisch mit dem aktuellen Datum versehen, sofern nicht ein anderes
Datum eingetragen wurde.
001 ALTER TABLE "Datum_SQLDefault_Aenderdatum"
002 ADD CHECK (COALESCE("AenderDatum","Datum")>="Datum");
Außerdem wird durch die Check-Bedingung sicher gestellt, dass der Eintrag in "AenderDatum"
immer größer oder gleich dem Eintrag in "Datum" ist.

Zeitstempel_Aenderstempel
Datenziel
Formular: Defaultdatum_Makro_Standardzeitstempel, Defaultdatum_Zeit_Makro,
Defaultdatum_Zeit_Subform_neue_Datensaetze
Abfrage:Zeitdifferenzen, Zeitdifferenzen_Formularvorlage

Bericht, Makro: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann auch als Auto-Wert-Feld gesetzt
werden.

Name

Text

Der Name einer Person oder irgendeine Bezeichnung. Steht
hier nur als Beispieleintrag, z.B. zur Ermittlung von Arbeitszeit.
Eingabe erforderlich: Nein

Zeitstempel

Datum/Zeit

Hier soll eine Startzeit zusammen mit Datum gespeichert
werden (TIMESTAMP), die nachher nicht mehr verändert wird.
Eingabe erforderlich: Nein

Zeitstempel_
Aenderung

Datum/Zeit

Hier soll beim Erstellen des Datensatzes die gleiche Zeit mit
Datum wie im Feld "Zeitstempel" abgespeichert werden. Dieses Feld wird beim erneuten Aufrufen des Datensatzes abergegebenenfalls überschrieben.
Eingabe erforderlich: Nein

Die Tabelle «Zeitstempel_SQLDefault_Aenderstempel» ist von den Feldern her völlig gleich aufgebaut. Hier ist allerdings über Extras → SQL hinterher ein SQL-Defaultwert eingestellt worden.
Der Tabellenname musste für FIREBIRD gekürzt werden, da FIREBIRD für Tabellennamen und Feldnamen nur maximal 30 Zeichen zulässt. Dabei sind Standardzeichen gemeint. Sonderzeichen

Aktuelle Standardwerte für Datum und Zeit

42

wie Umlaute nehmen in der Regel gleich den Platz für 2 Zeichen ein. In FIREBIRD heißt die
Tabelle also «Zeitstempel_SQLD_Aenderstempel».
Datenziel
Formular: Defaultdatum_Zeit_Makro_SQL
Abfrage, Bericht, Makro: keine direkt

001 ALTER TABLE "Zeitstempel_SQLDefault_Aenderstempel"
002 ALTER COLUMN "Zeitstempel"
003 SET DEFAULT CURRENT_TIMESTAMP;
001 ALTER TABLE "Zeitstempel_SQLDefault_Aenderstempel"
002 ALTER COLUMN "Zeitstempel_Aenderung"003 SET DEFAULT CURRENT_TIMESTAMP;
Damit werden in dieser Tabelle beim Abspeichern eines neuen Datensatzes die Felder "Zeitstempel" und "Zeitstempel_Aenderung" automatisch mit der aktuellen Zeit und dem aktuellen
Datum versehen, sofern nicht eine andere Zeit und ein anderes Datum eingetragen wurde.
001 ALTER TABLE "Zeitstempel_SQLDefault_Aenderstempel"
002 ADD CHECK(COALESCE("Zeitstempel_Aenderung","Zeitstempel")>="Zeitstempel");Außerdem wird durch die Check-Bedingung sicher gestellt, dass der Eintrag in"Zeitstempel_Aenderung" immer größer oder gleich dem Eintrag in "Zeitstempel" ist.

Ansichten
Diese Datenbank enthält für die HSQLDB keine Ansichten.
In FIREBIRD sind statt der Abfragen Zeitdifferenzen und Zeitdifferenzen_Formularvorlage Ansichten notwendig. Die Abfragen sind dort wegen veränderten Codes nur im direkten SQL-Modus
ausführbar und damit für die Ausgabe in Berichten nicht sortierbar und auch sonst nur
begrenzt brauchbar.
Die Ansichten in FIREBIRD haben die Bezeichnung «Ansicht_Zeitdifferenzen» und «Ansicht_Zeitdifferenzen_Form». Die zweite Benennung musste wegen der Beschränkung auf 30 Zeichen fürTabellennamen gekürzt werden.

Abfragen
Die Datenbank enthält drei Abfragen. Für die Eingabe des Standarddatums ist lediglich die
Abfrage «DatumZeitvorgabe» von Bedeutung. Die weiteren Abfragen dienen als Berichtsgrundlage («Zeitdifferenzen») oder als Vorlage für ein Formular, in dem gleichzeitig berechnete Differenzen betrachtet werden sollen («Zeitdifferenzen_Formularvorlage»).

DatumZeitvorgabe
Datenquelle
Tabelle:Datum_Aenderdatum

Datenziel
Formular: Defaultdatum_Subform_neue_Datensaetze,
Defaultdatum_Zeit_Subform_neue_Datensaetze

Die Abfrage «DatumZeitvorgabe» dient lediglich dazu, das aktuelle Datum und den aktuellen
Zeitstempel von der Datenbank zu erfragen. Damit kann ohne weitere Zuhilfenahme von direkAktuelle Standardwerte für Datum und Zeit

43

ter SQL-Eingabe oder Makros das aktuelle Datum und (etwas begrenzt) auch der Zeitstempel in
ein Formular übertragen werden.
001 SELECT DISTINCT CURRENT_TIMESTAMP AS "Jetzt",
002 CURRENT_DATE AS "Heute"
003 FROM "Datum_Aenderdatum"
Für eine Abfrage muss immer eine Tabelle als Basis stehen. Dies ist hier die Tabelle"Datum_Aenderdatum" (Zeile 3). In diesem Fall spielt es keine Rolle, welche Tabelle das ist, danur Standardwerte aus der Datenbank ermittelt werden sollen: das aktuelle Datum
CURRENT_DATE und die aktuelle Zeit kombiniert mit dem Datum CURRENT_TIMESTAMP. Durch den
Zusatz DISTINCT (Zeile 1) werden die Werte nur einmal dargestellt. Ansonsten würde die
Datenbank so viele Datensätze wiedergeben, wie die Tabelle bereits an Datensätzen hat – mitlauter gleichen Datums- und Zeitstempelangaben.

Zeitdifferenzen
Datenquelle
Tabelle:Zeitstempel_Aenderstempel

Datenziel
Bericht:

Bericht_Zeitdifferenzen

Zur Auswertung von Zeiteingaben in einem Bericht wurde die Abfrage «Zeitdifferenzen»
erstellt. Da in FIREBIRD diese Abfrage nur in etwas abgewandelter Form und in direktem SQLModus ausführbar ist (siehe unten) existiert dort stattdessen keine Abfrage sondern eine
Ansicht mit der Bezeichnung «Ansicht_Zeitdifferenzen».
001 SELECT "ID",002"Name",
003"Zeitstempel",004"Zeitstempel_Aenderung",
005
CAST ( EXTRACT ( YEAR FROM "Zeitstempel" ) || '-' ||RIGHT( '0' || EXTRACT ( MONTH FROM "Zeitstempel" ), 2 ) || '-' ||RIGHT( '0' || EXTRACT ( DAY FROM "Zeitstempel" ), 2 ) AS DATE )AS "StartDatum",006
DATEDIFF( 'mi', "Zeitstempel", "Zeitstempel_Aenderung" )AS "Differenz_Minuten"
007 FROM "Zeitstempel_Aenderstempel"
Zuerst wird die komplette Tabelle "Zeitstempel_Aenderstempel" ausgewählt. Das Feld "ID" istdabei nur notwendig, wenn so eine Abfrage für die Eingabe von Daten benutzt werden soll. Um
eine Abfrage editierbar zu halten muss der Primärschlüssel der in der Abfrage enthaltenen
Tabellen ebenfalls enthalten sein.
Aus dem Feld "Zeitstempel" ließe sich mit Hilfe der Formatierung auch lediglich das Datum darstellen. In der Abfrage wird zu Demonstrationszwecken gezeigt, wie die Datumsdarstellungauch erreicht werden kann:Tag, Monat und Jahr können nur über separate Abfragen des Feldes "Zeitstempel" ermittelt werden. Um auch bei einstelligen Tageszahlen und Monatszahlen die vorangestellte '0' zu erhalten
werden eine '0' und der ermittelte Tag zusammengefasst ('0' || DAY( "Zeitstempel" )) und
dann aus dem entstehenden Text von rechts aus die 2 übrigbleibenden Zeichen ausgelesen.
Das Datum wird dann in einem für SQL lesbares Datumsformat geschrieben: vierstellige Jahreszahl, zweistellige Monatszahl und zweistellige Tageszahl, verbunden mit einem Bindestrich(YYYY-MM-DD). Damit ist die Abfrage auch für andere Datumsschreibweisen nutzbar, denn dieser Text wird jetzt in das Datumsformat der Datenbank über CAST ( Datumstext AS DATE )umgewandelt. Dadurch kann die Ausgabe z.B. im Bericht wieder entsprechend nach lokalen
Einstellungen formatiert werden.

Aktuelle Standardwerte für Datum und Zeit

44

Mit DATEDIFF( 'mi', "Zeitstempel", "Zeitstempel_Aenderung" ) wird die Zeitdifferenz
zwischen der Startzeit und der Endzeit in Minuten ermittelt. Diese Zeitdifferenz kann später im
Bericht addiert werden und gegebenenfalls auch durch Umrechnung als eine Angabe von Minuten und Stunden erfolgen.
FIREBIRD benötigt hier eine andere Bezeichnung: DATEDIFF( minute, "Zeitstempel","Zeitstempel_Aenderung" ). Diese Schreibweise ohne die Maskierung mit einfachen Anführungszeichen versteht die GUI nicht, so dass dann die Abfrage nur noch im direkten SQL-Modus
funktioniert.

Zeitdifferenzen_Formularvorlage
Datenquelle
Tabelle:Zeitstempel_Aenderstempel

Datenziel
Formular: Defaultdatum_Zeitdifferenz_Makro
Bericht:

Bericht_Zeitdifferenzen_Formularvorlage

Als Formularvorlage ist die wesentlich umfangreichere Abfrage «Zeitdifferenzen_Formularvorlage» gedacht. Da in FIREBIRD diese Abfrage nur in etwas abgewandelter Form und in direktem
SQL-Modus ausführbar ist (siehe unten) existiert dort stattdessen keine Abfrage sonder eine
Ansicht mit der Bezeichnung «Ansicht_Zeitdifferenzen_Form».
001 SELECT "ID",002"Name",
003"Zeitstempel",004"Zeitstempel_Aenderung",
005
CONVERT ( EXTRACT ( YEAR FROM "Zeitstempel" ) || '-' ||RIGHT( '0' || EXTRACT ( MONTH FROM "Zeitstempel" ), 2 ) || '-' ||RIGHT( '0' || EXTRACT ( DAY FROM "Zeitstempel" ), 2 ) , DATE )AS "StartDatum",006( DATEDIFF( 'mi', "Zeitstempel", "Zeitstempel_Aenderung" ) MOD( DATEDIFF( 'mi', "Zeitstempel", "Zeitstempel_Aenderung" ),60 ) ) / 60 || ':' || RIGHT( '0' || MOD( DATEDIFF( 'mi',"Zeitstempel", "Zeitstempel_Aenderung" ), 60 ), 2 )AS "Differenz_Stunden",
007( ( SELECT SUM( DATEDIFF( 'mi', "Zeitstempel","Zeitstempel_Aenderung" ) ) FROM "Zeitstempel_Aenderstempel"
WHERE "Name" = "a"."Name" ) - MOD( ( SELECT SUM( DATEDIFF( 'mi',"Zeitstempel", "Zeitstempel_Aenderung" ) ) FROM
"Zeitstempel_Aenderstempel" WHERE "Name" = "a"."Name" ),60 ) ) / 60 || ':' || RIGHT( '0' || MOD( ( SELECT SUM( DATEDIFF('mi', "Zeitstempel", "Zeitstempel_Aenderung" ) ) FROM
"Zeitstempel_Aenderstempel" WHERE "Name" = "a"."Name" ), 60 ), 2
) AS "Gesamt_Stunden"
008 FROM "Zeitstempel_Aenderstempel" AS "a"Bis zur Abfrage des Startdatums ist diese Abfrage gleich der Abfrage «Zeitdifferenzen»: Danach
wird beständig die Zeitdifferenz in Minuten genutzt: DATEDIFF( 'mi', "Zeitstempel","Zeitstempel_Aenderung" ) (Zeile 6). Die Zahlenangabe kann nicht in ein Zeitformat derDatenbank umgewandelt werden, da in der Datenbank nur Zeiten bis maximal 24 Stunden verwaltet werden. Es wird hier also lediglich in einer Textform die Zeitdarstellung nachgebildet.
Zuerst werden die Minuten durch 60 dividiert, um die Anzahl in Stunden zu ermitteln. Eigentlichmüsste es hier genügen, beim Ergebnis die Nachkommastellen abzuschneiden oder auch abzurunden. Dies gelingt leider nicht, da bei den Befehlen TRUNCATE() und CEILING() das Ergebnis
immer als Dezimalzahl mit einer Nachkommastelle ausgegeben wird – auch wenn die NachAktuelle Standardwerte für Datum und Zeit

45

kommastelle 0 ist. Um ein Ergebnis ohne Nachkommastellen zu erhalten muss von vornhereinder verbleibende Rest der Division MOD ( Minuten, 60) subtrahiert werden. Dann wird bei
den anschließenden Division die korrekte Ganzzahl ausgegeben.
Anschließend wird ein Doppelpunkt über ||':' angehängt und an diesen wiederum der Rest
aus der Division der Minuten durch 60. Vor die Division wird eine führende '0' gesetzt, damit
auf jeden Fall eine zweistellige Zahl für die Minutendarstellung vorhanden ist. Wie beim Datum
werden mit RIGHT (Text,2) einfach die zwei Zeichen des Textes übernommen, die am weitesten rechts stehen.
Mit dem entsprechenden Verfahren wird auch die Gesamtzahl an Stunden ermittelt. Da diesepro Person ermittelt werden soll sind hier für die Ermittlung der Stunden und Minuten korrelierende Unterabfragen notwendig. Dazu ist zuerst einmal der Tabelle der Hauptabfrage "Zeitstempel_Aenderung" der Alias "a" zugeordnet. Anschließend wird in der Unterabfrage die
Summe SUM für die Zeitdifferenz in Minuten ( DATEDIFF( 'mi', "Zeitstempel","Zeitstempel_Aenderung" ) ) gebildet – aber nur bezogen auf den jeweiligen Namen, der imaktuellen Datensatz der Hauptabfrage steht: WHERE "Name" = "a"."Name". Die Berechnungenund Umwandlungen der Ergebnisse aus den korrelierenden Unterabfragen sind gleich den
Berechnungen und Umwandlungen aus der direkten Ermittlung der Zeitdifferenz in Minuten fürden jeweiligen Datensatz. Das Ergebnis wird als "Gesamt_Stunden" ausgegeben.
In FIREBIRD sind hier die Zeilen 6 und 7 etwas anders zu formulieren:
006 ( ( DATEDIFF(minute, "Zeitstempel", "Zeitstempel_Aenderung" ) MOD( DATEDIFF(minute, "Zeitstempel", "Zeitstempel_Aenderung" ), 60 ) ) /60 ) || ':' || RIGHT( '0' || MOD( DATEDIFF( minute, "Zeitstempel","Zeitstempel_Aenderung" ), 60 ), 2 ) AS "Differenz_Stunden",
007 ( ( ( SELECT SUM( DATEDIFF( minute, "Zeitstempel", "Zeitstempel_Aenderung") ) FROM "Zeitstempel_Aenderstempel" WHERE "Name" = "a"."Name" ) MOD( ( SELECT SUM( DATEDIFF( minute, "Zeitstempel","Zeitstempel_Aenderung" ) ) FROM "Zeitstempel_Aenderstempel" WHERE "Name"= "a"."Name" ), 60 ) ) / 60 ) || ':' || RIGHT( '0' || MOD( ( SELECT
SUM( DATEDIFF(minute, "Zeitstempel", "Zeitstempel_Aenderung" ) ) FROM
"Zeitstempel_Aenderstempel" WHERE "Name" = "a"."Name" ), 60 ), 2 ) AS
"Gesamt_Stunden"Zum einen ist wieder der Befehl DATEDIFF durch die Angabe von minute ohne die Maskierung
mit einfachen Anführungszeichen erforderlich. Dadurch muss die Abfrage als direkte SQLAbfrage ausgeführt werden.
Zum anderen hat FIREBIRD Probleme, Ohne eine entsprechende Klammerung Rechungen vonStrings zu unterscheiden. Deshalb ist in Zeile 6 und Zeile 7 eine schließende Klammer hinter/ 60 ) erforderlich. Natürlich muss entsprechend auch eine öffnende Klammer weiter vorne
mit eingesetzt werden. Nur so funktioniert die Rechnung, die innerhalb einer Verbindung mit ||erfolgt.

Formulare
Defaultdatum_Subform_neue_Datensaetze

Aktuelle Standardwerte für Datum und Zeit

46

1 MainForm (Abfrage: DatumZeitvorgabe)

1.1 SubForm (Tabelle: Datum_Aenderdatum)

Die Formulare «Defaultdatum_Subform_neue_Datensaetze» und«Defaultdatum_Zeit_Subform_neue_Datensaetze» arbeiten nach dem gleichen Prinzip. Wird das
Formular geöffnet, so ist bereits das Datum bzw. der Zeitstempel vorgegeben. Im Datumsformular erscheinen die Datensätze des aktuellen Tages und können auch nur Datensätze für den
aktuellen Tag eingegeben werden.
Es ist lediglich das Datenfeld für den Primärschlüssel "ID" sowie für das Textfeld "Name" ausfüllbar. Die in Grau gehaltenen Formularfelder sind schreibgeschützt.

Wird das Formular zum Bearbeiten geöffnet und der Formularnavigator aufgerufen, so wird die
Verbindung von Hauptformular MainForm und Unterformular SubForm sichtbar. Das Hauptformular hat als Inhalt die Abfrage «DatumZeitvorgabe». Von dem Hauptformular wird das ermittelte aktuelle Datum an das Unterformular übergeben. Das Feld "Heute" ist mit den Feldern"Datum" und "AenderDatum" des Unterformulars verknüpft.
Im Hauptformular des Datumformulars existieren keine Formularfelder.
Mit Hilfe der Hauptformular-Unterformular-Konstruktion kann auf einfache Weise das aktuelle
Datum übertragen und automatisch mit Datensätzen abgespeichert werden. Änderungen vonbereits eingegebenen Datumswerten sind nur über die direkte Eingabe per Hand möglich.

Aktuelle Standardwerte für Datum und Zeit

47

Defaultdatum_Zeit_Subform_neue_Datensaetze

1

MainForm (Abfrage: DatumZeitvorgabe)

1.1 SubForm (Tabelle: Zeitstempel_Aenderstempel)

Im Zeitformular erscheinen auf keinen Fall die vorhergehenden Datensätze, da die Zeit zumÖffnen des Formulars maßgebend ist.
Im Hauptformular des Zeitformulars ist ein Button enthalten. Dieser Button ist in den Eigenschaften: Schaltfläche → Allgemein → Aktion auf Formular aktualisieren eingestellt. Dadurchwerden die Zeiten gegebenenfalls neu eingelesen und übertragen. Würde dies nicht möglichsein, so hätten alle nach dem Öffnen des Formulars erstellten Datensätze den gleichen Zeitstempel.
Bei Zeitangaben muss schon eine laufende Aktualisierung durch einen Button erfolgen. Sie sindin dieser Konstruktion also nur bedingt brauchbar. Änderungen von bereits eingegebenen Zeitstempelwerten sind nur über die direkte Eingabe per Hand möglich.

Defaultdatum_Aenderung

1 MainForm (Tabelle: Datum_Aenderdatum)
Um ein Datum bzw. einen Zeitstempel zu ändern ist ein separates Formular erforderlich, hierdas Formular «Defaultdatum_Aenderung». Die Änderung kann nicht in dem vorhergehenden
Formular erfolgen, da das Änderungsfeld dort mit dem Hauptformular verbunden ist und bereitseine Datums- bzw. Zeitvorgabe hat.

Aktuelle Standardwerte für Datum und Zeit48

Defaultdatum_Makro_SQL

1 MainForm (Tabelle: Datum_SQLDefault_Aenderdatum)
Makros in Feldeigenschaften1

Button Neuer Datumsstempel (Aktion ausführen)

Module1.Update

Die Formulare «Defaultdatum_Makro_SQL» und «Defaultdatum_Zeit_makro_SQL» sind wiederum nach dem gleichen Prinzip aufgebaut.
Bei diesen Formularen erscheint beim Erstellen eines neuen Datensatzes kein Eintrag in den
Datums- bzw. Zeitstempel-Feldern. Die Tabelle, die mit dem Formular bearbeitet wird, hat einen
Defaultwert über SQL für die Datums- bzw. Zeitstempel-Felder erhalten. Dieser Wert wird beim
Abspeichern eines neuen Datensatzes in die Tabelle geschrieben. Der Nutzer des Formulars
sieht den Wert also erst, wenn ein Datensatz abgespeichert wurde.
Wenn ein Datensatz geändert werden soll, so kann ein neues Datum bzw. ein neuer Zeitstempel über den Button erstellt werden. Der Button ist hierfür über das Ereignis → Aktion ausführen einem Makro verbunden.

Defaultdatum_Zeit_Makro_SQL

1 MainForm (Tabelle: Zeitstempel_SQLDefault_Aenderstempel)
Makros in Feldeigenschaften1

Button Neuer Zeitstempel (Aktion ausführen)

Module1.Update

In den Zusatzinformationen das Buttons ist der SQL-Code angegeben, der bei einer Änderungausgeführt werden soll. Außerdem ist hinter einem Semikolon vermerkt, in welchen der Formularfelder der Wert für den Primärschlüssel steht. Der Primärschlüssel wird für die Änderung nur

Aktuelle Standardwerte für Datum und Zeit49

des einen Datensatzes benötigt. Ohne den Schlüssel würden Änderungen für alle Datensätze
gelten.
Eintrag für den Zeitstempel-Button:
001 UPDATE "Zeitstempel_SQLDefault_Aenderstempel" SET "Zeitstempel_Aenderung"= CURRENT_TIMESTAMP;fmtID
Eintrag für den Datumsstempel-Button:
001 UPDATE "Datum_SQLDefault_Aenderdatum" SET "AenderDatum" =CURRENT_DATE;fmtID
Der Button kann also so zum einen abspeichern, zum anderen auch direkt den Wert aus derDatenbank wieder auslesen, so dass die Eingabe auf jeden Fall für die Person, die das Formularbedient, erscheint.
Bei FIREBIRD muss im Befehl für den Zeitstempel der Tabellenname noch angepasst werden, da
die Bezeichnung über 30 Zeichen lang ist.

Defaultdatum_Makro

1 MainForm (Tabelle: Datum_Aenderdatum)
Makros in Formulareigenschaften1

Vor der Datensatzaktion

Module1.Datum_aktuell

In dem Formular befinden sich nur zwei beschreibbare Eingabefelder: Das Primärschlüsselfeld
und das Namensfeld. Die Werte in den Feldern für das Speicherdatum und das Änderungsdatum bzw. den Speicher-Zeitstempel und den Änderungs-Zeitstempel werden ohne Eingriff
des Nutzers automatisch geschrieben. So wird immer die letzte Änderung in dem Änderungsstempel festgehalten.

Aktuelle Standardwerte für Datum und Zeit

50

Defaultdatum_Zeit_Makro

1 MainForm (Tabelle: Zeitstempel_Aenderstempel)
Makros in Formulareigenschaften1

Vor der Datensatzaktion

Module1.Datum_Zeit_aktuell

Dieses Formular setzt immer automatisch über das entsprechende Makro den Zeitstempel ein.
Dierekt beim ersten Abspeichern sind so die beiden Felder mit gleichen Zeitstempeln versehen.
Beim erneuten Abspeichern, z.B. nach einer Datensatzänderung, wird dann nur das Feld für«Ändetrungsdatum und -zeit» mit einem neuen Wert versehen.

Defaultdatum_Makro_Standarddatum

1 MainForm (Tabelle: Datum_Aenderdatum)
Makros in Formulareigenschaften
1

Nach dem Datensatzwechsel

Module1.StandarddatumÜber die Eigenschaften eines Kontrollfeldes lassen sich Standardwerte festlegen. Beim Datumsfeld ist dies mit Eigenschaften: Datumsfeld → Allgemein → Standarddatum zu erreichen. Diese
Einstellungen sind dann dauerhaft in dem Formular gespeichert. Ein aktuelles Datum oder ein
aktueller Zeitstempel können in der grafischen Benutzeroberfläche nicht vorgegeben werden.
Die Standardwerte erscheinen, wie in dem Formular zu sehen, bereits beim Erstellen eines
neuen Datensatzes. Sie werden für das Formular allerdings nicht als Datensatzänderung verzeichnet. Erst wenn ein anderes Feld oder eben das Feld mit dem Standardwert geändert wurde
nimmt die grafische Benutzeroberfläche das als Änderung wahr und speichert den Datensatz
auf die übliche Art und Weise ab (Button »Speichern», Navigation zum nächsten Datensatz,
Navigation zum Unterformular u.ä.).
Bei diesem Formular ist zu Bedenken, dass der Wert beim Erreichen eines neuen Datensatzes
bereits im Formular erscheint. Bei Datumswerten ist das allerdings weniger von Bedeutung, es

Aktuelle Standardwerte für Datum und Zeit

51

sei denn, die Bearbeitung eines Datensatzes beginnt z.B. um 23:59 Uhr und endet um
00:01 Uhr des Folgetages.

Defaultdatum_Makro_Standarddatum_verlegt

1 MainForm (Tabelle: Datum_Aenderdatum)
Makros in Formulareigenschaften
1

Nach dem Datensatzwechsel

Module1.Standarddatum_verlegt

Beim Datumsfeld legt der Standardwert gleichzeitig fest, mit welcher Monatsansicht die Aufklappfunktion des Datumsfeldes startet. Werden z.B. häufig Datumswerte eingegeben, die im
Vormonat liegen, so kann über ein Standarddatum aus dem Vormonat die Auswahl entsprechend eingestellt werden.

Defaultdatum_Makro_Standardzeitstempel

1 MainForm (Tabelle: Zeitstempel_Aenderstempel)
Makros in Formulareigenschaften
1

Nach dem Datensatzwechsel

Module1.StandardZeitstempelÜber die Eigenschaften eines Kontrollfeldes lassen sich Standardwerte festlegen. Hier beim formatierten Feld für den Zeitstempel über Eigenschaften: Formatiertes Feld → Allgemein → Standardwert.
Bei diesem Formular ist zu Bedenken, dass der Wert beim Erstellen eines neuen Datensatzes
bereits im Formular erscheint. Es handelt sich also bei der Zeit nicht um den Zeitpunkt, zu dem
die Abspeicherung des Datensatzes erfolgte.

Aktuelle Standardwerte für Datum und Zeit

52

Defaultdatum_Zeitdifferenz_Makro

1 HSQDB: MainForm (Abfrage: Zeitdifferenzen_Formularvorlage)
1

FIREBIRD: MainForm (Tabelle: Zeitstempel_Aenderstempel)

1.1

FIREBIRD: SubForm (Ansicht:
Ansicht_Zeitdifferenzen_Form)

Makros in Formulareigenschaften1

Vor der Datensatzaktion

Module1.Datum_Zeit_aktuell

Makros in Feldeigenschaften1

Button Neue Endzeit (Aktion ausführen)

Module1.UpdateTimestamp

Dieses Formular zeigt neben dem Zeitstempel für die erste Abspeicherung und dem Zeitstempel für die Änderung des Datensatzes die Differenz zwischen den Zeiten sowie die Differenz
zwischen allen Zeiten, die mit der Person verbunden sind, an.
Wird auf den Button «Neue Endzeit» gedrückt, so wird das Änderungsdatum und die Änderungszeit neu gesetzt und die Zeitdifferenzen werden neu eingelesen.

Berichte
Für diese Beispieldatenbank wurden zwei Berichte erstellt. Beide greifen auf die gleiche Tabelle«Zeitstempel_Aenderstempel» zu – allerdings über verschiedene Abfragen. Bei dem ersten
Bericht ist vor allem mit Formeln innerhalb des Berichtes gearbeitet worden. Beim zweiten
Bericht ist das Ergebnis nahezu identisch – nur sind die Inhalte komplett aus der Abfrage übernommen worden, so dass innerhalb des Berichtes keine Rechnung mehr ausgeführt wird.

Aktuelle Standardwerte für Datum und Zeit

53

Bericht_Zeitdifferenzen

Datenquelle
Abfrage:Zeitdifferenzen

FIREBIRD:

Ansicht_Zeitdifferenzen

Der Bericht zeigt, aufgelistet nach Personen, die Zeitdifferenzen für jedes einzelne Startdatum
in der Maßeinheit Minuten. Außerdem wird die Summe der Zeitdifferenzen in Minuten undschließlich auch in Stunden angegeben. So könnte z.B. eine Zusammenstellung über Arbeitszeiten funktionieren.

Aktuelle Standardwerte für Datum und Zeit

54

Gruppierungen und Sortierungen greifen beim Report-Builder ineinander. Der Bericht ist zuerst
gruppiert nach dem Namen, zu dem es auch einen Gruppenfuß gibt. Dann erfolgt eine weitere
Gruppierung nach dem Startdatum. Diese Gruppierung steht zwar in der Übersicht, wird aber
im Bericht nicht gezeigt. Dadurch wird lediglich nach dem Startdatum sortiert. Seitenkopf undSeitenfuß sind hier ausgeblendet und stehen in den Eigenschaften auf Sichtbar → Nein.

Im Gruppenfuß befindet sich als erstes das Feld für die Summierung der Summe. Die Funktion
wurde händisch erstellt, da die Automatik von LO leider versagte. Über den Berichtsnavigator
ist so eine Funktion anschließend erreichbar und kann dann auch noch bearbeitet werden.
[Differenz_Minuten] ist der Feldwert aus der Abfrage «Zeitdifferenzen». Der erste Wert wird
gelesen, als Wert der Formel gespeichert und anschließend wird der nächste Wert der Abfrage
einfach hinzu addiert. Auf die Formel kann anschließend wie auf das Feld der Abfrage mit eckigen Klammern zugegriffen werden: [SummeMinuten].

Für die Zeitsumme in Stunden wird bei den Daten des Feldes eine Formel eingetragen. Sie liest
zuerst den Wert der Funktion [SummeMinuten] aus und teilt diesen durch 60. Mit der Funktion
INT werden daraus nur die Ganzzahlbestandteile übernommen. Anschließend wird mit &":"&
die Anzahl der verbleibenden Minuten angehängt. MOD([SummeMinuten];60) stellt den «Rest»
der Division der Minutenanzahl durch 60 dar.

Aktuelle Standardwerte für Datum und Zeit

55

Bericht_Zeitdifferenzen_Formularvorlage

Datenquelle
Abfrage:Zeitdifferenzen_Formularvorlage

FIREBIRD:

Ansicht_Zeitdifferenzen_Form

Dieser Bericht ist äußerlich gleich dem vorhergehenden aufgebaut. Nur ist hier die Differenz
zwischen den Zeiten direkt in Stunden ausgedrückt.
Dem Bericht liegt statt der Abfrage «Zeitdifferenzen» die Abfrage «Zeitdifferenzen_Formularvorlage» zu Grunde. In dieser Abfrage werden bereits die Zeiten berechnet, auch die Gesamtzeiten gruppiert nach dem Namen. Dadurch ist für den Bericht keine Funktion und keine Formel
notwendig.

MakrosUpdateAufruf aus
Formular: Defaultdatum_Makro_SQL, Defaultdatum_Zeit_Makro_SQL
001
002
003
004
005
006
007
008
009
010

SUB Update(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oFeldID AS OBJECT
DIM stTag AS STRING
DIM arList()
DIM loRow AS LONG
oFeld = oEvent.Source.ModeloForm = oFeld.Parent
loRow = oForm.getRow

Aktuelle Standardwerte für Datum und Zeit

56

011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026

stTag = oFeld.Tag
arList = Split(stTag, ";")
oFeldID = oForm.getByName(Trim(arList(1)))
IF NOT IsEmpty(oFeldID.currentValue) THEN
oDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = arList(0) + " WHERE ""ID"" = " + oFeldID.CurrentValueoSQL_Anweisung.executeUpdate(stSql)
oForm.reload
oForm.absolute(loRow)
END IF
END SUB

Über den Ursprung des Auslösers wird der Button ermittelt: oEvent.Source.Model. Das Formular ist Parent zu dem Button (Zeile 9). In Zeile 10 wird die aktuelle Datensatznummer ausgelesen. Die Zusatzinformationen werden aus oFeld.Tag ausgelesen und in einem Array in die beiden Teile aufgeteilt (Zeile 11 und 12). Im zweiten Teil steht der Name des Feldes, in dem derPrimärschlüssel steht. Mit Trim(arList(1)) werden eventuelle Leerzeichen vor und hinter dem
Eintrag entfernt (Zeile 13).
Ist das Primärschlüsselfeld leer, so wurde der Datensatz noch nicht abgespeichert. Es gibt alsoauch keine Änderung über den Button zu speichern. Nur wenn in dem Feld ein Wert steht, dannwird ein Kontakt zur Datenbank aufgebaut. Da aber das Formular bereits geöffnet ist müsste
dieser Kontakt sowieso bestehen. Die entsprechende Nachfrage ist also nur eine Absicherung(Zeile 15 bis 18).
Schließlich wird der SQL-Code aus den Informationen in den Zusatzinformationen und denzusätzlichen Informationen zu dem Wert in dem Primärschlüsselfeld zusammengestellt (Zeile21). Der SQL-Code wird an die Datenbank weitergegeben. Anschließend wird das Formular neu
geladen und das Ergebnis über oForm.absolute(loRow) direkt sichtbar im Formular dargestellt.

Datum_aktuell
Aufruf aus
Formular: Defaultdatum_Makro
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021

SUB Datum_aktuell
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oFeldAender AS OBJECT
DIM unoDate
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MainForm")oFeld = oForm.getByName("datDatum")oFeldAender = oForm.getByName("datDatumAender")
unoDate = createUnoStruct("com.sun.star.util.Date")
unoDate.Year = Year(Date)
unoDate.Month = Month(Date)
unoDate.Day = Day(Date)
IF isEmpty(oFeld.Date) THEN
oFeld.BoundField.updateDate(unoDate)
END IFoFeldAender.BoundField.updateDate(unoDate)
END SUB

Aktuelle Standardwerte für Datum und Zeit

57

Der Zugriff auf die Felder im Formular wird über das Formulardokument erstellt (Zeile 8 bis 12).
Anschließend wird das Datum als Struct zusammengebaut. In dieser Prozedur wird eine
Methode gezeigt, wie die einzelnen Werte den verschiedenen Teilen des Structs für ein Datum
zugeordnet werden. Ist das Feld mit dem Namen «datDatum» leer (Zeile 17), dann wird dasaktuelle Datum über oFeld.BoundField.updateDate() in das Feld geschrieben. Ansonsten
wird das aktuelle Datum nur in das Feld von «datDatumAender» geschrieben. Die endgültige
Abspeicherung erfolgt über das Formular. Schließlich wurde das Makro nur direkt vor der Ausführung einer Datensatzaktion eingesetzt, und diese Aktion bedeutet in diesem Falle ein Schreiben oder Ändern, gegebenenfalls auch löschen eines Datensatzes.

Datum_Zeit_aktuell
Aufruf aus
Formular: Defaultdatum_Zeit_Makro, Defaultdatum_Zeitdifferenz_Makro
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027

UpdateTimestamp

SUB Datum_Zeit_aktuell
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oFeldAender AS OBJECT
DIM unoStmp
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MainForm")oFeld = oForm.getByName("fmtDatumZeit")oFeldAender = oForm.getByName("fmtDatumZeitAender")unoStmp = createUnoStruct("com.sun.star.util.DateTime")
WITH unoStmp.Year = Year(Date).Month = Month(Date).Day = Day(Date).Hours = Hour(Time).Minutes = Minute(Time).Seconds = Second(Time).NanoSeconds = 000000
END WITH
IF isEmpty(oFeld.CurrentValue) THEN
oFeld.BoundField.updateTimestamp(unoStmp)
END IFoFeldAender.BoundField.updateTimestamp(unoStmp)
END SUB

Das Makro für den Zeitstempel läuft vom Prinzip her gleich ab wie das Makro für das aktuelle
Datum «Datum_aktuell». Hier wird lediglich ein anderes Struct, nämlich das DateTimeStruct, befüllt und der entsprechende Wert abgespeichert. Mit WITH unoStmp beginnt die
Zusammensetzung des Structes (Zeile 14). WITH bedeutet hier nur, dass im Prinzip vor jedes
der folgenden Elemente unoStmp gesetzt wird, bis eben END WITH auftaucht. Also
unoStmp.Year, unoStmp.Month usw.
In dem Struct ist es auch möglich, Nanosekunden weiter zu geben. Die spielen hier aber keine
Rolle. Das Struct wird also an dieser Position lediglich mit der entsprechenden Anzahl Nullen
aufgefüllt.
Schließlich wird der erstelle Timestamp nur dann in das Feld «fmtDatumZeit» geschrieben,
wenn das Feld leer ist. Ansonsten überschreibt der Timestamp nur den alten Timestamp im Feld
«fmtDatumZeitAender».

Aktuelle Standardwerte für Datum und Zeit

58

Standarddatum
Aufruf aus
Formular: Defaultdatum_Makro_Standarddatum
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016

SUB Standarddatum
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECT
DIM unoDate
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MainForm")oFeld = oForm.getByName("datDatum")unoDate = createUnoStruct("com.sun.star.util.Date")
unoDate.Year = Year(Date)
unoDate.Month = Month(Date)
unoDate.Day = Day(Date)oFeld.setPropertyValue("DefaultDate", unoDate)
END SUB

Das Makro läuft ähnlich ab wie das Makro «Datum_aktuell». Nachdem allerdings das Struct mit
dem aktuellen Datum befüllt ist wird jetzt das Standarddatum das Datumsfeldes damit gesetzt.
Dies geschieht über den allgemeinen Zugriff auf bestimmte Eigenschaften, nämlich übersetPropertyValue. Die Eigenschaft heißt beim Datumsfeld «DefaultDate». Der Wert, der hiergespeichert wird, muss vom Typ dem Struct entsprechen (Zeile 15).

Standarddatum_verlegt
Aufruf aus
Formular: Defaultdatum_Makro_Standarddatum_verlegt
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020

SUB Standarddatum_verlegt
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECT
DIM unoDate
DIM daNew AS DATEDIM i AS INTEGER
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MainForm")oFeld = oForm.getByName("datDatum")unoDate = createUnoStruct("com.sun.star.util.Date")
i = -2
daNew = DateAdd("m", i, Date)
unoDate.Year = Year(daNew)
unoDate.Month = Month(daNew)
unoDate.Day = Day(daNew)
oFeld.setPropertyValue("DefaultDate", unoDate)
END SUBDie Verlegung des Datums kam als Anfrage in einem Forum. Dort wurden häufig Daten bearbeitet, die die Eingabe eines Datums aus dem Vormonat erforderten. Dann musste in dem aufklappbaren Feld immer erst der Monat zurückgesetzt werden um dann den entsprechenden
Datumswert zu setzen.
Wird das Defaultdatum stattdessen direkt auf den Vormonat festgelegt, so kann das Zurückbewegen zum Vormonat entfallen.

Aktuelle Standardwerte für Datum und Zeit

59

Das Makro hat erst einmal den gleichen Aufbau wie «Standarddatum». Für die Umrechnung desDatums wird die Funktion DateAdd genutzt. In diesem Fall der in Zeile 14 hinterlegte Wert mittels dieser Funktions zum Monatswert des aktuellen Datums addiert (Zeile 15). Die Funktion
macht dann daraus einen gültigen Datumswert in der Zukunft (bei positiven Werte für i) oderin der Verangenheit (bei negativen Werten für i).
Schließlich wird der berechnet Wert wie beim Makro «Standarddatum» als vorgegebener Standardwert festgelegt.

StandardZeitstempel
Aufruf aus
Formular: Defaultdatum_Makro_Standardzeitstempel
001
002
003
004
005
006
007
008
009
010
011
012
013

SUB StandardZeitstempel
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECT
DIM stStmp AS STRING
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MainForm")oFeld = oForm.getByName("fmtZeitstempel")stStmp = Year(Date) & "-" & Month(Date) & "-" & Day(Date) & " " & Hour(Time) &":" & Minute(Time) & ":" & Second(Time)oFeld.setPropertyValue("EffectiveDefault", stStmp)
END SUB

Beim Zeitstempel muss der Wert anders festgelegt werden. Die Formularassistenten bilden
häufig den Zeitstempel in zwei getrennten Feldern ab: zum einen ein Datumsfeld, zum anderenein Zeitfeld. Hier wurde stattdessen ein formatiertes Feld gewählt. Dort ist der differenzierte
Aufbau von Datum und Zeit zusammen nicht als Zeitstempelfeld in den Standardwert übertragbar. Stattdessen wird für den Zeitstempel ein Text mit dem entsprechenden Format erstellt(Zeile 11). Der Aufbau von stStmp ist hier mit der für Datenbanken üblichen Datumsschreibweise versehen: Zuerst die vierstellige Jahreszahl, gefolgt von '-', dann die zweistellige Monatszahl und wieder '-' und schließlich die zweistellige Tageszahl. Zeit und Datum werden durcheine Leertaste getrennt. Als Verbindungselement der Variablen muss hier das «&» gewählt werden. Wird stattdessen, wie sonst bei Texten gewohnt, ein «+» gewählt, dann werden stattdessen die verschiedenen Werte einfach addiert.
Der Standardwert wird hier über EffectiveDefault eingefügt (Zeile 12). Zu den entsprechenden Standardwerten siehe unter anderen das Base-Handbuch.

UpdateTimestamp
Aufruf aus
Formular: Defaultdatum_Zeitdifferenz_Makro

Benötigt
Makro:
001
002
003
004
005
006
007

Datum_Zeit_aktuell

SUB UpdateTimestamp(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECTDIM loRow AS LONG
Datum_Zeit_aktuell
oFeld = oEvent.Source.ModeloForm = oFeld.Parent

Aktuelle Standardwerte für Datum und Zeit

60

008
009
010
011
012
013
014
015
016

loRow = oForm.getRow
IF oForm.IsNew THEN
oForm.insertRow
ELSEoForm.updateRow
END IFoForm.reload
oForm.absolute(loRow)
END SUB

Das Makro «UpdateTimestamp» greift direkt auf das Objekt zu, durch das es ausgelöst wurde:oEvent AS OBJECT. Dadurch wird im Folgenden das Formular ermittelt, das mit einer (neuen)Endzeit versehen wird (Zeile 6 und 7). Existiert noch keine Zeiteingabe, so wird die Zeit neu
geschrieben. Das Makro «Datum_Zeit_aktuell» wird dafür aus diesem Makro heraus zuerst einmal gestartet (Zeile 5).
Mit oForm.IsNew wird abgesichert, ob ein neuer Datensatz eingefügt (insertRow) oder ein
bestehender Datensatz geändert (updateRow) werden muss. Anschließend wird der aktuell
sichtbare Datensatz neu in das Formular eingelesen. Dazu wurde in Zeile 8 die aktuelle Datensatznummer ausgelesen. In Zeile 15 wird dann nach einem Neuladen des Formulars dieseDatensatznummer Damit wird entsprechend auch die Berechnung sichtbar durchgeführt.

Aktuelle Standardwerte für Datum und Zeit

61

Dateien einbinden

Dateien einbinden

62Einführung

Dass Bilder direkt in Base in eine Tabelle eingelesen werden können ist meist noch bekannt.
Allerdings funktioniert die Verwaltung von Bildern in separaten Verzeichnissen genau so gut.
Zusätzlich zur Bildaufnahme soll hier auch gezeigt werden, wie die Vorschau auf die Bilder an
den Viewer des Betriebssystem übergeben werden kann, so dass nicht nur kleine Bildchen zu
sehen sind und wie letztlich auch Bilder aus der Datenbank wieder nach außerhalb der Datenbank transportiert werden können.3
Inzwischen hat sich in Base an dieser Stelle seit der Version LO 5.0 etwas getan, so dass nicht
nur Bilder mit dem Grafischen Steuerelement aufgenommen werden können. Der Dialog wurde
auf alle Dateien erweitert. Von PDF-Dateien liefert das Grafische Steuerelement dann eine Vorschau auf die erste Seite des Dokumentes.

3

Beispieldatenbank Beispiel_Dateien_einbinden.odb

Dateien einbinden

63

Tabellen
Die Tabellen haben in dieser Datenbank die Funktion, entweder Dateien in den Tabellen zu speichern oder nur den Pfad zu Dateien aufzunehmen.

DateienDatenziel
Formular: Pfadeingabe, Pfadeingabe_Tabellenkontrollfeld
Ansicht:

Ansicht_Dateiendung

Abfrage, Bericht, Makro:

keine direkt

Feldname

Feldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Das Feld ist als
Auto-Wert-Feld gesetzt.

Beschreibung

Text

Soll einen Text aufnehmen, der als Dateibeschreibung dienen
kannEingabe erforderlich: Ja

Pfad

Text

Nimmt den Pfad zu der Datei auf.
Eingabe erforderlich: Ja

In dieser Tabelle werden nur die Pfade zu den Dateien gespeichert. Soll die Datenbankdatei auf
einem anderen Rechner funktionieren, so müssen die Dateien in der gleichen Pfadstruktur vorhanden sein. Die Dateien werden daher am besten in separaten Pfaden unterhalb der Datenbankdatei gelagert.

Dateien_intern
Datenziel
Formular: Dateiaufnahme, Dateiaufnahme_Name
Ansicht, Abfrage, Bericht, Makro: keine direkt

Feldname

Feldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Das Feld ist als
Auto-Wert-Feld gesetzt.

Beschreibung

Text

Soll einen Text aufnehmen, der als Dateibeschreibung dienen
kann.
Eingabe erforderlich: Nein

Datei

Bild

Nimmt die Datei als Binärdatenstrom auf.
Eingabe erforderlich: Ja

DateiName

Text

Sollte gegebenenfalls den Namen der Datei mit Dateiendung
speichern.
Eingabe erforderlich: Nein

Die Tabelle "Dateien_intern" steht in keiner Beziehung zur Tabelle "Dateien". Sie soll die interne
Speichermöglichkeit von Dateien und den Umgang mit diesen Dateien aufzeigen.
Auch wenn im Tabelleneditor für das Feld des Typs LONGVARBINARY in der Übersetzung als Feldtyp Bild angegeben wird, so ist dieser Feldtyp sehr wohl in der Lage, alle möglichen Binärdaten aufzunehmen. Dies trifft auch z.B. auf Writer-Dateien zu.

Dateien einbinden

64Bei der Migration von HSQLDB nach FIREBIRD wird aus dem Feldtyp für die "Datei" ein Feldtyp
Bild [BLOB] erstellt. Dieser Feldtyp arbeitet leider nicht korrekt mit dem Grafischen Kontrollfeld zusammen, so dass die Bilder nicht mehr angezeigt und auch nicht vergrößert dargestellt
werden können. Abhilfe: Der Name der Tabelle wird geändert und die gesamte Tabelle wird
kopiert. Beim Einfügen wird die Tabelle «Dateien_intern» neu erstellt. Jetzt wird beim Feldtyp
für "Datei" der Datentyp Blob [BLOB] gewählt. Dieser Feldtyp zeigt Bilder wieder korrekt an.

Ansichten
Diese Datenbank hat nur eine Ansicht, die Ansicht_Dateiendung
Datenquelle
Tabelle:DateienDatenziel
Abfrage:

Bilder001 SELECT "Dateien"."ID","Dateien"."Beschreibung","Dateien"."Pfad" ,002 LOWER ( SUBSTRING ( RIGHT( "Pfad", 5 )FROM POSITION( '.' IN RIGHT( "Pfad", 5 ) ) + 1 ) ) AS "D_Endung"
003 FROM "Dateien"
Die Ansicht dient lediglich dazu, aus den Pfadbezeichnungen für die Dateien die Dateiendungenzu separieren. Damit der Code einfacher zu handhaben ist, ist dies nicht direkt in die Abfrage
mit eingebaut worden.
In Zeile 2 wird zunächst nach den letzten 5 Zeichen der Pfadbezeichnung gesucht. In diesen 5
Zeichen muss die Dateiendung für Bilder stecken. Die Dateiendung ist durch einen '.' abgetrennt. Die Position dieses Punktes dient dazu, die Dateiendung schließlich ohne den Pfad zu
lesen. Die Endung wird hier durch LOWER immer klein geschrieben wiedergegeben, so dass inder darauf zugreifenden Abfrage die gesuchten Dateiendungen nur klein geschrieben werden
müssen.
Ein entsprechender Code ist auch für die Aufnahme der Dateien in die Datenbank selbst möglich, wenn dort der Dateiname mit abgespeichert wird.

Abfragen
Diese Datenbank hat nur eine Abfrage, die Abfrage Bilder
Datenquelle
Ansicht:

Ansicht_Dateiendung

Datenziel
Bericht:

Bericht_Bilder

001 SELECT * FROM "Ansicht_Dateiendung"
002 WHERE "D_Endung" IN ( 'jpg', 'jpeg', 'png', 'pdf' )Die Abfrage filtert aus den Dateien diejenigen heraus, deren Endung den in Zeile 2 aufgeführten Endungen entspricht. Dies ist für den Bericht notwendig, der sonst bei anderen Dateien als
Bilddateien und PDF-Dateien nur eine Fehlermeldung anzeigt. Dies Liste in Zeile 2 kann natürlich wesentlich alle Endungen enthalten, die auch in dem Dialog zur Aufnahme von Bildern enthalten sind.

Dateien einbinden

65Formulare
Die Formulare sind notwendiger Bestandteil zur Aufnahme von Bildern in die Datenbank. Die
Bildaufnahme geht nicht über die Tabellensicht oder Sicht einer Abfrage. Es wäre höchstensmöglich, eine Aufnahme per Makro an den Formularen vorbei zu realisieren.
Auch zur Betrachtung der Bilder sind wieder Formulare erforderlich. Lediglich ein Bericht kannneben den Formularen noch Bilder in der Base-Datei darstellen.

Pfadeingabe

1 Formular (Tabelle: Dateien)
Makros in Feldeigenschaften1

Schaltfläche Datei mit externem Programm
betrachten(Aktion ausführen)

Dateien einbinden

Module1.Betrachten

66Das Formular enthält neben den Eingabefeldern für «ID» und «Beschreibung» ein grafisches
Steuerelement. Das grafische Steuerelement ist mit dem Feld "Pfad" aus der Tabelle verbunden. Wird durch Doppelklick auf das Feld eine Datei aus dem Dateisystem ausgesucht, so wird
nur der Pfad (relativ zur Datenbankdatei) gespeichert. Die Datei wird trotzdem in dem grafischen Steuerelement angezeigt, wenn sie anzeigbar ist. Dies gilt neben normalen Bilddateienauch für die erste Seite von PDF-Dateien.
Das Feld für den Primärschlüssel «ID» ist schreibgeschützt, da es über Auto-Wert gefüllt wird.
Deshalb auch der gelbe Hintergrund.
Das grafische Steuerelement bietet nicht die Möglichkeit, Details des Bildes zu betrachten. Deshalb wird hier über Datei mit externem Programm betrachten der im System angegebenen
Dateibetrachter für die entsprechende Datei gestartet. Jetzt können Details betrachtet und alle
weiteren Funktionen des Viewers bzw. verknüpften Programms genutzt werden. Bei einer Writer-Datei wird z.B. direkt der Writer mit der datei gestartet und die Datei könnte auch bearbeitet werden.

Pfadeingabe_Tabellenkontrollfeld

1 Formular (Tabelle: Dateien)
Makros in Feldeigenschaften1

Schaltfläche Datei mit externem Programm
betrachten(Aktion ausführen)

Module1.Betrachten

Wird statt der einzelnen Kontrollfelder ein Tabellenkontrollfeld genutzt, so kann das Bild nicht
innerhalb des Tabellenkontrollfeldes dargestellt werden. Stattdessen wird hier der Pfad (relativ
zur Datenbankdatei) direkt angezeigt. Das grafische Steuerelement ist hier direkt neben dem
Tabellenkontrollfeld im selben Formular angeordnet. Es zeigt das Bild zu dem aktuellen markierten Datensatz an.
Die Felder «ID» und «Datei (Pfad)» sind schreibgeschützt. Leider erlaubt ein Tabellenkontrollfeld
nicht, solche Felder mit einer separaten Formatierung (gelber Hintergrund wie im vorhergehenden Formular) zu kennzeichnen.

Dateien einbinden

67

Dateiaufnahme

1 Formular (Tabelle: Dateien_intern)
Makros in Feldeigenschaften1

Schaltfläche Datei mit externem Programm
betrachten(Aktion ausführen)

Module1.DateiAuslesenÄußerlich unterscheidet sich dieses Formular nicht vom Formular «Pfadeingabe». Allerdings
wird hier die Datei in die Tabelle selbst eingelesen. Eine Verknüpfung noch außen wird nicht
gespeichert.
An dieser Stelle sollte aber darauf hingewiesen werden, dass das Abspeichern von Bilddaten
sehr schnell eine Datenbank von der Größe her deutlich aufbläht. Deshalb sollten intern möglichst nur kleinere Bilder, z.B. Icons, aber nicht komplette Fotos aktueller Digitalkameras abgespeichert werde. Das Abspeichern von normalen Textdateien dagegen ist kein Problem.

Dateiaufnahme_Name

1 Formular (Tabelle: Dateien_intern)
Makros in Formulareigenschaften1

Vor der Datensatzaktion

Dateien einbinden

Module1.DateiName

68

Makros in Feldeigenschaften1

Schaltfläche Datei mit externem Programm
betrachten(Aktion ausführen)

Module1.DateiAuslesen_mitName

1

Graphisches Feld «Datei (intern)» (Bei Fokusverlust)

Module1.DateiName

Das Formular «Dateiaufnahme_Name» hat lediglich ein zusätzliches Feld, in dem der Name derDatei abgespeichert werden kann, also z.B. «Testdatei.pdf». Dies könnte für Betriebssysteme,die nach der Dateiendung entsprechende Programme für das Öffnen von Dateien bereitstellen,
wichtig sein, da der eigentliche Bildname nach dem Speichern der Bilddaten in der Datenbank
nicht mehr nachvollzogen werden kann.
Der Name wird hier automatisch über ein Makro beim Verlassen des Grafischen Kontrollfeldesoder beim Speichern des Datensatzes ausgelesen. Das Feld ist, wie an der gelben Farbe vom
Design her zu sehen, ebenso wie das Feld «ID» schreibgeschützt.

Berichte
Diese Datenbank hat nur einen Bericht, den Bericht Bericht_Bilder

Datenquelle
Abfrage:

BilderDie Bilder werden, wie im Formular, in einem grafischen Kontrollfeld dargestellt. Das Kontrollfeld ist so voreingestellt, dass das Seitenverhältnis beibehalten wird. Sonst könnte es dazu
kommen, dass Bilder nicht mehr zu erkennen sind, weil nur ein Ausschnitt von ihnen gezeigt
oder das Bild verzerrt in den vorgesehenen Rahmen eingepasst wird.
Mit den Standardeinstellungen kann es dazu kommen, dass eine Datei, die nicht im grafischen
Kontrollfeld angezeigt werden kann, dafür sorgt, dass kein einziges Bild erscheint. Hier kann auf
zweierlei Weise gegengesteuert werden:

Dateien einbinden

69Der Bericht wird zum Editieren geöffnet. Das grafische Steuerelement wird markiert. In Eigenschaften → Allgemein → Als Verknüpfung vorbereiten wird «Nein» ausgewählt. Alle Bilder
erscheinen dann problemlos. Bei anderen Dateien wie z.B. einer Writer-Datei erscheint dann ein
kleines Rechteck mit einer Meldung, dass diese Datei nicht geladen werden konnte.
Bei dem obigen Bericht ist allerdings schon vorher darauf geachtet worden, dass Datensätze
mit Dateien, die nicht dargestellt werden können, gar nicht erst übernommen werden. Dies
wurde über die Abfrage «Bilder» geregelt.

HinweisNach dem erneuten Editieren von Berichten mit Bildern fällt auf, dass die Datenbankdatei deutlich größer wird. Bei den Tests zu dieser Beschreibung mit großen Bilddateien
entstand plötzlich eine Datei der Größe 16 MB. Innerhalb der *.odb-Datei legt Base aus
nicht nachvollziehbaren Gründen im Berichtsverzeichnis einen Ordner «ObjectReplacements» an. Dieser Ordner enthält dann eine Datei «report», die für eine entsprechende
Vergrößerung der *.odb-Datei sorgt.

Wenn die Datenbankdatei in einem Packprogramm geöffnet wird ist dieser Ordner mit
Inhalt im Verzeichnis «reports» im Unterverzeichnis zu dem jeweiligen Bericht sichtbar.
Dieses Verzeichnis kann über das Packprogramm gefahrlos gelöscht werden.
Wenn Berichte nicht wiederholt editiert werden, reicht ein einmaliges Löschen des Verzeichnisses aus.
Der Bug ist hier gemeldet: https://bugs.freedesktop.org/show_bug.cgi?id=80320

Dateien einbinden

70

Makros
Betrachten
Aufruf aus
Formular: Pfadeingabe, Pfadeingabe_Tabellenkontrollfeld
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

SUB Betrachten
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oShell AS OBJECT
DIM stUrl AS STRING
DIM stFeld AS STRING
DIM arUrl_Start()oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("Formular")oFeld = oForm.getByName("GraphischesFeld")
stUrl = oFeld.BoundField.getString
arUrl_Start = split(oDoc.Parent.Url,oDoc.Parent.Title)
oShell = createUnoService("com.sun.star.system.SystemShellExecute")
stFeld = convertToUrl(arUrl_Start(0) + stUrl)
oShell.execute(stFeld,,0)
END SUB

Das graphische Kontrollfeld im Formular wird aufgesucht. Da in der Tabelle nicht das Bild selbst,
sondern nur der Pfad als Text gespeichert wird, wird hier über getString dieser Text ausgelesen.
Anschließend wird der Pfad zu der Datenbankdatei ermittelt. Mit oDoc.Parent wird die *.odbDatei erreicht. Sie ist der Container für die Formulare. Über oDoc.Parent.Url wird schließlich
die gesamte URL incl. Dateinamen ausgelesen. Der Dateiname ist auch zu sehen in
oDoc.Parent.Title. Der Text wird jetzt mit der Funktion split aufgetrennt, wobei als Trenner
der Dateiname benutzt wird. Die Auftrennung gibt so nur als erstes und einziges Element desArrays den Pfad zur *.odb-Datei wieder (Zeile 17).
Externe Programme können über das Struct com.sun.star.system.SystemShellExecute
gestartet werden. Dem externen Programm wird hier nur der Pfad zur Datei mitgegeben, deraus dem Pfad zur Datenbankdatei und dem intern gespeicherten relativen Pfad von der Datenbankdatei aus zusammengesetzt wurde. Die grafische Benutzeroberfläche des Betriebssystems
entscheidet jetzt darüber, mit welchem Programm die entsprechende Datei zu öffnen ist.
Mit dem Kommando oShell.execute werden 3 Parameter übergeben (Zeile 20). Als erstes wird
eine ausführbare Datei oder der Pfad zu einer Datei aufgeführt, die im System mit einem Programm verbunden sind. Als zweites werden Parameter aufgeführt, mit denen das Programm
gestartet werden soll. Als drittes wird über eine Ziffer mitgeteilt, wie mit Fehlermeldungen desSystems bei missglückter Ausführung umzugehen ist. Hier stehen 0 (Standard), 1 (keine Meldung anzeigen) und 2 (nur das Öffnen von absoluten URLs erlauben) zur Verfügung.

DateiAuslesen
Aufruf aus
Formular: Dateiaufnahme
001
002
003

SUB DateiAuslesen
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECT

Dateien einbinden

71

004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026

DIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oStream AS OBJECTDIM oShell AS OBJECTDIM oPath AS OBJECTDIM oSimpleFileAccess AS OBJECT
DIM st AS STRING
DIM stPfad AS STRING
DIM stFeld AS STRING
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("Formular")oFeld = oForm.getByName("GraphischesFeld")oStream = oFeld.BoundField.getBinaryStream
oPath = createUnoService("com.sun.star.util.PathSettings")
st = ""
stPfad = oPath.Temp & "/DbDatei" & stoSimpleFileAccess = createUnoService("com.sun.star.ucb.SimpleFileAccess")oSimpleFileAccess.writeFile(stPfad, oStream)oShell = createUnoService("com.sun.star.system.SystemShellExecute")
stFeld = convertToUrl(stPfad)
oShell.execute(stFeld,,0)
END SUB

Der wichtigste Unterschied zu der externen Bildverwaltung ist gleichzeitig auch die entscheidende Frage für den Datenbanknutzer: Wie komme ich eigentlich an die Bilddaten wieder
heran, die jetzt in der Tabelle der Datenbank gespeichert liegen. Mit einem externen Viewer
kann ich schließlich nicht interne Bilder ansehen. Die internen Bilder müssen dafür zumindest
vorübergehend ausgelesen werden um damit über ein Betrachtungsprogramm drauf zugreifenzu können.
Mit getBinaryStream wird aus dem mit dem graphischen Kontrollfeld verbundenen Feld derDatenbanktabelle der Binärcode ausgelesen, der zu dem Bild gehört (Zeile 17).
Es kann, je nach Betriebssystem, notwendig sein, eine Dateiendung dem jeweils gewählten
Namen beizufügen. In der Variablen «st» kann so eine Endung aufgeführt werden (z. B. ".png"
oder ".jpg"). Aus den Pfadangaben in Extras → Optionen → LibreOffice → Pfade wird der Pfad für
temporäre Dateien ausgelesen. Dorthin soll das Bild gespeichert werden (Zeile 18 bis 20).
Mit dem Struct com.sun.star.ucb.SimpleFileAccess wird die ausgelesene Datei in das temporäre Verzeichnis geschrieben. Der weitere Verlauf des Makros ist wieder identisch mit derProzedur «Betrachten» für die externen Dateien.

DateiAuslesen_mitName
Aufruf aus
Formular: Dateiaufnahme_Name

In diesem Makro wird lediglich statt eines fest vorgegebenen Dateinamens die Möglichkeit
geboten, einen Dateinamen mit Endung aus der Datenbank auszulesen. Hier nur die Unterschiede zur Prozedur «DateiAuslesen»
001
002
003
004
005
006
007
008
009
010
011

SUB DateiAuslesen_mitName...
oFeld2 = oForm.getByName("DateiName")
stName = oFeld2.Text
IF stName = "" THEN
stName = "DbDatei"
END IF...
stPfad = oPath.Temp & "/" & stName...
END SUB

Dateien einbinden

72

Steht in dem Formularfeld "DateiName" eine Bezeichnung für die zu speichernde Datei, so wird
das Bild unter dem entsprechenden Namen abgespeichert (Zeile 4). Damit besteht die Möglichkeit, der erzeugten Datei eine Endung mitzugeben und außerdem auch noch direkt nacheinander mehrere Bilder abzuspeichern, ohne die vorhergehenden zu überschreiben. Die gespeicherten Bilder können anschließend aus dem temporären Verzeichnis ist das entsprechende
Wunschverzeichnis übertragen werden.

DateiName
Aufruf aus
Formular: Dateiaufnahme_Name
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018

SUB DateiName
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oField AS OBJECT
DIM stImageUrl AS STRING
DIM stFile AS STRING
GlobalScope.BasicLibraries.LoadLibrary("Tools")
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("Formular")oField = oForm.getByName("GraphischesFeld")
stImageUrl = oField.ImageUrl
IF stImageUrl <> "" THEN
stFile = FileNameoutofPath(stImageUrl)oForm.UpdateString(oForm.FindColumn("DateiName"), stFile)END IF
END SUB

Bei der Aufnahme der Datei über das Grafische Kontrollfeld wird die URL benötigt. Diese URL
kann nur zu diesem Zeitpunkt ausgelesen werden. Dies erfolgt in Zeile 13. Ist diese URL nicht
leer, d.h. wenn das Makro ausgelöst wurde, obwohl gerade keine neue Datei eingelesen wurde,
so kann auch kein neuer Name ausgelesen werden. Ansonsten wird über die Funktion
FileNameoutofPath (Zeile 15) der Dateiname bestimmt. Diese Funktion steht in den Standardbibliotheken zur Verfügung. Diese Bibliotheken müssen zuerst eingebunden werden (Zeile 8)
bevor die Funktion nutzbar ist.
Die Verzweigung ist nur deshalb notwendig, weil das grafische Kontrollfeld kein Ereignis «Modifiziert» enthält. Deswegen wird das Makro bei jeder Datensatzänderung des Formulars aufgerufen.

Dateien einbinden

73Serienbrief direkt

Serienbrief direkt

74

Einführung
Für Serienbriefe wird in der Regel zuerst einmal eine Writer-Datei gestartet und dann von dort
aus der Seriendruck in die Wege geleitet. Die folgende Variante soll aufzeigen, wie so etwas auf
verschiedene Art auch direkt aus einem Base-Formular heraus erfolgen kann.
Die Dokumente zum Serienbrief sind in der vorliegenden Fassung nur zu gebrauchen, wenn•

die Datenbank in LO angemeldet ist unter dem Namen "Beispiel_Datenbank_Serienbrief_direkt"4•alle Dateien in einem gemeinsamen Verzeichnis liegen•

die Makrofunktionalität ermöglicht wird.

Es wird über ein Makro die Serienbrieffunktionalität angesprochen. In der Datenbank befinden
sich zwei Makros, die entsprechend an die Serienbriefe angepasst wurden. So etwas lässt sichmit Variablen auch in einem Makro zusammen fassen, sollte hier aber nicht zusätzlich verwirren.
Über die Makros wird ein Brief erzeugt und direkt in dem Verzeichnis abgespeichert, in dem
sich auch der Serienbrief und die Datenbank befindet. Mehrmaliges Erstellen des Briefes führt
zu mehreren durchnummerierten Briefen mit dem Feld "Nachname" als Dateibeginn.
Die Rechnungserstellung hat so noch einen Schönheitsfehler:
Der Serienbrief arbeitet mit Tabulatoren. Wird die Beschreibung des Gegenstandes zu groß, soverschiebt sich entsprechend der folgende Inhalt. Hier muss gegebenenfalls nachgearbeitet
werden.
Bei den Textfeldern ist deshalb ein anderes Verfahren angewandt worden. Die Textfelder werden gefüllt. Gleichzeitig befindet sich in der Vorlage eine einzeilige Tabelle. Dieser Tabelle werden jeweils Datenzeilen hinzugefügt und anschließend wieder eine neue Tabellenzeile eingefügt.

TabellenAnschrift
Datenziel
Formular: Anschrift, Rechnung, Rechnung_Textfelder, Rechnung_Textfelder_Uebertrag, Rechnung_Textfelder_Uebertrag_Seriendruck
Abfrage:

4

Adresseingabe, Rechnung_einzeilig, Fehler: Verweis nicht gefunden, Rechnung_Textfelder

Feldname

Feldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Sie ist als Autowert
festgelegt.

VornameText

Eingabe erforderlich: Nein

Nachname

Text

Eingabe erforderlich: Ja

Geschlecht

Text

Länge des Textes ist 1 Zeichen. Hier wird nur 'm' oder 'w' eingetragen, damit später daraus eine Anrede ermittelt werden
kann.
Eingabe erforderlich: Nein

Straße_Nr

Text

Eingabe erforderlich: Nein

Beispieldatenbank Beispiel_Datenbank_Serienbrief_direkt.odb

Serienbrief direkt

75

Postleitzahl

Text

Länge des Textes ist 5 Zeichen. Postleitzahlen sollten nicht
als Zahlen abgespeichert werden. Führende Nullen sind sonstnicht möglich.
Eingabe erforderlich: Nein

Ort

Text

Eingabe erforderlich: Nein

Die Tabelle "Anschrift" zeigt das typische Anwendungsbeispiel eines Serienbriefes. Hier werden
beispielhaft lediglich die Adressdaten in ein Druckdokument übertragen, das natürlich mit entsprechenden Feldern beliebig erweitert werden könnte.
Die weiteren Tabellen dienen dazu, auch den Druck einer Rechnung direkt aus der Datenbank
heraus aufzuzeigen. Eine Rechnung unterscheidet sich in sofern von einem einfachen Serienbrief, da hier zu einem Datensatz (z.B. der Anschrift) viele Rechnungstitel gehören. Im ReportBuilder wird so etwas durch Gruppierungen gelöst. Hier ist der eingeschlagene Weg etwas
anders angefasst worden.

Rechnung
Datenziel
Formular: Rechnung, Rechnung_Textfelder, Rechnung_Textfelder_Uebertrag
Makro:

PDF_Export

Abfrage:

Rechnung_einzeilig, Rechnung_nicht_gedruckt, Rechnung_Textfelder

Feldname

Feldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Sie ist als Autowert
festgelegt. Der Primärschlüssel ist gleichzeitig die spätere
Rechnungsnummer.

Datum

Datum

Hier wird das Rechnungsdatum festgelegt.
Eingabe erforderlich: Ja

Anschrift_ID

Integer

Hier wird die "ID" der Tabelle "Anschrift" als Fremdschlüssel
gespeichert. Grundsätzlich kann so eine Person mehrere
Rechnungen erhalten ohne jedes Mal die gesamte Anschrift
erneuern zu müssen.
Eingabe erforderlich: Nein

Druckdatum

Datum

Beim Seriendruck von Rechnungen wird hier das Datum derÜbertragung des Rechnungsinhaltes in ein *.pdf-Dokument
gespeichert.
Eingabe erforderlich: Nein

Die Tabelle Rechnungsinhalt wird nur für die Durchführung des Rechnungsdruckes benötigt. Siewird durch ein Makro gefüllt und nicht irgendwie durch Formulare bearbeitet.

Rechnungsinhalt
Datenziel
Abfrage:

Rechnung_einzeilig

Makro:

Textfelder_Fuellen, Rechnungsinhalt_zusammenstellen, Tabellen_fuellen_Uebertrag

Formular: keine direkt

Serienbrief direkt

76

FeldnameFeldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Die ID ist identisch
mit der der Tabelle "Rechnung". Eine Beziehung wird hieraber nicht festgelegt.

Rechnungsinhalt

Text

Länge des Textes sollte groß gewählt werden, da hier für eineder Serienbrieffunktionen alle Rechnungsinhalte in einem
Datensatz gespeichert werden. Im Beispiel ist der Länge auf
8000 Zeichen festgelegt (momentanes Maximum FIREBIRD).
Eingabe erforderlich: Ja

Verkauf
Datenziel
Formular: Rechnung, Rechnung_Textfelder, Rechnung_Textfelder_Uebertrag, Rechnung_Textfelder_Uebertrag_Seriendruck
Abfrage:

Rechnung_Textfelder

Makro:

Rechnungsinhalt_zusammenstellen, Rechnungsinhalt_zusammenstellen_Tabelle,
Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag, Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck

Feldname

Feldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Sie ist als Autowert
festgelegt.

AnzahlTiny Integer

Je größer die Anzahl der Waren sein sollte, desto größer ist
natürlich der Umfang dieses Feldes festzulegen. Mit Tiny Integer geht die Anzahl von -128 bis +127 – für ein einfaches
Beispiel völlig ausreichend.
Eingabe erforderlich: Ja

Waren_ID

Integer

Hier wird die "ID" der Tabelle "Waren" als Fremdschlüssel
gespeichert.
Eingabe erforderlich: Ja

Rechnung_ID

Integer

Hier wird die "ID" der Tabelle "Rechnung" als Fremdschlüssel
gespeichert.
Eingabe erforderlich: Ja

Waren
Datenziel
Formular: Rechnung, Rechnung_Textfelder, Rechnung_Textfelder_Uebertrag, Rechnung_Textfelder_Uebertrag_Seriendruck, Waren
Abfrage:

Etiketten, Rechnung_einzeilig, Rechnung_Textfelder

Makro:

Rechnungsinhalt_zusammenstellen, Rechnungsinhalt_zusammenstellen_Tabelle,
Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag, Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck

Serienbrief direkt

77

FeldnameFeldtyp

Beschreibung

ID

Integer

Die ID ist Primärschlüssel dieser Tabelle. Sie ist als Autowert
festgelegt.

Ware

Text

Eingabe erforderlich: Ja

Preis

DezimalBei der Preisangabe ist darauf zu achten, dass 2 Nachkommastellen vorgesehen sind. Sonst sind nur ganzzahlige
Angaben möglich.
Das Feld kann auch entsprechend schon mit der Währungsangabe vorformatiert werden. Die Formatierung wird in
Tabellen, Abfragen und beim Druck über einen Serienbrief
berücksichtigt.
Eingabe erforderlich: Nein

Die Tabellen sind für die Rechnungserstellung folgendermaßen verbunden:

Die Tabelle Anschrift wird separat bestückt und dient als erstes Beispiel für den Seriendruck.
Die Kombination mit der Rechnungserstellung zeigt dann ein erweitertes Anwendungsbeispiel.

Abfragen
Adresseingabe
Datenquelle
Tabelle:Anschrift

Datenziel
Formular: Anschrift_Textfelder

001 SELECT "ID", "Vorname", "Nachname",002 "Geschlecht" AS "Geschl_kurz",
003 "Straße_Nr", "Postleitzahl", "Ort",
004 CASE WHEN "Geschlecht" = 'm' THEN 'Herrn' ELSE 'Frau' END AS "Geschlecht"
005 FROM "Anschrift"
Hier wird lediglich das ursprüngliche Feld "Geschlecht" wird mit einem Alias versehen, da dieselbe Bezeichnung jetzt für die Anrede genutzt werden soll. Mit der Funktion CASE WHEN wird
ausgelesen, ob in dem Feld "Geschlecht" der Wert 'm' steht. Steht dort dieser Wert, so wird
'Herrn' ausgegeben. Ansonsten erscheint 'Frau'.
Für FIREBIRD muss diese Abfrage direkt als SQL in das Formular «Anschrift_Textfelder» eingegeben werden. Dort wird zur Zeit (LO 6.4.4.2) ein Alias nicht einwandfrei ausgelesen, wenn er, wiehier, einer vorherigen Benennung eines anderen Feldes entspricht. Aus der Bezeichnung«Geschlecht» in Zeile 4 wird bei Nutzung der Abfrage schlicht «CASE».

Serienbrief direkt

78

Etiketten
Datenquelle
Tabelle:Waren

Datenziel
Formular: Etiketten_Serienbrief_im_Formular

001 SELECT RIGHT( '0000000' || "ID", 8 ) "WarenNr",
002 CASE WHEN LOCATE( CHAR( 10 ), "Ware" ) > 0 THEN SUBSTRING ( "Ware" FROM 1
FOR (LOCATE( CHAR( 10 ), "Ware", LOCATE( CHAR( 10 ), "Ware" ) + 1 )-1 ))ELSE "Ware" END AS "Warenbezeichnung",
003 REPLACE( "Preis" || ' €', '.', ',' ) "Preis"
004 FROM "Waren"
Die "WarenNr" wird hier aus dem Feld "ID" erstellt. Es werden einfach sieben Nullen vor die
Nummer gesetzt und anschließend der so entstehende Text mit den acht Zeichen von rechts
aus wieder gegeben. So werden alle Warennummern 8-stellig dargestellt.
Bei der "Ware" gibt es Einträge, die mit einem Zeilenumbruch mehrere Zeilen untereinander
darstellen können. Hier wird mit LOCATE gesucht, ob "Ware" so einen Eintrag enthält. Ist derEintrag gegeben, so wird der Text mit der Funktion SUBSTRING direkt vor einem eventuell weiteren Zeilenumbruch abgeschnitten. "Warenbezeichnung" enthält so höchsten zwei Zeilen.
Für FIREBIRD ist hier ein anderer Code zu wählen, da dort weder CHAR noch LOCATE vorhandensind:
002 CASE WHEN POSITION( ASCII_CHAR( 10 ) IN "Ware" ) > 0 THEN SUBSTRING( "Ware" FROM 1 FOR (POSITION( ASCII_CHAR( 10 ), "Ware", POSITION( ASCII_CHAR( 10 ) IN "Ware" ) + 1 )-1 )) ELSE "Ware" END AS"Warenbezeichnung",
Auch für den Preis, der auf der Etikette dargestellt werden soll, ist ein Eingriff notwendig. Sonst
wird der Preis nicht korrekt mit dem Eurozeichen dargestellt. An "Preis" wird eine Leerstelle unddas Eurozeichen angefügt. Mit REPLACE muss jetzt noch die Vorgabe der Datenbank für den
Dezimaltrenner, der Punkt, durch ein Komma ersetzt werden.

Rechnung_einzeilig
Datenquelle
Tabelle:Waren, Anschrift, Rechnung, Rechnungsinhalt

Datenziel
Makro:

Rechnung

001 SELECT "Anschrift"."Vorname", "Anschrift"."Nachname","Anschrift"."Straße_Nr", "Anschrift"."Postleitzahl", "Anschrift"."Ort",
002 "a"."Datum" AS "Rechnungsdatum", "a"."ID",003 "Rechnungsinhalt"."Rechnungsinhalt",
004 REPLACE( ( SELECT SUM( "Verkauf"."Anzahl" * "Waren"."Preis" ) FROM
"Verkauf", "Waren" WHERE "Verkauf"."Waren_ID" = "Waren"."ID" AND"Verkauf"."Rechnung_ID" = "a"."ID" ) || ' €', '.', ',' ) AS "Summe"
005 FROM "Rechnung" AS "a", "Anschrift", "Rechnungsinhalt"
006 WHERE "a"."Anschrift_ID" = "Anschrift"."ID" AND "a"."ID" ="Rechnungsinhalt"."ID"
Der Tabelle "Anschrift" werden alle notwendigen Daten für die Anschrift in dem Rechnungsformular entnommen. Die Tabelle "Rechnung" wird über einen Alias als "a" angesprochen. Dies ist
notwendig, damit in der korrelierenden Unterabfrage nur die Beträge addiert werden, die zu derSerienbrief direkt

79

entsprechenden Rechnungsnummer ("a"."ID") passen. Bei der Summenbildung
SUM("Verkauf"."Anzahl" * "Waren"."Preis") wird auf die Rechnungsnummer des Datensatzes außerhalb der Unterabfrage so Bezug genommen. Auch in der Abfrage ist schließlich
wieder das Ersetzen des Dezimalpunktes durch das Dezimalkomma anzutreffen.
Für FIREBIRD muss zur Zeit (LO 6.4.4.2) diese Abfrage in eine Ansicht umgewandelt werden.
FIREBIRD liefert sonst zusammen mit dem Makro eine Fehlermeldung über ein «Ambigous fieldname», weil in den benutzten Tabelle überall der Primärschlüssel "ID" lautet. Die Abfrage selbst
funktioniert aber auch in Firebird einwandfrei.

Rechnung_nicht_gedruckt
Datenquelle
Tabelle:Rechnung

Datenziel
Formular: Rechnung_Textfelder_Uebertrag_Seriendruck

001 SELECT * FROM "Rechnung" WHERE "Druckdatum" IS NULL
Hier wird lediglich der Inhalt der Tabelle "Rechnung" auf die Datensätze beschränkt, die nochkeinen Eintrag im Feld "Druckdatum" haben. Dieser Eintrag wird durch das Makro «PDF_Export»
der Tabelle "Rechnung" hinzugefügt.

Rechnung_Textfelder
Datenquelle
Tabelle:Anschrift, Rechnung, Verkauf, Waren

Datenziel
Formular: Rechnung_Textfelder, Rechnung_Textfelder_Uebertrag, Rechnung_Textfelder_Uebertrag_Seriendruck
Makro:

Rechnung_Textfelder

001 SELECT "Anschrift"."Vorname", "Anschrift"."Nachname","Anschrift"."Straße_Nr", "Anschrift"."Postleitzahl", "Anschrift"."Ort",
002 RIGHT( '0' || EXTRACT ( DAY FROM "a"."Datum" ), 2 ) || '.' || RIGHT( '0'|| EXTRACT ( MONTH FROM "a"."Datum" ), 2 ) || '.' || EXTRACT ( YEAR FROM
"a"."Datum" ) "Rechnungsdatum",
003 'RE_' || RIGHT( '0000000' || "a"."ID", 8 ) AS "Rechnungsnummer,
004 "a"."ID",005 REPLACE( ( SELECT ROUND( SUM( "Verkauf"."Anzahl" *"Waren"."Preis" )*1.00/1.19 , 2) FROM "Verkauf", "Waren" WHERE"Verkauf"."Waren_ID" = "Waren"."ID" AND "Verkauf"."Rechnung_ID" = "a"."ID") || ' €', '.', ',' ) AS "Netto",
006 REPLACE( ( SELECT ROUND( SUM( "Verkauf"."Anzahl" *"Waren"."Preis" )*0.19/1.19 , 2) FROM "Verkauf", "Waren" WHERE"Verkauf"."Waren_ID" = "Waren"."ID" AND "Verkauf"."Rechnung_ID" = "a"."ID") || ' €', '.', ',' ) AS "Umsatzsteuer",
007 REPLACE( ( SELECT SUM( "Verkauf"."Anzahl" * "Waren"."Preis" ) FROM
"Verkauf", "Waren" WHERE "Verkauf"."Waren_ID" = "Waren"."ID" AND"Verkauf"."Rechnung_ID" = "a"."ID" ) || ' €', '.', ',' ) "Summe"
008 FROM "Rechnung" AS "a", "Anschrift"
009 WHERE "a"."Anschrift_ID" = "Anschrift"."ID"Zuerst werden die Daten der Anschrift erfasst (Zeile 1).
Serienbrief direkt

80

Beim Rechnungsdatum ist darauf zu achten, dass es, sofern in ein Textfeld eingelesen wird, inder korrekten Formatierung übergeben wird (Zeile 2). Wird es nicht entsprechend vorformatiert,
so erscheint es in der Schreibweise, die bei Datenbanken Standard ist: 2014-05-07. Mit
EXTRACT ( DAY FROM "a"."Datum" ) wird aus der Tabelle "Rechnung" (hier angesprochenüber den Alias "a") der Tag des Datums ausgelesen – also z.B. «7». Der Tag soll allerdings zweistellig dargestellt werden, gegebenenfalls also mit einer führenden «0». Dies wird über RIGHT('0' || EXTRACT ( DAY FROM "a"."Datum" ), 2 ) schließlich erreicht. Es wird eine «0» vor
die «7» gesetzt und anschließend von rechts aus die letzten zwei Zeichen gelesen. Damit werden gleichzeitig zweistellige Tage, vor die eine «0» gesetzt wurde, wieder korrekt dargestellt.
Entsprechend dem Tag wird auch mit dem Monat verfahren. Die Punkte als Trenner zwischen
Tag und Monat sowie Monat und Jahr werden ebenfalls in die Verbindung über || mit einbezogen.
In Zeile 3 wird die Rechnungsnummer aus dem Primärschlüssel der Rechnung erstellt. Hier werden einfach 8 Nullen vor den Primärschlüsselwert geschrieben und die hinteren 8 Ziffern übernommen, so dass letztlich immer 8 Ziffern die Rechnungsnummer ergeben. Das Ganze wird
dann noch mit dem führenden Text 'RE_' versehen.
Der Primärschlüssel der Rechnung muss noch separat mit aufgenommen werden (Zeile 4), daein Auslesen aus der Nummer mittels Makro zu umständlich wäre, der Schlüsselwert aber inden Makros benötigt wird.
In den Zeilen 5 bis 7 werden die Berechnungen der Summe, bezogen auf die jeweilige Rechnung, vorgenommen. Die Zeilen 5 und 6 berechnen hier auf Basis der gleichen Rechnung wie in
Zeile 7 die Summe, ermitteln daraus aber durch Multiplikation und Division den Nettowert undden Umsatzsteuerwert (bezogen auf 19%). Die Summenbildung sowie die Versorgung mit
Dezimalkomma und €-Zeichen worden bereits in den Abfrage «Etiketten» und «Rechnung_einzeilig» erklärt. Hier deshalb nur noch die Umrechnungen für «Netto» und «Umsatzsteuer».
In Zeile 5 wird zuerst die Summe mit dem Wert 1,00 multipliziert. Dies ist deswegen notwendig,
weil die Summe selbst nur 2 Nachkommastellen hat, für eine bessere Rundung aber mehr
Nachkommastellen benötigt werden. Durch die Multiplikation mit 1,00 werden der bisherigen
Summe lediglich 2 weitere Nachkommastellen hinzugefügt. Die Bruttosumme entspricht 119%,also wird die Nettosumme durch eine Division durch 1,19 ermittelt. Hier wird also fest voneinem Steuersatz von 19% ausgegangen. Dies könnte bei einer komplexer aufgebauten Datenbank natürlich abhängig von dem Steuersatz, der bei der Ware gespeichert würde, berechnet
werden.
Die so ermittelte Zahl hat 4 Nachkommastellen. Um daraus wieder einen sinnvollen Währungsbetrag zu machen wird auf die 2. Nachkommastelle gerundet.
In Zeile 6 wird dann statt einer Multiplikation mit 1,00 (100%) eine Multiplikation mit 0,19 vorgenommen. Alle anderen Vorgehen stimmen mit Zeile 5 überein.

Formulare
Die Formulare greifen mit unterschiedlichem Schwierigkeitsgrad auf die Serienbrieffunktion
bzw. auf Textfelder eine Vorlage zu. Der einfachste Zugriff erfolgt für den Inhalt, der in derTabelle "Anschrift" gespeichert wird.

Serienbrief direkt

81

Anschrift

1 MainForm (Tabelle: Anschrift)
Makros in Feldeigenschaften1

Schaltfläche Druck über Writer (Aktion ausführen)

Module1.Serienbrief

Benötigt
Anmeldung der Datenbank über Extras → Optionen → LibreOffice Base → Datenbanken → Neu als«Beispiel_Datenbank_Serienbrief_direkt»

Das erste Formular stellt ein einfaches Eingabeformular für Adressen dar.
Das Formular speichert die Daten in der Tabelle "Anschrift" ab. Das Primärschlüsselfeld ist farblich abweichend gekennzeichnet, da der Wert automatisch erstellt wird.

Das Listenfeld wird nicht über eine zusätzliche Tabelle per SQL geladen, sondern ist hier direktüber eine Werteliste definiert. Unter Eigenschaften → Daten → Listeninhalt sind die Werte «m»und «w». Um die Werte in dieser Form einzugeben muss lediglich das vorgesehene Feld mit
dem Cursor betreten werden. Das Feld klappt auf und über die Eingabe von Strg + Enter kannnach der Eingabe des ersten Wertes in die zweite Zeile für einen zweiten Wert gewechselt werden.
Der Listeninhalt wird in die dem Formular zugrundeliegenden Tabelle "Anschrift" übertragen.
Dargestellt werden allerdings die unter Eigenschaften → Daten → Listen-Einträge gemachten
Serienbrief direkt

82

Einträge. Hier ist auf die selbe Reihenfolge zu achten, damit nicht bei der Anzeige «weiblich» in
der Tabelle «m» abgespeichert wird.
Wird in einem Datensatz der Button Druck über Writer betätigt, so wird eine neue Textdatei mit
dem im Serienbrief abgelegten Feldinhalt erstellt. Der Dateiname wird aus dem Nachnamen
und einer folgenden 0 sowie der Endung «odt» zusammengefügt. Die Datei hat den selben
Inhalt, wie ihn auch ein Druck, ausgelöst von der ursprünglichen Serienbriefdatei, hätte.
Der Serienbrief selbst wurde hier einfach über den Datenbankbrowser erstellt:

Mit F4 oder über Ansicht → Datenquelle oder auch über den entsprechenden Button in der
Symbolleiste werden die Datenquellen angezeigt. Aus der Datenquelle wird die entsprechende
Tabelle (hier «Anschrift») ausgesucht. Die Tabelle wird rechts davon angezeigt. Jetzt können die
Serienbrieffelder durch einen Druck mit der linken Maustaste auf den jeweiligen Tabellenkopf indas darunterliegende Writer-Dokument gezogen werden.
Die Feldauswahl der Serienbrieffelder kann alternativ auch über Einfügen → Feldbefehl →Andere → Datenbank → Seriendruckfeld erfolgen.
Statt aber diesen Vorlagenbrief zuerst zu starten, dann den Datensatz auszusuchen, den Druckzu starten und die Bedingungen dafür festzulegen, erfolgt die Erstellung des Zieldokumentes
bei Betätigung von Druck über Writer direkt, ohne das Ausgangsdokument auf dem Bildschirmzu öffnen.
Dieser Zugriff auf das Dokument wird über das Makro «Serienbrief» erreicht.

Serienbrief direkt

83

Anschrift_Textfelder

1 MainForm (Abfrage: Adresseingabe)
Makros in Feldeigenschaften1

Schaltfläche Druck über Writer (Aktion ausführen)

Module1.Textfelder_Fuellen

Das Formular «Anschrift_Textfelder» arbeitet nicht mit der Serienbrieffunktion von LibreOfficezusammen. Stattdessen wird auf Platzhalter zugegriffen.
Zuerst muss im Writer eine Vorlage mit Platzhaltern erstellt werden. Dies geschieht über Einfügen → Feldbefehl → Funktionen → Platzhalter. Die Platzhalter werden der Einfachheit halber sobenannt, wie das entsprechende Feld in der Tabelle bzw. in der Abfrage heißt, deren Inhalt derPlatzhalter annehmen soll.

Serienbrief direkt

84

Für einfache Zwecke reicht hier der Typ «Text», mit den anderen Varianten, z.B. «Grafik», können dann weitere Inhalte umgesetzt werden.
In dem Makro wird der Pfad zur Vorlage hinterlegt. Von dem Makro werden die Platzhalter
befüllt. Es wird ein neues Dokument erstellt, das direkt mit den Inhalten gefüllt wird. Das damit
geöffnete Dokument muss nur noch abgespeichert oder direkt ausgedruckt werden.

Etiketten_Serienbrief_im_Formular

Serienbrief direkt85

1 Formular (keine Datenquelle angegeben, benötigt wird aber Abfrage Etiketten)
Benötigt
Anmeldung der Datenbank über Extras → Optionen → LibreOffice Base → Datenbanken → Neu als«Beispiel_Datenbank_Serienbrief_direkt»

Die bisherigen Formulare zeigen, wie ein Seriendruck mit einer Vorlage außerhalb der Datenbankdatei bewerkstelligt wird. In diesem Formular wird ausgenutzt, dass die enthaltenen Formulare grundsätzlich über das Writer-Modul von LibreOffice erstellt werden. Es ist also auchmöglich, einen Serienbrief als Formulardokument zu erzeugen. Wird dann das Formular aufgerufen und die Druckfunktion gewählt, so erscheint automatisch die Nachfrage, ob ein Seriendruck erfolgen soll. Die Datenbank muss für diese Funktion allerdings in LibreOffice angemeldetsein.
Um so ein Formular zu erstellen sollte bei der Erstellung darauf geachtet werden dass Ansicht→ Drucklayout im Formular gewählt wird. Auch wenn das Formular später im Weblayout geöffnet wird richtet sich der Ausdruck nach den Einstellungen für die Druckseite.
In diesem Formular sollen Etiketten erstellt werden. Dazu wird zunächst der Rand der Seite
recht klein gestellt. Hier gibt das vorliegende Etikettenmaterial die Größen vor. Für ein Etikett
wird ein Rahmen ohne Rand und ohne Umlauf aufgezogen. Anschließend wird der Datenbankbrowser gestartet und die gewünschten Felder werden hinzugefügt.

Serienbrief direkt86Über Einfügen → Feldbefehl → Andere → Datenbank wird als Abschlussfeld eines jeden Etiketts
Nächster Datensatz → TRUE gewählt. Lediglich das letzte Etikett der Seite ist hiervon ausgenommen, da mit einem Seitenumbruch automatisch zum nächsten Datensatz gewechselt wird.
Die Rahmen werden in der gewünschten Anzahl kopiert, neu eingefügt und an entsprechender
Stelle auf dem Blatt positioniert. In der Beispieldatei wurde dabei jeder Rahmen an die Seite
gebunden – dies kann natürlich nach Belieben anders gehandhabt werden.
Die Informationen, die auf den Etiketten erscheinen sollen, sind in der Abfrage «Etiketten»
gesammelt.
Als kleiner Zusatz wurde noch ein Button eingefügt, der direkt den Druckbefehl aufruft.

Serienbrief direkt

87Der Druckbefehl wird gestartet, indem in Eigenschaften: Schaltfläche → Allgemein die Aktion →Dokument/Webseite öffnen und als URL → .uno:Print eingefügt wird.

Serienbrief direkt

88

Rechnung

1 MainForm (Tabelle: Rechnung)
1 Listenfeld Kunde (Tabelle: Anschrift)
1.1 SubForm(Tabelle: Verkauf)

1.1 Listenfeld Ware (Tabelle: Waren)
Makros in Feldeigenschaften1

Schaltfläche Rechnung drucken (Aktion ausführen)

Module1.Rechnung

Benötigt
Anmeldung der Datenbank über Extras → Optionen → LibreOffice Base → Datenbanken → Neu als«Beispiel_Datenbank_Serienbrief_direkt»

Das Listenfeld «Kunde» wird wie folgt mit Daten versorgt: Kontextmenü → SteuerelementEigenschaften → Daten → Art des Listeninhalts «SQL». Dort wird die folgende Abfrage erstellt:
001 SELECT "Nachname"||', '||"Vorname", "ID"002 FROM "Anschrift"
003 ORDER BY "Nachname" ASC, "Vorname" ASC"Als erstes Feld wird der Nachname und der Vorname, getrennt durch ein Komma, zusammengefasst. Das zweite Feld ist dann der Primärschlüssel zu dieser Person (Zeile 1). In Zeile 3 wird
dann die Abfrage sinnvollerweise zuerst nach dem Nachnamen und dann nach dem Vornamensortiert.
Beim Formular «Rechnung» werden einige zusätzliche Problemstellungen angegangen. Die
erste Problemstellung zeigt bereits der Screenshot während einer Eingabe: Im Listenfeld sindneben den Namen die Preis angezeigt – allerdings gleich so, dass das ganze wie eine Tabelle
aussieht.
Um ein Listenfeld so wie oben erscheinen zu lassen muss zuerst einmal die Schriftart voneiner proportionalen Schriftart zu einer Schriftart mit fester Zeichenbreite umgewandelt wer-

Serienbrief direkt

89

den. Das Tabellenkontrollfeld wird markiert. Über das Kontextmenü → Kontrollfeld → Eigenschaften: Tabellen-Steuerelement → Allgemein → Schrift wird die Schriftart «Liberation Mono,
Standard» ausgesucht.
Anschließend wird der Tabellenkopf «Ware» angeklickt. Über das Kontextmenü → Spalte →Eigenschaften: Listenfeld → Daten → Art des Listeninhalts wird «SQL» gewählt. Danach wird
der Listeninhalt festgelegt:
001 SELECT
002
LEFT("Ware"|| SPACE(25), 25) || ' - ' ||REPLACE( RIGHT( SPACE(8) ||"Preis", 8),'.',',') || ' €',003"ID"
004 FROM "Waren"
005 ORDER BY ("Ware" || ' - ' || "Preis" || ' €') ASC
Es werden Daten aus der Tabelle "Waren" ausgelesen. Mit || werden Elemente verschiedener
Felder bzw. auch Text verbunden. An den Inhalt des Feldes werden 25 Leerzeichen (SPACE)
angehängt. Anschließend wird der so entstandene Text von links aus (LEFT) auf 25 Zeichen
zurechtgestutzt. Dadurch ist die Zeichenlänge für die Darstellung der Ware festgelegt. Der"Preis" wird ebenfalls mit einer entsprechenden Anzahl an Leerzeichen gekoppelt. Da der Preisaber rechtsbündig dargestellt wird, werden jetzt 8 Zeichen von rechts aus (RIGHT) gelesen.
Höhere Preise sind in der Datenbank sowieso nicht vorgesehen. Damit der Preis außerdem mit
einem Dezimalkomma und nicht mit einem Dezimalpunkt dargestellt wird, wird über REPLACE
der Punkt durch ein Komma ersetzt. Schließlich wird noch das €-Zeichen an den Gesamtstring
angehängt.
Komplizierter wird es, wenn in dem Inhalt des Listenfeldes auch nicht druckbare Zeichen wie
z.B. Zeilenumbrüche enthalten sind. Dann muss der Code entsprechend angepasst werden:
001 SELECT
002
LEFT(REPLACE("Ware",CHAR(10),' ')||SPACE(25), 25) || ' - ' || ...
Damit wird ein Zeilenvorschub in Linux durch ein Leerzeichen ersetzt.
Für FIREBIRD muss dieser Code etwas angepasst werden002

RPAD(REPLACE("Ware",ASCII_CHAR(10),' '),25) || ' - ' ||REPLACE(LPAD("Preis",8),'.',',') || ' €',Zum einen ist wieder CHAR durch ASCII_CHAR zu ersetzen. Zum anderen ist die Funktion SPACE
unbekannt, so dass hier mit der Funktion RPAD mit Leerzeichen von rechts aus und mit der
Funktion LPAD mit Leerzeichen von links aus aufgefüllt wird. Diese beiden Funktionen sind wiederum in der HSQLDB unbekannt.
Wie die Anzahl der erforderlichen Leerzeichen auch automatisch ermittelt werden kann ist im
Base-Handbuch beschrieben.
Wird über Rechnung drucken der Seriendruck gestartet, so läuft der Prozess genau wie beim
Seriendruck aus dem Formular «Anschrift» im Hintergrund ab. Es wird eine Datei erstellt, diewie im folgenden Bild zu sehen gefüllt ist:

Serienbrief direkt

90Durch die Anzeige der Steuerzeichen wird im Ausdruck deutlich, wie eine Formatierung inTabellenform erreicht wurde. Der Rechnungsinhalt wurde durch Tabulatoren und weiche Umbrüche in das Dokument eingefügt. Die Lage der Tabulatoren im Absatz ist über das Absatzformat
der Vorlage definiert. Sie kann natürlich noch angepasst werden, falls die Preise oder auch die
Anzahl mehr Platz benötigen. Die Länge des Gesamtinhaltes ist allerdings durch die Zeile
begrenzt.

In der Vorlage erscheint für den Rechnungsinhalt nur das Serienbrieffeld< Rechnungsinhalt >. Es wird, wie alle anderen Felder, aus einem einzigen Datensatz derDatenquelle ausgelesen. Der wesentliche Unterschied zur Erstellung eines Seriendrucks für dieAnschrift liegt in der Erstellung des Inhaltes für das Feld "Rechnungsinhalt".

Rechnung_Textfelder

Serienbrief direkt

91

1 MainForm (Tabelle: Rechnung)
1 Listenfeld Kunde (Tabelle: Anschrift)
1.1 SubForm(Tabelle: Verkauf)

1.1 Listenfeld Ware (Tabelle: Waren)
1.2 Druck (Abfrage: Rechnung_Textfelder)
Makros in Feldeigenschaften
1.2

Schaltfläche Rechnung
drucken (Aktion ausführen)

Module1.Rechnungsinhalt_zusammenstellen_Tabelle

Der Ausdruck aus dem Formular «Rechnung_Textfelder» ähnelt erst einmal sehr dem Ausdrucküber den Serienbrief aus dem Formular «Rechnung».

Serienbrief direkt

92

Bei genauer Beobachtung vor allem der Steuerzeichen wird allerdings klar, dass hier für den
Rechnungsinhalt eine andere Darstellung gewählt wurde. Hinter der Anzahl und den Warenbezeichnungen sind Absatzendmarken zu sehen. Die Zeilen werden also nicht mittels eines
Tabulators in tabellarischer Form gehalten.

Die Vorlage gibt näheren Aufschluss darüber, wie diese Absatzendmarken zustande kommen.
Sie gehören zu den ersten zwei Spalten einer Tabelle. Die anderen Spalten sind rechtsbündig
ausgerichtet und haben daher eine nur wenig sichtbare sichtbare Absatzendmarke. Die Tabelle
Serienbrief direkt

93ist hier nur zum besseren Verständnis über Ansicht → Tabellenbegrenzungen eingefärbt worden. Vorteil dieser Vorlage gegenüber der des Formulars «Rechnung» ist, dass der Inhalt ineiner Tabellenzelle auch ruhig etwas größer sein kann. Das Layout wird nicht durcheinander
kommen, wenn die Warenbeschreibung über mehrere Zeilen geht. Leider erfordert dieser Vorteil auch eine etwas erweiterte Anwendung der Makroprogrammierung.
Lediglich die in der Abfrage erstellten Felder werden auch als Felder in dem Ausdruck benötigt.
Der restliche Inhalt wird über ein Makro direkt in die betreffenden Zellen geschrieben.

Rechnung_Textfelder_Uebertrag

1 MainForm (Tabelle: Rechnung)
1 Listenfeld Kunde (Tabelle: Anschrift)
1.1 SubForm(Tabelle: Verkauf)

1.1 Listenfeld Ware (Tabelle: Waren)
1.2 Druck (Abfrage: Rechnung_Textfelder)
Makros in Feldeigenschaften
1.2

Schaltfläche Rechnung
drucken (Aktion ausführen)

Module1.Rechnungsinhalt_zusammenstellen_Tabelle_
Uebertrag

Das Formular «Rechnung_Textfelder_Uebertrag» unterscheidet sich von dem Formular «Rechnung_Textfelder» äußerlich überhaupt nicht. Hinter dem Button Rechnung drucken wird lediglich ein etwas erweitertes Makro angesteuert.
Der Druck ergänzt die ursprüngliche Druckfunktion so, dass beim Übergang von einer zur
nächsten Seite bei entsprechend vielen Rechnungstiteln ein Übertrag am Seitenschluss der vorhergehenden und am Seitenanfang der folgenden Seite erscheint.

Serienbrief direkt

94

Das Bild zeigt solch einen Übertrag beim Seitenwechsel. Der Übertrag berücksichtigt dabei
auch, dass vielleicht mehrzeilige Eingaben bei den Waren erscheinen. Die in dem Screenshot
enthaltenen Steuerzeichen zeigen, dass hier weiterhin die Tabelle genutzt wird und dem Übertrag am Seitenende wegen des zusätzlichen Platzes noch zwei leere Tabellenzeilen folgen. Die
verbuchte Ware benötigt hier schließlich vier Zeilen, die aber in der Tabelle auf der Vorseite keinen Platz mehr gehabt hätten. Außerdem wäre dann auch noch die Summierung alleine auf derFolgeseite erschienen - alles Dinge, die durch das Makro ausgeschlossen werden sollen.
Einen Nachteil hat diese Konstruktion noch: Die zugrundeliegende Abfrage berücksichtigt nochnicht, dass eventuelle einzelne gleiche Waren mehrfach in einer Rechnung vorkommen. Hiererscheinen auf der oberen Seite zwei Ultrabooks und von der gleichen Kategorie gibt es dannnoch einmal drei Ultrabooks. Zugegeben in dem Beispiel sich ein seltenerer Fall, aber bei Quittungen an Supermarktkassen schon häufiger zu sehen.
Die gleichen Waren fasst das folgende Formular in der Rechnung zusammen.

Serienbrief direkt

95

Rechnung_Textfelder_Uebertrag_Seriendruck

1 MainForm (Abfrage: Rechnung_nicht_gedruckt)
1 Listenfeld Kunde (Tabelle: Anschrift)
1.1 SubForm(Tabelle: Verkauf)

1.1 Listenfeld Ware (Tabelle: Waren)
1.2 Druck (Abfrage: Rechnung_Textfelder)
Makros in Feldeigenschaften
1.2

Schaltfläche alle offenen Rechnungen drucken(Aktion ausführen)
Eigenschaften → Allgemein → Zusatzinformation: «Seriendruck»

Module1.Direktdruck_Start

1.2

Schaltfläche einzelne Rechnung drucken(Aktion ausführen)
Eigenschaften → Allgemein → Zusatzinformation: «Einzeldruck»

Module1.Direktdruck_Start

Das Prinzip in diesem Formular ist gleich dem des Formulars «Rechnung_Textfelder_Uebertrag».
Hier wird dann allerdings statt der Darstellung des Inhaltes in einem Writerdokument eine*.pdf-Datei für jede Rechnung erstellt, für die noch kein Ausdruck erfolgt ist. Das Formular hat
hier lediglich einen zusätzlichen Button, über den der Seriendruck angesteuert wird.
Die Rechnung berücksichtigt dabei außerdem, dass gleiche Waren in einem Feld zusammengefasst werden. Es tauchen also keine «Doppler» mehr auf.
Im Formular werden hier nur die Rechnungen angezeigt, die noch nicht ausgedruckt wurden.
Dies wird über Abfragen für das Formular geregelt, die sich lediglich dadurch zusätzlich auszeichnet, dass das "Druckdatum" aus der Tabelle "Rechnung" leer sein muss.

Serienbrief direkt

96Über den Button «Alle offenen Rechnungen drucken» werden alle Rechnungen, zu denen nochkein Druckdatum existiert, in einzelne PDF-Dateien geschrieben. Das Druckdatum wird dabei in
die Tabelle "Rechnung" eingetragen und «MainForm» nach jedem Eintrag aktualisiert, so dassbei Betätigung des Buttons nach und nach alle Einträge aus dem Formular verschwinden und
die Dateien anschließend im Verzeichnis der Datenbankdatei liegen - fertig zum Absenden per
Mail, zum Ausdruck o.ä.

Waren

1 MainForm (Tabelle: Waren)
Das Formular «Waren» wurde nur benötigt, um für die Waren auch eine mehrzeilige Eingabe zu
ermöglichen. Es bietet hier lediglich ein mehrzeiliges Textfeld, damit auch Absätze gespeichert
werden. Für den Druck hat dieses Formular keine weitere Bedeutung.

Berichte
Diese Beispieldatenbank enthält keine Berichte, die in dem Berichtsordner angezeigt werden.
Alle Ausdrucke werden über Makros direkt an Writer weitergeleitet. Die Berichte werden in diesem Fall über Textfelder und Writer-Tabellen erstellt.

Makros
Rechnung
Aufruf aus
Formular: Rechnung

Benötigt
Makro:

Rechnungsinhalt_zusammenstellen, Output_MailMerge

Vorlage:

Beispiel_Rechnung.odt

Abfrage:

Rechnung_einzeilig

001
002
003
004
005
006
007
008

SUB Rechnung
DIM oForm AS OBJECTDIM loFeldID AS LONG
DIM loID AS LONG
oForm = thisComponent.Drawpage.Forms.MainForm
loFeldID = oForm.findColumn("ID")loID = oForm.getLong(loFeldID)
Rechnungsinhalt_zusammenstellen(loID)

Serienbrief direkt

97

009
010

Output_MailMerge(loID, "Beispiel_Rechnung.odt", 1, "Rechnung_einzeilig")
END SUB

Aus dem Formular heraus wird der Wert für das Primärschlüsselfeld "ID" ermittelt (Zeile 5 - 7).
Anschließend wird die Prozedur «Rechnungsinhalt_zusammenstellen» für den Datensatz mit
diesem Feldinhalt gestartet.
Nachdem der Rechnungsinhalt zusammengestellt wurde, wird dieser über die Prozedur «Output_MailMerge» als Serienbrief ausgegeben. Dabei dient das Dokument «Beispiel_Rechnung.odt» als Inhalt für den Serienbrief, dessen Datenbankfelder gefüllt werden. Basis für den
Serienbrief ist eine Abfrage (Zahl '1'). Name der Abfrage ist «Rechnung_einzeilig».
Für FIREBIRD muss statt der Abfrage eine Ansicht (Zahl '0') erstellt werden. Sonst stört sich Firebird an den in der Abfrage enthaltenen Tabellen, die alle ein Feld "ID" haben - auch wenn die
Abfrage selbst anstandslos ausgeführt wird. Also:011

Output_MailMerge(loID, "Beispiel_Rechnung.odt", 0, "Rechnung_einzeilig")

Rechnungsinhalt_zusammenstellen
Aufruf aus
Makro:

Rechnung

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014

015
016
017
018
019
020
021
022
023

Verkauf, Waren, Rechnungsinhalt

SUB Rechnungsinhalt_zusammenstellen(loID AS LONG)
DIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oAbfrageergebnis AS OBJECT
DIM stSql AS STRING
DIM stText AS STRING
oDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Verkauf"".""Anzahl"" || CHAR( 9 ) || ""Waren"".""Ware"" ||CHAR( 9 ) || REPLACE( ""Waren"".""Preis"" || ' €', '.', ',' ) || CHAR( 9 ) ||REPLACE( ""Verkauf"".""Anzahl"" * ""Preis"" || ' €', '.', ',' ) ||CHAR( 13 )||CHAR( 10 ) FROM ""Verkauf"", ""Waren"" WHERE ""Verkauf"".""Waren_ID"" =""Waren"".""ID"" AND ""Verkauf"".""Rechnung_ID"" = "+loID+""
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stText = stText + oAbfrageergebnis.getString(1)WEND
stSql = "DELETE FROM ""Rechnungsinhalt"" WHERE ""ID"" = "+loID+""oSQL_Anweisung.executeUpdate(stSql)
stSql = "INSERT INTO ""Rechnungsinhalt"" (""ID"",""Rechnungsinhalt"") VALUES("+loID+", '"+stText+"')"oSQL_Anweisung.executeUpdate(stSql)
END SUBDie HSQLDB bietet keine Funktion wie GroupConcat (MySQL) oder List (Firebird). Deswegen werden in dieser Prozedur alle Inhalte, die in der Rechnung auftauchen sollen, zusammen in ein
Feld der Tabelle "Rechnungsinhalt" geschrieben.
Der Kontakt zur Datenbank wird in den Zeilen 8 - 12 erstellt. Anschließend wird ein einem SQLString der gesamte Inhalt, der aus einem Datensatz in der Rechnung erscheinen soll, in einem
Feld zusammengefasst. Mit CHAR(9) wird ein Tabulator zwischen die Feldinhalte gesetzt. Bei
Preisangaben muss außerdem das Eurozeichen ergänzt sowie aus dem Dezimalpunkt derSerienbrief direkt

98Datenbank ein Dezimalkomma gemacht werden. CHAR(13)||CHAR(10) sorgt bei allen Systemen dafür, dass zum Schluss ein Zeilenumbruch erfolgt.
Bei FIREBIRD müssen hier wieder die CHAR( ) - Funktionen durch ASCII_CHAR( )- Funktionen
ersetzt werden. Außerdem muss die Multiplikation (""Verkauf"".""Anzahl"" * ""Preis"")in Klammern gesetzt werden, da sie sonst als String interpretiert wird.

Serienbrief
Aufruf aus
Formular: Anschrift

Benötigt
Makro:

Output_MailMerge

Vorlage:

Beispiel_Serienbrief.odt

001
002
003
004
005
006
007
008
009

SUB Serienbrief
DIM oForm AS OBJECTDIM loFeldID AS LONG
DIM loID AS LONG
oForm = thisComponent.Drawpage.Forms.MainForm
loFeldID = oForm.findColumn("ID")loID = oForm.getLong(loFeldID)
Output_MailMerge(loID, "Beispiel_Serienbrief.odt", 0, "Anschrift")
END SUB

Diese Prozedur ist in vielen Teilen gleich der Prozedur «Rechnung».
Es wird zuerst der Wert des Primärschlüssels "ID" über das Formular ausgelesen. Danach
braucht beim Serienbrief allerdings nicht mehr eine Rechnung zusammengestellt zu werden.
Stattdessen wird direkt MailMerge gestartet.
An die Prozedur «Output_MailMerge» wird an Variablen zuerst der Primärschlüsselwert und das
Dokument mit den Datenbankfeldern «Beispiel_Serienbrief.odt» weitergegeben. Mit '0' wird vermittelt, dass es sich bei dem Eintrag «Anschrift» um eine Tabelle als Datenquelle handelt.

Output_MailMerge
Aufruf aus
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018

Rechnung, Serienbrief

SUB Output_MailMerge(loID AS LONG, stFile AS STRING, inCommandType AS INTEGER,
stCommand AS STRING)DIM oDB AS OBJECT
DIM stDir AS STRING
DIM arProps()
MailMerge = CreateUnoService("com.sun.star.text.MailMerge")oDB = ThisComponent.Parent
stDir = Left(oDB.Location,Len(oDB.Location)-Len(oDB.Title))
WITH MailMerge.DataSourceName = "Beispiel_Datenbank_Serienbrief_direkt".DocumentURL = stDir + stFile.CommandType = inCommandType.Command = stCommand.Filter = "ID ="+loID.SaveAsSingleFile = true
.FileNameFromColumn = true
.FileNamePrefix = "Nachname".OutputType = 2.OutputUrl = stDir

Serienbrief direkt

99

019
020
021

END WITH
MailMerge.execute(arProps())
END SUBDie Prozedur «Output_MailMerge» wird mit verschiedenen Parametern über die Startprozeduren
versorgt. In der Variablen loID steckt der Primärschlüssel des Datensatzes, der gedruckt werden soll. Die Variable stFile gibt an, wie die Datei heißt, in der sich die Datenbankfelder fürden Druck befinden. Als numerischer Wert wird mit inCommandType weiter gegeben, ob es sichbei dem SQL-Kommando um eine Tabelle (0), eine Abfrage (1) oder um SQL-Code (2) handelt.
stCommand schließlich beinhaltet entweder den Namen der Tabelle oder Abfrage oder den SQLCode.
Zuerst wird der UnoService MailMerge mit einer Variablen erstellt, an die alle weiteren Parameter weitergegeben werden.
In Zeile 6 und 7 wird der Pfad zu der aufrufenden Datenbank ausgelesen. In diesem Pfad befinden sich auch die Dateien, die mit Inhalt gefüllt werden sollen.
Anschließend werden von Zeile 8 bis Zeile 19 die verschiedenen Variablen für MailMerge aufgelistet, die zum Schluss bei der Ausführung von MailMerge (Zeile 20) die Ausgabe bestimmen.
Zeile 9 enthält den Datenbanknamen, wie er in LibreOffice unter Extras → Optionen →LibreOffice Base → Datenbanken → Registrierter Name aufgeführt ist.
Zeile 10 gibt den Pfad zu der Datei weiter, die als stFile der Prozedur «Output_MailMerge»übergeben wurde.
Die Zeile 11 enthält die Information, ob es sich um eine Tabelle, Abfrage oder einen reinen SQLCode handelt. In Zeile 12 ist dann entweder der Name für die Tabelle oder Abfrage enthaltenoder eben der SQL-Code.
Aus der Datenquelle soll nur ein bestimmter Datensatz gedruckt werden. Deswegen ist in Zeile
13 ein Filter nach dem Primärschlüssel eingebaut.
Das «Druckergebnis» soll nicht über den Drucker direkt herausgehen, sondern als einzelne
Datei gespeichert werden (Zeile 14). Der Dateiname soll dabei anhand eines Feldes der Datenquelle (Zeile 15), in diesem Fall des Feldes "Nachname" (Zeile 16) erstellt werden.
Zeile 17 gibt dann den Ausgabetyp von MailMerge wieder. Hier stehen für den Drucker die '1',
für die Ausgabe in eine Datei '2' und für die Weitergabe als E-Mail eine '3'. Da in der Prozedur
bereits festgelegt wurde, wie der Dateiname heißen soll, steht hier natürlich eine '2'. Bisher ist
noch nicht festgelegt, welcher Pfad zu der Datei führen sollte. Der Einfachheit halber ist hierder Pfad zu der Datenbankdatei auch als Pfad für die abzuspeichernde Datei gewählt worden(Zeile 18). In der Praxis wird hier vielleicht eher ein Unterverzeichnis der Datenbankdatei angegeben, so dass alle Briefausgaben von anderen Inhalten getrennt aufbewahrt werden.

Textfelder_Fuellen
Aufruf aus
Formular: Anschrift_Textfelder

Benötigt
Makro:

Textfelder_Fuellen_ID

Vorlage:

Beispiel_Textfelder.ott

001
002
003
004
005
006
007

SUB Textfelder_Fuellen(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oDB AS OBJECTDIM oNewDoc AS OBJECT
DIM stDir AS STRINGoForm = oEvent.Source.Model.Parent
oDB = ThisComponent.Parent

Serienbrief direkt

100

008
009
010
011
012
013
014
015

stDir = Left(oDB.Location,Len(oDB.Location)-Len(oDB.Title))
stDir = stDir & "Beispiel_Textfelder.ott"
DIM args(0) AS NEW com.sun.star.beans.PropertyValueargs(0).Name = "AsTemplate"args(0).Value = TrueoNewDoc = StarDesktop.loadComponentFromURL(stDir,"_blank",0,args)
Textfelder_fuellen_ID(oNewDoc, oForm)END SUB

Der Kontakt zum Formular wird über den auslösenden Button hergestellt (Zeile 6). Der Pfad zur
Datenbank wird ausgelesen, der Dateiname davon abgetrennt und dann mit der Vorlagedatei«Beispiel_Textfelder.ott» verknüpft (Zeile 7 bis 9).
Anschließend wird eine neue Datei auf Basis dieser Vorlage erstellt, die dann bis zur Speicherung die Bezeichnung «Unbenannt.odt» hat (Zeile 10 bis 13).
In Zeile 14 wird die Funktion «Textfelder_Fuellen_ID» mit den Parametern für die neue Datei unddas Formular aufgerufen. Der Rückgabewert dieser Funktion wird für die Ausfüllung der Textfelder nicht benötigt. Würde nur die Funktionalität des Formulars «Anschrift_Textfelder» benötigt,
so könnte die Funktion «Textfelder_Fuellen_ID» auch direkt hier integriert werden.

Textfelder_Fuellen_ID
Aufruf aus
Makro:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023

Textfelder_Fuellen, Rechnungsinhalt_zusammenstellen_Tabelle,
Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag, Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck

FUNCTION Textfelder_Fuellen_ID(oNewDoc AS OBJECT, oForm AS OBJECT) AS LONG
DIM oColumns AS OBJECTDIM oTextfields AS OBJECTDIM oTextfield AS OBJECT
DIM stColumnname AS STRING
DIM stDir AS STRING
DIM inIndex AS INTEGERDIM loID AS LONG
oColumns = oForm.Columns
oTextfields = oNewDoc.Textfields.createEnumeration
DO WHILE oTextfields.hasMoreElements
oTextfield = oTextfields.nextElement
IF oTextfield.supportsService("com.sun.star.text.TextField.JumpEdit") THEN
stColumnname = oTextfield.PlaceHolder
IF oColumns.hasByName(stColumnname) THEN
inIndex = oForm.findColumn(stColumnname)oTextfield.Anchor.String = oForm.getString(inIndex)
END IF
END IF
LOOP
loID = oForm.getLong(oForm.findColumn("ID"))
Textfelder_fuellen_ID = loID
END SUBDie Funktion «Textfelder_Fuellen_ID» ist aus den anderen Prozeduren ausgegliedert, da sie
sonst gleich mehrfach vorkäme und Nachbesserungen an so einer Prozedur gegebenenfalls inallen anderen Prozeduren auf gleiche Weise erfolgen müssten.
Die Spalten der Datenquelle des Formulars werden in der Variablen oColumns gespeichert(Zeile 9)
Die Vorlage mit den Textfeldern ist in ein neues Dokument oNewDoc geladen worden. Die Textfelder aus dem Dokument werden ausgelesen (Zeile 10). Zu beachten ist, dass hier für die
Gesamtzahl der Felder die Variable oTextfields (mit einem «s»), für das einzelne Textfeld
oTextfield gewählt wurde.

Serienbrief direkt101

In einer Schleife von Zeile 11 bis 20 werden alle Textfelder des Dokuments angesteuert. Die
Textfelder haben dabei eine Bezeichnung, den Placeholder. Ist dieser gleich der Bezeichnung
einer der Spalten der Datenquelle des Formulars, so liegen dafür auch Daten vor (Zeile 15). Bei
gleichen Bezeichnungen wird aus der Datenquelle des Formulars der entsprechende String ausgelesen und als String in das Textfeld eingefügt (Zeile 17).
Nachdem die Schleife durchlaufen worden ist wird noch der Primärschlüssel "ID" aus dem aktuellen Datensatz ausgelesen (Zeile 21) und als Wert der Funktion an die auslösende Prozedur
zurückgegeben (Zeile 22). Dies wird in den folgenden Prozeduren für den Ausdruck von Rechnungen benötigt, die in Abhängigkeit von diesem Schlüsselwert die Rechnungsposten zusammenstellen.

Tabellen_fuellen
Aufruf aus
Makro:

Rechnungsinhalt_zusammenstellen_Tabelle

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029

Rechnungsinhalt

SUB Tabellen_fuellen(oNewDoc AS OBJECT, stSql AS STRING)DIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oAbfrageergebnis AS OBJECTDIM oTabelle AS OBJECTDIM oTabellen AS OBJECTDIM oRows AS OBJECT
DIM i AS INTEGER
oTabellen = oNewDoc.getTextTables
oTabelle = oTabellen.getByName("Rechnungsinhalt")oDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
oTabelle.getCellByPosition(0,i).setValue(oAbfrageergebnis.getInt(1))
oTabelle.getCellByPosition(1,i).setString(oAbfrageergebnis.getString(2))
oTabelle.getCellByPosition(2,i).setValue(oAbfrageergebnis.getDouble(3))
oTabelle.getCellByPosition(3,i).setValue(oAbfrageergebnis.getDouble(4))
oRows = oTabelle.getrows()
oRows.insertByIndex(oRows.getCount(),1)
i = i + 1
WEND
oRows.removeByIndex(oRows.getCount()-1,1)
END SUBIn den Vorlagen für die Textfelder existiert für die Erfassung der Rechnungsinhalte eine Tabelle.
Diese Tabelle ist über Tabelleneigenschaften → Name mit dem Namen «Rechnungsinhalt» versehen worden, so dass sie per Makro sicher angesteuert werden kann (Zeile 11).
Dieser Prozedur wird beim Aufruf aus einer anderen Prozedur über stSql ein SQL-Code für den
Inhalt der Rechnungstabelle mitgegeben. Dabei enthält immer das erste Feld die Anzahl derGegenstände, das zweite Feld die Benennung der Ware, das dritte Feld den Einzelpreis und dasvierte Feld den Gesamtpreis für alle gleichen Waren.
Diese Abfrage wird in Zeile 18 ausgeführt und anschließend in die Tabelle Zeile für Zeile ineiner Schleife von Zeile 19 bis 27 eingefügt.

Serienbrief direkt102

Da bereits eine Zeile in der Tabelle existiert, können zuerst die Feldinhalte in die jeweiligen Zellen übertragen werden. Die Zellbezeichnungen geben mit getCellByPosition() zuerst die
Spalte und dann die Zeile an. Links oben in der Tabelle befindet sich also das Feld mit der Position (0,0).
Sind alle Inhalte einer Datenzeile eingefügt, so wird zunächst gezählt, wie viele Zeilen die
Tabelle bereits hat (Zeile 24). Anschließend wird unterhalb der letzten Tabellenzeile eine neue
Zeile eingefügt und der Zähler für die Schleife um '1' erhöht.
Nachdem alle Inhalte in die Tabelle eingetragen wurden ist aufgrund der Schleifenkonstruktion
in die Tabelle eine Tabellenzeile zu viel eingetragen wurden. Diese Tabellenzeile wird anschließend in Zeile 28 wieder entfernt.

Rechnungsinhalt_zusammenstellen_Tabelle
Aufruf aus
Formular: Rechnung_Textfelder

Benötigt
Tabelle:Verkauf, Waren

Makro:

Textfelder_Fuellen_ID, Tabellen_fuellen

Vorlage:

Beispiel_Rechnung_Textfelder.ott

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018

019
020

SUB Rechnungsinhalt_zusammenstellen_Tabelle(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oDB AS OBJECTDIM oNewDoc AS OBJECT
DIM stDir AS STRING
DIM stSql AS STRINGoForm = oEvent.Source.Model.Parent
oForm.reload()
oForm.last()oDB = ThisComponent.Parent
stDir = Left(oDB.Location,Len(oDB.Location)-Len(oDB.Title))
stDir = stDir & "Beispiel_Rechnung_Textfelder.ott"
DIM args(0) AS NEW com.sun.star.beans.PropertyValueargs(0).Name = "AsTemplate"args(0).Value = TrueoNewDoc = StarDesktop.loadComponentFromURL(stDir,"_blank",0,args)loID = Textfelder_fuellen_ID(oNewDoc, oForm)
stSql = "SELECT ""Verkauf"".""Anzahl"", ""Waren"".""Ware"", ""Waren"".""Preis"",""Verkauf"".""Anzahl"" * ""Waren"".""Preis"" FROM ""Verkauf"", ""Waren""
WHERE ""Verkauf"".""Waren_ID"" = ""Waren"".""ID""AND ""Verkauf"".""Rechnung_ID"" = "+loID+""
Tabellen_fuellen(oNewDoc, stSql)
END SUB

Über das auslösende Ereignis des Buttons wird das Formular ermittelt (Zeile 7). Das Formular
ist Unterformular für die eigentliche Eingabe der Rechnungsposten. Nach der letzten Eingabe
muss in diesem Unterformular der Inhalt aktualisiert werden, da sonst die angezeigten Summen nicht stimmen könnten (Zeile 8 und 9).
Der Pfad zu der Vorlage wird über den Pfad zu der Datenbankdatei bestimmt (Zeile 10 bis 12).
Danach wird ein neues Dokument «Unbenannt.odt» aufgrund dieser Vorlage erstellt (Zeile 13
bis 16).
In dem neuen Dokument werden zuerst die Textfelder über die Funktion «Textfelder_fuellen_ID»
gefüllt. Die Funktion gibt hier den Primärschlüssel für die entsprechende Rechnung zurück(Zeile 17).

Serienbrief direkt

103

Zum Schluss wird der SQL-Code für die Rechnungsinhalte erstellt. Dafür ist der Primärschlüssel
aus der vorhergehenden Funktion wichtig, damit nur die Rechnungsposten zugeordnet werden,
die mit der aktuellen Rechnung zu tun haben (Zeile 18). Dieser Code wird zusammen mit dem
Objekt des Dokumentes «Unbenannt.odt» an die Prozedur «Tabellen_fuellen» weitergeleitet.
Die Rechnung ist danach erstellt und kann ausgedruckt oder abgespeichert werden.

Tabellen_fuellen_Uebertrag
Aufruf aus
Makro:

Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag, Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046

Rechnungsinhalt

SUB Tabellen_fuellen_Uebertrag(oNewDoc AS OBJECT, stSql AS STRING)DIM oTabellen AS OBJECTDIM oTabelle AS OBJECTDIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oAbfrageergebnis AS OBJECTDIM oCursor AS OBJECTDIM oTxtCursor AS OBJECTDIM oRows AS OBJECT
DIM i AS INTEGER
DIM ink AS INTEGER
DIM inStartSeite AS INTEGER
DIM inFolgezeilen AS INTEGER
DIM doUebertrag AS DOUBLE
oTabellen = oNewDoc.getTextTables
oTabelle = oTabellen.getByName("Rechnungsinhalt")oDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
inStartSeite = 1
inFolgezeilen = 3
oCursor = oNewDoc.CurrentController.ViewCursor
WHILE oAbfrageergebnis.next
doAnzahlPreis = oAbfrageergebnis.getDouble(4)oTabelle.getCellByPosition(0,i).setValue(oAbfrageergebnis.getInt(1))
oTabelle.getCellByPosition(1,i).setString(oAbfrageergebnis.getString(2))
oTabelle.getCellByPosition(2,i).setValue(oAbfrageergebnis.getDouble(3))
oTabelle.getCellByPosition(3,i).setValue(doAnzahlPreis)doUebertrag = doUebertrag + doAnzahlPreisoRows = oTabelle.getrows()
oRows.insertByIndex(oRows.getCount(),1)
oCursor.JumpToLastPage()oCursor.JumpToEndOfPage()oCursor.goUp(inFolgezeilen,False)
IF oCursor.Page > inStartSeite THEN
oCursor.goUp(1,False)
FOR ink = 1 TO 2i = i + 1
oRows = oTabelle.getRows()
oRows.insertByIndex(oRows.getCount()-2,1)oTabelle.getCellByPosition(2,i-1).setString("Übertrag:")

Serienbrief direkt

104

047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068

oTxtCursor = oTabelle.getCellByPosition(2,i-1).createTextCursorByRange(oTabelle.getCellByPosition(2,i-1).text)
oTxtCursor.CharPosture = com.sun.star.awt.FontSlant.ITALIC
oTabelle.getCellByPosition(3,i-1).setValue(doUebertrag - doAnzahlPreis)oTxtCursor = oTabelle.getCellByPosition(3,i-1).createTextCursorByRange(oTabelle.getCellByPosition(3,i-1).text)
oTxtCursor.CharPosture = com.sun.star.awt.FontSlant.ITALICNEXT ink
oCursor.gotoRange(oTxtCursor,false)
Wait(100)
DO WHILE oCursor.Page = inStartSeite
oRows = oTabelle.getRows()
oRows.insertByIndex(oRows.getCount()-3,1)
oCursor.gotoRange(oTxtCursor,false)i = i + 1
LOOP
inStartSeite = inStartSeite + 1
END IFi = i + 1
WEND
oRows.removeByIndex(oRows.getCount()-1,1)
oCursor.JumpToLastPage()oCursor.JumpToEndOfPage()
END SUB

Der grundlegende Aufbau dieser Prozedur ist wie der Aufbau der Prozedur «Tabellen_fuellen».
Auch hie wird die Tabelle «Rechnungsinhalt» in dem neuen Dokument aufgesucht und mit
Inhalten gefüllt, die aus der Abfrage hervorgehen, die beim Aufruf dieser Prozedur mit übermittelt wird.
Zusätzlich muss hier allerdings mit dem ViewCursor gearbeitet werden, damit klar wird, wannein Seitenumbruch stattfindet. Ein Objekt dieses Cursors wird in Zeile 27 erstellt. Dieser Cursor
wird nach dem Einfügen von Daten in die Tabelle immer wieder auf die letzte Zeile der letzten
Seite eingestellt (Zeile 37 und 38). Anschließend wird er um die Zeilenanzahl zurückgesetzt,
die für Elemente außerhalb der Tabelle vorgesehen sind (Zeile 39). Das sind in diesem Fall
«Nettobetrag:», «Umsatzsteuer 19%:» und «Insgesamt zu zahlen:», also 3 Zeilen. Steht der
Cursor nach diesen Bewegungen auf einer neuen Seite (Zeile 40), so wird das Einfügen einesÜbertrags notwendig. Dies wird in der Verzweigung von Zeile 40 bis Zeile 62 erledigt.
Der ViewCursor wird zuerst noch eine Zeile weiter nach oben, oberhalb des letzten Eintrags,
geschoben. Dann wird der «Übertrag» in einer Schleife von Zeile 42 bis Zeile 52 doppelt eingetragen. Um den Eintrag kursiv zu formatieren ist neben dem ViewCursor hier der TextCursor
notwendig.
Nach der Schleife wird der ViewCursor auf die letzte Position des TextCursors ausgerichtet.
Dies ist notwendig, damit wieder klar wird, auf welcher Seite sich die zweite Zeile des Übertags
befindet. Manchmal funktionierte die Positionierung hier nicht einwandfrei. Dies ließ sich durcheinen Wait(100)-Befehl regulieren, so dass der bei der Abfrage nach der aktuellen Seite in
Zeile 55 tatsächlich die aktuelle Seite des Cursors und damit des zweiten Eintrags zum Übertrag mitgeteilt wurde.
Solange der 2. Eintrag des Übertags nicht auf der Folgeseite erscheint wird immer wieder eine
Zeile dazwischen geschoben und neu überprüft (Zeile 55 bis 60). Anschließend wird der interne
Seitenzähler um '1' erhöht (Zeile 61) und der Zähler für die aktuelle zu befüllende Zeile in derTabelle ebenfalls um '1' erhöht (Zeile 63). Weitere Einträge in die Rechnungstabelle können vorgenommen werden.
In Zeile 65 wird wieder die letzte Zeile der Tabelle entfernt, da sie ja leer ist. Dann wird derViewCursor an die letzte Position gesetzt, so dass das Dokument immer so erscheint, dassdirekt der Rechnungsbetrag sichtbar ist. Hier könnte natürlich auch ein Sprung direkt auf dieerste Seite in die erste Zeile erfolgen, wenn dies gewünscht wird.

Serienbrief direkt

105

Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag
Aufruf aus
Formular: Rechnung_Textfelder_Uebertrag

Benötigt
Tabelle:Verkauf, Waren

Makro:

Textfelder_Fuellen_ID, Tabellen_fuellen_UebertragVorlage:

Beispiel_Rechnung_Textfelder.ott

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

020
021

SUB Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oDB AS OBJECTDIM oNewDoc AS OBJECT
DIM stDir AS STRING
DIM stSql AS STRING
DIM loID AS LONG
oForm = oEvent.Source.Model.Parent
oForm.reload()
oForm.last()oDB = ThisComponent.Parent
stDir = Left(oDB.Location,Len(oDB.Location)-Len(oDB.Title))
stDir = stDir & "Beispiel_Rechnung_Textfelder.ott"
DIM args(0) AS NEW com.sun.star.beans.PropertyValueargs(0).Name = "AsTemplate"args(0).Value = TrueoNewDoc = StarDesktop.loadComponentFromURL(stDir,"_blank",0,args)loID = Textfelder_fuellen_ID(oNewDoc, oForm)
stSql = "SELECT ""Verkauf"".""Anzahl"", ""Waren"".""Ware"", ""Waren"".""Preis"",""Verkauf"".""Anzahl"" * ""Waren"".""Preis"" FROM ""Verkauf"", ""Waren""
WHERE ""Verkauf"".""Waren_ID"" = ""Waren"".""ID""AND ""Verkauf"".""Rechnung_ID"" = "+loID+""
Tabellen_fuellen_Uebertrag(oNewDoc, stSql)
END SUB

Das Formular wird wieder über das auslösende Ereignis (Betätigung des Druck-Buttons)
bestimmt (Zeile 8). Der Pfad wird über die Datenbank ermittelt. Statt der Datenbankdatei wird
die Vorlage «Beispiel_Rechnung_Textfelder.ott» an den Pfad angehängt (Zeile 11 bis 13).
Anschließend wird das neue Dokument auf Basis dieser Vorlage geöffnet und die Textfelder mit
dem entsprechenden Inhalt über die Funktion «Textfelder_fuellen_ID» versehen. Die Funktion
gibt dabei den Primärschlüsselwert für die Rechnung zurück (Zeilen 17 und 18).
Schließlich wird der SQL-Befehl für das Füllen des Dokuments mit den Rechnungsposten erstellt
und über die Prozedur «Tabelle_fuellen_Uebertrag» weiter verarbeitet, so dass zum Schluss ein
fertiges Writer-Dokument vorliegt.

Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck
Aufruf aus
Makro:

Direktdruck_Start

Benötigt
Tabelle:Verkauf, Waren

Makro:

Textfelder_Fuellen_ID, Tabellen_fuellen_Uebertrag, PDF_Export

Vorlage:

Beispiel_Rechnung_Textfelder.ott

001

SUB Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck(oEvent AS OBJECT)

Serienbrief direkt

106

002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023

024
025
026

DIM oForm AS OBJECTDIM oDB AS OBJECTDIM oNewDoc AS OBJECT
DIM stDir AS STRING
DIM stPrintDir AS STRING
DIM stSql AS STRING
DIM loID AS LONG
oForm = oEvent.Source.Model.Parent
oForm.reload()
oForm.last()oDB = ThisComponent.Parent
stDir = Left(oDB.Location,Len(oDB.Location)-Len(oDB.Title))
stPrintDir = stDir
stDir = stDir & "Beispiel_Rechnung_Textfelder.ott"
DIM args(1) AS NEW com.sun.star.beans.PropertyValueargs(0).Name = "AsTemplate"args(0).Value = Trueargs(1).Name = "Hidden"args(1).Value = TrueoNewDoc = StarDesktop.loadComponentFromURL(stDir,"_blank",0,args)loID = Textfelder_fuellen_ID(oNewDoc, oForm)
stSql = "SELECT SUM(""Verkauf"".""Anzahl"") AS ""SAnzahl"", ""Waren"".""Ware"",""Waren"".""Preis"", SUM(""Verkauf"".""Anzahl"" * ""Waren"".""Preis"") AS""SGesamt"" FROM ""Verkauf"", ""Waren"" WHERE ""Verkauf"".""Waren_ID"" =""Waren"".""ID"" AND ""Verkauf"".""Rechnung_ID"" = "+loID+" GROUP BY""Waren"".""Ware"", ""Waren"".""Preis"""Tabellen_fuellen_Uebertrag(oNewDoc, stSql)
PDF_Export(oNewDoc, loID, stPrintDir)
END SUB

Diese Prozedur unterschiedet sich in zwei Punkten von der Prozedur «Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag».
•

Die Inhalte der Rechnung werden vor dem Übertrag in das Dokument zusammengefasst,
so dass gleiche Rechnungsposten nicht mehrfach in der Rechnung auftauchen. Dies wird
durch die Gruppierung in der Abfrage (Zeile 23) erreicht.

•

Die Rechnung selbst wird im Hintergrund erstellt (Zeile 19 und 20) und anschließend an
die Prozedur «PDF_Export» weitergeleitet. Dem Export-Befehl wird ein Pfad mitgegeben,
unter dem die PDF-Dateien abgespeichert werden sollen. Es ist hier der gleiche Pfad wie
der, in dem sich die Datenbankdatei befindet.

PDF_Export
Aufruf aus
Makro:

Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013

Rechnung

SUB PDF_Export(oNewDoc AS OBJECT, loID AS LONG, stPrintDir AS STRING)DIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECT
DIM stDatei AS STRING
DIM stSql AS STRING
stDatei = "RE_" & Right("0000000" & loID, 8) & ".pdf"
stPrintDir = stPrintdir & stDatei
DIM arg(0) AS NEW com.sun.star.beans.PropertyValuearg(0).name = "FilterName"arg(0).value = "writer_pdf_Export"
oNewDoc.storeToURL(stPrintDir, arg())
oNewDoc.close(true)

Serienbrief direkt

107

014
015
016
017
018
019
020

oDatenquelle = thisDatabaseDocument.CurrentController
IF NOT (oDatenquelle.isConnected()) THEN oDatenquelle.connect()oVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "UPDATE ""Rechnung"" SET ""Druckdatum"" = CURRENT_DATE WHERE ""ID"" ="+loIDoSQL_Anweisung.executeUpdate(stSql)
END SUB

Zuerst wird der abzuspeichernden Datei im Dateinamen die Rechnungsnummer zugewiesen
Zeile 7). Damit sind alle Dateien unterscheidbar.
Die Datei wird mittels des *.pdf-Exports (Zeile 11) erstellt und in dem entsprechenden Pfadabgespeichert sowie geschlossen (Zeile 12).
Damit eine Rechnung nicht mehrmals ausgedruckt wird, wird das aktuelle Druckdatum
anschließend in der Tabelle "Rechnung" eingetragen. So können dann auch alle offenen Rechnungen über eine Schleife gedruckt werden. Es wird dann einfach jede Rechnung gedruckt, dienoch nicht mit einem Druckdatum versehen ist.

Direktdruck_Start
Aufruf aus
Formular: Rechnung_Textfelder_Uebertrag_Seriendruck

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022

Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_Direktdruck

SUB Direktdruck_Start(oEvent AS OBJECT)
DIM oMainForm AS OBJECT
DIM stSerie AS STRING
DIM stIDField AS STRING
DIM stID AS STRING
stSerie = oEvent.Source.Model.Tag
oMainForm = oEvent.Source.Model.Parent.Parent
stIDField = oMainForm.findColumn("ID")stID = oMainForm.getString(stIDField)
IF stSerie = "Seriendruck" THEN
DO WHILE stID <> ""Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_DirektdruckoMainForm.reload()stID = oMainForm.getString(stIDField)
LOOP
ELSE
IF stID <> "" THEN
Rechnungsinhalt_zusammenstellen_Tabelle_Uebertrag_DirektdruckoMainForm.reload()
END IF
END IF
END SUB

Der Direktdruck wird über diese separate Prozedur gestartet. Dies ist erforderlich, weil das Formular neben dem Druck einer einzelnen Rechnung auch den Druck aller offenen Rechnungenermöglichen soll.
Steht in dem auslösenden Button in den Zusatzinformationen (Zeile 6) der Begriff «Seriendruck» (Zeile 10), so wird zuerst der Druck gestartet, dann das darüber liegende Formular(Parent.Parent Zeile 7) wieder neu geladen und der Primärschlüsselwert aus dem jetzt
geladenen Formular wieder ausgelesen. Das Neuladen ist notwendig, da in dem Formular nur
die Rechnungen angezeigt werden, bei denen das Druckdatum noch leer ist.
Handelt es sich nicht um einen Seriendruck und ist das Feld für den Primärschlüssel nicht leer,
so wird stattdessen nur die Rechnung zum aktuellen Datensatz gedruckt (Zeile 18).
Serienbrief direkt

108

Serienbrief direkt

109

Terminübersicht
Ferienhäuser

Terminübersicht Ferienhäuser

110

Einführung

Die einfache Eingabe von Daten für die Belegung eines Ferienhauses ist in Base problemlos
möglich. Wie aber erhalte ich einen Überblick, zu welchen Terminen das Ferienhaus noch frei
ist? Und wie erst, wenn es sich gleich um mehrere Häuser handelt? Dazu muss eine Jahresübersicht erstellt werden, die am besten alle Datumswerte des Jahres enthält. Wie gebe ich jetzt
sicher ein, dass ein Ferienhaus nicht doppelt belegt wird. Auch hierzu bietet das Formular dieser Datenbank einen Lösungsvorschlag5.

Tabellen
Die Tabellen decken nur den mindestens nötigen Inhalt ab, um die Organisation der Vermietungstermine zu demonstrieren. Eine Adressverwaltung fehlt ebenso wie eine Auflistung derKosten o.ä.

MieterDatenziel
Abfrage:

Belegung, Bericht

Formular, Ansicht, Bericht, Makro: keine direkt

5

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann als Auto-Wert-Feld gesetzt werden.

Name

Text

Name des Mieters.
Eingabe erforderlich: Ja

Beispieldatenbank Beispiel_Terminuebersicht_Ferienhausvermietung.odb

Terminübersicht Ferienhäuser

111

Diese Tabelle enthält in diesem Fall lediglich die Namen der Mieter. Ein Formular, das dieseTabelle beschickt, ist nicht vorgesehen. Die wenigen Mieter, die zu Demonstrationszwecken
benötigt werden, wurden direkt in die Tabelle eingegeben. Deswegen enthält die Tabelle auchkein Auto-Wert-Feld als Primärschlüssel.

Haus
Datenziel
Abfrage:

Bericht

Formular, Ansicht, Bericht, Makro: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann als Auto-Wert-Feld gesetzt werden.

Haus

Text

Bezeichnung des Hauses.
Eingabe erforderlich: Ja

Auch die Tabelle "Haus" ist nur mit der Mindestinformation, der Bezeichnung für das Haus, versehen. Da die vorgesehenen Häuser begrenzt sind ist auch hier keine Auto-Wert-Feld für den
Primärschlüssel vorgesehen.

Reservierung
Datenziel
Formular: Haus
Abfrage:

Belegung, Bericht

Makro:Startdatum, Enddatum

Ansicht, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

Haus_ID

Integer

Fremdschlüssel der Tabelle "Haus". Gleichzeitig einer der drei
Primärschlüssel dieser Tabelle.

Mieter_ID

Integer

Fremdschlüssel der Tabelle "Mieter". Gleichzeitig einer derdrei Primärschlüssel dieser Tabelle.

Startdatum

Datum

Nimmt den Beginn der Mietperiode auf. Muss zusammen mit
den vorhergehenden Feldern ein Primärschlüssel sein, dasonst ein Mieter nicht zweimal das gleiche Haus mieten
kann.

Enddatum

Datum

Hier wird das Ende der Mietperiode verzeichnet.
Eingabe erforderlich: Ja

Die Tabelle "Reservierung" verbindet die Tabelle "Mieter" mit der Tabelle "Haus".
Das Ende der Mietperiode könnte noch mit einer Bedingung so versehen werden, dass das eingegebene Datum auf jeden Fall nach dem Startdatum liegt. Hier wurde darauf verzichtet, dadie Eingabe komplett über das Formular gesteuert wird.

Terminübersicht Ferienhäuser

112

Die drei oben genannten Tabellen sind die Tabellen, in denen beständig Eingaben erfolgen sollen. Daneben existiert noch wieder eine Tabelle "Filter", die dazu dient, nach bestimmten Zeiträumen die Eingaben im Formular zu filtern.

FilterDatenziel
Formular: Haus
Ansicht:

Ansicht_Datum

Abfrage:

Belegung

Bericht, Makro:

keine direkt

Feldname

Feldtyp

Beschreibung

ID

Ja/NeinPrimärschlüssel der Tabelle, wird zum Start auf 'Ja' gesetzt
und danach nicht mehr verändert.

BeginnDatum

Datum

Startdatum für Anzeige in Formular und Bericht

EndeDatum

Datum

Enddatum für Anzeige in Formular und Bericht

Diese Tabelle filtert in dem Formular die Zeitspanne, für die die Termine angezeigt werden.

NrBis31
Datenziel
Ansicht:

Ansicht_Datum

Formular, Abfrage, Bericht, Makro: keine direkt

Feldname

Feldtyp

Beschreibung

Nr

TinyInteger

Fortlaufende Nummerierung von 1 bis 31 für die Erstellung
von Datumsübersichten, kein Primärschlüssel, dadurch nichtbeschreibbar.

Aus der Tabelle "NrBis31" wurde der Primärschlüssel nach der Eingabe der Ziffern entfernt,
damit nicht irrtümlich eine der Zahlen gelöscht wird. Solch eine Tabelle kann auch einfach überCalc und die automatische Nummerierung erzeugt werden. Mit Hilfe dieser Tabelle wird eine
fortlaufende Datumsübersicht erzeugt.
Für FIREBIRD ist eine andere Abfragetechnik und daher auch eine andere Hilfstabelle notwendig.
Die Tabelle «NrBis1100» speichert hier alle Zahlen bis 1100, deckt also 3 Jahre und einige
wenige Tage ab.

Terminübersicht Ferienhäuser

113

Ansichten
Ansicht_Datum
Datenquelle
Tabelle:NrBis31, Filter

Datenziel
Ansicht:

Ansicht_Datum_lfdNr

Abfrage:

Belegung

Makro:

Enddatum001 SELECT DISTINCT
002 CAST("Y"."Nr" + (SELECT EXTRACT( YEAR FROMCOALESCE("BeginnDatum",CURRENT_DATE)) FROM "Filter" WHERE "ID" = True) - 2|| '-' ||RIGHT('0'||"M"."Nr",2)|| '-' ||RIGHT('0'||"D"."Nr",2)
AS DATE ) AS "Datum"
003 FROM "NrBis31" AS "D", "NrBis31" AS "M", "NrBis31" AS "Y"
004 WHERE "Y"."Nr" <= 3005 AND "M"."Nr" <= 12006 AND "D"."Nr" <= 31
Die Tabelle "NrBis31" enthält lediglich die Zahlen von 1 bis 31. Aus diesen Zahlen wird ein
Datumswert zusammengesetzt. Das Datum muss dabei in der Reihenfolge YYYY-MM-DD vorliegen. Erst dann kann es durch die HSQLDB in einen Datumswert umgewandelt werden.
Die Tabelle "NrBis31" wird dreimal durch einen Alias eingebunden (Zeile 3). Da zwischen den
verschiedenen Alias-Bezeichnungen keine Beziehung erstellt wird ermittelt die Abfrage alle
Kombinationen der Werte, die aus der Tabelle zu entnehmen sind.
Das Jahr beginnt mit Werten, die ein Jahr vor dem Jahr liegen, in dem das "BeginnDatum" derTabelle "Filter" liegt. Bei den Monats- und Tagesbezeichnungen wird jeweils eine '0' vorangestellt und dann die letzten beiden Ziffern für die Verarbeitung zum Datum genutzt. So bleibt die
Monats- und die Tageszahl immer zweistellig. Insgesamt ergibt sich daraus ein Datum in Textform, der jetzt noch in einen Datumswert über CAST … AS DATE umgewandelt wird (Zeile 2).
Es werden die Datumswerte für 3 Jahre ermittelt (Zeile 4). Für den Monat werden die Werte von1 bis 12 ausgelesen (Zeile 5) und für den Tag die Werte von 1 bis 31 (Zeile 6).
Das Startjahr wird aus dem "BeginnDatum" der Tabelle "Filter" ausgelesen. Das Startjahr wird
zu dem jeweiligen Datensatz aus "NrBis31" addiert. Da die Zahlenwerte aus der Tabelle"NrBis31" mit 1 beginnen und die Jahreszahlen 1 Jahr vor dem Jahr beginnen sollen, das aktuellangezeigt wird, wird von dem Betrag des Startjahres 2 subtrahiert.
Durch die Umwandlung erstellt die HSQLDB aus einem Datumswert wie z.B. 31.02.2015 stattdessen den 3.03.2015. Schließlich hat der Februar nicht 31 Tage. Tageswerte über 31 akzeptiert
die HSQLDB allerdings nicht. Sonst wäre über diesen Weg auch leicht eine Addition von Tagenwie mit DATEADD in anderen Datenbanken zu einem Datum möglich. Diese Umwandlung
erzeugt jetzt bei einigen Datumswerten doppelte Einträge wie eben bei dem 3.03.2015. Diese
Doppler sollen unterbunden werden. Das geschieht schließlich mit dem Zusatz DISTINCT (Zeile
1).
Die Ansicht wird von der HSQLDB direkt in der korrekten Datumsreihenfolge ausgegeben.

Terminübersicht Ferienhäuser

114

Für FIREBIRD ist diese Ansicht nicht möglich (Fehlermeldung bei fehlerhaften Datumswerten wie'29.02.2019'). Die Ansicht ist aber auch nicht nötig, da Tage zu Datumswerten addiert werden
können. Stattdessen wird direkt die Ansicht «Ansicht_Datum_lfdNr» erstellt.

Ansicht_Datum_lfdNr
Datenquelle
Ansicht:

Ansicht_Datum

Abfrage:

Bericht

Makro:Startdatum

001 SELECT "a"."Datum",
002(SELECT COUNT(*) FROM "Ansicht_Datum" WHERE "Datum" <= "a"."Datum")AS "lfdNr"
003 FROM "Ansicht_Datum" AS "a"Diese Ansicht greift auf die vorher erstellte Ansicht zu. Sie fügt allerdings noch eine Spalte
hinzu, die eine fortlaufende Nummerierung der Datumswerte erstellt. Es werden durch einekorrelierende Unterabfrage alle Datumswerte gezählt, die kleiner oder gleich dem aktuellen
Datumswert des angezeigten Datensatzes sind.
Eine solche Auflistung mit fortlaufender Nummerierung macht es möglich, zu einem Datum
eine bestimmte Anzahl von Tagen zu addieren oder zu subtrahieren und als Ergebnis wieder ein
Datum zu erhalten. Hierdurch kann also in Grenzen die Funktion DATEADD nachgestellt werden,
die es in der alten Version der HSQLDB nicht gibt.
Mit FIREBIRD wird diese Ansicht direkt erstellt:
001 SELECT CAST( ( (SELECT EXTRACT( YEAR FROM COALESCE ( "BeginnDatum", CURRENT_DATE ) )FROM "Filter" WHERE "ID" = TRUE
) - 2 ) || '-12-31' AS DATE ) + "Nr" AS "Datum",
002 "Nr" AS "lfdNr"
003 FROM "NrBis1100"
Es wird hier das "BeginnDatum" aus der Tabelle "Filter" genommen und davon dann das Jahr
extrahiert. Gestartet wird mit dem 31.12. 2 Jahre vor dem "BeginnDatum". Zu diesem datum
wird '1' addiert, so dass das erste angezeigte Datum der 1.1. im Jahr vor dem "BeginnDatum"ist (Zeile 1). In Zeile 2 wird einfach die fortlaufende Nummer aus der Tabelle "NrBis1100" übernommen. Die Ansicht in FIREBIRD entspricht dann bis auf ein paar zusätzliche Tage genau derAnsicht, die über Umwege mit der HSQLDB konstruiert wurde.

Abfragen
Die Abfragen greifen auf zwei unterschiedliche Ansichten zu. Die Ansichten dienen dazu, laufende Datumswerte wieder zu geben bzw. auch noch mit einer laufenden Nummerierung zu
versehen.

Belegung
Datenquelle
Tabelle:Mieter, Reservierung, Filter

Ansicht:

Ansicht_Datum

Terminübersicht Ferienhäuser

115

Datenziel
Formular: Haus

001 SELECT CASE WHEN DAYOFWEEK( "a"."Datum" ) = 1 THEN '▓ ' ||TO_CHAR( "a"."Datum", 'D' ) WHEN DAYOFWEEK( "a"."Datum" ) = 7 THEN '▒ ' ||TO_CHAR( "a"."Datum", 'D' ) ELSE '░ ' || TO_CHAR( "a"."Datum", 'D' ) END
AS "Wochentag",
002 "a"."Datum",003 ( SELECT "Mieter"."Name" FROM "Mieter", "Reservierung" WHERE "Mieter"."ID"= "Reservierung"."Mieter_ID" AND "Reservierung"."Haus_ID" = 1 AND"a"."Datum" BETWEEN "Reservierung"."Startdatum" AND"Reservierung"."Enddatum" ) AS "Haus1",
004 ( SELECT "Mieter"."Name" FROM "Mieter", "Reservierung" WHERE "Mieter"."ID"= "Reservierung"."Mieter_ID" AND "Reservierung"."Haus_ID" = 2 AND"a"."Datum" BETWEEN "Reservierung"."Startdatum" AND"Reservierung"."Enddatum" ) AS "Haus2",005 ...
006 ... AS "Haus8"
007 FROM "Ansicht_Datum" AS "a"008 WHERE "a"."Datum" BETWEEN
009 COALESCE( ( SELECT "BeginnDatum" FROM "Filter" WHERE "ID" = TRUE ),CURRENT_DATE ) AND COALESCE( ( SELECT "EndeDatum" FROM "Filter" WHERE "ID"= TRUE ), CURRENT_DATE )In der ersten Spalte (Zeile 1) wird der Wochentag in Kurzschreibweise zusammen mit einem
grafischen Element dargestellt, das die Wochenenden kennzeichnen soll. Da diese Abfrage
innerhalb eines Tabellenkontrollfeldes genutzt werden soll sind hier die Möglichkeiten zur Kennzeichnung von Wochenenden für eine bessere Übersicht sehr begrenzt.
In der zweiten Spalte wird das Datum aus der "Ansicht_Datum" ausgelesen (Zeile 2). Dabei ist
durch Einträge in der Tabelle "Filter" festgelegt, von welchem bis zu welchem Datum die
Anzeige erfolgen soll (Zeile 9). Die gesamte Abfrage bezieht sich nur auf "Ansicht_Datum"(Zeile 7).
In einer korrelierenden Unterabfrage wird dann für jedes Haus (Zeilen 3 bis 6) abgefragt, ob das
Datum der ersten Abfragespalte Zwischen "Startdatum" und "Enddatum" eines Eintrags zu finden ist, der in der Tabelle "Reservierung" existiert. Dann wird der jeweilige Name des Mieters
herausgesucht und in die Übersicht aufgenommen.
Die Abfrage zu Belegung muss in FIREBIRD anders gestellt, weil der Befehl DAYOFWEEK unbekannt
ist und Firebird zur Zeit in der internen Version bei der Verwendung von BETWEEN den Kontakt
zur internen Datenbank abbricht.
001
002
003
004
005
006
007

008

009
010
011

SELECT CASE
WHEN EXTRACT( WEEKDAY FROM "a"."Datum" ) = 0 THEN '▓ ' || 'So.'
WHEN EXTRACT( WEEKDAY FROM "a"."Datum" ) = 6 THEN '▒ ' || 'Sa.'
WHEN EXTRACT( WEEKDAY FROM "a"."Datum" ) = 1 THEN '░ ' || 'Mo.'...
END AS "Wochentag", "a"."Datum",( SELECT "Mieter"."Name" FROM "Mieter", "Reservierung" WHERE "Mieter"."ID"= "Reservierung"."Mieter_ID" AND "Reservierung"."Haus_ID" = 1 AND"a"."Datum" >= "Reservierung"."Startdatum" AND "a"."Datum" <="Reservierung"."Enddatum" ) "Haus1",( SELECT "Mieter"."Name" FROM "Mieter", "Reservierung" WHERE "Mieter"."ID"= "Reservierung"."Mieter_ID" AND "Reservierung"."Haus_ID" = 2 AND"a"."Datum" >= "Reservierung"."Startdatum" AND "a"."Datum" <="Reservierung"."Enddatum" ) "Haus2",...
... "Haus8"
FROM "Ansicht_Datum_lfdNr" AS "a" WHERE "a"."Datum" >= COALESCE ( ( SELECT"BeginnDatum" FROM "Filter" WHERE "ID" = TRUE ), CURRENT_DATE ) AND

Terminübersicht Ferienhäuser

116

"a"."Datum" <= COALESCE ( ( SELECT "EndeDatum" FROM "Filter" WHERE "ID" =TRUE ), CURRENT_DATE )Zuerst werden wieder die Tagesbezeichnungen zusammen mit der Kennzeichnung für die Tageerstellt. FIREBIRD nutzt für die Tagesnummern den Befehl EXTRACT( WEEKDAY FROM ...), wobei
die Tageszählung beim Sonntag mit '0' beginnt. Leider ist intern nicht irgendwo ein Kürzel für
die Wochentage vorhanden, so dass jeder Wochentag einzeln deklariert werden muss. Hier
gezeigt an den Zeile 2 bis 4.
Um die Mieter einzubelenden muss der Befehl "Datum" BETWEEN "Startdatum" AND"Enddatum" geändert werden zu "Datum" >= "Startdatum" AND "Datum <= "Enddatum" (AbZeile 7, entsprechend auch in Zeile 11).
Da der Befehl WEEKDAY nicht in der GUI läuft muss die Abfrage auf SQL-Befehl direkt ausführen
eingestellt werden. Sobald aber eine Abfrage mit dieser Einstellung läuft kann sie nicht mehr ineinem Formular gefiltert und sortiert werden. Bestimmte Makroeigenschaften stehen damit
nicht mehr zur Verfügung. Außerdem ergab sich bei einem ersten Test, dass das Tabellenkontrollfeld außer den Datumswerten nicht weiter anzeigte, obwohl die Abfrage einwandfrei lief.
Aus diesem Grunde wurde die Abfrage «Belegung» in eine «Ansicht_Belegung» umgewandelt.
Sie behebt für FIREBIRD die beiden oben geschilderten Probleme.

Bericht
Datenquelle
Tabelle:Mieter, Reservierung, Haus

Ansicht:

Ansicht_Datum_lfdNr

Datenziel
Bericht:

Hausbelegung

001 SELECT "Haus"."Haus", "Mieter"."Name",002"a"."Startdatum", "a"."Enddatum",003
DATEDIFF( 'dd', "a"."Startdatum", "a"."Enddatum" ) AS "Tage",004( SELECT "Datum" FROM "Ansicht_Datum_lfdNr" WHERE "lfdNr" =( SELECT "lfdNr" FROM "Ansicht_Datum_lfdNr" WHERE "Datum" ="a"."Startdatum" ) - 30 )AS "AnzahlungBis"
005 FROM "Reservierung" AS "a", "Haus", "Mieter"
006 WHERE "a"."Haus_ID" = "Haus"."ID"007
AND "a"."Mieter_ID" = "Mieter"."ID"008 ORDER BY "Haus"."Haus" ASC, "a"."Startdatum" ASC
Diese Abfrage dient als Basis für den Bericht. Hier sollen Hausbezeichnung und Name des Mieters neben den Informationen aus der Tabelle "Reservierung" erscheinen. Außerdem kommen
noch Informationen hinzu, die die Mietdauer und den Termin betreffen, zu dem eine Anzahlungzu erfolgen hat.
Da auf die Tabelle "Reservierung" wegen einer korrelierenden Unterabfrage durch ein Alias
zugegriffen werden muss, werden alle damit verbundenen Felder über das Alias "a" angesprochen. Die Verbindung der Tabellen entspricht der Verbindung innerhalb der erklärten Beziehungen dieser Datenbank. Die ersten vier Felder dieser Abfrage sind also leicht über die GUI
zusammen zu klicken. Die Tage, die ein Mieter in einem Haus ist, werden über die Funktion
DATEDIFF berechnet. 'dd' berechnet hier den Unterschied in Tagen zwischen den folgenden
beiden Datumswerten.
Um das Datum zu berechnen, bis zu dem die Anzahlung fällig ist, wird auf die"Ansicht_Datum_lfdNr" zugegriffen. Durch diese Unterabfrage wird die in der HSQLDB fehlende
Funktion DATEADD in diesem Falle ersetzt (Zeile 4). Zu dem "Startdatum" des aktuellen Daten-

Terminübersicht Ferienhäuser

117

satzes wird die entsprechende "lfdNr" aus "Ansicht_Datum_lfdNr" ermittelt. Von dieser Nummer
wird 30 als Anzahl der Tage subtrahiert, die das Anzahlungsdatum vor dem Startdatum liegensoll. Dann wird in der äußeren Abfrage zu dieser neuen "lfdNr" das entsprechende Datum ausder "Ansicht_Datum_lfdnr" ermittelt. Diese Abfragetechnik funktioniert leider nur solange, wie
beide Datumswerte in der Ansicht vertreten sind.
In FIREBIRD ließe sich eine Abfrage mit ähnlichem Code nur mit direktem SQL ausführen. Dafür
stellt Firebird aber mehr Möglichkeiten bei der Datumsverarbeitung zur Verfügung, so dass das
Ergebnis wesentlich einfacher zu erreichen ist:001 SELECT "Haus"."Haus", "Mieter"."Name",002"a"."Startdatum", "a"."Enddatum",003"a"."Enddatum" - "a"."Startdatum" "Tage",004"a"."Startdatum" - 30 "AnzahlungBis"
005 FROM "Reservierung" AS "a", "Haus", "Mieter"
006 WHERE "a"."Haus_ID" = "Haus"."ID"007
AND "a"."Mieter_ID" = "Mieter"."ID"008 ORDER BY "Haus"."Haus" ASC, "a"."Startdatum" ASC
Hier können Datumswerte voneinander subtrahiert werden und das Ergebnis ist eine Tagesausgabe (Zeile 3). Außerdem können von Datumswerten ganze Zahlen subtrahiert werden. Das
Ergebnis ist eine Datumsausgabe (Zeile 4).

Formulare
Diese Datenbank enthält nur ein Formular, das Formular Haus. Es gliedert sich in einen Eingabebereich für neue Buchungstermine, eine Auswahl der bestehenden Buchungstermine undeine Anzeige zur Übersicht über Buchungstermine in einem bestimmten Zeitraum.

Terminübersicht Ferienhäuser

118

1 Formular (Tabelle: Reservierung)2 Filter (Tabelle: Filter)
3 Uebersicht (Abfrage: Belegung)4 BuchungLoeschen (Tabelle: Reservierung)
Makros in Formulareigenschaften
1

Nach der Datensatzaktion

Module1.NeueBuchung4

Nach der Datensatzaktion

Module1.BuchungsAenderung

Makros in Feldeigenschaften
1

Listenfeld Haus (Modifiziert)

Module1.Startdatum

1

Listenfeld Startdatum (Modifiziert)

Module1.Enddatum

4

Startdatum (Maustaste gedrückt)Module1.DatumsAenderung

Terminübersicht Ferienhäuser

119

Der größte Aufwand wird betrieben, um bei der Auswahl des Startdatums bzw. des Enddatums
nur tatsächlich mögliche Werte bereit zu stellen. Aus diesem Grunde sind die Felder auch nicht
als Datumsfelder, sondern als Listenfelder vorgegeben. Für die Listenfelder kann über den SQLCode die Auswahlmöglichkeit des Inhaltes gesteuert werden. Beide Listenfelder sind darauf
eingestellt, dass sie im SQL-Code den Wert des Feldes, den sie anzeigen, anschließend auch indie darunterliegende Tabelle übertragen: Eigenschaften → Daten → gebundenes Feld → 0.
Würde in obigem Beispiel über das Listenfeld für das Haus 'Haus 4' gewählt, dann sind fürbeide Datumsfelder sämtliche Eingaben offen. Das Enddatums-Feld darf allerdings nur eine Eingabe möglich machen, die mindestens einen Tag hinter dem Startdatum liegt.
Schwieriger wird es z.B. bei der Auswahl von 'Haus 1'. Dort sind im Listenfeld für das Startdatum nur die Werte möglich, die eben bisher nicht mit 'belegt' gekennzeichnet sind. Würde aber
jetzt z.B. der 26.6.20 für dieses Haus gewählt, dann wäre kein Enddatum mehr möglich. Das
Listenfeld darf dieses Datum also nicht anbieten, obwohl es nicht belegt ist. Nur noch die
Datumswerte vom 5.7. an dürfen angeboten werden, da sie in der Zukunft liegen. Die aktuellen
Einträge wurden nämlich am 11.6. testweise erstellt. Termine für die Vergangenheit zu buchen
wird also direkt ausgeschlossen.
Es muss also zuerst das Haus, dann das Startdatum und erst danach das Enddatum gewählt
werden. Entsprechend sind die Makros auch mit den Listenfeldern für das Haus und das Startdatum verbunden.
Sobald das Startdatum gewählt wird stellt sich die Übersicht auf dieses Startdatum ein. Das
Datum wird in der ersten Zeile der Übersicht angezeigt, die in dem Screenshot grün markiert
ist. Jetzt muss noch ein Enddatum gewählt werden, das schon so weit vorgegeben ist, dass es
auf jeden Fall hinter dem Startdatum liegt und auf keinen Fall die folgende Belegung beeinflusst.
Sobald der Datensatz über die Schaltfläche Speichern abgespeichert wurde wird das Formular
für die Eingabe auf einen neuen Datensatz eingestellt. Die Schaltfläche Speichern ist in den
Eigenschaften mit der Aktion → Neuer Datensatz verbunden. Gleichzeitig wird über das Formular die Übersicht aktualisiert, so dass die eingegebene Belegung mit dem entsprechenden Mieter in der Liste sichtbar erscheint.
Das Tabellenkontrollfeld rechts oben im Formular dient lediglich zum Löschen von Eingaben.
Eine Änderung von Eingaben ist nicht vorgesehen, da dies auch schnell über die Neueingabe
erfolgen kann. Auch bei diesem Tabellenkontrollfeld ist die Möglichkeit gegeben, den Datensatz
vorher in der Übersicht über einen Doppelklick auf das Feld «Startdatum» noch einmal anzusehen. Um dies zu ermöglichen darf das Formular selbst nicht die Datenänderung verbieten.
Stattdessen sind alle Felder in dem Formular auf Nur Lesen → Ja eingestellt. Würde die Datenänderung im Formular verboten oder das Datumsfeld inaktiv geschaltet, so ließe sich keine
Aktion von diesem Feld mehr auslösen.

Terminübersicht Ferienhäuser

120

Berichte
Diese Beispieldatenbank enthält lediglich einen Bericht, den Bericht Hausbelegung.

Datenquelle
Abfrage:

Bericht

Der Bericht gibt eine Übersicht über die Belegung der Häuser zurück. Er ist gruppiert nach den
Hausbezeichnungen. Im Gruppenfuß steht zusätzlich, wie viele Tage das Haus insgesamt belegt
ist. Es gibt also so etwas wie eine Auslastung des Hauses in der angegebenen Zeitspanne wieder. Die Funktion für die Summierung ist eine der eingebauten Funktionen. Das Textfeld wurde
aufgezogen und anschließend in Eigenschaften → Daten die folgenden Angaben gemacht:•

Datenfeld-Typ:

Funktion•

Datenfeld:

Tage

•

Funktion:

Summe

•

Geltungsbereich: Gruppe: Haus

MakrosStartdatum
Aufruf aus
Formular: Haus

Benötigt
Tabelle:Reservierung

Ansicht:

Ansicht_Datum

001
002
003
004

SUB Startdatum(oEvent AS OBJECT)
DIM oFeldStart AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECT

Terminübersicht Ferienhäuser

121

005
006
007
008
009
010
011
012

013
014
015
016

DIM inID AS INTEGER
DIM stSql(0) AS STRING
DIM stTeilsql AS STRING
oFeldStart = oEvent.Source.ModeloForm = oFeldStart.Parent
oFeld = oForm.getByName("Startdatum")inID = oFeldStart.CurrentValuestTeilsql = " ( SELECT ""a"".""Datum"", ""a"".""lfdNr"",( SELECT 'belegt' FROM ""Reservierung"" WHERE ""Haus_ID"" = '"+inID+"' AND""a"".""Datum"" BETWEEN ""Startdatum"" AND ""Enddatum"" ) AS ""Haus""
FROM ""Ansicht_Datum_lfdNr"" AS ""a"" WHERE ""a"".""Datum"" >= CURRENT_DATE )WHERE ""Haus"" IS NULL "stSql(0) = "SELECT ""Datum"" FROM "+stTeilsql+" AND ""lfdNr""+1 IN(SELECT ""lfdNr"" FROM "+stTeilsql+")"oFeld.ListSource = stSql
oFeld.refresh
END SUB

Diese Makro soll das Listenfeld mit Inhalt versorgen, das das Startdatum abspeichern soll. Das
Makro liest zuerst die Position für das Listenfeld in dem Formular über das auslösende Ereignis
aus (Zeile 8 bis 11). Der Wert für inID stellt den Wert dar, der von dem Listenfeld für die Hausauswahl bereitgestellt wird. Die Hausauswahl bestimmt schließlich, welche Datumswerte nochfrei sind.
Das Listenfeld selbst wird mit einem Array of String über stSql(0) beschickt. Der SQLCode wird in Zeile 12 und 13 erstellt, in Zeile 14 als Code an das Feld weitergegeben und in
Zeile 15 schließlich durch den Refresh auch in dem Feld angezeigt.
Der SQL-Code verlangt hier eine etwas ausführlichere Erklärung, da mehrere verschachtelte
Unterabfragen enthalten sind.
Eine für 2 Elemente gleichlautende Unterabfrage ist als stTeilsql aus der äußeren Abfrage
abgetrennt worden (Zeile 12).
Mit ( SELECT 'belegt' ... ) AS "Haus" wird zuerst einmal für alle Datumswerte ein 'belegt'
eingefügt, wenn das betreffende Haus für die Vermietung nicht mehr frei ist. Diese Unterabfrage ist gleich doppelt vertreten und beinhaltet jedes Mal den aus dem Feld für das Haus
ermittelten Wert inID.
Um diese Abfrage herum ist eine weitere Abfrage konstruiert: ( SELECT "a"."Datum" ...
WHERE "a"."Datum" >= CURRENT_DATE). Dadurch wird jetzt eine Liste erstellt, die die folgenden Kriterien erfüllt: Alle Datumswerte werden aus der Ansicht "Ansicht_Datum_lfdNr" übernommen, die gleich oder größer als das aktuelle Datum sind, also in der Zukunft liegen. Daneben ist für Berechnungszwecke die "lfdNr" enthalten sowie eine Spalte "Haus", in der an
bestimmten Stellen 'belegt' steht. Diese gesamte Abfrage ist ebenfalls doppelt enthalten. Einmal dient sie dazu, direkt die Datumswerte anzugeben, bei denen nicht das Wort 'belegt' steht,
die also leer sind: "Haus" IS NULL.
In Zeile 13 wird dann diese Unterabfrage in die entsprechende Rahmenabfrage eingebaut.
Benötigt wird aus der Unterabfrage nur das Datum. Allerdings soll zu dem Datum auch ein
direkter Nachfolger existieren, damit neben dem Startdatum auch ein Enddatum möglich ist eine Buchung also von einem Tag zum nächsten. Deswegen wird das Datum nur ausgegeben,
wenn die "lfdNr" + 1 IN der Menge der Datensätze der Unterabfrage enthalten ist. Die
Arbeit mit der lfdNr ist hier wieder notwendig, da die HSQLDB die Addition von Tagen zu einem
Datum nicht beherrscht.

Enddatum
Aufruf aus
Formular: Haus

Terminübersicht Ferienhäuser

122

Benötigt
Tabelle:Reservierung

Ansicht:

Ansicht_Datum

Makro:

DatumsAenderung

001
002
003
004
005
006
007
008
009
010
011
012
013
014

015
016
017
018
019

SUB Enddatum(oEvent AS OBJECT)
DIM oDatField AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECT
DIM inID AS INTEGER
DIM stSql(0) AS STRING
DIM stTeilsql AS STRING
DIM stDatum AS STRING
oDatField = oEvent.Source.Model
stDatum = oDatField.CurrentValue.Year & "-" &Right("0" & oDatField.CurrentValue.Month , 2) & "-" &Right("0" & oDatField.CurrentValue.Day , 2)
oForm = oDatField.Parent
oFeld = oForm.getByName("Enddatum")inID = oForm.getByName("Haus_ID").CurrentValuestTeilsql = "( SELECT ""a"".""Datum"", ( SELECT 'belegt' FROM ""Reservierung""
WHERE ""Haus_ID"" = '"+inID+"' AND ""a"".""Datum"" BETWEEN ""Startdatum""AND ""Enddatum"" ) AS ""Haus"" FROM ""Ansicht_Datum"" AS ""a""WHERE ""a"".""Datum"" > '"+stDatum+"')"
stSql(0) = "SELECT ""Datum"" FROM "+stTeilsql+" WHERE ""Datum"" <COALESCE((SELECT MIN(""Datum"") FROM "+stTeilsql+"
WHERE ""Haus"" = 'belegt'),(SELECT MAX (""Datum"") FROM ""Ansicht_Datum""))"
oFeld.ListSource = stSql
oFeld.refresh
DatumsAenderung(oEvent)END SUB

Das Enddatum wird in Abhängigkeit von dem Startdatum und dem Haus in dem Listenfeld zur
Verfügung gestellt.
Aus dem Feld für das Startdatum wird der Datumswert in SQL-Form ausgelesen (Zeile 9 und
10). Dann wird über das Formular die Verbindung zum Feld für das Enddatum sowie zu dem
Feld für das Haus Bezug genommen (Zeile 11 bis 13). Aus dem Feld für das Haus wird derSchlüsselwert inID ausgelesen, der später für die Erstellung des SQL-Codes benötigt wird.
Der SQL-Code wird erstellt, an das Listenfeld weitergegeben (Zeile 16), das Listenfeld neu eingelesen (Zeile 17) und anschließend die Datumsänderung an die entsprechende Prozedur zur
Steuerung der Übersicht weitergegeben, damit auch in der Übersicht alle passenden Eingaben
für das Enddatum ersichtlich sind.
Der SQL-Code ist zur besseren Erklärbarkeit wieder in zwei Teile aufgesplittet. Die innere
Abfrage stTeilsql gibt vom Prinzip her die gleiche Datenmenge wie in der Prozedur Startdatum
wieder (Zeile 14). Einzige zusätzliche Einschränkung ist hier, dass das ausgewählte Datum größer sein muss als das Datum, das als Startdatum ausgesucht wurde. Außerdem werden alle
Datumswerte ermittelt - auch die Werte, bei denen das Haus belegt ist.
Mit dem an das Listenfeld weitergegebenen Code (Zeile 15) wird wieder das Datum übermittelt.
Bedingungen für dieses Datum sind, dass•es größer ist als das Startdatum (stTeilsql)•

und kleiner ist als das kleinste dann noch in der stTeilsql vorhandene Datum, an dem
das Haus belegt ist•

oder, sofern keine Belegung folgt, so groß ist wie das maximal verfügbare Datum der
Ansicht "Ansicht_Datum".

Damit werden in dem Listenfeld nur die möglichen auswählbaren Datumswerte angezeigt. Es
ist nicht möglich, ein Datum auszuwählen, das in eine Belegungszeit fällt. Es ist auch nicht

Terminübersicht Ferienhäuser

123

möglich, ein Datum auszuwählen, bei der das Startdatum vor einer Belegungszeit, das Enddatum aber nach dieser Belegungszeit liegt.
Für FIREBIRD existiert «Ansicht_Datum» nicht, so dass stattdessen «Ansicht_Datum_lfdNr» eingesetzt werden muss.

BuchungsAenderung
Aufruf aus
Formular: Haus
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013

NeueBuchung

SUB BuchungsAenderung
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECT
DIM iRow AS INTEGER
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("Uebersicht")
iRow = oForm.getRowoForm.reload
oForm.last
oForm.absolute(iRow)
END SUB

Wird an der Buchung etwas geändert, also eine Buchung hinzugefügt oder gelöscht, so mussdie Übersicht für die Buchungen neu eingelesen werden. Damit die Übersicht dann nicht einfach zum ersten Datensatz zurückspringt wird in Zeile 9 die aktuelle Zeile der Datenmenge
ausgelesen. Nach dem Reload in Zeile 10 wird dann das Formular auf den letzten Datensatz
gesetzt (Zeile 11) um anschließend auf den aktuellen Datensatz zurückzuspringen (Zeile 12).
Das hat den Vorteil, dass der aktuelle Datensatz immer die erste Zeile im Tabellenkontrollfeld
ist und nebenbei auch noch die Angabe der maximal verfügbaren Datensätze in der Navigationsleiste des Tabellenkontrollfeldes genau stimmig gehalten wird.

NeueBuchungAufruf aus
Formular: Haus

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010

BuchungsAenderung

SUB NeueBuchung
BuchungsAenderung
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECT
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("BuchungLoeschen")oForm.reload
END SUB

Ist eine neue Buchung erfolgt, so wird zuerst die Buchungsänderung an die Übersicht im unteren Tabellenkontrollfeld weiter gegeben. Zusätzlich muss dann noch der Datensatz in den möglicherweise löschbaren Datensätzen erscheinen. Deswegen wird das Formular «BuchungLoeschen» ebenfalls neu geladen.

Terminübersicht Ferienhäuser

124

DatumsAenderung
Aufruf aus
Formular: Haus
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028

Enddatum

SUB DatumsAenderung(oEvent AS OBJECT)
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oDatField AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECTDIM oResult AS OBJECT
DIM iRow AS INTEGER
DIM stDatum AS STRING
DIM stSql AS STRING
oDatField = oEvent.Source.Model
stDatum = oDatField.CurrentValue.Year & "-" &Right("0" & oDatField.CurrentValue.Month , 2) & "-" &Right("0" & oDatField.CurrentValue.Day , 2)
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("Uebersicht")oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
stSql = oForm.SingleSelectQueryComposer.QueryoResult = oSQL_Statement.executeQuery(stSql)
Do
oResult.next
iRow = iRow + 1
IF oResult.isLast THEN Exit Do
LOOP UNTIL stDatum = oResult.getString(2)oForm.last
oForm.absolute(iRow)
END SUB

Bei einer Datumsauswahl des Startdatums soll die Übersicht auf das aktuelle Datum so eingestellt werden, dass es am besten als erste Feld in der Übersicht erscheint.
Zuerst wird das Datumsfeld ermittelt und das darin enthaltene Datum in SQL-Schreibweise ausgelesen (Zeile 12 und 13). Anschließend wird das Formular «Übersicht» angesteuert. Aus dem
Formular wird die Datenbankverbindung ausgelesen (Zeile 17).
In dem Objekt des Formulars ist nicht nur das eigentliche SQL-Kommando enthalten, das beimStart eines Formulars den Inhalt bestimmt. Es kann ja passieren, dass die Daten mit einem Filter gefiltert werden oder sortiert werden. Der genaue Befehl hierfür liegt etwas versteckt inoForm.SingleSelectQueryComposer.Query (Zeile 19). Nur über diesen aktuellen SQL-Befehl
lässt sich genau die Datenzeile ermitteln, die für das gewünschte Datum anzusteuern ist. Dieser Composer steht nur zur Verfügung, wenn die Abfrage nicht als direkte SQL-Abfrage gestellt
wird. Direkte SQL-Abfragen können nicht richtig gefiltert und sortiert werden.
Die Abfrage, die auch den Inhalt des Tabellenkontrollfeldes bestimmt, wird hier noch einmal
genutzt. In Zeile 20 wird die Abfrage gestellt. Von Zeile 21 bis 25 läuft eine Schleife so langeab, bis das Datum aus der Abfrage dem Datum entspricht, das aus dem Datumsfeld in Zeile 13
ausgelesen wurde. Sobald also die Abfrage eine Ergebniszeile liefert (Zeile 22), wird die Zeilenanzahl um 1 erhöht (Zeile 23). Sollte die letzte Zeile erreicht sein ohne dass das Datum gefunden wurde, so muss aus der Schleife herausgesprungen werden (Zeile 24). Dies kann z.B. bei
einer Filterung der Daten passieren, wenn z.B. der Filter nur auf den Samstag eingestellt ist,aber das Datum an einem Freitag liegt.

Terminübersicht Ferienhäuser

125

Die genaue Zeilennummer ist jetzt ermittelt. Es wird in Zeile 26 zum letzten Datensatz des Formulars gesprungen und in Zeile 27 dann wieder zurück zur ermittelten Zeilennummer. So
erscheint die Zeile schließlich als erste in der Übersicht des Tabellenkontrollfeldes.

Terminübersicht Ferienhäuser

126

Leider wird für Firebird isLast (Zeile 24) nicht unterstützt. Deshalb muss der Code zum Schluss
etwas anders gestaltet werden:
020
021
022
023
024
025
026
027
028
029

oResult = oSQL_Statement.executeQuery(stSql)
oForm.lastiLastRow = oForm.getRowDo
oResult.next
iRow = iRow + 1
IF iRow = iLastRow THEN Exit Do
LOOP UNTIL stDatum = oResult.getString(2)oForm.absolute(iRow)
END SUB

Hier wird jetzt für Firebird der Sprung ans Ende des Formular vorgezogen und die letzte Datensatznummer ausgelesen (Zeile 21 und 22). Sind die aktuell ermittelte Datensatznummer unddie letzte Datensatznummer gleich, so wird die anschließende Schleife verlassen. Ansonsten
springt das Formular zur aktuell ermittelten Datensatznummer (Zeile 28).

Terminübersicht Ferienhäuser

127

Mailaufruf

Mailaufruf

128

Einführung
In Foren kam immer wieder der Wunsch auf, von einer Datenbank aus die in einer Adressverwaltung gespeicherten Mailadressen auch direkt zum Mailversand nutzen zu können.

Die Datenbank «Mailaufruf»6 erlaubt sowohl den Direktstart des Mailprogramms mit vorgegebener Mailadresse als auch den Direktaufruf einer vorgegebenen Website über den Browser.
Zum Versand kompletter Mails wird in dieser Datenbank immer das Mailprogramm genutzt, dasüber das Betriebssystem zur Verfügung gestellt wird. In der Datenbank kann aber der gesamte
Mailverkehr abgelegt werden. Nur die letzte Kontrolle, ob denn nun die E-Mail tatsächlich vom
Mailprogramm abgeschickt wurde, ist von Base heraus nicht möglich.
Diese Datenbank funktioniert mit der HSQLDB und mit der internen FIREBIRD gleich. Dies liegt an
der einfachen Struktur und daran, dass nur in den Makros einfache Abfragen erfolgen, die sichvom Code her in den Datenbanken nicht unterscheiden.

Tabellen
Die Datenbank besteht aus vier Tabellen.

Kontakte
Datenziel
Formular: Kontakte_Mail_Web, Mail_mit_Anhang
Makro:

Mail_Aufruf, Mail_Attachment

Abfrage, Bericht, Makro: keine

6

Beispieldatenbank Beispiel_Mailaufruf.odb

Mailaufruf

129

FeldnameFeldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann auch als Auto-Wert-Feld gesetzt
werden.

Name

Text

Der Name der Person, an die die Mail verschickt werden soll
und zu der auch die Webadresse gehört.

E_MailText

Die E-Mail-Adresse der Person

Web

Text

Falls die Person eine Website führt, so kann z.B. auch aus
Base heraus die Website aufgerufen werden.
Eingabe erforderlich: Nein

Diese Tabelle könnte natürlich für die Nutzung von Serienbriefen entsprechend geändert underweitert werden. So fehlt hier die Möglichkeit der Eingabe des Geschlechts, das dann eine entsprechende Anrede ermöglicht. Vorname und Nachname werden außerdem nicht getrennt undeine Adresse ist auch nicht vorgesehen. Würden diese Felder mit aufgenommen, so ließe sichauch von Base heraus ein Serienbrief starten, der sowohl einen personalisierten Inhalt im Mailtext als auch im Mailanhang hat.

Mails
Datenziel
Formular: Kontakte_Mail_Web, Mail_mit_Anhang
Abfrage, Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld sollte als Auto-Wert-Feld gesetzt werden.

Betreff

Text

Betreff der zu versendenden E-Mail

InhaltText

Der Inhalt der Mail kann viel Platz einnehmen. Die Standardeinstellung des Feldes mit 100 Zeichen dürfte also in den
wenigsten Fällen ausreichen. In dem Beispiel ist dies auf
1000 Zeichen erweitert worden

Kontakte_ID

Integer

Falls die Person eine Website führt, so kann z.B. auch aus
Base heraus die Website aufgerufen werden.

Mail_CC
Datenziel
Formular: Kontakte_Mail_Web, Mail_mit_Anhang
Makro:

Mail_Aufruf, Mail_Attachment

Abfrage, Bericht: keine

Mailaufruf

130

FeldnameFeldtyp

Beschreibung

Mails_ID

Integer

Die Mails_ID wird als Fremdschlüssel aus der Tabelle "Mails"
bezogen. Dadurch können mehrere Personen eine Mailkopie
erhalten.

Kontakte_ID

Integer

Die Kontakte_ID dienst dazu, die Mailadresse von anderen
Personen aus der Tabelle "Kontakte" zu ermitteln.
Mails_ID und Kontakte_ID bilden zusammen den Primärschlüssel der Tabelle "Mail_CC"

BCC

Ja/NeinMuss nur angeklickt werden, wenn die Person eine Blindkopie
erhalten soll.

Anhaenge
Datenziel
Formular: Mail_mit_Anhang
Makro:

Mail_Attachment

Abfrage, Bericht: keine

Feldname

Feldtyp

Beschreibung

Mails_ID

Integer

Die Mails_ID wird als Fremdschlüssel aus der Tabelle "Mails"
bezogen. Dadurch können mehrere Anhänge erzeugt werden

Anhang

Text

Hier wird der Pfad zum Anhang als relativer Pfad gespeichert.
Mails_ID und Anhang bilden zusammen den Primräschlüssel
der Tabelle "Anhaenge".

Alle vier Tabellen werden in den Beziehungen noch miteinander verbunden.

Wechselt eine Person im Laufe der Zeit die E-Mail-Adresse, so hat das keinen weiteren Einfluss
auf die gespeicherten E-Mails. Nur der Inhalt sowie die Verbindung zu entsprechenden Personen wird gespeichert. Einmal überschriebene Mailadressen erscheinen nicht mehr in derDatenbank, sondern nur noch in der Liste der versandten Mails des Mailprogramms.
Einem Kontakt können beliebig viele Mails zugeordnet werden. Das Verhältnis von "Kontakte"
zu "Mails" ist ein Verhältnis 1:n.
Einer Mail können beliebig viele Kopieempfänger zugeordnet werden. Das Verhältnis von"Mails" zu "Mail_CC" ist 1:n.
Ein Kontakt kann auch mehrmals als Kopieempfänger zugeordnet werden. Das Verhältnis von"Kontakte" zu "Mail_CC" ist 1:n.
Ausgeschlossen ist aber, dass eine Person mehrere Kopien der gleichen Mail erhält. "Mails_ID"
und "Kontakte_ID" sind zusammen Primärschlüssel und müssen einzigartig sein.

Mailaufruf

131

Ansichten
Diese Datenbank enthält keine Ansichten.

Abfragen
Die Datenbank enthält keine Abfragen.

Formulare
Kontakte_Mail_Web

1

Formular (Tabelle: Kontakte)

1.1

Unterformular (Tabelle:
Mails)

1.1.1

FormMailsCC (Tabelle:
Mail_CC)

Makros in Feldeigenschaften
1

Feld «Web» (Maus innerhalb)

Module1.Mauszeiger

1

Feld «Web» (Maustaste gedrückt)Module1.Website_Aufruf

Mailaufruf

132

1

Feld «E-Mail» (Maus innerhalb)

Module1.Mauszeiger

1

Feld «E-Mail» (Maustaste gedrückt)Module1.Website_Aufruf

1.1

Button E-Mail an Mailprogramm übergeben (Aktion ausführen)

Module1.Mail_Aufruf

Im Hauptformular wird der Kontakt ausgesucht oder ein neuer Kontakt eingegeben. Die Eingabe der Mailadresse und gegebenenfalls einer Website erscheinen in blauer Farbe und unterstrichen. Wird die Maus auf diese Elemente bewegt, so ändert sich der Mauszeiger. Mit einem
Klick wird direkt das Mailprogramm mit dieser Mailadresse oder der Browser mit dieser Webadresse gestartet.
Im Unterformular werden Mails an die im Hauptformular ausgesuchte Person verfasst. In diesem Formular ist nur die Eingabe des Betreffs und des Mailinhaltes möglich, da das Makro, mit
dem der Mailversand gestartet wird, nicht bei jedem Mailprogramm sicher das Versenden vonAnhängen erlaubt. Der Parameter «attachment» ist beim Versenden über die Kommandozeile
nicht einheitlich anerkannt.
Soll die so verfasste Mail auch noch an weitere Personen gehen, so können deren Mailadressen
im Unter-Unterformular «FormMailsCC» ausgesucht werden. Blindkopien, die für die anderen
Empfänger nicht sichtbar sind, können hier über ein Ja/Nein-Feld gekennzeichnet werden.
Der Inhalt des Formulars «FormMailsCC» wird bei Betätigung des Buttons in «Unterformular»abgespeichert. Dies liegt daran, dass ein Verlassen eines Formulars auf einer gemeinsamen
Formularoberfläche mit anderen Formularen so eine Speicherung automatisch ausführt.

Mailaufruf

133

Mail_mit_Anhang

1

Formular (Tabelle: Kontakte)

1.1

Unterformular (Tabelle:
Mails)

1.1.1

FormMailsCC(Tabelle: Mail_CC)

1.1.2

FormAnhaenge(Tabelle: Anhaenge)

Makros in Feldeigenschaften
1

Feld «Web» (Maus innerhalb)

Module1.Mauszeiger

1

Feld «Web» (Maustaste gedrückt)Module1.Website_Aufruf

1

Feld «E-Mail» (Maus innerhalb)

Module1.Mauszeiger

1

Feld «E-Mail» (Maustaste gedrückt)Module1.Website_Aufruf

1.1

Button E-Mail an Mailprogramm übergeben (Aktion ausführen)

Module1.Mail_Attachment

1.1.2

Tabellenkontrollfeld «Anhänge» (Text modifiziert)

Module1.Attachment

Dieses Formular erlaubt zusätzlich zu dem Formular «Kontakte_Mail_Web» die Auswahl und
Linkspeicherung von Anhängen. Die Links werden über das grafische Kontrollfeld ausgesucht.
Zum einen erlaubt dieses Kontrollfeld seit LO 5.0 die Abspeicherung von Links nicht nur zu grafischen Dateien, sondern auch zu allen anderen Dateien. Zum anderen müsste ein Dateiaus-

Mailaufruf

134wahlfeld erst umständlich mit Hilfe von Makros an die Datenbank angebunden werden, da es
nicht mit einem Datenfeld verbunden werden kann. Letztlich hat dann das grafische Kontrollfeld auch noch den Vorteil, dass es eben Bilddateien, die sich im Anhang befinden, direkt anzeigen kann.
Mit jedem neu ausgesuchten Element für den Anhang wird ein neuer Datensatz hinzugefügt.
Soll ein Anhang wieder entfernt werden, so muss das unter dem grafischen Kontrollfeld befindliche Tabellen-Kontrollfeld aufgesucht werden. Über einen rechten Mausklick auf den Zeilenkopf
steht hier das Löschen des jeweiligen Inhaltes zur Verfügung.

HinweisFür die einwandfreie Funktion dieses Formulars ist es unter Linux notwendig, unter
Extras → Optionen → Internet → E-Mail das E-Mail-Programm auszuwählen. Sonst gibt es
Probleme mit Kommas und Zeilenumbrüchen.

Berichte
Die Datenbank enthält keine Berichte.

MakrosMauszeiger
Aufruf aus
Formular: Kontakte_Mail_Web, Mail_mit_Anhang
001
002
003
004
005
006

SUB Mauszeiger(oEvent AS OBJECT)
DIM oPointer AS OBJECT
oPointer = createUnoService("com.sun.star.awt.Pointer")oPointer.setType(28)oEvent.Source.Peer.SetPointer(oPointer)
END SUB

Der Mauszeiger wird über den entsprechenden UnoService beeinflusst. Die Typen für den
Mauszeiger sind in com.sun.star.awt.SystemPointer verzeichnet. Die 28 verweist hier auf
das Aussehen des Zeigers als Hand mit einem zeigenden Finger. Dieses Makro wird dann ausgelöst, wenn sich die Maus innerhalb des Formularfeldes befindet: Textfeld → Eigenschaften →Ereignisse → Maus innerhalb. Da das Ereignis direkt von dem Formularfeld heraus gesteuert
wird (und nicht durch einen Button von außerhalb) muss hier nicht erst das Formularfeld aufgesucht werden. Das Formularfeld ist Urheber des Ereignisses, oEvent.Source. Darüber wird
schließlich beim Formularfeld der Mauszeigen neu eingestellt (Zeile 5).

FormSave
Aufruf aus
Makro:
001
002
003
004
005
006

Mail_Aufruf, Attachment, Mail_Attachment

SUB FormSave(oForm AS OBJECT)
IF oForm.IsModified THEN
IF oForm.isNew THEN
oForm.InsertRow
ELSEoForm.UpdateRow

Mailaufruf

135

007
008
009

END IF
END IF
END SUB

Das Speichern eines Formulars funktioniert intern über zwei unterschiedliche Befehle. Ist derInhalt des Formulars neu (Zeile 4), dann muss ein Insert erfolgen. Existierte der Datensatz
schon und wurde nur geändert, so muss ein Update erfolgen (Zeile 6). Diese kleine Prozedur
wurde ausgelagert, da sie gleich in mehreren anderen Prozeduren sonst wiedrholt hätte
geschrieben werden müssen.

Website_Aufruf
Aufruf aus
Formular: Kontakte_Mail_Web, Mail_mit_Anhang
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020

SUB Website_Aufruf(oEvent AS OBJECT)
DIM oFeld AS OBJECTDIM oShell AS OBJECT
DIM stFeld AS STRING
oFeld = oEvent.Source.Model
stFeld = oFeld.Text
IF stFeld = "" THEN
EXIT SUBEND IFIF InStr(stFeld,"@") THEN
stFeld = "mailto:"+stFeld
ELSEIF InStr(stFeld,"http://") OR InStr(stFeld,"https://") THEN
stFeld = convertToUrl(stFeld)
ELSE
stFeld = "http://"+stFeld
stFeld = convertToUrl(stFeld)
END IFoShell = createUnoService("com.sun.star.system.SystemShellExecute")
oShell.execute(stFeld,,0)
END SUB

Mit dieser Prozedur wird eine Mailadresse als Empfängeradresse im E-Mail-Programm der grafischen Benutzeroberfläche des Betriebssystems oder eine Website im Browser geöffnet. Die
Art, eine Datei über das damit verknüpfte Programm zu öffnen, lässt sich auf alle möglichen
Dateitypen übertragen, für die eben ein Programm zur Verfügung steht.
Ausgangspunkt ist direkt das Feld, in dem der Klick mit der Maus erfolgt (Zeile 5). Der Text, den
das Feld anzeigt, kann ausgelesen werden (Zeile 6). Enthält das Feld keinen Text, so braucht
auch kein Browser oder Mailprogramm gestartet zu werden (Zeile 7 bis 9). Die Prozedur wird
also bei einem leeren Feld abgebrochen.
Enthält der Text ein «@», so handelt es sich um eine E-Mail-Adresse. Die Gültigkeit der Adresse
wird hier nicht weiter überprüft. Für eine E-Mail-Adresse startet der Aufruf des Mailprogramms
mit mailto: (Zeile 10 und 11).
Beginnt der Text mit http:// oder https://, so handelt es sich um eine vollständige Internetadresse (Zeile 12). Solch eine Adresse kann direkt mit convertToUrl in eine Schreibweiseumgewandelt werden, die auch URL-konform ist. Das sonst übliche file:/// wird durch die
Funktion nicht vor die Adresse gesetzt.
Ist in dem Text weder @ noch http:// enthalten, so wird hier davon ausgegangen, dass es sichum eine Internetadresse ohne den entsprechenden Vorspann handelt (Zeile 15 und 16). Entsprechend wird die Adresse mit einem http:// ergänzt. Würde dies nicht erfolgen, so würde
die Adresse wie ein Dateipfad auf dem eigenen Rechner interpretiert. Bei einem tatsächlich
existierenden Pfad zu z. B. einer *.png-Datei würde dann ein Bildbetrachtungsprogramm geöffnet.

Mailaufruf

136

Auch für die Internetadressen gilt, dass sie innerhalb des Makros nicht auf Gültigkeit überprüft
werden. Es wird in Zeile 19 lediglich die entsprechende Adresse über die Shell an die Benutzeroberfläche weiter gegeben, die dann ein dafür entsprechendes Programm sucht und öffnet.

Mail_Aufruf
Aufruf aus
Formular: Kontakte_Mail_Web, Mail_mit_Anhang

Benötigt
Tabelle:Kontakte, Mail_CC

Makro:FormSave

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047

SUB Mail_Aufruf
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld1 AS OBJECTDIM oFeld2 AS OBJECTDIM oFeld3 AS OBJECTDIM oFeld4 AS OBJECTDIM oShell AS OBJECTDIM oColumns AS OBJECTDIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oAbfrageergebnis AS OBJECT
DIM inIndex AS INTEGER
DIM i AS INTEGERDIM loMails_ID AS LONG
DIM stFeld1 AS STRING
DIM stFeld2 AS STRING
DIM stFeld3 AS STRING
DIM stFeld4 AS STRING
DIM stSql AS STRING
DIM stMail_CC AS STRING
DIM stMail_BCC AS STRING
DIM st_CC AS STRING
DIM st_BCC AS STRING
DIM arSo_Zeichen()
DIM arSo_Ersatz()
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("Formular")oFeld1 = oForm.getByName("E_Mail")oSubForm = oForm.getByName("UnterFormular")oFeld2 = oSubForm.getByName("Betreff")
oFeld3 = oSubForm.getByName("Inhalt")
stFeld1 = oFeld1.Text
IF stFeld1 = "" THEN
msgbox "Keine Mailadresse vorhanden." & CHR(13) &"Das Mailprogramm wird nicht aufgerufen" , 48, "Mail senden"
EXIT SUBEND IF
FormSave(oSubForm)oColumns = oSubForm.Columns
inIndex = oSubForm.findColumn("ID")loMails_ID = oSubForm.getLong(inIndex)
stFeld2 = Mid(ConvertToUrl(oFeld2.Text),9)
stFeld3 = Mid(ConvertToUrl(oFeld3.Text),9)
arSo_Zeichen = Array("\","^","~","[","]"," ","&","+",",",";",":","=","?","@")

Mailaufruf

137

048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084

arSo_Ersatz = Array("5C","5E","7E","5B","5D","20","26","2B","2C","3B","3A","3D","3F","40")
FOR i = LBound(arSo_Zeichen()) TO UBound(arSo_Zeichen())
stFeld3 = Join(Split(stFeld3,arSo_Zeichen(i)),"%" & arSo_Ersatz(i))
NEXT
stMail_CC = ""stMail_BCC = ""st_CC = ""st_BCC = ""oDatenquelle = ThisComponent.Parent.CurrentController
IF NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Kontakte"".""E_Mail"", ""Mail_CC"".""BCC"" FROM ""Mail_CC"",""Kontakte"" WHERE ""Mail_CC"".""Kontakte_ID"" = ""Kontakte"".""ID"" AND""Mail_CC"".""Mails_ID"" = '"+ loMails_ID +"'"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
IF oAbfrageergebnis.getBoolean(2) = TRUE THEN
stMail_BCC = stMail_BCC + oAbfrageergebnis.getString(1) + ","ELSE
stMail_CC = stMail_CC + oAbfrageergebnis.getString(1) + ","END IFWENDIF stMail_BCC <> "" THEN
st_BCC = Left(stMail_BCC,Len(stMail_BCC)-1)
stMail_BCC = "&bcc="+st_BCC
st_BCC = ",bcc='"+st_BCC+"'"END IFIF stMail_CC <> "" THEN
st_CC = Left(stMail_CC,Len(stMail_CC)-1)
stMail_CC = "&cc="+st_CC
st_CC = ",cc='"+st_CC+"'"END IFoShell = createUnoService("com.sun.star.system.SystemShellExecute")
oShell.execute("mailto:"+stFeld1+"?subject="+stFeld2+"&body="+stFeld3+stMail_CC+stMail_BCC,,0)' Shell("thunderbird -compose ""to="+stFeld1+st_CC+st_BCC+",subject="+stFeld2+",body="+stFeld3+",attachment='file:///home/user/Testdatei.pdf'""")
END SUB

Über den Druck auf den Button E-Mail an Mailprogramm übergeben werden Adresse, Betreff,Inhalt, Kopieadressen und Blindkopieadressen an das Mailprogramm weiter gereicht. Der Mailaufruf erfolgt mit mailto:Empfänger?subject=...&body=...&cc=...&bcc=... (Zeile 83).
Anhänge sind laut Definition von mailto nicht definiert. Manchmal funktioniert allerdings trotzdem attachment=... . Bei dem Mailprogramm Thunderbird ist attachment=... aus Sicherheitsgründen vom Betrieb mit mailto ausgenommen. Am Beispiel von Thunderbird wird nochgezeigt, wie auch der Versand mit Mailanhängen auf diesem Weg gelingen kann (Zeile 84). Das
Formular müsste dazu aber sinnvollerweise noch mindestens mit einem Dateiauswahlfeld
ergänzt werden, da vermutlich nicht jede Person den gleichlautenden Mailanhang erhalten soll.
Das Anhangsproblem ist mit dem Formular Mail_mit_Anhang und der entsprechenden Prozedur
Mail_Attachment gelöst worden.
Nachdem alle Variablen deklariert sind wird der Pfad zu den verschiedenen Feldern geklärt (Zeilen 29 bis 35). Enthält der Datensatz aus der Tabelle "Kontakte" keinen Eintrag (E-MailAdresse), so wird die Prozedur direkt abgebrochen (Zeile 36 bis 39).
Anschließend wird das «UnterFormular», dem die Tabelle "Mails" zugrunde liegt, noch einmalabgespeichert (Zeile 41) und das Feld "ID" aufgesucht. Ein über oSubForm.Columns angesprochenes Feld muss dafür nicht in dem Formular als Feld hinterlegt sein. Es ist in der Datenquelle
des Formulars enthalten und kann deshalb für den aktuellen Datensatz ausgelesen werden(Zeile 44).

Mailaufruf

138

Der Text im «Betreff» und im «Inhalt» muss in einen URL-konformen Text umgewandelt, dasonst Sonderzeichen und Zeilenumbrüche nicht übernommen werden. Der Eintrag file:///wird dabei automatisch hinzugefügt und muss wieder entfernt werden. Der eigentliche Text
beginnt also ab dem 9. Zeichen des URL-konformen Textes (Zeile 45 und 46).
Leider greift die Konvertierung zur URL bei einigen Sonderzeichen nicht. Nach Kommas bricht
so z.B. der Text ab. Deswegen muss der bereits über die Funktion ConverToUrl bearbeitete
Text noch einmal auf verschiedene Zeichen durchsucht werden. Über zwei Arrays werden hiernoch diverse Sonderzeichen zusätzlich getestet und ersetzt (Zeile 47 und 48). Das erste Array
enthält solche Sonderzeichen, das zweite Array an genau der gleichen Arrayposition die URLkonforme Schreibweise im Hexadezimalcode. Der Text für den Mailinhalt wird hier an jeder
Stelle, an der ein Zeichen aus arSo_Zeichen vorkommt, in Einzelteile zerschnitten. Anschließend wird das Ergebnis wieder mit dem entsprechenden Zeichen aus arSo_Ersatz zusammengefügt.
Die Variablen für das CC(Carbon Copy - Kopie) bzw. BCC (Blind Carbon Copy - Blindkopie) werden als leere Textvariablen vordefiniert. Die Variablen st_CC und st_BCC sind hier nur eingefügt
worden, da die Behandlung dieser Felder direkt über den Shellaufruf von Thunderbird (Zeile 83)
anders funktioniert als mittels mailto.
Über eine Abfrage werden die Mailadressen ermittelt, an die ein Kopie bzw. Blindkopie weitergereicht werden soll (Zeile 62 bis 70). Eine Abfrage ist hier notwendig, da über das Formular
nur auf genau einen Datensatz zugegriffen werden kann. Anschließend werden die ausgelesenen Werte für das abschließende Kommando zusammengestellt, mit dem das Mailprogramm
gestartet werden soll.
Die Variablen stMail_CC sowie stMail_BCC sind nicht immer mit Inhalt belegt. Durch die in dervorhergehenden Zeile erfolgte Formulierung (Zeile 71 und 76) wird vermieden, dass ein«&cc=» ohne irgendeinen Inhalt in der Ausführung erscheint.
Für Thunderbird wird in Zeile 83 noch gezeigt, wie der Aufruf mit Parametern erfolgen kann.
Dieser Aufruf ermöglicht es schließlich auch zusätzlich noch Anhänge an das Mailprogramm
weiter zu geben. Der Aufruf von Thunderbird in den verschiedenen Betriebssystemen ist unterschiedlich. Hier wird nur der funktionierende Aufruf aus einer Linux-Umgebung gezeigt. Details
zu dem Aufruf mit Parametern siehe auch: http://www.thunderbird-mail.de/wiki/Aufrufparameter_von_Thunderbird
Statt des UnoServices wird hier direkt mit dem Kommando Shell() die Kommandozeile desSystems angesprochen. Die angehängte Datei ist hier direkt im Code enthalten und müsste bei
Nutzung dieser Funktion natürlich über das Formular ermittelt werden. Für nur einen Anhangreicht ein Feld in der Tabelle "Mails", für mehrere Anhänge müsste aber ähnlich wie bei derTabelle "Mail_CC" eine Tabelle "Anhang" erstellt werden. Erst so lassen sich mehrere Anhänge
mit einer Mail verschicken.
Der Shell-Befehl ist in dem der Beispieldatenbank beigefügten Makro natürlich als Kommentar
geschrieben, so dass das Makro mit jedem in dem Betriebssystem verankerten Mailprogramm
ohne Fehlermeldungen zu Ende laufen müsste.

Attachment
Aufruf aus
Formular: Mail_mit_Anhang

Benötigt
Makro:001
002
003

FormSave

SUB Attachment(oEvent AS OBJECT)
DIM oForm AS OBJECT
oForm = oEvent.Source.Model.Parent.Parent

Mailaufruf

139

004
005

FormSave(oForm)END SUBDie Anhänge werden über ein grafisches Kontrollfeld eingefügt. Eine Änderung in dem zugehörogen Tabellen-Kontrollfeld erzeugt über dieses Makro immer eine Änderung des aktuellen
Datensatzes. Dass der Pfad innerhalb eines Tabellenkontrollfeldes liegt zeigt auch die Beziehung zwischen auslösendem Feld und Formular in Zeile 3: Hier wird nicht nur das direkte
Elternelement (Parent) aufgesucht. Das direkte Elternelement für das Textfeld in dem TabellenKontrollfeld ist nämlich das Tabellen-Kontrollfeld. Erst das Elternelenment des Tabellen-Kontrollfeldes ist das Formular, in dem dieses Feld liegt.

Mail_Attachment
Aufruf aus
Formular: Mail_mit_Anhang

Benötigt
Tabelle:Kontakte, Mail_CC, Anhaenge

Makro:FormSave

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041

SUB Mail_Attachment
DIM arAttach(0)DIM arCC(0)DIM arBCC(0)DIM arURL_Start()
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oSubForm AS OBJECTDIM oFeld1 AS OBJECTDIM oFeld2 AS OBJECTDIM oFeld3 AS OBJECTDIM oColumns AS OBJECTDIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oAbfrageergebnis AS OBJECTDIM oMailProgram AS OBJECTDIM oNewMessage AS OBJECT
DIM inIndex AS INTEGER
DIM i AS INTEGERDIM k AS INTEGERDIM loMails_ID AS LONG
DIM stFeld1 AS STRING
DIM stFeld2 AS STRING
DIM stFeld3 AS STRING
DIM stSql AS STRING
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("Formular")oFeld1 = oForm.getByName("E_Mail")oSubForm = oForm.getByName("UnterFormular")oFeld2 = oSubForm.getByName("Betreff")
oFeld3 = oSubForm.getByName("Inhalt")
stFeld1 = oFeld1.Text
IF stFeld1 = "" THEN
msgbox "Keine Mailadresse vorhanden." & CHR(13) & "Das Mailprogramm wird
nicht aufgerufen" , 48, "Mail senden"
EXIT SUBEND IF
FormSave(oForm)stFeld2 = oFeld2.Text

Mailaufruf

140

042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096

stFeld3 = oFeld3.Text
oColumns = oSubForm.Columns
inIndex = oSubForm.findColumn("ID")loMails_ID = oSubForm.getLong(inIndex)
oDatenquelle = ThisComponent.Parent.CurrentController
IF NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Kontakte"".""E_Mail"", ""Mail_CC"".""BCC"" FROM ""Mail_CC"",""Kontakte"" WHERE ""Mail_CC"".""Kontakte_ID"" = ""Kontakte"".""ID"" AND""Mail_CC"".""Mails_ID"" = '"+ loMails_ID +"'"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
i = 0
k = 0
WHILE oAbfrageergebnis.next
IF oAbfrageergebnis.getBoolean(2) = TRUE THEN
IF i > 0 THEN
ReDim Preserve arBCC(i)END IF
arBCC(i) = oAbfrageergebnis.getString(1)i = i + 1
ELSE
IF k > 0 THEN
ReDim Preserve arCC(k)END IF
arCC(k) = oAbfrageergebnis.getString(1)k = k + 1
END IF
WEND
stSql = "SELECT ""Anhang"" FROM ""Anhaenge"" WHERE ""Mails_ID"" ='"+ loMails_ID +"'"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
arUrl_Start = split(oDoc.Parent.Url,oDoc.Parent.Title)
i = 0
WHILE oAbfrageergebnis.next
IF i > 0 THEN
ReDim Preserve arAttach(i)END IF
arAttach(i) = ConvertToUrl(arUrl_Start(0) & oAbfrageergebnis.getString(1))
i = i + 1
WEND
IF GetGuiType() = 1 THEN
oMailer = createUnoService("com.sun.star.system.SimpleSystemMail")' Sonst Linux/Mac
ELSE
oMailer = createUnoService("com.sun.star.system.SimpleCommandMail")
END IFoMailProgram = oMailer.querySimpleMailClient()oNewMessage = oMailProgram.createSimpleMailMessage()oNewMessage.Recipient = stFeld1
oNewMessage.CcRecipient = arCC()
oNewMessage.BccRecipient = arBCC()
oNewMessage.Subject = stFeld2
oNewMessage.Body = stFeld3
oNewMessage.setAttachement(arAttach())
oMailProgram.sendSimpleMailMessage(oNewMessage, 0 )END SUB

Diese Prozedur hat erst einmal sehr viele Ähnlichkeiten mit der Prozedur Mail_Aufruf. Zu Beginn
unterscheiden sich die Variablen lediglich dadurch, dass die Attachments, CCs und BCCs in
Arrays zusammengefasst werden (Zeile 2 bis 4).

Mailaufruf

141

Unter Linux funktioniert diese Prozedur nur dann einwandfrei, wenn in Extras → Optionen →Internet → E-Mail ein Mailprogramm ausgesucht wird. Ohne Angabe des Mailprogramm bereiten
Kommas und Zeilenumbrüche Probleme.
Nach dem Auslesen der Inhalte aus den Formularfeldern erfolgt durch den SQL-Code in Zeile 52
das Auslesen der Einträge für das CC bzw. das BCC. In einer Schleife von Zeile 56 bis 70 wird
wieder zwischen BCC und CC unterschieden. Beim ersten Eintrag wird der Wert direkt in das
passende Array geschrieben (Zeile 61 bzw. Zeile 67). Der Zähler für das jeweilige Array wird
um 1 erhöht. Muss ein neuer Wert eingetragen werden, so muss erst einmal das Array neu
dimensioniert werden (Zeile 59 und 65). Diese Dimenionierung erfolgt so, dass der alte Inhalt
erhalten bleibt. Erst nach der Dimensionierung kann auch ein weiterer Wert in das, jetzt größere, Array eingefügt werden.
Anschließend erfolgt ein ähnliches Vorgehen für die Liste der Anhänge (Zeile 71 bis 81). Hiermuss lediglich zusätzlich beachtet werden, dass die Abspeicherung in der Base-Datei mit relativen Pfaden erfolgt. Das Mailprogramm braucht aber zum Auffinden des Anhangs den absoluten
Pfad. So wird in Zeile 73 über Split der absolute Pfad der Basedatei in den Pfad zur Datei undden Dateinamen aufgeteilt. Der erste Teil dieses Arrays wird anschließend vor die jeweiligen
relativen Pfade zu den Anlagen geschrieben (Zeile 79).
In Abhängigkeit vom Betriebssystem (Zeile 82) wird entweder SimpleSystemMail (Windows)
oder SimpleCommandMail (Linux, Mac) für die Zusammenstellung der Mail genutzt.
Über Zeile 87 wird die Verbindung zum Mailprogramm hergestellt. Zeile 88 liefert dann den
Code für die Mail, die versandt werden soll. Zu beachten ist hier, dass eben die Einträge zu CC,
BCC und Attachment jeweils Arrays sind. Der Inhalt für die eigentliche Mail wird in Body (Zeile93) weitergegeben. Diese Variable ist erst seit LO 4.2 enthalten und hat nicht, wie die anderen
Variablen, auch noch den Zugang z.B. über setSubject oder setAttachment. Zu den möglichen
Parametern siehe: https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1system_1_1XSimpleMailMessage.html.
In Zeile 95 wird die Mail schließlich an das Mailprogramm zur Absendung übergeben. Zu denmöglichen Absendeparametern siehe https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1system_1_1XSimpleMailClient.html.
Es ist möglich, diese Mail auch unter Umgehung des Mailprogramms zu senden. Die Parameterzu sendSimpleMailMessage: DEFAULTS = 0; NO_USER_INTERFACE = 1;
NO_LOGON_DIALOG = 2
Ein reibungsloser Ablauf bedingt bei 1: Der Empfänger muss angegeben sein
Ein reibungsloser Ablauf bedingt bei 2: Es darf kein Dialog zum Einloggen erforderlich sein
Woher bei 1 und 2 die Zugangsdaten, das Passwort und eventuell eine Verschlüsselung kommen sollen bleibt unklar.

Mailaufruf

142

Suchen und FilternSuchen und Filtern

143Einführung
Base selbst bietet sowohl eine Suchfunktion als auch eine Möglichkeit, Daten zu filtern.
Beim Suchen wird schrittweise der gesamte Datenbestand einer Tabelle durchlaufen und die
Treffer nacheinander markiert. Die Anzahl der Ergebnisdatensätze wird nicht auf Ergebnisse
eingeschränkt, auf die das Suchkriterium zutrifft.
Beim Filtern wird durch einen oder mehrere Filterbegriffe direkt die Ergebnismenge aus derTabelle heraus beeinflusst. Taucht das gewünschte Kriterium nur bei 3 von 100 Datensätzen
auf, so werden eben nur 3 Datensätze angezeigt. Die Suchfunktion hingegen würde die 100
Datensätze durchlaufen und die einzelnen Treffer nacheinander markieren.
Die eingebaute Suchfunktion ist allerdings recht langsam, die Datenfilterung erfordert jeweils
Einstellungen unterschiedlicher Werte und Zuordnungen. Die Datenfilterung ist sehr gut universell nutzbar, sofern sich sämtliche zu durchsuchenden Daten in einer Tabelle oder Abfrage
befinden. Bei immer wiederkehrenden ähnlichen Filterungen führt sie aber nicht so schnell zum
Erfolg, wie dies vor allem in Formularen erwünscht wird.
Als praktische Beispiel7 soll ein Teil einer Mediendatenbank dienen, die in verschiedene Tabellen aufgeteilt ist. Gerade eine Verteilung auf mehrere Tabellen erfordert für ein ordnungsgemäßes Filtern von Daten mehr als nur die Nutzung der internen Funktionen von Base. Schließlich ist die eingebaute Funktionalität auf das Filtern innerhalb einer Tabelle bzw. eines Formulars
ohne Unterformular beschränkt.
Der Begriff «Suche» wird in den folgenden Beispielen allerdings so genutzt, wie er auch imInternet üblich ist: Die Eingabe eines beliebigen Suchbegriffs soll zu Ergebnissen führen.
Die Reihenfolge in der Beschreibung entspricht nicht der Reihenfolge, die Abfragen und Tabellen in der Datenbank haben. Dies liegt daran, dass die inhaltliche Reihenfolge vom Einfacheren
zum Umfassenderen nicht mit der alphabetischen Reihenfolge in der Datenbank übereinstimmt.

TabellenÜber Extras → Beziehungen ist eine Übersicht der Tabellen zusammengefügt, durch die hindurch die Datensuche und Datenfilterung vorgenommen werden soll.
Zentrale Tabelle ist die Tabelle "Medien". Die Tabelle "Kategorien", "Medienart" und "Ort" sind
mit der Tabelle über Fremdschlüssel verbunden. Für einen Datensatz aus "Medien" kann esmehrere Datensätze aus "Verfasser" geben. Ebenso kann es für einen Datensatz aus "Verfasser" mehrere Datensätze aus "Medien" geben. Aus dem Grunde ist über die Tabelle"rel_Medien_Verfasser" eine n:m-Beziehung erstellt worden.

7

Beispieldatenbank Beispiel_Suchen_Filtern.odb

Suchen und Filtern144

Neben diesen Tabellen existiert in der Datenbank noch eine Tabelle "Filter", die zur Filterung
und Suche genutzt wird und sonst keine Beziehung zu den anderen Tabellen aufweist.

FilterDatenziel
Formular: Filter_Formular, Filter_Formular_Nullwerte, Filter_Formular_Subformular, Filter_Formular_Subformular_3Filter, Makro_Filter_Formular, Filter_Formular_Listfeldbegrenzung,
Makro_Filter_Formular_Subformular, Filter_Formular_Subformular_bedingende_Filter,
Filter_Formular_Subformular_bedingende_Filter_allround, Filter_Formular_Subformular_bedingende_Filter_Datensaetze,
Suche_Filter_Formular_Subformular_ohne_Makros,
Suche_Filter_hierarchisch_mit_Makros,
Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros, Parameter_Suche_Formular_Subformular, Suche_Formular, Suche_Formular_Subformular, Makro_Suche_Formular, Makro_Suche_Formular_Subformular
Abfrage:

Filter_Form, Filter_Form_Subform, Filter_Form_Subform_3Filter, Filter_Ansicht,
Suche_Filter_Form_Subform_3Filter, Filter_Leerfeld, Filter_Such_Ansicht, Suche_Form,
Suche_Form_Subform

Makro:

Filter_Suche_hierarchisch_Start

Ansicht, Bericht: keine

Feldname

Feldtyp

Beschreibung

ID

Ja/NeinPrimärschlüssel der Tabelle. Dieses Feld wird zum Start einmal
auf 'Ja' (True) gesetzt. Damit steht der erste und einzige Datensatz fest.

Suche

Text

Texteingabe für eine freie Textsuche
Eingabe erforderlich: Nein

Filter_Kategorie

Integer

Eingabe des Fremdschlüsselwertes für die Tabelle "Kategorie"
Eingabe erforderlich: Nein

Filter_Verfasser

Integer

Eingabe des Fremdschlüsselwertes für die Tabelle "Verfasser"
Eingabe erforderlich: Nein

Filter_Medienart

Integer

Eingabe des Fremdschlüsselwertes für die Tabelle "Medienart"
Eingabe erforderlich: NeinpTitelText

Zwischenspeicher für eine Parameterabfrage über ein Formular,
Feld "Titel"
Eingabe erforderlich: NeinpVerfasser

Text

Zwischenspeicher für eine Parameterabfrage über ein Formular,
Feld "Verfasser"
Eingabe erforderlich: Nein

pOrt

Text

Zwischenspeicher für eine Parameterabfrage über ein Formular,
Feld "Ort"
Eingabe erforderlich: NeinpMedienart

Text

Zwischenspeicher für eine Parameterabfrage über ein Formular,
Feld "Medienart"
Eingabe erforderlich: Nein

Die Filtertabelle wird nur mit einem Datensatz versehen. Hier werden alle Filter- und Suchergebnisse zwischengespeichert. Wird der Inhalt nicht gelöscht, so bleiben die letzten Suchergebnisse gespeichert.

Suchen und Filtern145

Kategorien
Datenziel
Formular: Listenfeld «Kategorie» in jedem Formular, Filter_Formular, Makro_Filter_Formular
Ansicht:

Ansicht_Suche

Abfrage:

Listenfeld_Kategorie_bedingt, Listenfeld_Kategorie_bedingt_Datensaetze,
Listenfeld_Kategorie_ohne_Eintrag, Listenfeld_Kategorie_bedingt_Suche_Filter, Listenfeld_Kategorie_bedingt_Suche_Filter_Datensaetze

Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann ist als Auto-Wert-Feld gesetzt.

SystemText

Kürzel für die systematische Einordnung
Eingabe erforderlich: Ja

Kategorie

Text

Ausführliche Beschreibung der Kategorie
Eingabe erforderlich: Ja

MedienDatenziel
Formular: Filter_Formular, Standardfilter_Formular_Subformular, Standardfilter_Formular_Subformular_Datensatzkorrektur, Makro_Filter_Formular
Ansicht:

Ansicht_Suche

Abfrage:

Filter_Form, Parametersuche_Medien, Filter_Form_Subform, Filter_Form_Subform_3Filter, Suche_Filter_Form_Subform_3Filter, Filter_Form_Nullwerte,
Listenfeld_Kategorie_ohne_Eintrag, Suche_Form, Suche_Form_Subform

Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld kann auch als Auto-Wert-Feld gesetzt
werden.

katIDInteger

Fremdschlüssel zur Tabelle "Kategorie"."ID"
Eingabe erforderlich: Nein

artID

Integer

Fremdschlüssel zur Tabelle "Medienart"."ID"
Eingabe erforderlich: Nein

Titel

Text

Titel des Mediums
Eingabe erforderlich: Ja

ortID

Integer

Fremdschlüssel zur Tabelle "Ort"."ID"
Eingabe erforderlich: Nein

Jahr

Small Integer

Erscheinungsjahr des Mediums
Eingabe erforderlich: Nein

Suchen und Filtern146

Medienart
Datenziel
Formular: Listenfeld «Medienart» in jedem Formular
Ansicht:

Ansicht_Suche

Abfrage:

Listenfeld_Medienart_bedingt, Listenfeld_Medienart_bedingt_Datensaetze,
Listenfeld_Medienart_bedingt_Suche_Filter, Listenfeld_Medienart_bedingt_Suche_Filter_Datensaetze

Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

ArtKurz

Text(fix)Kurzform der Medienart, auf 2 Zeichen festgelegt
Eingabe erforderlich: Ja

Medienart

Text

Medienart komplett beschrieben, auf 25 Zeichen festgelegt
Eingabe erforderlich: Ja

LeihzeitTiny Integer

Ausleihzeit für das Medium in Tagen
Eingabe erforderlich: Nein

Ort
Datenziel
Formular: Listenfeld «Ort» in jedem Formular
Ansicht:

Ansicht_Suche

Abfrage, Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Ort

Text

Erscheinungsort des Mediums
Eingabe erforderlich: Ja

rel_Medien_Verfasser
Datenziel
Formular: Filter_Formular, Filter_Formular_Nullwerte, Filter_Formular_Subformular, Filter_Formular_Subformular_3Filter, Standardfilter_Formular_Subformular, Standardfilter_Formular_Subformular_Datensatzkorrektur, Makro_Filter_Formular,
Filter_Formular_direkt_Nullwerte, Filter_Formular_Listfeldbegrenzung,
Makro_Filter_Formular_Subformular, Filter_Formular_Subformular_bedingende_Filter,
Filter_Formular_Subformular_bedingende_Filter_allround, Filter_Formular_Subformular_bedingende_Filter_Datensaetze, Suche_Filter_hierarchisch_mit_Makros, Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros, Parameter_Suche_Formular_Subformular, Suche_Formular, Suche_Formular_Subformular, Makro_Suche_Formular,
Makro_Suche_Formular_Subformular
Ansicht:

Ansicht_Suche

Abfrage:

Listenfeld_Verfasser_bedingt, Listenfeld_Verfasser_bedingt_Suche_Filter

Suchen und Filtern147

Datenziel
Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

medID

IntegerPrimärschlüssel der Tabelle zusammen mit vefID. Fremdschlüssel zur Tabelle "Medien"."ID"vefID

IntegerPrimärschlüssel der Tabelle zusammen mit medID. Fremdschlüssel zur Tabelle "Verfasser"."ID"

VerfSort

Tiny Integer

Sortierreihenfolge für die Verfassereinträge, damit die wichtigsten Personen nachher auch am Anfang erscheinen.
Eingabe erforderlich: Nein

Verfasser
Datenziel
Formular: Listenfeld «Verfasser» in jedem Formular
Ansicht:

Ansicht_Suche

Abfrage:

Listenfeld_Verfasser_bedingt, Listenfeld_Verfasser_bedingt_Suche_Filter

Bericht, Makro: keine

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Verfasser

Text

Der Name der Person, die Autor des Mediums ist. Über die
Tabelle "rel_Medien_Verfasser" können beliebig viele Verfasser einem Medium zugeordnet werden.

Ansichten
Ansicht_Suche
Datenquelle
Tabelle:Kategorien, Medien, Medienart, Ort, rel_Medien_Verfasser, Verfasser

Datenziel
Formular: Standardfilter_Formular_Subformular
Abfrage:

Parametersuche_Medien, Filter_Form_Subform, Filter_Form_Subform_3Filter,
Filter_Ansicht, Suche_Filter_Form_Subform_3Filter, Filter_Such_Ansicht,
Suche_Form_Subform, Listenfeld_Verfasser_bedingt_Datensaetze_langsam

001 SELECT "Medien"."ID", "Medien"."katID", "Medien"."artID","Medien"."Titel", "Medien"."ortID", "Medien"."Jahr","Kategorien"."System", "Kategorien"."Kategorie", "Medienart"."ArtKurz","Medienart"."Medienart", "Medienart"."LeihZeit", "Ort"."Ort","Verfasser"."Verfasser", "rel_Medien_Verfasser"."VerfSort","rel_Medien_Verfasser"."vefID",
002 COALESCE("rel_Medien_Verfasser"."vefID", 0 ) AS "VerfasserID",
003 COALESCE("Medien"."katID", 0 ) AS "KategorieID",
Suchen und Filtern

148

004
005
006
007
008
009

COALESCE("Medien"."artID", 0 ) AS "MedienartID"
FROM "Medien"
LEFT JOIN "Kategorien" ON "Medien"."katID" = "Kategorien"."ID"
LEFT JOIN "Medienart" ON "Medien"."artID" = "Medienart"."ID"
LEFT JOIN "Ort" ON "Medien"."ortID" = "Ort"."ID"
LEFT JOIN "rel_Medien_Verfasser" ON "rel_Medien_Verfasser"."medID" ="Medien"."ID"
010 LEFT JOIN "Verfasser" ON "rel_Medien_Verfasser"."vefID" = "Verfasser"."ID"Mit dieser Ansicht werden alle Inhalte aus den Tabellen der Datenbank in einer Ansicht zusammengefasst. Dabei wird von der Tabelle "Medien" als zentraler Tabelle ausgegangen (Zeile 5).
Alle anderen Tabellen werden über einen LEFT JOIN mit "Medien" verbunden.
Die Zeilen 2 bis 4 weisen den Feldern, die nicht über einen Fremdschlüssel mit den entsprechenden Tabellen verbunden sind, für eine entsprechende Abfrage den Wert '0' zu. Die Zählungdes Primärschlüssel in den Tabellen geht in dieser Beispieldatenbank erst ab '1' los, so dasseine '0' dort nicht vorkommt.

Ansicht_Filter_Start
Datenquelle
Ansicht:

Ansicht_Suche

Datenziel
Ansicht:

Ansicht_Standardfilter

001 SELECT "ID", "System", "Kategorie", "Titel", "Jahr", "ArtKurz","Medienart", "LeihZeit", "Ort",
002 ( SELECT COUNT( "ID" ) FROM "Ansicht_Suche" WHERE "ID" = "a"."ID" AND"vefID" <= "a"."vefID" ) AS "GruppenNr",
003 "Verfasser"
004 FROM "Ansicht_Suche" AS "a"Diese Ansicht stellt neben einigen Inhalten aus der "Ansicht_Suche" eine fortlaufende Nummerierung für die Verfasser jedes einzelnen Mediums zur Verfügung (Zeile 2). Die interne HSQLDB
ist nicht in der Lage, die Inhalte des Feldes "Verfasser" so zusammen zu fassen, dass pro
Medium die Verfasser in einem Feld erscheinen. Die ursprüngliche Ansicht zeigt dadurch deutlich mehr Medien an als wirklich vorhanden sind, da eben einige Medien mehrere Verfasser
haben.
Leider ist diese Ansicht recht langsam.

Ansicht_Standardfilter
Datenquelle
Ansicht:

Ansicht_Filter_Start

Datenziel
Formular: Standardfilter_Formular_Subformular_Datensatzkorrektur

001 SELECT DISTINCT "ID", "System", "Kategorie", "Titel", "Jahr", "ArtKurz","Medienart", "LeihZeit", "Ort",
002 ( SELECT "Verfasser" FROM "Ansicht_Filter_Start" WHERE "ID" = "a"."ID" AND"GruppenNr" = 1 ) ||003 COALESCE( '; ' || ( SELECT "Verfasser" FROM "Ansicht_Filter_Start" WHERE"ID" = "a"."ID" AND "GruppenNr" = 2 ), '' ) ||Suchen und Filtern149004 COALESCE( '; ' || ( SELECT "Verfasser" FROM "Ansicht_Filter_Start" WHERE"ID" = "a"."ID" AND "GruppenNr" = 3 ), '' ) ||005 COALESCE( '; ' || ( SELECT "Verfasser" FROM "Ansicht_Filter_Start" WHERE"ID" = "a"."ID" AND "GruppenNr" = 4 ), '' ) ||006 COALESCE( '; ' || ( SELECT "Verfasser" FROM "Ansicht_Filter_Start" WHERE"ID" = "a"."ID" AND "GruppenNr" = 5 ), '' ) ||007 COALESCE( '; ' || ( SELECT "Verfasser" FROM "Ansicht_Filter_Start" WHERE"ID" = "a"."ID" AND "GruppenNr" = 6 ), '' ) AS "Verfasser"
008 FROM "Ansicht_Filter_Start" AS "a"Über die Gruppennummer werden die einzelnen Verfasser aus der "Ansicht_Filter_Start" ineinem Feld "Verfasser" zusammengefasst. Die einzelnen Einträge werden hier über ein Semikolon voneinander getrennt. Ergibt sich bei einer "GruppenNr" ein leerer Wert, so wird stattdessen ein leerer Text weitergegeben. Sonst würden bei dieser Zusammenfassung im Feld "Verfasser" nur die Felder Inhalt aufweisen, bei denen 6 Verfasser vorhanden sind.
Nachteil dieser für die HSQLDB notwendigen Variante ist, dass die Anzahl der zusammengefassten Verfasser durch den Code begrenzt ist und dass leider die Ansicht durch die vorhergehende
Ansicht recht langsam ist.

Standardfilter_Firebird
001 SELECT "ID", "Titel", "Jahr", "System", "Kategorie", "ArtKurz","Medienart", "LeihZeit", "Ort",
002 LIST("Verfasser",';') AS "Verfasser"
003 FROM "Ansicht_Suche"
004 GROUP BY "Ansicht_Suche"."ID", "Ansicht_Suche"."Titel","Ansicht_Suche"."Jahr", "Ansicht_Suche"."System","Ansicht_Suche"."Kategorie", "Ansicht_Suche"."ArtKurz","Ansicht_Suche"."Medienart", "Ansicht_Suche"."LeihZeit","Ansicht_Suche"."Ort"
Dieser Code könnte in FIREBIRD statt der Kombination von Ansicht_Filter_Start und Ansicht_Standardfilter genutzt werden. Er ist wesentlich schneller und erfüllt den gleichen Zweck: Die Einträge für die Verfasser werden hier durch ein Semikolon getrennt zu jedem Medium einzeln aufgelistet (Zeile 2). Die Anzahl der Datensätze stimmt mit der Anzahl der Medien überein. Dieser
Code ist in der Firebird-Variante der Beispieldatenbank unter den Abfragen gespeichert.

AbfragenZur besseren Übersicht sind die Abfragen in verschiedene Bereiche untergliedert. Neben den
Abfragen, die nur für die Filterung oder die Suche von Daten gedacht sind existieren auch viele
Abfragen, die für Versorgung von Listenfeldern mit entsprechendem SQL-Code gedacht sind.

FilterDiese Abfragen werden nur für die Filterung von Daten genutzt.

Filter_Form
Datenquelle
Tabelle:Medien, Filter

Datenziel
Formular: Filter_Formular, Filter_Formular_Nullwerte, Makro_Filter_Formular,
Filter_Formular_Listfeldbegrenzung

Suchen und Filtern

150

001
002
003
004

SELECT "Medien".*,COALESCE( "katID", 0 ) AS "kat"FROM "Medien"
WHERE "kat" = COALESCE( ( SELECT "Filter_Kategorie" FROM "Filter" WHERE"ID" = TRUE ), "kat" )Alle Datensätze der Tabelle "Medien" werden angezeigt. Zusätzlich wird ein Feld mit dem Alias"kat" erzeugt, das bei einer leeren "katID" stattdessen eine '0' setzt (Zeile 2). Die Daten werden
durch eine Abfrage des Wertes von "Filter_Kategorie" in der Tabelle "Filter" eingeschränkt(Zeile 4). Ist dieses Feld leer, so wird stattdessen "kat" direkt mit "kat" verglichen. Und da "kat"grundsätzlich einen Eintrag hat (Zeile 2) werden durch diese Bedingung bei einem leeren Feld
"Filter_Kategorie" alle Datensätze aus "Medien" angezeigt.
In FIREBIRD führt diese Abfrage zu einer Fehlermeldung. FIREBIRD bekommt hier die Verbindungdes Alias "kat" aus Zeile 2 mit dem "kat" in Zeile 4 nicht geregelt. Deswegen wurde dort derCode geändert:
001 SELECT *002 FROM "Medien"
003 WHERE COALESCE ( "katID", 0 ) = COALESCE ( ( SELECT "Filter_Kategorie"
FROM "Filter" WHERE "ID" = TRUE ), COALESCE ( "katID", 0 ) )Der Inhalt aus der oberen Abfrage für Zeile 2 wurde einfach in die Bedingung direkt geschrieben. Da jetzt nur die Datensätze aus "Medien" angegeben werden entfällt der Hinweis zu"Medien" in Zeile 1. Statt "Medien".* reicht einfach *.

Filter_Form_Subform
Datenquelle
Tabelle:Medien, Filter

Ansicht:

Ansicht_Suche

Datenziel
Formular: Filter_Formular_Subformular, Makro_Filter_Formular_Subformular

001 SELECT * FROM "Medien"
002 WHERE "ID" IN (003 SELECT DISTINCT "ID" FROM "Ansicht_Suche" WHERE "VerfasserID" =COALESCE( ( SELECT "Filter_Verfasser" FROM "Filter" WHERE "ID" = TRUE ),"VerfasserID" )004 )Die Angaben für die Verfasser befinden sich im Unterformular der jeweiligen Formulare. Deswegen ist hier eine gesonderte Vorgehensweise erforderlich.
Alle Datensätze aus der Tabelle "Medien" werden angezeigt (Zeile 1). Voraussetzung ist, dass
der Primärschlüssel "ID" des jeweiligen Mediums in der Liste auftaucht, die durch die Unterabfrage in Zeile 3 erstellt wird. Die Unterabfrage listet nur die Werte auf, zu denen eine dem Feld
"Filter_Verfasser" entsprechender Datensatz in "Ansicht_Suche"."VerfasserID" existiert. Wird
kein Wert im Feld "Filter_Verfasser" eingegeben, so wird stattdessen "VerfasserID" gleich "VerfasserID" gesetzt. Alle Datensätze werden angezeigt.
Abfragen, die mit dem Vergleich von Datenmengen funktionieren (IN, Zeile 2), beanspruchen
grundsätzlich etwas mehr Ressourcen. In der Praxis macht sich das bei dieser Abfrage allerdings nicht bemerkbar.

Suchen und Filtern151Filter_Form_Subform_3Filter
Datenquelle
Tabelle:Medien, Filter

Ansicht:

Ansicht_Suche

Datenziel
Formular: Filter_Formular_Subformular_3Filter, Filter_Formular_Subformular_bedingende_Filter,
Filter_Formular_Subformular_bedingende_Filter_allround, Filter_Formular_Subformular_bedingende_Filter_Datensaetze
Abfrage:

Listenfeld_Kategorie_bedingt, Listenfeld_Medienart_bedingt,
Listenfeld_Verfasser_bedingt, Listenfeld_Kategorie_bedingt_Datensaetze,
Listenfeld_Medienart_bedingt_Datensaetze,
Listenfeld_Verfasser_bedingt_Datensaetze_langsam

001
002
003
004
005

SELECT *FROM "Medien"
WHERE "ID" IN (SELECT DISTINCT "ID" FROM "Ansicht_Suche" WHERE"VerfasserID" = COALESCE( ( SELECT "Filter_Verfasser" FROM "Filter" WHERE"ID" = TRUE ), "VerfasserID" )006 AND "MedienartID" = COALESCE( ( SELECT "Filter_Medienart" FROM "Filter"
WHERE "ID" = TRUE ), "MedienartID" )007 AND "KategorieID" = COALESCE( ( SELECT "Filter_Kategorie" FROM "Filter"
WHERE "ID" = TRUE ), "KategorieID" )008 )Diese Abfrage funktioniert nach dem gleichen Prinzip wie Filter_Form_Subform. Nur wird hiernicht nur nach den Datensätzen für die "VerfasserID" (Zeile 5) sondern auch nach denen für die"MedienartID" (Zeile 6) und die "KategorieID" (Zeile 7) gefiltert. Dabei ergänzen sich die Filterüber AND. Das bedeutet, dass die Filterungen einander einschränken. Je mehr Filter gesetzt werden, desto kleiner ist die Ergebnismenge.

Filter_Ansicht
Datenquelle
Tabelle:FilterAnsicht:

Ansicht_Suche

Datenziel
Abfrage:

Listenfeld_Verfasser_bedingt_Datensaetze_optimiert

001
002
003
004
005

SELECT *FROM "Ansicht_Suche"
WHERE "ID" IN (SELECT DISTINCT "ID" FROM "Ansicht_Suche" WHERE"VerfasserID" = COALESCE( ( SELECT "Filter_Verfasser" FROM "Filter" WHERE"ID" = TRUE ), "VerfasserID" )006 AND "MedienartID" = COALESCE( ( SELECT "Filter_Medienart" FROM "Filter"
WHERE "ID" = TRUE ), "MedienartID" )007 AND "KategorieID" = COALESCE( ( SELECT "Filter_Kategorie" FROM "Filter"
WHERE "ID" = TRUE ), "KategorieID" )008 )Der Code gleicht dem aus Filter_Form_Subform_3Filter. Allerdings wird hier nicht direkt die
Tabelle "Medien", sondern die Ansicht "Ansicht_Suche" angezeigt. Diese Fassung ist für die Lis-

Suchen und Filtern152tenfeldabfrage deutlich besser nutzbar als die Abfrage von "Medien", da in der Ansicht bereits
die Verbindung der Verfasser zu den Medien (n:m-Beziehung) vorher abgefragt wird.

Filter_Form_Nullwerte
Datenquelle
Tabelle:MedienDatenziel
Formular: Filter_Formular_direkt_Nullwerte

001 SELECT "Medien".*, COALESCE( "katID", 0 ) AS "kat" FROM "Medien"
Damit bei leeren Felder für "katID" auch eine Auswahl dieser Felder möglich gemacht wird, wird
in dieser Abfrage "katID" gegebenenfalls '0' zugewiesen. So kann in dem Formular nach den
Datensätzen gefiltert werden, für die kein Eintrag existiert. Gleichzeitig ist aber auch eineÄnderung der anderen Daten möglich.
Diese Abfrage ist für die direkte Filterung über den Formularfilter notwendig.

Filter_Leerfeld
Datenquelle
Tabelle:FilterDatenziel
Formular: Filter_Formular_direkt_Nullwerte,

001 SELECT "Filter".*, ( SELECT NULL FROM "Filter" ) AS "Leer"
002 FROM "Filter" WHERE "ID" = TRUE
Der Tabelle "Filter" wird ein leeres Feld hinzugefügt. Dem Feld wird der Alias "Leer" zugewiesen.
Dies ist notwenig als Datenbasis für das Listenfeld im Formular Filter_Formular_direkt_Nullwerte, da ein Listenfeld nur funktioniert, wenn es mit einer Datenquelle verbunden ist. Das Listenfeld soll aber keine Daten speichern.

Suche
Die Suche arbeitet grundsätzlich mit frei eingebbarem Text. Hier sind also keine Fremdschlüsselwerte vorgesehen wie bei der Filterung.

Suche_Form
Datenquelle
Tabelle:Filter, Medien

Datenziel
Formular: Suche_Formular, Makro_Suche_Formular

001 SELECT *002 FROM "Medien"
003 WHERE LOWER ( "Titel" ) LIKE COALESCE( '%' || LOWER ( ( SELECT "Suche"
FROM "Filter" WHERE "ID" = TRUE ) ) || '%', LOWER ( "Titel" ) )Aus der Tabelle "Medien" werden alle Datensätze angezeigt (Zeile 1 und 2), bei denen die Kleinschreibung von "Titel" LOWER("Titel") den klein geschriebenen Inhalt aus dem Feld "Suche"
Suchen und Filtern153

der Tabelle "Filter" an irgendeiner Stelle enthält. Durch die voran- und nachgestellten '%' darf
dieses Element an jeder Stelle des Feldes "Titel" auftauchen. Enthält das Feld "Suche" keinen
Eintrag, so wird über COALESCE stattdessen der Eintrag in LOWER("Titel") mit sich selbst verglichen. Das bedeutet, dass alle Datensätze angezeigt werden.
Durch die Anzeige aller Datensätze aus "Medien", auch des Primärschlüssels, ist diese Abfrage
zur Eingabe von Daten im Formular geeignet.

Suche_Form_Subform
Datenquelle
Tabelle:Filter, Medien

Ansicht:

Ansicht_Suche

Datenziel
Formular: Suche_Formular_Subformular, Makro_Suche_Formular_Subformular

001
002
003
004
005

SELECT *FROM "Medien"
WHERE "ID" IN (SELECT DISTINCT "ID" FROM "Ansicht_Suche"
WHERE LOWER ( "Titel" ) LIKE COALESCE( '%' || LOWER ( ( SELECT "Suche"
FROM "Filter" WHERE "ID" = TRUE ) ) || '%', LOWER ( "Titel" ) )006 OR LOWER ( "Kategorie" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'007 OR LOWER ( "Medienart" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'008 OR LOWER ( "Ort" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM "Filter"
WHERE "ID" = TRUE ) ) || '%'009 OR LOWER ( "Verfasser" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'010 )Diese Abfrage erweitert die Abfrage Suche_Form so, dass in mehreren Elementen gleichzeitig
mit dem Suchbegriff nach Übereinstimmungen gesucht werden kann. Die Textbestandteile von"Kategorie", "Medienart", "Ort" und "Verfasser" sind allerdings nicht Bestandteil der Tabelle"Medien". Daher muss hier innerhalb von "Ansicht_Suche" nach den Elementen gesucht werden.
Zuerst werden alle Felder der Tabelle "Medien" angezeigt (Zeile 1 und 2), so dass die Abfrage
für die Dateneingabe im Formular geeignet ist.. Von "Medien" werden nur die Datensätze angezeigt, deren Wert für "ID" IN einer bestimmten Datenmenge enthalten ist (Zeile 3). Diese
Datenmenge wird von Zeile 4 bis Zeile 10 definiert.
Die kleingeschriebene Variante von "Titel" soll an irgendeiner Stelle den Inhalt des Feldes"Suche" aus der Tabelle "Filter" enthalten. Enthält das Feld "Suche" keinen Eintrag, so soll stattdessen die keingeschriebne Variante von "Titel" mit sich selbst verglichen werden. Es werden
dann also alle Datensätze angezeigt (Zeile 5). Entsprechend wird für alle anderen zu durchsuchenden Feldern in "Ansicht_Suche" (Zeile 4) vorgegangen. Dabei muss nur eines der Felder
den Inhalt enthalten. Dies wird durch die Verknüpfung mit OR erreicht.
Damit der Wert für "ID" nicht mehrmals als Treffer auftaucht ("Ansicht_Suche" enthält durch die
Verknüpfung zu "Verfasser" pro "Verfasser" jeweils einen Datensatz aus "Medien"), wird die zu
vergleichende Menge durch DISTINCT auf einzelne, von einander unterschiedliche Werte eingeschränkt (Zeile 4).

Suchen und Filtern154

Parametersuche_Medien
Datenquelle
Tabelle:Medien, Ansicht_Suche

Datenziel
Formular: Parameter_Suche_Formular_Subformular

001 SELECT *002 FROM "Medien"
003 WHERE "ID" IN (004
SELECT "ID" FROM "Ansicht_Suche" WHERE
005
COALESCE( LOWER ( "Titel" ), '' ) LIKE '%' || COALESCE( LOWER( :Titel ), '' ) || '%' AND
006
COALESCE( LOWER ( "Medienart" ), '' ) LIKE '%' || COALESCE( LOWER( :Medienart ), '' ) || '%' AND
007
COALESCE( LOWER ( "Ort" ), '' ) LIKE '%' || COALESCE( LOWER( :Ort ), '' ) || '%' AND
008
COALESCE( LOWER ( "Verfasser" ), '' ) LIKE '%' || COALESCE( LOWER( :Verfasser ), '' ) || '%'009 )In der "Ansicht_Suche" befinden sich die Verfasser, Orte usw. in ausgeschriebener Form. Sie
können also nach bestimmten Textelementen untersucht werden. Sämtliche Parameter werden
durch '%' vor und hinter dem Parameter so abgefragt, dass die Textlemente an beliebiger
Stelle in dem jeweiligen Feld stehen können. Außerdem wird sowohl der Feldinhalt als auch derParameterinhalt in Kleinbuchstaben verglichen, so dass Groß- und Kleinschreibweise keine Rolle
spielen.
Enthält ein Feld keinen Inhalt, so wir stattdessen ein leerer Text als Inhalt angenommen. So
wird aus dem leeren Feld "Titel" gegebenenfalls ein leerer Text. Auch ein lererer Parameter wird
als leerer Text angenommen. So wird aus :Titel ohne Inhalt ein leerer Text. Und leere Texte
miteinander verglichen sind gleich. Würde hingegen einer der beiden Teile NULL sein, also tatsächlich ohne Inhalt, so wäre kein Vergleich möglich.
Alle Elemete aus "Medien", die dem Suchergebnis entsprechen, werden angezeigt. Durch die
Anzeige aller Element, auch des Primärschlüssels, ist diese Abfrage für die weitere Dateneingabe geeignet.
Diese Parametersuche funktioniert prinzipiell auch ohne Formular. In dem Falle erscheint derfolgende Dialog:

Suchen und Filtern155Nach Angabe der Parameter wird die Abfrage gestartet. Ein leerer Parameter wird dank derAbfrage so übersetzt, dass aus dem entsprechenden Feld alle Datensätze dem Inhalt entsprechen.

Filter und Suche kombiniert
Suche_Filter_Form_Subform_3Filter
Datenquelle
Tabelle:Medien, Filter

Ansicht:

Ansicht_Suche

Datenziel
Formular: Suche_Filter_Formular_Subformular_ohne_Makros,
Suche_Filter_hierarchisch_mit_Makros,
Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros
Abfrage:

Listenfeld_Medienart_bedingt_Suche_Filter, Listenfeld_Kategorie_bedingt_Suche_Filter, Listenfeld_Verfasser_bedingt_Suche_Filter,
Listenfeld_Medienart_bedingt_Suche_Filter_Datensaetze,
Listenfeld_Kategorie_bedingt_Suche_Filter_Datensaetze

001
002
003
004
005

SELECT *FROM "Medien"
WHERE "ID" IN (SELECT DISTINCT "ID" FROM "Ansicht_Suche"
WHERE "VerfasserID" = COALESCE( ( SELECT "Filter_Verfasser" FROM "Filter"
WHERE "ID" = TRUE ), "VerfasserID" )006 AND "MedienartID" = COALESCE( ( SELECT "Filter_Medienart" FROM "Filter"
WHERE "ID" = TRUE ), "MedienartID" )007 AND "KategorieID" = COALESCE( ( SELECT "Filter_Kategorie" FROM "Filter"
WHERE "ID" = TRUE ), "KategorieID" )008 AND (Suchen und Filtern156009 LOWER ( "Titel" ) LIKE COALESCE( '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%', LOWER ( "Titel" ) )010 OR LOWER ( "Kategorie" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'011 OR LOWER ( "Medienart" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'012 OR LOWER ( "Ort" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM "Filter"
WHERE "ID" = TRUE ) ) || '%'013 OR LOWER ( "Verfasser" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'
014 )
015 )
Diese Abfrage ist eine Kombination der Abfragen Filter_Form_Subform_3Filter undSuche_Form_Subform. Aus der Tabelle "Medien" werden alle Datensätze angezeigt, für die derPrimärschlüssel "ID" IN der von Zeile 4 bis 14 definierten Ergebnismenge enthalten ist. Untersucht wird hier wieder "Ansicht_Suche", da nur in dieser Ansicht neben dem Feld "Titel" auchdie anderen Felder nicht nur durch einen Fremdschlüssel enthalten sind. Außerdem ist"Ansicht_Suche" auch erforderlich, um den Fremdschlüssel zum "Verfasser" zu ermitteln, da"Verfasser" und "Medien" über die Tabelle "rel_Medien_Verfasser" in einer n:m-Beziehung stehen.
Von Zeile 5 bis 7 wird die Filterung für die Fremdschlüsselfelder vorgenommen. Von Zeile 9 bis
13 wird die Suche durch die angegebenen Felder realisiert. Beide Bedingungen sind durch ein
AND in Zeile 8 verknüpft, wobei eine Klammerung um die zu durchsuchenden Felder (Zeile 8
und Zeile 14) bewirkt, dass die Verknüpfungen mit OR innerhalb der Suche nicht zu unerwarteten Ergebnissen führen. So könnte ohne die Klammerung z. B. ein Datensatz angezeigt werden, wenn zwar der Suchbegriff allein in "Verfasser" vorkommt, aber gleichzeitig ein Verfasserüber das Filterfeld ausgesucht wurde, der dem Suchbegriff gar nicht entspricht.
Diese Abfrage musste für FIREBIRD umbenannt werden, da dort eine Grenze von 30 Zeichen fürBezeichnungen von Feldern und Tabellen vorgesehen ist. Die Benennung der Abfrage selbst
macht dabei nichts aus. Aber andere Abfragen können nicht auf diese Abfrage zugreifen. Die
Abfrage hat unter Firebird deswegen die Bezeichnung «Suche_Filter_Form_Subform_3F» Alle
sich darauf beziehenden Abfragen und Formulare mussten entsprechend angepasst werden.

Filter_Such_Ansicht
Datenquelle
Tabelle:FilterAnsicht:

Ansicht_Suche

Datenziel
Abfrage:

001
002
003
004
005
006
007
008
009

Listenfeld_Verfasser_bedingt_Suche_Filter_Datensaetze

SELECT *FROM "Ansicht_Suche"
WHERE "ID" IN (SELECT DISTINCT "ID" FROM "Ansicht_Suche"
WHERE "VerfasserID" = COALESCE( ( SELECT "Filter_Verfasser" FROM "Filter"
WHERE "ID" = TRUE ), "VerfasserID" )AND "MedienartID" = COALESCE( ( SELECT "Filter_Medienart" FROM "Filter"
WHERE "ID" = TRUE ), "MedienartID" )AND "KategorieID" = COALESCE( ( SELECT "Filter_Kategorie" FROM "Filter"
WHERE "ID" = TRUE ), "KategorieID" )AND (LOWER ( "Titel" ) LIKE COALESCE( '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%', LOWER ( "Titel" ) )Suchen und Filtern157

010 OR LOWER ( "Kategorie" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'011 OR LOWER ( "Medienart" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'012 OR LOWER ( "Ort" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM "Filter"
WHERE "ID" = TRUE ) ) || '%'013 OR LOWER ( "Verfasser" ) LIKE '%' || LOWER ( ( SELECT "Suche" FROM
"Filter" WHERE "ID" = TRUE ) ) || '%'
014 )
015 )
Diese Abfrage ist auf den ersten Blick identisch mit der Abfrage
Suche_Filter_Form_Subform_3Filter. Ein kleiner Unterschied besteht allerdings in der Datenquelle, aus der alle Datensätze angezeigt werden sollen. In dieser Abfrage handelt es sich nämlich um alle Datensätze aus "Ansicht_Suche", nicht aus "Medien". Der Grund dafür ist, dassdiese Abfrage alle Datensätze zu dem Verfasser anzeigen soll, damit diese als Grundlagen fürein Listenfeld genommen werden können, das der Suchbedingung entspricht.

ListenfeldabfragenPrinzipiell könnten viele dieser Abfragen auch gut direkt bei den Listenfeldern als SQL-Code eingetragen werden. Über die Abfragen wird lediglich einfacher der SQL-Code erreichbar und fürden Nutzer einsehbar.

Listenfeld_Kategorie_ohne_Eintrag
Datenquelle
Tabelle:Kategorien, Medien

Datenziel
Formular: Filter_Formular_Nullwerte, Filter_Formular_Subformular_3Filter,
Filter_Formular_direkt_Nullwerte, Suche_Filter_Formular_Subformular_ohne_Makros

001
002
003
004
005
006
007
008
009

SELECT "Kat", "ID"FROM (
SELECT '1' AS "C", 'ohne Eintrag' AS "Kat", 0 AS "ID" FROM "Kategorien"
UNION
SELECT "Kategorien"."System",
LEFT( "Kategorien"."System" || '', 7 )|| ' - ' ||LEFT("Kategorien"."Kategorie",50) ||CASE WHEN CHAR_LENGTH("Kategorien"."Kategorie") > 50 THEN '…' ELSE ''END
010
AS "Kat",011"Kategorien"."ID"012
FROM "Kategorien", "Medien" WHERE "Kategorien"."ID" = "Medien"."katID"
013 UNION
014 SELECT '0' AS "C", '' AS "Kat", NULL AS "ID" FROM "Kategorien")
Das Listenfeld soll auch die Möglichkeit bieten die Datensätze heraus zu suchen, die keinen Eintrag in dem entsprechenden Feld haben. Diesen Feldern ist über eine Abfrage der Wert '0'
zugewiesen worden. Da die Abfrage mit dem Befehl UNION arbeitet ist sie nur mit direktem SQL
ausführbar.
In Zeile 3 wird die erste Unterabfrage gestellt. Das erste Feld dieser Abfrage dient nur zur Sortierung innerhalb des UNION-Verbundes. Als zweites Feld wird lediglich 'ohne Eintrag' geschrieben. Als drittes Feld erscheint dann die '0', die über das Listenfeld weitergegeben wird.

Suchen und Filtern

158

In Zeile 5 beginnt die zweite Unterabfrage, die an die vorherige Abfrage durch UNION angehängt wird. Als Ordnungsmerkmal wird hier der Eintrag aus "Kategorien"."System" genutzt.
Die Kategorien werden für das Listenfeld so dargestellt, dass der Systemeintrag kompletterscheinen soll, Leerstellen aber gegebenenfalls aufgefüllt werden. Der längste Systematikeintrag beträgt hier 7 Zeichen, so dass in Zeile 6 auf insgesamt 7 Zeichen von links aus ausgelesen wird. Zeile 7 enthält lediglich das Verbindungszeichen zwischen dem Systematikeintrag
und der ausführlicheren Kategorienbeschreibung. In Zeile 8 wird die Kategorienbeschreibung
auf 50 Zeichen begrenzt, da sonst das Listenfeld viel zu breit wird. Beträgt die Länge des Eintrags mehr als 50 Zeichen, so wird in Zeile 9 noch ein '…' angehängt.
Die Unterabfrage schließt mit dem Primärschlüsseleintrag aus der Tabelle "Kategorien" ab.
In Zeile 14 wird noch eine Unterabfrage angehängt, die notwendig ist, damit das Listenfeld
anschließend auch eine leere Zeile darstellt. Diese Zeile ist notwendig, damit die Filterung auchwieder ausgeschaltet werden kann. Sie wird standardmäßig bei Listenfeldern mit eingeblendet,
sofern der SQL-Code nicht direkt ausgeführt werden muss.
Sortierungsmerkmal ist hier die '0'. Damit erscheint der Eintag an erster Stelle. In dem Listenfeld soll nicht angezeigt werden - ein leerer String. Von dem Listenfeld soll der Wert NULL weitergegeben werden.
Wird diese Unterabfrage an erster Position gestellt, so versteht die HSQLDB die gesamte Kombination als eine Kombination von Werten mit NULL. Die gesamten Einträge im Feld "ID" bleibendann leer.
Auf diese Unterabfragen wird nun durch eine äußere Abfrage für das Listenfeld zugegriffen, dielediglich die Felder "Kat" und "ID" anzeigt (Zeile 1).

Listenfeld_Kategorie_bedingt
Datenquelle
Tabelle:Kategorien

Abfrage:

Filter_Form_Subform_3Filter

Datenziel
Formular: Filter_Formular_Subformular_bedingende_Filter, Filter_Formular_Subformular_bedingende_Filter_allround

001 SELECT "System" || ' - ' || "Kategorie" AS "Kat", "ID"002 FROM "Kategorien"
003 WHERE "ID" IN
004 ( SELECT "katID" FROM "Filter_Form_Subform_3Filter" )005 ORDER BY "Kat" ASC
In Zeile 1 werden für die Anzeige die Inhalte aus "System" und "Kategorie" zusammengefasst.
Diese Zusammenfassung erhält den Aliasbegriff "Kat", auf den sich dann in Zeile 5 die Sortierung beziehen kann.
Die Anzeige der Inhalte wird durch die vorhandenen Einträge für "katID" in der Abfrage"Filter_Form_Subform_3Filter" eingeschränkt (Zeile 4). Dadurch werden nur noch die Datenangezeigt, die in der zu filternden Menge auch noch zur Verfügung stehen. Das vermeidet, dassbei einer Auswahl über das Listenfeld plötzlich gar keine Ergebnisse mehr in der Ergebnismenge vorkommen.
Listenfelder mit einem Inhalt wie diesem müssen nach jeder Eingabe in einem anderen Listenfeld aufgefrischt werden. Dies kann über die Tastatur erfolgen, ist aber einfacher über Makros
zu bedienen.

Suchen und Filtern159

Listenfeld_Kategorie_bedingt_Datensaetze
Datenquelle
Tabelle:Kategorien

Abfrage:

Filter_Form_Subform_3Filter

Datenziel
Formular: Filter_Formular_Subformular_bedingende_Filter_Datensaetze

001 SELECT "System" || ' - ' || LEFT("Kategorie",40) ||CASE WHEN CHAR_LENGTH("Kategorie") > 40 THEN '…' ELSE '' END || ' → ' || (002 SELECT COUNT( "katID" ) FROM "Filter_Form_Subform_3Filter" WHERE "katID" ="a"."ID"003 ) || ' Treffer' AS "Kat", "ID"004 FROM "Kategorien" AS "a"005 WHERE "ID" IN
006 ( SELECT "katID" FROM "Filter_Form_Subform_3Filter" )007 ORDER BY "Kat" ASC
Neben der vorgefilterten Anzeige der Kategorien (siehe Listenfeld_Kategorie_bedingt) soll hiernoch angezeigt werden, wie viele Ergebnisse bei der Auswahl der entsprechend Kategorie zu
erwarten sind. Dies wird durch eine korrelierende Unterabfrage in Zeile 2 bewerkstelligt. Durchdiese Unterabfrage werden alle Vorkommen des Feldes "katID" in "Filter_Form_Subform_3Filter"zusammengezählt, die dem Wert der "ID" des aktuellen Datensatzes entsprechen. Auf den
aktuellen Datensatz bezieht sich die Abfrage dadurch, dass der Tabelle "Kategorien" in Zeile 4
ein Alias "a" zugewiesen wird und in Zeile 2 dann genau dieses "a"."ID" auftaucht.
Die Anzeige in dem Listenfeld wird für "Kategorie" auf 40 Buchstaben begrenzt, da sonst beim
Aufklappen ein sehr breites Feld entsteht. Enthält "Kategorie" mehr als 40 Buchstaben, so soll
ein '…' angehängt werden (Zeile 1).
Dieses Listenfeld zeigt nur die Kategorien an, zu denen noch Filterergebnisse möglich sind. Felder ohne Treffer werden durch die Vorfilterung in Zeile 6 unterbunden.

Listenfeld_Kategorie_bedingt_Suche_Filter
Datenquelle
Tabelle:Kategorien

Abfrage:

Suche_Filter_Form_Subform_3Filter

Datenziel
Formular: Suche_Filter_hierarchisch_mit_Makros

001 SELECT "System" || ' - ' || "Kategorie" AS "Kat", "ID"002 FROM "Kategorien"
003 WHERE "ID" IN
004 ( SELECT "katID" FROM "Suche_Filter_Form_Subform_3Filter" )005 ORDER BY "Kat" ASC
Diese Abfrage entspricht nahezu komplett der Abfrage Listenfeld_Kategorie_bedingt. Lediglich
die Abfrage, durch die die anzuzeigenden Datensätze eingeschränkt werden, ändert sich hierauf "Suche_Filter_Form_Subform_3Filter" (Zeile 4).

Suchen und Filtern160

Listenfeld_Kategorie_bedingt_Suche_Filter_Datensaetze
Datenquelle
Tabelle:Kategorien

Abfrage:

Suche_Filter_Form_Subform_3Filter

Datenziel
Formular: Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros

001 SELECT "System" || ' - ' || "LEFT("Kategorie",40) ||CASE WHEN CHAR_LENGTH("Kategorie") > 40 THEN '…' ELSE '' END || ' → ' || (002 SELECT COUNT( "katID" ) FROM "Suche_Filter_Form_Subform_3Filter" WHERE"katID" = "a"."ID"003 ) || ' Treffer' AS "Kat", "ID"004 FROM "Kategorien" AS "a"005 WHERE "ID" IN
006 ( SELECT "katID" FROM "Suche_Filter_Form_Subform_3Filter" )007 ORDER BY "Kat" ASC
Diese Abfrage entspricht nahezu komplett der Abfrage
Listenfeld_Kategorie_bedingt_Datensaetze. Lediglich die Abfrage, durch die die anzuzeigenden
Datensätze eingeschränkt werden, ändert sich hier auf "Suche_Filter_Form_Subform_3Filter"(Zeile 6). Natürlich wird auch die Trefferzahl nach dieser Abfrage ermittelt (Zeile 2).

Listenfeld_Medienart_bedingt
Datenquelle
Tabelle:Medienart

Abfrage:

Filter_Form_Subform_3Filter

Datenziel
Formular: Filter_Formular_Subformular_bedingende_Filter, Filter_Formular_Subformular_bedingende_Filter_allround

001 SELECT "Medienart", "ID"002 FROM "Medienart"
003 WHERE "ID" IN
004 ( SELECT "artID" FROM "Filter_Form_Subform_3Filter" )005 ORDER BY "Medienart" ASCAlle Datensätze aus "Medienart" werden angezeigt, zu deren Primärschlüssel "ID" es in derDatenmenge von "Filter_Form_Subform_3Filter" einen entsprechenden Eintrag "artID" gibt.

Listenfeld_Medienart_bedingt_Datensaetze
Datenquelle
Tabelle:Medienart

Abfrage:

Filter_Form_Subform_3Filter

Datenziel
Formular: Filter_Formular_Subformular_bedingende_Filter_Datensaetze

001 SELECT "Medienart" || ' → ' || (002 SELECT COUNT( "artID" ) FROM "Filter_Form_Subform_3Filter" WHERE "artID" ="a"."ID"
Suchen und Filtern161

003 ) || ' Treffer' AS "Med", "ID"004 FROM "Medienart" AS "a"005 WHERE "ID" IN ( SELECT "artID" FROM "Filter_Form_Subform_3Filter" )006 ORDER BY "Med" ASC
Diese Abfrage entspricht in den Grundzügen Listenfeld_Kategorie_bedingt_Datensaetze. Allerdings enthält das Feld "Medienart" nicht so lange Texte, so dass der Inhalt hier nicht begrenzt.
wird.

Listenfeld_Medienart_bedingt_Suche_Filter
Datenquelle
Tabelle:Medienart

Abfrage:

Suche_Filter_Form_Subform_3Filter

Datenziel
Formular: Suche_Filter_hierarchisch_mit_Makros

001 SELECT "Medienart", "ID"002 FROM "Medienart"
003 WHERE "ID" IN
004 ( SELECT "artID" FROM "Suche_Filter_Form_Subform_3Filter" )005 ORDER BY "Medienart" ASC
Im Unterschied zu Listenfeld_Medienart_bedingt bezieht sich diese Abfrage auf die Abfrage"Suche_Filter_Form_Subform_3Filter". Ansonsten ist die Funktion gleich.

Listenfeld_Medienart_bedingt_Suche_Filter_Datensaetze
Datenquelle
Tabelle:Medienart

Abfrage:

Suche_Filter_Form_Subform_3Filter

Datenziel
Formular: Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros

001 SELECT "Medienart" || ' → ' || (002 SELECT COUNT( "artID" ) FROM "Suche_Filter_Form_Subform_3Filter" WHERE"artID" = "a"."ID"003 ) || ' Treffer' AS "Med", "ID"004 FROM "Medienart" AS "a" WHERE "ID" IN
005 ( SELECT "artID" FROM "Suche_Filter_Form_Subform_3Filter" )006 ORDER BY "Med" ASC
Im Unterschied zu Listenfeld_Medienart_bedingt_Datensaetze bezieht sich diese Abfrage auf
die Abfrage "Suche_Filter_Form_Subform_3Filter". Ansonsten ist die Funktion gleich.

Listenfeld_Verfasser_bedingt
Datenquelle
Tabelle:rel_Medien_Verfasser, Verfasser

Abfrage:

Filter_Form_Subform_3Filter

Suchen und Filtern162

Datenziel
Formular: Filter_Formular_Subformular_bedingende_Filter, Filter_Formular_Subformular_bedingende_Filter_allround

001 SELECT DISTINCT "Verfasser"."Verfasser", "Verfasser"."ID"002 FROM "rel_Medien_Verfasser", "Verfasser"
003 WHERE "rel_Medien_Verfasser"."vefID" = "Verfasser"."ID"004 AND "rel_Medien_Verfasser"."VerfSort" < 99005 AND "rel_Medien_Verfasser"."medID" IN
006 ( SELECT "ID" FROM "Filter_Form_Subform_3Filter" )007 ORDER BY "Verfasser"."Verfasser" ASC
Bei den Listenfeldabfragen für den Verasser muss immer wieder ein Sinderweg genutzt werden.
Schließlich steht die Tabelle "Verfasser" nicht in einer direkten Beziehung zur Tabelle "Medien".
Deshalb ist hier die Beziehung auf 2 Tabellen notwendig (Zeile 2).
Aus der Tabelle "rel_Medien_Verfasser" werden nur die Fremdschlüssel "vefID" übernommen,
bei denen "VerfSort" kleiner als '99' ist. Diese Werte sind Formulierungen wie '(Hrsg.)', '(u.a.)'oder anderen vorbehalten. Die Eingabe der entsprechenden Sortierungen ist in dieser Datenbank nicht vorgesehen. Die Datenbank dient schließlich in der Hauptsache als Quelle für die
Demonstration von Filter- und Suchfunktionen.
Die Verfasser, die angezeigt werden, sollen auch in den Medien verfügbar sein, die in derAbfrage "Filter_Form_Subform_3Filter" (Zeile 6) enthalten sind.

Listenfeld_Verfasser_bedingt_Datensaetze_langsam
Datenquelle
Ansicht:

Ansicht_Suche

Abfrage:

Filter_Form_Subform_3Filter

Datenziel
keins, da die Abfrage zu langsam läuft

001 SELECT DISTINCT "a"."Verfasser" || ' → ' || (002 SELECT COUNT( "vefID" ) FROM "Ansicht_Suche" WHERE "vefID" = "a"."vefID"
AND "ID" IN ( SELECT "ID" FROM "Filter_Form_Subform_3Filter" )003 ) || ' Treffer' AS "Ver", "a"."vefID"
004 FROM "Ansicht_Suche" AS "a"005 WHERE "a"."VerfSort" < 99 AND "a"."ID" IN ( SELECT "ID" FROM
"Filter_Form_Subform_3Filter" )006 ORDER BY "Ver" ASC
Diese Abfrage liefert die gleichen Ergebnisse wie
Listenfeld_Verfasser_bedingt_Datensaetze_optimiert, hat sich aber als zu langsam erwiesen.
Dies liegt daran, dass sich die Abfrage nicht nur auf eine Datenquelle mit sehr vielen Datensätzen bezieht ("Ansicht_Suche"), sondern auch noch über die Abfrage "Filter_Form_Subform_3Filter" noch einmal den Bezug auf die große Tabelle "Medien" nimmt. Auch solche misslungenen
Ansätze sollten dokumentiert werden. Schließlich funktioniert die Abfrage bei wenigen Datensätzen erst einmal problemlos. Das Erwachen kommt dann, wenn die Datenbank entsprechend
gefüllt ist.
Unter FIREBIRD bringt diese Abfrage LO insgesamt zum Absturz. Sie wurde daher direkt entfernt.

Listenfeld_Verfasser_bedingt_Datensaetze_optimiert
Datenquelle
Abfrage:

Filter_Ansicht

Suchen und Filtern163

Datenziel
Formular: Filter_Formular_Subformular_bedingende_Filter_Datensaetze

001 SELECT DISTINCT "Verfasser" || ' → ' || (002 SELECT COUNT( "vefID" ) FROM "Filter_Ansicht" WHERE "vefID" = "a"."vefID"
003 ) || ' Treffer' AS "Ver", "vefID"
004 FROM "Filter_Ansicht" AS "a"005 WHERE "VerfSort" < 99006 ORDER BY "Ver" ASC
Im Gegensatz zur Abfrage Listenfeld_Verfasser_bedingt_Datensaetze_langsam bezieht sichdiese Abfrage über "Filter_Ansicht" nur auf eine große Datenquelle, nämlich Ansicht_Suche.
Dadurch erhöht sich die Geschwindigkeit deutlich.
Die "Filter_Ansicht" selektiert die Daten bereits so weit vor, dass nur die passenden Datensätze
zur Verfügung stehen, die bei der Auswahl über das entsprechende Filter-Listenfeld auch nochzu Treffern führen. In Zeile 2 werden diese Treffer über eine korrelierende Unterabfrage ermittelt. Diese Unterabfrage bezieht sich über "a"."vefID" auf die äußere Abfrage, weil der Datenquelle "Filter_Ansicht" das Alias "a" zugewiesen wurde.
Wie in Listenfeld_Verfasser_bedingt werden auch hier alle Datensätze mit "VerfSort" '99' vonder Anzeige ausgeschlossen (Zeile 5).

Listenfeld_Verfasser_bedingt_Suche_Filter
Datenquelle
Tabelle:rel_Medien_Verfasser, Verfasser

Abfrage:

Suche_Filter_Form_Subform_3Filter

Datenziel
Formular: Suche_Filter_hierarchisch_mit_Makros

001 SELECT DISTINCT "Verfasser"."Verfasser", "Verfasser"."ID"002 FROM "rel_Medien_Verfasser", "Verfasser"
003 WHERE "rel_Medien_Verfasser"."vefID" = "Verfasser"."ID"004 AND "rel_Medien_Verfasser"."VerfSort" < 99005 AND "rel_Medien_Verfasser"."medID" IN
006 ( SELECT "ID" FROM "Suche_Filter_Form_Subform_3Filter" )007 ORDER BY "Verfasser"."Verfasser" ASC
Im Unterschied zu Listenfeld_Verfasser_bedingt bezieht sich diese Abfrage auf die Abfrage"Suche_Filter_Form_Subform_3Filter". Ansonsten ist die Funktion gleich.

Listenfeld_Verfasser_bedingt_Suche_Filter_Datensaetze
Datenquelle
Abfrage:

Filter_Such_Ansicht

Datenziel
Formular: Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros

001 SELECT DISTINCT "Verfasser" || ' → ' ||002 ( SELECT COUNT( "vefID" ) FROM "Filter_Such_Ansicht" WHERE "vefID" ="a"."vefID" )003 || ' Treffer' AS "Ver", "vefID"
004 FROM "Filter_Such_Ansicht" AS "a"Suchen und Filtern164005 WHERE "VerfSort" < 99006 ORDER BY "Ver" ASC
Im Unterschied zu Listenfeld_Verfasser_bedingt_Datensaetze_optimiert bezieht sich dieseAbfrage auf die Abfrage "Filter_Such_Ansicht". Ansonsten ist die Funktion gleich.

Formulare
Zur besseren Übersicht sind die Formulare teilweise in Unterordner verschoben worden. Die
entsprechenden Unterordner sind hier in den Kapiteln abgebildet.

Filter_ohne_Makros
Die hierin enthaltenen Formulare arbeiten komplett ohne Makros. Sie Filtern die Daten über Listenfelder nach den entsprechenden Fremdschlüsselwerten in der Tabelle "Medien".

Filter_Formular

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Filter_Form)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Listenfeld «Filter Kategorie» (Tabelle: Kategorien, Medien)
001 SELECT DISTINCT LEFT( "Kategorien"."System" || '', 7 ) || ' - ' ||LEFT("Kategorien"."Kategorie", 50)|| CASE WHEN
CHAR_LENGTH("Kategorien"."Kategorie") > 50 THEN '…' ELSE '' END
AS "Kat",002 "Kategorien"."ID"003 FROM "Kategorien", "Medien"
004 WHERE "Kategorien"."ID" = "Medien"."katID"
005 ORDER BY "Kat" ASC
Schriftart für dieses Listenfeld: Liberation Mono, Standard, 8. Durch die Schriftart mit gleichmäßiger Breite für jeden Buchstaben lässt sich ein Spalteneffekt in dem Listenfeld erzielen.

Suchen und Filtern165

Der Button Filtern ist mit der Aktion «Formular aktualisieren» verbunden. Durch die Betätigung
des Buttons wird das Formular 1 abgespeichert und das Formular 2 aktualisiert.
Die Filtertabelle soll jeweils nur mit neuen Daten in dem aktuellen Datensatz beschrieben werden. Die Formulareigenschaften sind deshalb wie im folgenden Screenshot eingestellt:

Formulareigenschaften des Formulars «Filter»

Suchen und Filtern

166Filter_Formular_Nullwerte

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Filter_Form)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_ohne_Eintrag)
Dieses Formular unterscheidet sich nur in Bezug auf das Listenfeld «Filter Kategorie» vom Filter_Formular.

Filter_Formular_Subformular

Suchen und Filtern167

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Filter_Form_Subform)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Mit Hilfe der Abfrage in Formular 2 ist es möglich, auch Angaben für das Feld «Verfasser» indem Unterformular 2.1 zu suchen.

Filter_Formular_Subformular_3Filter

1 Filter (Tabelle: Filter)
2

MainForm (Abfrage:
Filter_Form_Subform_3Filter)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_ohne_Eintrag)
Auch die Filterung über mehrere Filterfelder ist ohne Makros möglich. Nachteil ist allerdings,
dass andere Filter nicht an den bereits ausgewählten Filter angepasst werden. Das kann schnell
dazu führen, dass gar kein Datensatz mehr anzeigbar ist. So existieren beispielsweise zu deroben ausgewählten Verfasserin nur Bücher und eine Video-DVD in dem Bestand. Die zur Auswahl stehenden anderen Medienarten führen zu einem leeren Suchergebnis.

Suchen und Filtern168

Standardfilter_Formular_Subformular1

Filter (Ansicht:
Ansicht_Suche)

1.1 SubForm (Tabelle: Medien)

1.1.1

SubSubForm (Tabelle:
rel_Medien_Verfasser)Über den Standardfilter wird «Ansicht_Suche» im Formular 1 gefiltert. Das Ergebnis dieser Filterung wird an das Formular 1.1 weiter gegeben.
An der unten eingeblendeten Symbolleiste ist zu sehen, dass diese Filterung für die GUI nicht
so gut handhabbar ist. Die gesamte Symbolleiste ist ausgegraut. Dieses Formular kann nicht
zur Eingabe neuer Daten genutzt werden. Bestehende Daten lassen sich aber sehr wohländern.
Das Formular hat außerdem den Nachteil, dass die zugrundeliegende Ansicht für jeden Verfasser eines Mediums einen neuen Datensatz erstellt. Obwohl die beigefügte Tabelle "Medien" nur4992 Datensätze enthält werden vor der Filterung 6106 Datensätze angegeben. Es gibt eben
Medien mit mehreren Verfassern, so dass auch bei der Filterung ein Medium mehrmals vorkommen kann.

Suchen und Filtern169

Standardfilter_Formular_Subformular_Datensatzkorrektur

1

Filter (Ansicht:
Ansicht_Standardfilter)

1.1 SubForm (Tabelle: Medien)

1.1.1

SubSubForm (Tabelle:
rel_Medien_Verfasser)

Dieses Formular startet bei der internen Hsqldb deutlich langsamer als das Formular Standardfilter_Formular_Subformular. Dafür zeigt die Ansicht aber die korrekte Anzahl der Datensätze
an. Dies liegt daran, dass die Verfasser, die zu einem Datensatz in der Tabelle "Medien" gehören, in einem Feld zusammengefasst wurden.
Auch bei diesem Formular gilt allerdings: Eine Datenänderung ist möglich, eine Neueingabe
von Daten nicht.

Filter_mit_Makros
In diesen Formularen läuft die Filterung direkt nach der Auswahl in den Listenfeldern ab. Die
Betätigung eines Buttons ist nicht mehr notwendig.

Suchen und Filtern170Filter_Formular

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Filter_Form)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Filter Kategorie» (Modifiziert)

Module1.Mauszeiger

Listenfeld «Filter Kategorie» (Tabelle: Kategorien, Medien)
001 SELECT DISTINCT LEFT( "Kategorien"."System" || '', 7 ) || ' - ' ||LEFT("Kategorien"."Kategorie", 50)|| CASE WHEN
CHAR_LENGTH("Kategorien"."Kategorie") > 50 THEN '…' ELSE '' END AS"Kat",
002 "Kategorien"."ID"003 FROM "Kategorien", "Medien"
004 WHERE "Kategorien"."ID" = "Medien"."katID"
005 ORDER BY "Kat" ASC
Schriftart für dieses Listenfeld: Liberation Mono, Standard, 8. Durch die Schriftart mit gleichmäßiger Breite für jeden Buchstaben lässt sich ein Spalteneffekt in dem Listenfeld erzielen.
Wesentlicher Unterschied zu den Formularen ohne Makros ist erst einmal, dass der Button zum
Auslösen der Aktion fehlt Nach dem Verlassen desSuchen und Filtern

171Filter_Formular_direkt_Nullwerte

1 Filter (Abfrage: Filter_Leerfeld)
2 MainForm (Abfrage: Filter_Form_Nullwerte)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Filter Kategorie» (Modifiziert)

Module1.Filter_Formular_direkt

Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_ohne_Eintrag)
Bei diesem Formular können nicht nur die Felder, die keinen Eintrag haben, zusätzlich ausgefiltert werden. Hier wird der Filter für das Formular 2 «MainForm» direkt in das Formular geschrieben. Deswegen ist der Filter auch über die Navigationsleiste direkt ein- und ausschaltbar. In
dem obigen Screenshot ist der Filter für den Kategorieeintrag 'Aal' gerade aktiv. Würde jetzt
das aktive Filtersymbol gedrückt, dann wären statt der 37 aktiv erreichbaren Datensätze wieder alle Datensätze erreichbar.

Suchen und Filtern172Filter_Formular_Listfeldbegrenzung

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Filter_Form)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Filter Kategorie» (Modifiziert)

Module1.Mauszeiger

1

Feld «Filter Kategorie» (Taste losgelassen)

Module2.ListFilter

Manchmal ist ein Listenfeld einfach zu umfangreich für die Anzeige aller Daten. In diesem Formular kann deshalb über die Eingabe von Anfangsbuchstaben der Bereich für die angezeigten
Kategorien eingeschränkt werden. Hier werden von den 2482 Kategorien nur die 3 Kategorien
angezeigt, die mit dem Anfangsbuchstaben 'C' beginnen. Dabei ist es unerheblich, ob mit Kleinbuchstaben oder Großbuchstaben gefiltert wird. Auch die Eingabe von mehreren Buchstaben istmöglich, führt aber schnell zu einem komplett leeren Listenfeld für die Filterung. Aus dieser
misslichen Lage geht es über ESC wieder zurück.
Der Code für das Listenfeld «Filter Kategorie» ist hier so gehalten, dass er anschließend möglichst einfach über das Makro zur Filterung ergänzt werden kann:001 SELECT "Tab".* FROM
002
(SELECT DISTINCT "Kategorien"."Kategorie" AS "Field",
003"Kategorien"."ID",004"Kategorien"."Kategorie" AS "Sort"
005
FROM "Kategorien", "Medien"006
WHERE "Kategorien"."ID" = "Medien"."katID")
007 AS "Tab"008 ORDER BY "Sort" ASC
Die eigentliche Abfrage zur Darstellung der Listeninhalte befindet sich in einer Unterabfrage.
So können dort entsprechende Bedingungen wie hier z.B. die Beziehungen der Tabellen unter
gebracht werden ohne die Bedingung der Filterung zu beeinflussen. Für das Makro werden zwei
Felder mit festliegenden Alias benannt: «Field» ist das Feld, das gefiltert werden soll und «Sort»
das Feld, nach dem die Sortierung erfolgt. Im Makro kann dann einfach der Teil, der vor dem
Tabellenalias «AS "Tab"» steht, ermittelt und gefiltert werden.
Suchen und Filtern173

Filter_Formular_Subformular

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Filter_Form_Subform)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Filter Verfasser» (Modifiziert)

Module1.Mauszeiger

Wie im entsprechenden Formular ohne Makros lässt sich natürlich auch in diesem Formular mit
Makroeinsatz der Inhalt aus dem Unterformular gleich mit filtern. Hier also gerade eine Filterung des Formulars nach dem Verfasser-Eintrag 'Abendroth, Wolfgang'.

Suchen und Filtern174Filter_Formular_Subformular_bedingende_Filter

1 Filter (Tabelle: Filter)
2

MainForm (Abfrage:
Filter_Form_Subform_3Filter)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Filter Kategorie» (Modifiziert)

Module1.Filter_bedingt

1

Feld «Filter Verfasser» (Modifiziert)

Module1.Filter_bedingt

1

Feld «Filter Medienart» (Modifiziert)

Module1.Filter_bedingt

Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_bedingt)
Listenfeld «Filter Verfasser» (Abfrage: Listenfeld_Verfasser_bedingt)
Listenfeld «Filter Medienart» (Abfrage: Listenfeld_Medienart_bedingt)
Bei der Filterauswahl kann mit einem beliebigen Listenfeld gestartet werden. Jedes Listenfeld
löst zum einen eine Filterung des Formulars 2 aus, zum anderen auch eine Auffrischung deranderen Listenfelder. Dadurch stehen in den weiteren Listenfeldern nur noch Filtereinträge zur
Auswahl, zu denen auch wirklich ein Datensatz existiert. So gibt es denn in dieser Datenbank
zu der Verfasserin 'Lindgren, Astrid' nur Bücher und Video-DVDs, nicht aber Zeitschriften oder
Spiele, wie sie in dem Formular ohne Makros, Filter_Formular_Subformular_3Filter, noch zur
Auswahl angeboten werden.

Suchen und Filtern

175Filter_Formular_Subformular_bedingende_Filter_allround
Makros in Feldeigenschaften
1

Feld «Filter Kategorie» (Modifiziert)

Module1.Filter_bedingt_allround1

Feld «Filter Verfasser» (Modifiziert)

Module1.Filter_bedingt_allround1

Feld «Filter Medienart» (Modifiziert)

Module1.Filter_bedingt_allround

Dieses Formular unterscheidet sich von Filter_Formular_Subformular_bedingende_Filter durch
das verarbeitende Makro. Um eine allgemeine Verarbeitung der Filterung zu erledigen wird inden Listenfeldern in den Zusatzinformationen ein Text hinterlegt:
Listenfeld «Filter_Kategorie» Allgemein → Zusatzinformation: lboFilter2,lboFilter3
Listenfeld «Filter_Verfasser» Allgemein → Zusatzinformation: lboFilter1,lboFilter3
Listenfeld «Filter_Medienart» Allgemein → Zusatzinformation: lboFilter1,lboFilter2
In den Zusatzinformationen befindet sich also lediglich die Information, unter welchem Namen
die anderen beiden Listenfelder im Formular zu erreichen sind.

Filter_Formular_Subformular_bedingende_Filter_Datensaetze

1 Filter (Tabelle: Filter)
2

MainForm (Abfrage:
Filter_Form_Subform_3Filter)

Suchen und Filtern

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

176

Makros in Feldeigenschaften
1

Feld «Filter Kategorie» (Modifiziert)

Module1.Filter_bedingt

1

Feld «Filter Verfasser» (Modifiziert)

Module1.Filter_bedingt

1

Feld «Filter Medienart» (Modifiziert)

Module1.Filter_bedingt

Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_bedingt_Datensaetze)
Listenfeld «Filter Verfasser» (Abfrage: Listenfeld_Verfasser_bedingt_Datensaetze_optimiert)
Listenfeld «Filter Medienart» (Abfrage: Listenfeld_Medienart_bedingt_Datensaetze)
Zusätzlich zu den Funktionen von Filter_Formular_Subformular_bedingende_Filter bietet dieses
Formular eine Übersicht über die jeweilige Anzahl an vorhandenen Datensätze für die aktuelle
Filterung an.

Suche_ohne_Makros
In das Suchfeld wird ein Begriff eingegeben. Groß- und Kleinschreibung wird, wie bei Suchmaschinen, nicht unterschieden. Der Suchbegriff kann an irgendeiner Stelle des zu durchsuchenden Feldes stehen. Die Suche wird über den entsprechenden Button gestartet.

Suche_Formular

1 Suche (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Form)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

In das Textfeld «Suche» wird ein Begriff eingegeben. Der Button Suchen speichert diesen
Begriff durch Verlassen des Formulars «Suche» und aktualisiert das Formular «MainForm», indem er liegt. Die Suche beschränkt sich auf das Feld «Titel», ergibt also in dem obigen Beispiel
insgesamt 4 Datensätze wieder, bei denen im Titel an irgendeiner Stelle der Begriff 'lind' unabhängig von Groß- und Kleinschreibung vorkommt.

Suchen und Filtern177Suche_Formular_Subformular

1 Suche (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Form_Subform)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Gegenüber dem Formular Suche_Formular ist die Suchmöglichkeit auf weitere Felder ausgedehnt worden. Die Suche findet jetzt durch die entsprechende Abfrage auch Begriffe im Unterformular, die zu dem Suchbegriff 'lind' passen. Die Datensatzzahl steigt dadurch auf insgesamt
42.

Suchen und Filtern178

Parameter_Suche_Formular_Subformular1

Suche (Tabelle: Filter)

1.1

SubForm (Abfrage: Parametersuche_Medien)

1.1.1

SubSubForm (Tabelle:
rel_Medien_Verfasser)

Die Verknüpfung des Formulars «Suche» zum Formular «SubForm» erfolgt über alle Parameter,
die in der Parameterabfrage vertreten sind.
Die freien Texteingaben suchen nur in dem jeweils angegebenen Feld. Die Suche wird überSuchen abgeschickt.
Grundsätzlich ist es möglich, auch alle Felder mit einem Parameter zu versorgen. Auch könnte
eine Parameterabfrage über entsprechende Listenfelder zum Filtern genutzt werden. Beide
Ziele sind hier nicht weiter verfolgt worden, da die Parameterabfrage gegenüber anderen
Abfragen nur dann ihre Vorteile ausspielen kann, wenn sie tatsächlich direkt als Abfrage
genutzt wird und die Parameter in den entsprechenden Dialog eingegeben werden.

Suche_mit_Makros
Die Suche mit Makros läuft wieder ohne einen Button ab. Sobald mehr als 2 Zeichen Text angegeben werden, wird in dem Formular das Suchergebnis eingeblendet. Dies kann dann durch
weitere Eingaben noch verfeinert werden. Soll die Suche wieder beim vollen Datenbestand
beginnen, so muss nur das Feld geleert werden.

Suchen und Filtern179

Suche_Formular

1 Suche (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Form)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Suche» (Text modifiziert)

Module1.Filter_Suche

Der einzige Unterschied zu Suche_Formular ohne Makros ist der, dass hier der Einsatz eine Buttons nicht nötig ist. Es reicht, in das Eingabefeld für «Suche» 3 Zeichen einzugeben. Danach
wird das Ergebnis sofort in dem Formular sichtbar und kann weiter verfeinert werden.

Suchen und Filtern180

Suche_Formular_Subformular

1 Suche (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Form_Subform)

2.1 SubForm (Tabelle: rel_Medien_Verfasser)

Makros in Feldeigenschaften
1

Feld «Suche» (Text modifiziert)

Module1.Filter_Suche

Auch bei diesem Formular gilt: Das Formular unterscheidet sich von Suche_Formular_Subformular in der Variante ohne Makros nur durch die Möglichkeit, ohne einen Button das entsprechende Suchergebnis anzuzeigen. Dabei wird ein Suchergebnis erst dann angezeigt, wenn mindestens 3 Buchstaben in das Feld eingegeben wurden. Das Ergebnis wird während der Eingabe
präsentiert.

Suchen und Filtern181

Suche_Filter_Formular_Subformular_ohne_Makros

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Filter_Form_Subform_3Filter)

2.1

SubForm (Tabelle:
rel_Medien_Verfasser)

Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_ohne_Eintrag)
Hier wird die freie Inhaltssuche mit der Filterung durch feste Vorgaben aus den Listenfeldern
kombiniert. Ohne Makrozusatz zeigen allerdings die Listenfelder immer den gesamten verfügbaren Inhalt an. So kann eine entsprechende Recherche schnell dazu führen, dass keine Datensätze angezeigt werden. Im aktuellen Fall wurde im Unterformular 2.1 der Verfasser 'Kästner,
Erich' gefunden. Bei der Kategorie wurden dann dessen Werke auf die Kategorie 'Pgr 2 ...' eingegrenzt.

Suchen und Filtern182

Suche_Filter_hierarchisch_mit_Makros

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Filter_Form_Subform_3Filter)

2.1

SubForm (Tabelle:
rel_Medien_Verfasser)

Makros im Formulardokument
Extras → Anpassen → Ereignisse → Ansicht wurde
erzeugt

Module1.Filter_Suche_hierarchisch_Start

Makros in Feldeigenschaften
1

Feld «Suchbegriff» (Bei Fokusverlust)

Module1.Filter_Suche_hierarchisch1Feld «Filter Medienart» (Modifiziert)

Module1.Filter_Suche_hierarchisch1Feld «Filter Kategorie» (Modifiziert)

Module1.Filter_Suche_hierarchisch1Feld «Filter Verfasser» (Modifiziert)

Module1.Filter_Suche_hierarchischListenfeld «Filter Medienart» (Abfrage: Listenfeld_Medienart_bedingt_Suche_Filter)
Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_bedingt_Suche_Filter)
Listenfeld «Filter Verfasser» (Abfrage: Listenfeld_Verfasser_bedingt_Suche_Filter)
Um eine Aktivierung der Filterfelder zu erledigen wird in den Listenfeldern in den Zusatzinformationen ein Text hinterlegt:
Textfeld «Suchbegriff» Allgemein → Zusatzinformation: lboFilter1,lboFilter2
Listenfeld «Filter_Medienart» Allgemein → Zusatzinformation: lboFilter2,lboFilter3
Suchen und Filtern183

Listenfeld «Filter_Kategorie» Allgemein → Zusatzinformation: lboFilter3
Listenfeld «Filter_Verfasser» Allgemein → Zusatzinformation: kein Eintrag
Das Formular unterscheidet sich in den folgenden Punkten vom Formular Suche_Filter_Formular_Subformular_ohne_Makros:•

Die Suche und Filterung läuft automatisch ab und ist nicht an einen Button gebunden.

•

Jede Eingabe in dem Such- und Filterblock löst sofort die Filterung auf.

•

Jede Eingabe schränkt die weiteren Eingaben ein. Dies gilt allerdings nicht für die freie
Eingabe von Text in dem Textfeld «Suchbegriff».

•

Die Listenfelder sind hierarchisch aufgebaut. Das bedeutet, dass nur von oben nach
unten gewählt werden kann. Die Folgefelder sind anfangs inaktiv. Erst nach einer Auswahl im Listenfeld «Medienart» wird das Listenfeld «Kategorie» für eine Eingabe aktiviert.

Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros

1 Filter (Tabelle: Filter)
2 MainForm (Abfrage: Suche_Filter_Form_Subform_3Filter)

2.1

SubForm (Tabelle:
rel_Medien_Verfasser)

Makros im Formulardokument
Extras → Anpassen → Ereignisse → Ansicht wurde
erzeugt

Suchen und Filtern

Module1.Filter_Suche_hierarchisch_Start

184

Makros in Feldeigenschaften
1

Feld «Suchbegriff» (Bei Fokusverlust)

Module1.Filter_Suche_hierarchisch_allround1

Feld «Filter Medienart» (Modifiziert)

Module1.Filter_Suche_hierarchisch_allround1

Feld «Filter Kategorie» (Modifiziert)

Module1.Filter_Suche_hierarchisch_allround1

Feld «Filter Verfasser» (Modifiziert)

Module1.Filter_Suche_hierarchisch_allround

Listenfeld «Filter Medienart» (Abfrage:
Listenfeld_Medienart_bedingt_Suche_Filter_Datensaetze).
Die folgenden Listenfelder beziehen sich zwar auf die entsprechenden Abfragen. Zum Formularstart ist die Abfrage aber leer. Dadurch wird das Feld erst geladen, wenn es aktiv wird:
Listenfeld «Filter Kategorie» (Abfrage: Listenfeld_Kategorie_bedingt_Suche_Filter_Datensaetze)
Listenfeld «Filter Verfasser» (Abfrage: Listenfeld_Verfasser_bedingt_Suche_Filter_Datensaetze)
Um eine Aktivierung der Filterfelder zu erledigen und die nicht aktivierten Felder mit einer
Datenquelle zu verbinden wird in den Listenfeldern in den Zusatzinformationen ein Text hinterlegt:
Textfeld «Suchbegriff» Allgemein → Zusatzinformation:
lboFilter1>Listenfeld_Medienart_bedingt_Suche_Filter_Datensaetze,lboFilter2
Listenfeld «Filter_Medienart» Allgemein → Zusatzinformation:
lboFilter2>Listenfeld_Kategorie_bedingt_Suche_Filter_Datensaetze,lboFilter3
Listenfeld «Filter_Kategorie» Allgemein → Zusatzinformation:
lboFilter3>Listenfeld_Verfasser_bedingt_Suche_Filter_Datensaetze
Listenfeld «Filter_Verfasser» Allgemein → Zusatzinformation: kein Eintrag
Das Formular unterscheidet sich in den folgenden Punkten vom Formular Suche_Filter_hierarchisch_mit_Makros:•

Neben den normalen Informationen in den Listenfeldern werden auch die möglichen
Treffer angezeigt.

•

Die inaktiven Listenfelder werden erst dann mit einer Datenquelle versehen, wenn sie
aktiviert werden. Dadurch muss zum Start des Formulars weniger an Daten geladen
werden.

Berichte
Die Datenbank enthält keine Berichte.

Makros
Die Makros sind in 2 Modulen untergebracht. In Modul 1 befinden sich alle Makros zum Filtern
und Suchen im Formular. Modul 2 enthält nur die Prozedur, die bei einem Listenfeld die Anzahl
der anzuzeigenden Datensätze bei der Eingabe begrenzt.

FilterAufruf aus
Formular: Filter_Formular, Filter_Formular_Listfeldbegrenzung, Filter_Formular_Subformular
001
002
003
004
005
006

SUB Filter
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECTDIM oFeldList AS OBJECT

Suchen und Filtern

185

007
008
009
010
011
012
013
014
015

oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm1 = oDrawpage.forms.getByName("Filter")oForm2 = oDrawpage.forms.getByName("MainForm")oFeldList = oForm1.getByName("lboFilter")oFeldList.commit()oForm1.updateRow()oForm2.reload()
END SUBDie Zugriffe zu den beiden nebeneinander stehenden Formularen werden definiert (Zeile 9 und
10). Das Listenfeld für die Filterung mit dem Namen «lboFilter» liegt im Formular «Filter». Dereingestellte Wert des Listenfeldes wird in dem aktuellen Datensatz des Formulars durchcommit() abgelegt (Zeile 12). Er muss jetzt noch in den Datensatz geschrieben werden. Diesgeschieht durch updateRow() im entsprechenden Formular. Es handelt sich hier um ein Update,da ja keine neuen Datensätze geschrieben werden, sondern nur der (einzige) Datensatz mit
dem Primärschlüssel "ID" = TRUE geändert wird.
Nach dem Abspeichern wird das Formular, das das Ergebnis der Filterung anzeigen soll, mit
reload() neu eingelesen (Zeile 14).

Filter_Formular_direkt
Aufruf aus
Formular: Filter_Formular_direkt_Nullwerte
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020

SUB Filter_Formular_direkt
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECTDIM oFeldList AS OBJECT
DIM stListValue AS STRING
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm1 = oDrawpage.forms.getByName("Filter")oForm2 = oDrawpage.forms.getByName("MainForm")oFeldList = oForm1.getByName("lboFilter")stListValue = oFeldList.getCurrentValue()
IF stListValue = "" THEN
oForm2.Filter = ""ELSEoForm2.Filter = "kat = '" + stListValue + "'"END IFoForm2.reload()
END SUB

Durch die Erstellung eines direkten Filters in den Formulareigenschaften wird es möglich, den
Filter auch über die Navigationsleiste des Formulars an- und auszustellen.
Auch hier wird wieder zuerst der Zugriff auf die Formulare und das Listenfeld erstellt. Anschließend wird der Wert des Listenfeldes ausgelesen. Dies ist nicht der angezeigte Text, sondern(seit LO 4.1) der Inhalt, der an die dem Formular zugrundeliegende Tabelle weiter gegeben werden soll (Zeile 13). In diesem Falle handelt es sich also um den Primärschlüssel der Tabelle"Kategorie".
Wird im Listenfeld das leere Feld gewählt (Listenfeld ganz oben), so ist auch der weitergegebene Inhalt leer. Da die Variable als Text deklariert wurde reicht hier ein stListValue = "", umdiesen Zustand zu erkennen. Bei einem leeren Filterwert soll der Filter ausgeschaltet werden:oForm2.Filter = "".
Wird im Listenfeld ein Eintrag gewählt, so wird der entsprechende Schlüssel an die Filterformulierung weiter gegeben. Der gesamte Filter lautet dann z.B. "kat = '13'". Nach Einstellung

Suchen und Filtern186

des Filters wird das Hauptformular wieder neu geladen (Zeile 19). Damit wird der Filter übernommen.

Filter_bedingt
Aufruf aus
Formular: Filter_Formular_Subformular_bedingende_Filter, Filter_Formular_Subformular_bedingende_Filter_Datensaetze
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024

SUB Filter_bedingt
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECTDIM oFeldList1 AS OBJECTDIM oFeldList2 AS OBJECTDIM oFeldList3 AS OBJECT
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm1 = oDrawpage.forms.getByName("Filter")oForm2 = oDrawpage.forms.getByName("MainForm")oFeldList1 = oForm1.getByName("lboFilter1")
oFeldList2 = oForm1.getByName("lboFilter2")
oFeldList3 = oForm1.getByName("lboFilter3")
oFeldList1.commit()
oFeldList2.commit()
oFeldList3.commit()oForm1.updateRow()oFeldList1.refresh()oFeldList2.refresh()oFeldList3.refresh()oForm2.reload()
END SUB

Dieses Makro ersetzt den Aktualisierungsbutton in dem zu filternden Formular. Die Einstellungeines Filters beeinflusst gleichzeitig die anzuzeigenden Inhalte der anderen Filter. Dadurch wird
die Auswahl der anderen Filter auf die möglichen Treffer reduziert.
Die Variablen werden deklariert und der Zugang zum Filter-Formular, jedem einzelnen Listenfeld sowie dem Formular, das das Filterergebnis darstellen soll erstellt (Zeile 9 bis 15). Unabhängig davon, welcher Filter ausgelöst wurde, werden von allen Filtern die Werte in die dem
Formular zugrundeliegende Filtertabelle übertragen: commit(). Anschließend wird der geänderte Datensatz abgespeichert: updateRow(). Danach können die Listenfelder neu eingelesen
werden, da ihr Inhalt auf die Einträge in der Filtertabelle zugreift: refresh(). Schließlich muss
nur noch das Formular zur Darstellung des Filterergebnisses mit reload() neu geladen werden.

Filter_bedingt_allround
Aufruf aus
Formular: Filter_Formular_Subformular_bedingende_Filter_allround
001
002
003
004
005
006
007
008
009
010

SUB Filter_bedingt_allround(oEvent AS OBJECT)
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECTDIM oFeld AS OBJECT
DIM stTag AS String
DIM arList()
DIM i AS INTEGER
oFeld = oEvent.Source.Model

Suchen und Filtern

187

011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026

oForm1 = oFeld.Parent
stTag = oFeld.Tag
arList = Split(stTag, ",")
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm2 = oDrawpage.forms.getByName("MainForm")oFeld.commit()oForm1.updateRow()
FOR i = LBound(arList()) TO UBound(arList())
oForm1.getByName(arList(i)).refresh()NEXT
IF IsEmpty(oFeld.currentValue) THEN
oFeld.refresh()
END IFoForm2.reload()
END SUB

Mit diesem Makro werden die anderen Listenfelder vom Inhalt her neu eingelesen, wenn ein
Listenfeld von der Auswahl her geändert wurde. Das Listenfeld, dessen Auswahl geändert
wurde, wird nur dann neu eingelesen, wenn das leere Feld ausgewählt wurde, also nur eine
Auswahl komplett zurück genommen wurde.
Bereits der Aufruf des Makros berücksichtigt, von welchem Listenfeld aus das Signal kam:oEvent AS OBJECT. Anschließend werden die Variablen deklariert. Besonders hinzuweisen ist
hier auf die Deklaration des Arrays, die im voraus erfolgen muss: arList().
Das Feld wird über das auslösende Ereignis ermittelt (Zeile 10). Das entsprechende Formular ist
direkt dem Feld übergeordnet: oFeld.Parent. Entsprechend muss auch der Name des Formulars nicht mehr gesondert im Makro erscheinen.
Aus den Zusatzinformationen sollen die Bezeichnungen für die Listenfelder ausgelesen werden,
die aktualisiert werden sollen. Die Zusatzinformationen sind gespeichert in oFeld.Tag. Die
Bezeichnungen der Listenfelder werden anhand des Kommas voneinander getrennt und in das
bereits definierte Array eingelesen: arList = Split(stTag, ","). Hier wird davon ausgegangen, dass die Listenfeldbezeichnungen nur durch ein Komma getrennt werden. Sonst müssten
gegebenenfalls noch Leerstellen mit Hilfe von Trim() entfernt werden.
Des weiteren erfolgt noch ein Zugriff auf das Formular, das die gefilterten Daten darstellen soll
(Zeile 14 bis 16). Dieses Formular muss zum Schluss neu geladen werden (Zeile 25).
Das Listenfeld gibt seinen Wert ab (commit()). Die Änderung in dem Formular, in dem das Listenfeld steht, wird abgespeichert.
Anschließend wird in einer Schleife das Array durchgegangen (Zeile 19 bis 21).
LBound(arList()) zeigt den unteren Wert '0' des Arrays an. Ubound(arList()) verweist auf
den höchsten Wert, im praktischen Beispiel '1'.
Jede Bezeichnung für ein Listenfeld wird eingelesen. So präsentiert arList(0) das erste Listenfeld aus den Zusatzinformationen, aus dem obigen Screenshot also lboFilter2, arList(1) ist
gleich lboFilter3. Das entsprechende Feld wird durch refresh() neu eingelesen.
Schließlich wird noch nachgesehen, ob der Wert für das aktuelle Listenfeld, den Auslöser für
das Ereignis, jetzt leer ist. Nur dann soll auch das aktuelle Listenfeld neu eingelesen werden(Zeile 22 bis 24). Ansonsten kann einfach ein anderer Wert aus der alten Liste gewählt werden,
falls der erste Wert nicht zum Erfolg führte.

Filter_Suche
Aufruf aus
Formular: Suche_Formular, Suche_Formular_Subformular
001
002

SUB Filter_Suche(oEvent AS OBJECT)
DIM oDoc AS OBJECT

Suchen und Filtern

188

003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018

DIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECT
DIM stFeld AS STRING
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm2 = oDrawpage.forms.getByName("MainForm")oFeld = oEvent.Source.Model
stFeld = oFeld.CurrentValueIF Len(stFeld) > 2 OR Len(stFeld) > 0
OR oFeld.ImplementationName = "com.sun.star.form.OListBoxModel" THEN
oForm1 = oFeld.Parent
oFeld.commit()oForm1.updateRow()oForm2.reload()
END IF
END SUB

Mit diesem Makro wird aus einem Suchfeld heraus eine Suche vorgenommen, wenn entsprechend viele Buchstaben in dem Feld vorhanden sind. Die Aktualisierung ist fortlaufend. Die Verzweigung zum Ablauf der Aktualisierung der Suche läuft dann ab, wenn das Suchfeld mehr als
2 Buchstaben enthält oder die enthaltenen Buchstaben alle entfernt wurden (Zeile 12 bis 17).
Damit das Makro auch mit Listenfeldern zur Filterung genutzt werden kann werden die Listenfelder von dieser Begrenzung ausgenommen (Zeile 12).
Der Kontakt zum Formular, das das Suchergebnis darstellen soll, wird auf die übliche Weiseüber die Drawpage erstellt.
Der Kontakt zum Suchformular und zu dem Eingabefeld entsteht durch das auslösende Ereignis. Dadurch wird das Feld und das Formular ermittelt. Mit commit() wird der Wert in die Tabelle"Filter" übertragen, mit updateRow() wird der geänderte Datensatz gespeichert. Anschließend
wird das Formular zur Darstellung des Suchergebnisses über reload() neu geladen.

Filter_Suche_hierarchischAufruf aus
Formular: Suche_Filter_hierarchisch_mit_Makros
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026

SUB Filter_Suche_hierarchisch(oEvent AS OBJECT)
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECTDIM oFeld AS OBJECT
DIM stTag AS String
DIM arList()
DIM i AS INTEGER
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm2 = oDrawpage.forms.getByName("MainForm")oFeld = oEvent.Source.ModeloFeld.commit()oForm1 = oFeld.Parent
oForm1.updateRow()
stTag = oFeld.Tag
arList = Split(stTag, ",")
FOR i = LBound(arList()) TO UBound(arList())
IF IsEmpty(oFeld.currentValue) THEN
oForm1.getByName(arList(i)).Enabled = FALSE
ELSEoForm1.getByName(arList(0)).Enabled = TRUEEND IFoForm1.getByName(arList(i)).BoundField.UpdateNULL()oForm1.updateRow()

Suchen und Filtern

189

027
028
029
030

oForm1.getByName(arList(i)).refresh()NEXToForm2.reload()
END SUBDie Verbindungen zu den Formularen und zum aktuellen Formularfeld werden wie bereits in den
vorhergehenden Makros erstellt. Das aktuelle Feld wird abgespeichert (Zeile 14 bis 16). Der
Inhalt aus den Zusatzinformationen (Tag) wird in das Array «arList» eingelesen. Anschließend
wird das Array in einer Schleife durchlaufen (Zeile 19 bis 28).
Wenn das leere Feld gewählt wurde, dann wird für jeden Eintrag in der Arrayliste das Listenfeld
deaktiviert: Enabled = FALSE. Wenn das gewählte Feld nicht leer ist wird das erste Feld aus
dem Array aktiviert: Enabled = TRUE. Das zweite Feld bleibt hingegen deaktiviert.
Für alle Listenfelder aus dem Array wird der Datenbestand aus dem Formular entfernt:
UpdateNull(). Danach wird der Datensatz gespeichert und das Listenfeld neu eingelesen, alsoohne Wert dargestellt (Zeile 26 und 27). So kann mit dem aktuellen Listenfeld auch eine gerade
erstellte Wahl wieder geändert werden, ohne dass dadurch plötzlich kein Datensatz mehr in derErgebnismenge vorhanden ist.
Schließlich wird das zu filternde Formular neu geladen.

Filter_Suche_hierarchisch_Start
Aufruf aus
Formular: Suche_Filter_hierarchisch_mit_Makros,
Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016

FilterSUB Filter_Suche_hierarchisch_Start
DIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECT
DIM stSql AS STRING
oDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "DELETE FROM ""Filter"""oSQL_Anweisung.executeUpdate(stSql)
stSql = "INSERT INTO ""Filter"" (""ID"") VALUES (True)"oSQL_Anweisung.executeUpdate(stSql)
END SUBDie Einstellungen in der Tabelle "Filter" werden standardmäßig gespeichert und stehen beim
nächsten Start des Formulars wieder zur Verfügung. Mit diesem Makro werden die Filtereinstellung beim Start eines Formulars gelöscht, damit die Filterung komplett neu beginnt.
Zuerst wird die Datenquelle für das aktuelle Datenbankdokument aufgesucht. Interne Formulare einer Base-Dokumentes erzeugen beim Öffnen automatisch eine Verbindung zu der Datenquelle. Besteht bisher keine Verbindung, so wird anschließend eine Verbindung zur Datenbank
hergestellt (Zeile 6 bis 9).
Jede SQL-Anweisung muss über createStatement() zuerst vorbereitet werden. Der Inhalt für
die Anweisung wird anschließend in einer Variablen gespeichert. Anschließend wird die Variable
mit dem Befehl executeUpdate() an die Datenbank weiter gegeben. Eine Ausgabe der Daten
wird nicht erwartet.

Suchen und Filtern190Mit DELETE FROM "Filter" werden alle Datensätze in der Tabelle "Filter" gelöscht. Im Makrosind hier doppelte Anführungszeichen um den Tabellennamen gesetzt, damit die einfachen
Anführungszeichen in dem Kommando weiter gegeben werden. Anführungszeichen werden mit
Anführungszeichen maskiert.
Mit INSERT INTO "Filter" ("ID") VALUES (TRUE) wird der für die Filterung benötigte Datensatz wieder erstellt. Einziger Eintrag ist hier der Wert für das Primärschlüsselfeld, der bei derFilterung existieren muss.

Filter_Suche_hierarchisch_allround
Aufruf aus
Formular: Suche_Filter_hierarchisch_allround_Datensaetze_mit_Makros
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040

SUB Filter_Suche_hierarchisch_allround(oEvent AS OBJECT)
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECTDIM oFeld AS OBJECT
DIM stTag AS String
DIM arList()
DIM arTmp()
DIM stSql(0) AS STRING
DIM i AS INTEGER
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm2 = oDrawpage.forms.getByName("MainForm")oFeld = oEvent.Source.ModeloFeld.commit()oForm1 = oFeld.Parent
oForm1.updateRow()
stTag = oFeld.Tag
arList = Split(stTag, ",")
FOR i = LBound(arList()) TO UBound(arList())
IF i = 0 THEN
arTmp = Split(arList(0), ">")
arList(0) = arTmp(0)stSql(0) = arTmp(1)END IFIF IsEmpty(oFeld.currentValue) THEN
stSql(0) = ""oForm1.getByName(arList(i)).Enabled = FALSE
oForm1.getByName(arList(i)).ListSource = stSql
ELSEoForm1.getByName(arList(0)).Enabled = TRUE
oForm1.getByName(arList(0)).ListSource = stSql
END IFoForm1.getByName(arList(i)).BoundField.UpdateNULL()oForm1.updateRow()oForm1.getByName(arList(i)).refresh()NEXToForm2.reload()
END SUB

Der Grundaufbau dieser Prozedur entspricht erst einmal der Prozedur «Filter_Suche_hierarchisch». Bereits bei der Deklaration kommen zwei Arrays als Variablen hinzu. Das ArrayarTmp() nimmt nur zwischendurch das Ergebnis der Funktion Split() auf und gibt die Teile
entsprechend weiter. Das Array stSql() wird von vornherein als ein Array definiert, das nur
einen Datensatz enthält, der außerdem ein STRING sein muss (Zeile 9 und 10).
Die Inhalte aus den Zusatzinformationen werden, wie bei «Filter_Suche_hierarchisch», ausgelesen und in einer Schleife abgearbeitet. Nur beim ersten Durchgang der Schleife, wenn also i =Suchen und Filtern191

0, wird das erste Element des Arrays arList() erneut gesplittet (Zeile 22 bis 26), jetzt aber bei
dem Zeichen «>». Der erste Teil enthält dann den Namen des Listenfeldes, z.B. 'lboFilter2'. Der
zweite Teil enthält den Namen der Abfrage, die das Listenfeld mit den auszuwählenden Datenversehen soll. Der zweite Teil ist der einzige Eintrag, der in das Array stSql als stSql(0) eingetragen wird.
Ist der ausgewählte Inhalt aus dem Listenfeld leer, so soll auch der Name der Datenquelle wieder gelöscht werden. Daher steht hier stSql(0) = "". Ein Listenfeld, das anschließend deaktiviert wird, braucht nicht den gesamten Datenbestand wieder einzulesen. Mit ListSource =stSql wird das gesamte Array als Datenquelle angegeben. ListSource erwartet ein Array vom
Typ STRING – als Hinweis auf eine Abfrage, als Abfrage direkt, als Datensammlung …
Ist ein Inhalt in dem aktuellen Listenfeld ausgewählt worden (oFeld.currentValue ist nichtleer), so wird das in der Hierarchie folgende Listenfeld aktiviert sowie mit einer Datenquelleversehen (Zeile 32 und 33).
Standardmäßig werden die Formularfelder für alle dem aktuellen Listenfeld in der Hierarchie
folgenden Listenfelder auf NULL gesetzt, der Filter mit updateRow() auf die entsprechenden
Werte eingestellt und die folgenden Listenfelder neu eingelesen (Zeile 35 bis 37).
Zum Schluss wird der entsprechende Filterwert auf das Formular, das das Filterergebnis anzeigen soll, übertragen: oForm2.reload().

ListFilter
Aufruf aus
Formular: Filter_Formular_Listfeldbegrenzung
001
002

GLOBAL stListStart AS STRING
GLOBAL lZeit AS LONG

001
002
003

SUB ListFilter(oEvent AS OBJECT)
oFeld = oEvent.Source.ModelIF oEvent.KeyCode < 538 OR oEvent.KeyCode = 1283 OR oEvent.KeyCode = 1284
OR oEvent.KeyCode = 1281 THEN
DIM stSql(0) AS STRING
DIM stText AS STRING
DIM stFeld AS STRING
DIM stQuery AS STRING
ar() = Split(oFeld.ListSource(0)," AS ""Tab""")stQuery = ar(0) & " AS ""Tab"" WHERE"
IF oEvent.KeyCode = 1281 THEN 'Taste ESC
stListStart = "" 'Bei ESC wieder den gesamten Inhalt anzeigen
ELSEIF lZeit > 0 AND Timer() - lZeit < 5 THEN
stListStart = stListStart & oEvent.KeyChar
ELSE
stListStart = oEvent.KeyChar
END IFlZeit = Timer()stText = LCase( stListStart & "%")
stSql(0) = stQuery + "LOWER(""Field"") LIKE '"+stText+"'
ORDER BY ""Sort"""oFeld.ListSource = stSql
oFeld.refresh
END IF
END SUB

004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023

Zuerst werden globale Variablen erstellt. Diese Variablen sind notwendig, damit nicht nur nacheinem Buchstaben, sondern nach dem Betätigen weiterer Tasten schließlich auch nach einer
Buchstabenkombination gesucht werden kann.

Suchen und Filtern192

In der globalen Variablen stListStart werden die Buchstaben in der eingegebenen Reihenfolge gespeichert.
Die globale Variable lZeit wird mit der aktuellen Zeit in Sekunden versorgt. Bei einer längeren
Pause zwischen den Tastatureingaben soll die Variable stListStart wieder zurückgesetzt werden können. Deswegen wird jeweils der Zeitunterschied zur vorhergehenden Eingabe abgefragt.
Das Makro wird durch einen Tastendruck ausgelöst. Eine Taste hat innerhalb der API einenbestimmten Zahlencode, der unter com>sun>star>awt>Key nachgeschlagen werden kann. Er
lässt sich aber auch einfach über msgbox oEvent.KeyCode bei der Eingabe in ein Formularfeld
erfahren. Sonderzeichen wie das «ä», «ö» und «ü» haben den KeyCode 0, alle anderen Schriftzeichen und Zahlen haben einen KeyCode kleiner als 538. Den KeyCode 1283 belegt die Backspace-Taste. Wird dieser Code mit ausgelesen, so können auch Korrekturen durchgeführt werden. Mit dem KeyCode 1284 wird auch die Leertaste in die möglichen Zeichen aufgenommen.
Hinter dem KeyCode 1281 steckt ESC . Die Taste soll zum Zurücksetzen des Listenfeldes dienen(Zeile 3).
Die Abfrage des KeyCode ist hier wichtig, da auch der Schritt mit der Tabulatortaste auf das
Auswahlfeld natürlich das Makro auslöst. Der KeyCode für die Tabulatortaste liegt allerdings bei
1282, so dass der weitere Code der Prozedur hier nicht ausgeführt wird.
Der SQL-Code für das Listenfeld wird in einem Array gespeichert. Das Array hat im Falle des
SQL-Codes aber nur ein Datenfeld. Deshalb ist das Array direkt auf stSql(0) begrenzt.
Der SQL-Code wird nach der Deklaration der Variablen für die weitere Verwendung aufgesplittet. Für das Feld, das gefiltert werden soll, wird nach dem vordefinierten Alias AS "Tab" derCode abgetrennt (Zeile 8).
Anschließend wird an den Code der Alias zusammen mit dem Start für die Filterbedingung(WHERE) wieder angefügt.
In der folgenden Verzweigung wird zuerst wird die Bedingung abgefragt, ob ESC gedrückt
wurde (Zeile 10). Hier kann nicht der KeyChar an die Abfrage weiter gegeben werden. Stattdessen muss ein leerer String weitergegeben werden, der die Suchergebnisse auf alle Daten ausdehnt.
Ist bereits einmal eine Zeit in der globalen Variablen abgespeichert worden und beträgt die
Distanz zu dieser Zeit zum Zeitpunkt der Eingabe weniger als 5 Sekunden, so wird der eingegebene Buchstabe an die vorher eingegebenen Buchstaben angehängt (Zeile 12 und 13). Anderenfalls wird der eingegebene Buchstabe als einzige (neue) Eingabe verstanden (Zeile 15). Das
Listenfeld wird dann einfach neu nach dem entsprechenden Buchstaben gefiltert. Anschließend
wird die aktuelle Zeit wieder in der globalen Variablen lZeit gespeichert.
Der SQL-Code wird schließlich zusammengefügt (Zeile 18 und 19). Die Kleinschreibweise desFeldinhaltes wird mit der Kleinschreibweise des eingegebenen Buchstabens verglichen. Der
Code wird dem Listenfeld hinzugefügt und das Listenfeld aufgefrischt, so dass nur noch dergefilterte Inhalt nachgeschlagen werden kann.

Suchen und Filtern

193

Haushaltsbuch

Haushaltsbuch

194Einführung
Wo ist im Haushalt bloß das Geld geblieben. Das Nachverfolgen des Finanzstromes lässt sich
mit entsprechenden Abfragetechniken direkt innerhalb einer Abfrage auch berechnen und dannnoch zusätzlich in Formularen übersichtlich geordnet nach Konten und Kategorien darstellen.
Die Datenbank8 lässt sich mit unterschiedlichen Formularen, gegebenenfalls auch ganz ohne
Makrozusatz, bedienen.

Tabellen
Kasse
Datenziel
Ansicht:

Ansicht_Kasse_Kategorie, Ansicht_Kasse_mit_Umbuchungen, Ansicht_Bericht_Kategorie

Abfrage:

Kasse_Saldo_Konto_Kategorie, Kasse_Saldo_Konto_Kategorie_Umbuch, Fehler: Verweis
nicht gefunden, Saldo_Kategorie, Saldo_Kategorie_ungesplittet

Formular: Konto_Salden_komplett_Umbuchung_eM, Konto_Salden_komplett_Umbuchung
Makro:

periodische_Buchung, SplitRest

Bericht: keiner direkt

8

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

BetragZahl

Der Betrag, der aus dem folgenden Konto abgebucht oderhinzu gebucht wird. Bei der Felderstellung ist auf 2 Nachkommastellen zu achten.
Eingabe erforderlich: Ja

Konto_ID

Tiny Integer

Fremdschlüssel aus der Tabelle "Konto".
Eingabe erforderlich: Ja

Datum

Datum

Hier soll das Datum gespeichert werden, zu dem die Einnahme/Ausgabe erfolgt ist.
Eingabe erforderlich: Ja

Adressat_ID

Integer

Fremdschlüssel aus der Tabelle "Adressat".
Eingabe erforderlich: Nein

Verwendungszweck

Text

Wofür ist das Geld verwandt worden? Dieses Feld speichert
Zusatzinformationen zu den Kategorien.
Eingabe erforderlich: Nein

Umbuch_Kont
o_ID

Tiny Integer

Wird Geld z.B. vom Girokonto abgehoben, so wechselt das
Geld lediglich das Konto von «Giro» zu «Bargeld», ist abernoch nicht anderweitig ausgegeben.
Eingabe erforderlich: Nein

periodisch

Ja/Nein

Wenn eine Buchung häufiger in ähnlicher Art und Weise vorkommt, so soll sie in diesem Feld gekennzeichnet werden.
Diese Buchungen können in einem Formular als neue Datensätze bis auf das Datum übernommen werden.
Eingabe erforderlich: Nein

Beispieldatenbank Beispiel_Haushaltsbuch.odb

Haushaltsbuch

195FeldnameFeldtyp

Beschreibung

Kategorie_ID

Tiny Integer

Zuweisung der Kategorie, wenn Splitbuchungen nicht geplantsind. Ermöglicht es, Formulare ohne Unterformulare zu erstellen.
Eingabe erforderlich: Nein

Um die Umbuchung von einem Konto zum anderen Konto so abzusichern, dass tatsächlich voneinem Konto zum anderen gebucht wird und nicht das abgebende Konto gleich dem aufgebenden Konto ist, wird innerhalb der Tabelle ein CONSTRAINT definiert. Über Extras → SQL wird eingegeben:
001 ALTER TABLE "Kasse" ADD CONSTRAINT "Konto ungleich Umbuchungskonto" CHECK("Umbuch_Konto_ID" <> "Konto_ID");
Sofern ein Eintrag im Feld "Umbuch_Konto_ID" erstellt wird wird dieser Vergleich vorgenommen.
Das Feld "Umbuch_Konto_ID" darf also sehr wohl leer sein, darf eben nur nicht den gleichen
Wert wie "Konto_ID" haben.
Das Feld für die Eingabe der Kategorie soll standardmäßig mit '0' beschrieben werden, wennvon dem Formular aus keine Eingabe erfolgt. Dafür muss vorher in der Tabelle Kategorie derentsprechende Datensatz mit der "ID" = '0' eingefügt worden sein.
001 ALTER TABLE "Kasse" ALTER COLUMN "Kategorie_ID" SET DEFAULT 0
Den Primärschlüsselwert '0' hat in der Tabelle "Kategorie" der Eintrag 'ohne Zuweisung'. Wird
also keine Angabe im Feld "Kategorie_ID" gemacht, so wird der Betrag 'ohne Zuweisung'
geführt. Dass dieses Feld in der Tabelle nicht mit einer erforderlichen Eingabe versehen worden
ist liegt daran, dass vorher bereits Eingaben ohne Zuweisung der Kategorie, vor allem bei
Umbuchungen, getätigt wurden. Außerdem ist das Feld "Kategorie_ID" bei den Formularen, die
die Splitbuchung benutzen, gar nicht belegt. Sollte also die Splitbuchung nicht benötigt werden, so ließe sich hier mit einer klaren Anweisung von Eingabe erforderlich → Ja und dem Setzen des SQL-Default-Wertes der Code in einigen Abfragen vereinfachen.

Adressat
Datenziel
Ansicht:

Ansicht_Kasse_Kategorie, Ansicht_Kasse_mit_Umbuchungen

Makro:

Adressat_neu

Abfrage, Formular, Bericht: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Adressat

Text

Woher stammt die Rechnung, wer erhält das Geld?
Eingabe erforderlich: Ja

Diese Tabelle kann natürlich beliebig erweitert werden, wenn z.B. mittels Kontoverbindung entsprechende Überweisungen gemacht werden sollten. Hier ist nur die einfache Variante mit
einem Namen erstellt worden. Diese Tabelle wird entweder direkt oder über eine Input-Box mittels Makro beschrieben.

Haushaltsbuch

196

KontoDatenziel
Ansicht:

Ansicht_Kasse_Kategorie, Ansicht_Kasse_mit_UmbuchungenAbfrage:

Kasse_Saldo_Konto_Kategorie, Kasse_Saldo_Konto_Kategorie_Umbuch

Formular: Konto_oM, Konto_Salden_oM, Konto_Salden_separat_oM, Konto_Salden_komplett_eM,
Konto_Salden_komplett_Umbuchung_eM, Konto, Konto_Salden, Konto_Salden_komplett, Konto_Salden_komplett_Umbuchung, Buchung_Umbuchung_Salden
Bericht, Makro: keine direkt

Feldname

Feldtyp

Beschreibung

ID

Tiny Integer

Primärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Die Kontoverbindungen bleiben überschaubar, so
dass lediglich eine kleine Zahl ohne Auto-Wert notwendig ist.

Konto

Text

Bezeichnung für das Konto, wie es auch in den Listenfeldern
erscheint.
Eingabe erforderlich: Ja

IBAN

Text

IBAN-Kontobezeichnung
Eingabe erforderlich: Nein

Bank

Text

Bezeichnung der Bankverbindung
Eingabe erforderlich: Nein

BIC

Text

BIC-Bankbezeichnung für internationalen Zahlungsverkehr
Eingabe erforderlich: NeinDie einfache Variante wäre hier die alleinige Bezeichnung des Kontos. In dem Beispiel existieren lediglich 3 Konten: Giro, Bargeld und Sparbuch. Die Einträge neben dem Konto dienen nur
zur zusätzlichen Information, werden sonst aber nicht weiter genutzt.

Kategorie
Datenziel
Ansicht:

Ansicht_Kasse_Kategorie, Ansicht_Bericht_Kategorie

Abfrage:

Saldo_Kategorie, Saldo_Kategorie_ungesplittet

Formular, Bericht, Makro: keine direkt

Feldname

Feldtyp

Beschreibung

ID

Tiny Integer

Primärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Die Kategorien bleiben überschaubar, so dasslediglich eine kleine Zahl ohne Auto-Wert notwendig ist.

Kategorie

Text

Wofür ist die Ausgabe erfolgt oder woher kommt die Einnahme?
Eingabe erforderlich: Ja

001 INSERT INTO "Kategorie" VALUES ( 0 , 'ohne Zuweisung')
Das Feld mit der "ID" = '0' wird zum Beginn ausgefüllt. Sonst kann in den Tabellen Kasse und
rel_Kasse_Kategorie nicht der entsprechende Defaultwert zugewiesen werden. Das Ausfüllen
des Datensatzes mit der "ID" = '0' kann natürlich auch über die grafische Benutzeroberfläche
erfolgen.

Haushaltsbuch

197rel_Kasse_Kategorie
Datenziel
Ansicht:

Ansicht_Kasse_Kategorie

Formular: Kasse, Kasse_Umbuchung, Konto, Konto_Salden, Konto_Salden_komplett, Konto_Salden_komplett_Umbuchung, Buchung_Umbuchung_Salden
Makro:

SplitRest

Abfrage, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

Kasse_ID

Integer

Zusammen mit "Kategorie_ID" Primärschlüssel der Tabelle.
Gleichzeitig Fremdschlüssel für die Verbindung zur Tabelle"Kasse".

Kategorie_ID

Tiny Integer

Zusammen mit "Kasse_ID" Primärschlüssel der Tabelle.
Gleichzeitig Fremdschlüssel für die Verbindung zur Tabelle"Kategorie".

BetragZahl

Der Betrag, der aus dem folgenden Konto abgebucht oderhinzu gebucht wird. Bei der Felderstellung ist auf 2 Nachkommastellen zu achten.
Eingabe erforderlich: Ja

Das Feld für die Eingabe der Kategorie soll standardmäßig mit '0' beschrieben werden, wennvon dem Formular aus keine Eingabe erfolgt. Dafür muss vorher in der Tabelle Kategorie derentsprechende datensatz mit der "ID" = '0' eingefügt worden sein.
001 ALTER TABLE "rel_Kasse_Kategorie" ALTER COLUMN "Kategorie_ID"
SET DEFAULT 0
Den Primärschlüsselwert '0' hat in der Tabelle "Kategorie" der Eintrag 'ohne Zuweisung'. Wird
also keine Angabe im Feld "Ketegorie_ID" gemacht, so wird der Betrag 'ohne Zuweisung'
geführt.
Diese Tabellen werden unter Extras → Beziehungen wie folgt verknüpft:Je nach Absicht kann entweder die Kategorie direkt in die Tabelle "Kasse" eingegeben werdenoder eben über die n:m-Verbindung in die Tabelle "rel_Kasse_Kategorie". Die erste Varianteermöglicht keine Splitbuchungen beim gleichen Datensatz von "Kasse". Es müsste dann also
der Inhalt des Beleges vorher in einzelne Einträge in "Kasse" gesplittet werden.
Schließlich gibt es noch die Tabelle "Filter", die allerdings nicht zu den anderen Tabelle in Beziehung steht.

Haushaltsbuch

198

FilterDatenziel
Ansicht:

Ansicht_Filter

Formular: Buchung_Umbuchung_Salden
Makro:

Aktuelles_Buchungsdatum, periodische_BuchungAbfrage, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

ID

Ja/NeinPrimärschlüssel der Tabelle. Diese Tabelle nimmt nur einen
Datensatz auf.

Kasse_ID

Integer

Zwischenspeicherfeld für den Wert des Primärschlüssel aus"Kasse". Dieses Feld wird für die periodische Buchung benötigt.
Eingabe erforderlich: Nein

StartDatum

Datum

Zwischenspeicher für das Datum, von dem ab der Bericht
erstellt werden soll.
Eingabe erforderlich: Nein

EndDatum

Datum

Zwischenspeicher für das Datum, bis zu dem der Bericht
erstellt werden soll
Eingabe erforderlich: Nein

DatensatzDatum

Datum

Zwischenspeicher für das jeweilige Datum des Datensatzes,
der im Formular gerade bearbeitet wird. Wird bei einem
neuen Datensatz mit dem aktuellen Datum zur Zeit der Neueingabe versehen.
Eingabe erforderlich: Nein

Diese Tabelle dient vor allem zur Eingrenzung der anzuzeigenden Datenmengen. Sie enthält
nur eine Zeile, in der die aktuellen Daten eingegeben werden. Auf diese Tabelle wird vor allemüber die «Ansicht_Filter» Bezug genommen. In dieser Ansicht werden Standardwerte vordefiniert, falls die Werte in der Tabelle «Filter» leer sind.

Ansichten
Ansichten arbeiten grundsätzlich schneller als Abfragen, da der Code nicht noch durch die GUIübersetzt werden muss. Bei laufend wiederkehrenden Elementen in Abfragen ist daher eine
Ansicht als Quelle oft hilfreich. Ansichten können allerdings nicht, wie viele Abfragen, für die
Eingabe von Daten über die GUI genutzt werden.
Auch bei Berichten sollte der Ansicht der Vorzug gegeben werden, wenn nicht gerade derBericht mit einer Parameterabfrage gestartet werden soll. Der Report-Buildder muss für die
Gruppierungsfunktion gegebenenfalls den SQL-Code interpretieren können und macht dawesentlich eher Fehler als dies z.B. bei Formularen der Fall ist. Ansichten sind für den ReportBuilder nur einfache Tabellen und somit einfacher handhabbar.

Ansicht_Kasse_Kategorie
Datenquelle
Tabelle:Kasse, Adressat, Konto, Kategorie, rel_Kasse_Kategorie

Haushaltsbuch

199

Datenziel
Ansicht:

Ansicht_Kategorie_Diagramm

Abfrage:

Kategorieverlauf, Fehler: Verweis nicht gefunden, Saldo_Kategorie

001 SELECT "Kasse"."ID", "Kasse"."Betrag" AS "Gesamtbetrag",
002 COALESCE( "rel_Kasse_Kategorie"."Betrag", "Kasse"."Betrag" ) AS
"Splitbetrag",
003 "Konto"."Konto", "Kasse"."Datum", "Adressat"."Name","Kasse"."Verwendungszweck",004 "rel_Kasse_Kategorie"."Kategorie_ID",
005 COALESCE( "Kategorie"."Kategorie",(SELECT "Kategorie" FROM "Kategorie" WHERE "ID" = 0)) AS "Kategorie",
006 EXTRACT( YEAR FROM "Datum" ) || '-' || RIGHT( '0' || EXTRACT( MONTH FROM
"Datum" ), 2 ) AS "JahrMonat"
007 FROM "Kasse"008 LEFT JOIN "Konto" ON "Kasse"."Konto_ID" = "Konto"."ID"009 LEFT JOIN "Adressat" ON "Kasse"."Adressat_ID" = "Adressat"."ID"
010 LEFT JOIN "rel_Kasse_Kategorie" ON "Kasse"."ID" ="rel_Kasse_Kategorie"."Kasse_ID"
011 LEFT JOIN "Kategorie" ON "rel_Kasse_Kategorie"."Kategorie_ID" ="Kategorie"."ID"012 WHERE "Kasse"."Umbuch_Konto_ID" IS NULL
013 UNION
014 SELECT "a"."ID", "a"."Betrag" AS "Gesamtbetrag",
015 "a"."Betrag" - COALESCE((SELECT SUM( "Betrag") FROM "rel_Kasse_Kategorie"
WHERE "Kasse_ID" = "a"."ID" ),0) AS "Splitbetrag",
016 "Konto"."Konto", "a"."Datum", "Adressat"."Name", "a"."Verwendungszweck",017 0 AS "Kategorie_ID",
018 (SELECT "Kategorie" FROM "Kategorie" WHERE "ID" = 0) AS "Kategorie",
019 EXTRACT( YEAR FROM "a"."Datum" ) || '-' || RIGHT( '0' || EXTRACT( MONTHFROM "a"."Datum" ), 2 ) AS "JahrMonat"
020 FROM "Kasse" AS "a"021 LEFT JOIN "Konto" ON "a"."Konto_ID" = "Konto"."ID"022 LEFT JOIN "Adressat" ON "a"."Adressat_ID" = "Adressat"."ID"023 LEFT JOIN "rel_Kasse_Kategorie" ON "a"."ID" ="rel_Kasse_Kategorie"."Kasse_ID"
024 LEFT JOIN "Kategorie" ON "rel_Kasse_Kategorie"."Kategorie_ID" ="Kategorie"."ID"025 WHERE "a"."Umbuch_Konto_ID" IS NULL AND "Splitbetrag" <> 0
Für FIREBIRD muss in der letzten Zeile eine Änderung vorgenommen werden. FIREBIRD versteht esnicht, wenn sich die Sortierung einer Abfrage auf einen Alias bezieht, der in dieser Abfrage
steht. Der Begriff "Splitbetrag" ist FIREBIRD also nicht bekannt. Stattdessen muss der Originalcode aus Zeile 15 wieder aufgerufen werden:
025 WHERE "a"."Umbuch_Konto_ID" IS NULL AND ("a"."Betrag" - COALESCE((SELECT
SUM( "Betrag") FROM "rel_Kasse_Kategorie" WHERE "Kasse_ID" ="a"."ID" ),0)) <> 0Diese Ansicht führt alle Tabellen zusammen: "Kasse", "Konto", "Adressat", "rel_Kasse_Kategorie" und "Kategorie" (Zeile 7 bis 11). Diese Daten werden nicht vor gefiltert, so dass die Tabelle"Filter" hier nicht auftaucht. An die Tabelle "Kasse" werden alle anderen Tabellen mit einem
LEFT JOIN angebunden, so dass auch Datensätze existieren können, für die es z.B. keinen
Adressaten gibt. Es werden nur Datensätze übernommen, die keinen Eintrag in der Tabelle"Kasse" im Feld "Umbuch_Konto_ID" haben (Zeile 12). Einträge von "Kasse"."Betrag" werden
durch die Umbuchung wieder aufgehoben. Der Betrag wird dem Umbuchkonto gut geschrieben.
Würde wie vorgesehen mit der Umbuchung gerechnet, so würde der Verlust zwar z.B. auf dem
Girokonto berücksichtigt, das Geld aber nicht vom Kassenautomaten im Portemonnaie landen.
Aus "Kasse" werden die Felder "ID" und "Betrag" direkt übernommen, wobei dem Feld "Betrag"
der Alias "Gesamtbetrag" zugewiesen wird. In dem nächsten Feld wird dann der "Splitbetrag"
Haushaltsbuch

200angegeben (Zeile 2). Gibt es keinen entsprechenden Eintrag ( COALESCE )im Bereich der Kategorien, so wird stattdessen als "Splitbetrag" der Betrag aus der Tabelle "Kasse" übernommen.
Die Felder in Zeile 3 und 4 werden wieder direkt übernommen. In Zeile 5 wird dann zu allen Feldern eine Kategoriezuweisung vorgenommen. Existiert diese Zuweisung nicht, so wird derInhalt genommen, der für "ID" = '0' in der Tabelle "Kategorie" steht.
Zeile 6 enthält für die monatlichen Zusammenfassung einen Eintrag, der aus der Jahreszahl under Monatszahl zusammengesetzt wird. Auf diese Art können die Ergebnisse auch leicht monatlich gruppiert ausgelesen werden.
An diese erste SELECT-Anweisung wird über UNION (Zeile 13) eine zweite SELECT-Anweisung
angehängt. Diese Anweisung enthält vom Aufbau her die gleichen Felder, soll aber die Beträge
erfassen, für die bisher keine Zuweisungen bei der Kategorie erfolgt sind.
In Zeile 15 wird aus diesem Grunde der Betrag ausgerechnet, der sich ergibt, wenn von einem
Datensatz aus "Kasse" sämtliche passenden Beträge aus der Tabelle "rel_Kasse_Kategorie" subtrahiert werden. Ist dieser Betrag ungleich '0' (Zeile 25), so wird der entsprechende Datensatz
aufgenommen. Schließlich fehlt für einen Betrag oder Teilbetrag eine Kategoriezuweisung. Der
Bezug zwischen innerer Abfrage und äußerer "Kasse"."ID" wird darüber sichergestellt, dass derTabelle "Kasse" ein Alias "a" mitgegeben wird (Zeile 20). Dadurch wird die Abfrage in Zeile 15
zu einer korrelierenden Unterabfrage. In Zeile 17 wird diesem Fehlbetrag die "Kategorie_ID" '0'zugewiesen. In Zeile 18 wird schließlich die Bezeichnung für "ID" = '0' aus der Tabelle "Kategorie" ausgelesen.
Alle anderen Felder sind gleich den Feldern in dem ersten Abfrageteil. Durch die "ID" derTabelle "Kasse" wird der Betrag ohne Kategorie dann richtig zugeordnet.

Ansicht_Kasse_mit_UmbuchungenDatenquelle
Tabelle:Kasse, Adressat, Konto

Datenziel
Ansicht:

Ansicht_Konto_Diagramm

Abfrage:

Kontoverlauf, Fehler: Verweis nicht gefunden, Saldo_Konto

001 SELECT "Kasse"."ID", "Kasse"."Betrag","Kasse"."Konto_ID", "Konto"."Konto", "Kasse"."Datum","Kasse"."Adressat_ID", "Adressat"."Name", "Kasse"."Verwendungszweck",
EXTRACT( YEAR FROM "Kasse"."Datum" ) || '-' || RIGHT( '0' ||EXTRACT( MONTH FROM "Kasse"."Datum" ), 2 ) AS "JahrMonat"
002 FROM "Kasse"
003 LEFT JOIN "Konto" ON "Kasse"."Konto_ID" = "Konto"."ID"004 LEFT JOIN "Adressat" ON "Kasse"."Adressat_ID" = "Adressat"."ID"005 UNION
006 SELECT "Kasse"."ID", "Kasse"."Betrag" * -1 AS "Betrag","Kasse"."Umbuch_Konto_ID", "Konto"."Konto", "Kasse"."Datum","Kasse"."Adressat_ID", "Adressat"."Name", "Kasse"."Verwendungszweck",
EXTRACT( YEAR FROM "Kasse"."Datum" ) || '-' || RIGHT( '0' ||EXTRACT( MONTH FROM "Kasse"."Datum" ), 2 ) AS "JahrMonat"
007 FROM "Kasse"008 LEFT JOIN "Konto" ON "Kasse"."Umbuch_Konto_ID" = "Konto"."ID"009 LEFT JOIN "Adressat" ON "Kasse"."Adressat_ID" = "Adressat"."ID"
010 WHERE NOT "Kasse"."Umbuch_Konto_ID" IS NULL
Diese Ansicht dient als Vorstufe dazu, eine monatliche Übersicht über den Kassenstand zu
erhalten. Wieder handelt es sich um zwei Teilabfragen, die durch UNION miteinander verbundensind. Solche Abfragen sind nur in direktem SQL möglich. Zusammen mit einer Ansicht macht
Haushaltsbuch

201

das aber keinen wahrnehmbaren Unterschied, da ja die Dateninhalte in Ansichten sowieso
schreibgeschützt sind.
Die obere Abfrage ist ein Zusammenschluss von den Tabellen "Kasse", "Konto" und "Adressat"(Zeile 2 bis 4). Es werden durch LEFT JOIN alle Datensätze aus "Kasse" angezeigt, auch wenndafür kein "Adressat" existiert. Lediglich das letzte Feld in der Abfrage wird nicht direkt ausgelesen sondern als ein Zusammenhang von Jahr und Monat des jeweiligen Datums erstellt. Das
Jahr wird über EXTRACT( YEAR FROM ... ) ausgelesen. Beim Monat ist hier zusätzlich notwendig, die führende Null zu ergänzen, da sonst eine Sortierung nach "JahrMonat" durcheinander
kommen würde. Die Formulierung mit EXTRACT ist hier gegenüber Kurzformen der HSQLDB
gewählt, weil sie auch in FIREBIRD funktioniert.
Die untere Abfrage enthält wieder die gleichen Felder. Nur werden hier allein die Datensätze
ausgelesen, bei denen der Eintrag im Feld "Umbuch_Konto_ID" nicht leer ist (Zeile 10). Diese
Konten erhalten ja schließlich einen entsprechenden Betrag aus den abgebenden Konten, deraber sonst nirgendwo auftaucht. Entsprechend wird auch die "Umbuch_Konto_ID" aus derTabelle "Kasse" mit der "ID" aus "Konto" verbunden, so dass die entsprechende Kontenbezeichnung auslesbar wird (Zeile 8). Einziger weiterer Unterschied zur oberen Abfrage ist, dass in
Zeile 6 nicht einfach der "Betrag" übernommen wird, sondern der "Betrag" mit '-1' multipliziert
wird. Was dem bei dem einen Konto ein Minus ist, ist dadurch bei dem anderen Konto ein Plus.
Für die Umbuchung selbst ist es also egal, ob der Nutzer die Umbuchung auf dem Konto aufschreibt, das den Betrag abgibt oder auf dem Konto aufschreibt, das den Betrag erhält.

Ansicht_Filter
Datenquelle
Tabelle:FilterDatenziel
Ansicht:

Ansicht_Kategorie_Diagramm, Ansicht_Konto_Diagramm, Ansicht_Bericht_Kategorie

Formular: Konto_Salden_komplett_eM, Konto_Salden_komplett_Umbuchung_eM,
Konto_Salden_komplett, Konto_Salden_komplett_Umbuchung,
Buchung_Umbuchung_Salden
Abfrage:

Kategorieverlauf, Kontoverlauf, Fehler: Verweis nicht gefunden, Fehler: Verweis nicht
gefunden

001 SELECT "Filter"."ID", "Filter"."Kasse_ID",
002 COALESCE("Filter"."StartDatum",CAST( EXTRACT( YEAR FROM CURRENT_DATE ) ||'-01-01' AS DATE )) AS "StartDatum",
003 COALESCE("Filter"."EndDatum",CAST( EXTRACT( YEAR FROM CURRENT_DATE ) || '12-31' AS DATE )) AS "EndDatum",
004 "Filter"."DatensatzDatum",
005 CASE WHEN (SELECT MAX( "Datum" ) FROM "Kasse") > CURRENT_DATE THEN (SELECT
MAX( "Datum" ) FROM "Kasse") ELSE CURRENT_DATE END AS "Datum_Max"
006 FROM "Filter"
007 WHERE "ID" = TRUE
Diese Ansicht gibt alle Werte aus "Filter" wieder. Sie ergänzt diese Werte um Standardwerte,
die sonst in vielen Abfragen wieder vorkommen müssten. So wird in Zeile 2 das Startdatum desaktuellen Jahres hinterlegt, das alternativ zu einem vielleicht fehlenden "StartDatum" abgerufen werden kann. Dabei wird das Jahr aus dem aktuellen Datum ausgelesen und mit dem 1.
Januar als Text verbunden. Um als Datum weiter genutzt werden zu können wird dieser so entstandene Text über CAST( ... AS DATE) in ein Datum umgewandelt. In Zeile 3 wird das entsprechend mit dem Enddatum des aktuellen Jahres gemacht. In Zeile 5 wird schließlich das
Datum herausgesucht, das höher ist: Entweder das aktuelle Datum oder ein Datum, zu dem
eine bestimmte Buchung vorgesehen ist.

Haushaltsbuch

202

Ansicht_Kategorie_Diagramm
Datenquelle
Ansicht:

Ansicht_Kasse_Kategorie, Ansicht_Filter

Datenziel
Formular: Kategorie_Konto_Auswertung

001 SELECT "R"."RowDescription", "C"."ColumnDescription",002 (SELECT SUM( "Splitbetrag" ) FROM "Ansicht_Kasse_Kategorie" WHERE"Kategorie" = "R"."RowDescription" AND "JahrMonat" ="C"."ColumnDescription") AS "Data",
003 'BC' AS "Type"004 FROM
005 (SELECT DISTINCT "Kategorie" AS "RowDescription"
FROM "Ansicht_Kasse_Kategorie"
WHERE "Datum" BETWEEN (SELECT "StartDatum" FROM "Ansicht_Filter") AND(SELECT "EndDatum" FROM "Ansicht_Filter")) AS "R",006 (SELECT DISTINCT "JahrMonat" AS "ColumnDescription"
FROM "Ansicht_Kasse_Kategorie"
WHERE "Datum" BETWEEN (SELECT "StartDatum" FROM "Ansicht_Filter") AND(SELECT "EndDatum" FROM "Ansicht_Filter")) AS "C"
007 ORDER BY "R"."RowDescription", "C"."ColumnDescription"
Mit dieser Ansicht soll ein Säulendiagramm beschickt werden, das für jede vorkommende Kategorie und jeden Monat eine Säule zeichnet.
In dieser Ansicht muss jede Kategorie vertreten sein, die in dem abgefragten Zeitraum einen
Eintrag hat. Diese Kategorien werden in Zeile 5 zusammengestellt und mit einem Alias "R" inder äußeren Abfrage verfügbar gemacht. Der Zeitraum wird dabei über "Ansicht_Filter" und die
entsprechenden Datumswerte bestimmt.
Diese Ansicht muss für jeden Monat, der in dem Zeitraum vorkommt und zu dem irgendwie ein
Eintrag existiert, Datensätze zusammenstellen. Sämtliche "JahrMonat"-Felder werden in Zeile 6
zusammengestellt, wieder vom Zeitraum her eingeschränkt und mit dem Alias "C" versehen.
Beide Unterabfragen werden mit DISTINCT darauf eingestellt, dass sie nur die unterschiedlichen Werte auflisten.
Die Unterabfragen werden nicht miteinander durch eine Bedingung verknüpft, so dass alle
Datensätze der einen Unterabfrage mit der anderen kombiniert werden. Für jede Kategorie stehen also alle Monate zur Verfügung, sofern auch nur in einer Kategorie dazu eine Eingabe
gemacht wurde. In Zeile 1 wird diese Kombination aus "Kategorie" ("RowDescription") und"JahrMonat" ("ColumnDescription") aufgelistet. Die Begriffe für die Spalten sollen nur darauf
hindeuten, was in dem Diagramm dahinter steckt: Zeilenbeschriftungen und Spaltenbeschriftungen der Tabelle, die als Basis für die Daten genommen wird.
In Zeile 2 wird ermittelt, wie groß die Summe der Beträge für die aktuelle Kategorie in dem
aktuellen Monat ist. Die Unterabfrage bezieht sich dabei auf die beiden Felder in Zeile 1.
Zeile 3 enthält durchgängig die Bezeichnung 'BC' als Diagrammtyp. Diese Bezeichnung ist nur
dann von Bedeutung, wenn es sich um ein XY-Diagramm handelt, da dies im Makro etwas
anders angesprochen werden muss.

Ansicht_Konto_Diagramm
Datenquelle
Ansicht:

Ansicht_Kasse_mit_Umbuchungen, Ansicht_Filter

Haushaltsbuch

203

Datenziel
Formular: Kategorie_Konto_Auswertung

001
002
003
004
005

SELECT "Konto" AS "RowDescription", '' AS "ColumnDescription",
SUM( "Betrag" ) AS "Data",'CC' AS "Type"
FROM "Ansicht_Kasse_mit_Umbuchungen"
WHERE "Datum" BETWEEN ( SELECT "StartDatum" FROM "Ansicht_Filter" ) AND( SELECT "EndDatum" FROM "Ansicht_Filter" )006 GROUP BY "Konto"
007 ORDER BY "Konto" ASC
Das Diagramm zur Darstellung des Kontobestandes soll nur eine Übersicht im Kreisdiagramm
sein. Dafür ist keine Ermittlung der Monatsbeträge wie beim Säulendiagramm für die Kategorien notwendig. Spaltenbeschriftungen gibt es beim Kreisdiagramm nicht. Der Punkt bleibt also
leer: '', Zeile 1. Vom Betrag wird für jedes Konto (siehe GROUP BY - Anweisung Zeile 6) die
Summe des Feldes "Betrag" erstellt (Zeile 2). Der Type des Diagramms wird mit 'CC' gekennzeichnet.

Ansicht_Bericht_Kategorie
Datenquelle
Tabelle:Kasse, Kategorie

Ansicht:

Ansicht_Filter

Datenziel
Bericht:

Bericht_Kategorie

001 SELECT "Kasse"."ID", "Kasse"."Betrag", "Kasse"."Datum" AS "Datum","Kasse"."Verwendungszweck",002 COALESCE("Kategorie"."Kategorie", (SELECT "Kategorie" FROM "Kategorie"
WHERE "ID" = 0)) AS "Kategorie",
003 ( SELECT "StartDatum" FROM "Ansicht_Filter" ) AS "StartDatum",004 ( SELECT "EndDatum" FROM "Ansicht_Filter" ) AS "EndDatum"
005 FROM "Kasse"
006 LEFT JOIN "Kategorie" ON "Kasse"."Kategorie_ID" = "Kategorie"."ID"
007 WHERE "Kasse"."Umbuch_Konto_ID" IS NULL
008 AND "Kasse"."Datum" BETWEEN ( SELECT "StartDatum" FROM "Ansicht_Filter" )AND ( SELECT "EndDatum" FROM "Ansicht_Filter" )009 ORDER BY "Datum" ASC
Diese Ansicht wird für die Erstellung des einen Berichtes benötigt. Grundsätzlich ist es besser,
Ansichten für Berichte zu nutzen. Ansichten kann der Report-Builder wie Tabellen nutzen. Er
braucht nicht noch zusätzlich den SQL-Code zu interpretieren.
Die Abfrage stellt alle Datensätze zu den Kategorien zusammen, die direkt mit der Tabelle"Kasse" über den Fremdschlüssel "Kategorie_ID" verknüpft sind (Zeile 6). Sie ist also nicht fürdie Funktion der Splitbuchung ausgelegt. Es werden nur die Datensätze übernommen, die
innerhalb der Zeitspanne liegen, die in "Ansicht_Filter" durch "StartDatum" und "EndDatum"
festgelegt sind (Zeile 8).
Da die Tabelle "Kasse" über einen LEFT JOIN mit der Tabelle "Kategorie" verbunden ist, werdenalle Datensätze aus "Kasse" angezeigt. Es kann also sein, dass "Kategorie_ID" leer ist und somit
keine "Kategorie" für einen Eintrag existiert. Hier wird in Zeile 2 dann statt des leeren Feldes
der Wert für "ID" = '0' übernommen: 'ohne Zuweisung'.

Haushaltsbuch

204

AbfragenKategorieverlauf
Datenquelle
Ansicht:

Ansicht_Kasse_Kategorie, Ansicht_Filter

Datenziel
Formular: Kategorie_Konto_Auswertung

001 SELECT "a"."Kategorie", "a"."Datum", "a"."Splitbetrag" "Betrag",002 ( SELECT SUM( "Splitbetrag" ) FROM "Ansicht_Kasse_Kategorie" WHERE"Kategorie" = "a"."Kategorie" AND ( "Datum" < "a"."Datum" OR ( "Datum" ="a"."Datum" AND "ID" <= "a"."ID" ) ) ) AS "Kategorieverlauf",
003 ( SELECT "StartDatum" FROM "Ansicht_Filter" ) AS "StartDatum",004 ( SELECT "EndDatum" FROM "Ansicht_Filter" ) AS "EndDatum"
005 FROM "Ansicht_Kasse_Kategorie" AS "a"006 WHERE "a"."Datum" BETWEEN "StartDatum" AND "EndDatum"
007 ORDER BY "a"."Kategorie" ASC, "a"."Datum" ASC, "a"."ID" ASC
Hier wird eine Übersicht über sämtliche Buchungen sortiert nach der "Kategorie", dem "Datum"
und der "ID"erstellt (Zeile 7). Die "ID" ist hier wichtig, damit eine eindeutige Sortierung bei gleichem Datum erfolgen kann.
In der Spalte "Kategorieverlauf" wird der Betrag für die jeweilige Kategorie abhängig vom"Datum" und der "ID"aufgelistet (Zeile 2). Alle "Splitbeträge" zu der aktuellen "Kategorie" werden aufgelistet, deren "Datum" kleiner ist als das Datum des aktuellen Datensatzes: "Datum" <"a"."Datum". Ist das Datum gleich dem Datum des aktuellen Datensatzes, so muss die "ID"kleiner oder gleich dem aktuellen Datensatz sein: "Datum" = "a"."Datum" AND "ID" <="a"."ID".
Es werden nur die Daten angezeigt, die in dem in der "Ansicht_Filter" beschriebenen Zeitraum
zwischen "StartDatum" und "EndDatum" liegen.
Auch hier muss bei FIREBIRD nachgearbeitet werden, da FIREBIRD die Nutzung des Alias in Zeile 6
nicht versteht:
006 WHERE "a"."Datum" BETWEEN ( SELECT "StartDatum" FROM "Ansicht_Filter" )AND ( SELECT "EndDatum" FROM "Ansicht_Filter" )Die Unterabfragen aus Zeile 3 und Zeile 4 müssen also erneut durchgeführt werden.

Kasse_Saldo_Konto_Kategorie
Datenquelle
Tabelle:Kasse, Konto

Datenziel
Formular: Kasse_oM, Konto_oM, Konto_Salden_oM, Konto_Salden_separat_oM,
Konto_Salden_komplett_eM, Kasse, Konto, Konto_Salden, Konto_Salden_komplett

001
002
003
004

SELECT "a".*,CASE WHEN "a"."Betrag" > 0 THEN "a"."Betrag" ELSE 0 END AS "Einnahme",
CASE WHEN "a"."Betrag" < 0 THEN "a"."Betrag" ELSE 0 END AS "Ausgabe",( SELECT "Konto" FROM "Konto" WHERE "ID" = "a"."Konto_ID" ) AS
"Kontoname",

Haushaltsbuch

205

005 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Konto_ID" = "a"."Konto_ID"
AND "Umbuch_Konto_ID" IS NULL AND ( "Datum" < "a"."Datum" OR ( "Datum" ="a"."Datum" AND "ID" <= "a"."ID" ) ) ) AS "Saldo_Konto",
006 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Kategorie_ID" ="a"."Kategorie_ID" AND "Umbuch_Konto_ID" IS NULL AND ( "Datum" <"a"."Datum" OR ( "Datum" = "a"."Datum" AND "ID" <= "a"."ID" ) ) ) AS
"Saldo_Kategorie",
007 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" IS NULL AND("Datum" < "a"."Datum" OR ( "Datum" = "a"."Datum" AND "ID" <="a"."ID" ) )) AS "Saldo_gesamt"
008 FROM "Kasse" AS "a"009 WHERE "a"."Umbuch_Konto_ID" IS NULL
010 ORDER BY "Datum", "ID" ASC
Die Salden werden in dieser Abfrage in Abhängigkeit vom Konto und in Abhängigkeit von derKategorie ermittelt. Die Ermittlung des Saldos zur Kategorie bezieht sich auf die Einträge zur
Kategorie direkt in der Tabelle "Kasse". Zeile 6 ist nur dann notwendig, wenn die Kategorienicht zur Splitbuchung genutzt wird.
Aus der Tabelle "Kasse" (Zeile 9) werden alle Datensätze ausgelesen, bei denen die"Umbuch_Konto_ID" leer ist. Umbuchungen beeinflussen die Salden. Umbuchungen müssten
stattdessen separat in zwei unterschiedlichen Datensätzen erscheinen.
Alle Felder aus "Kasse" werden angezeigt. Da "Kasse" die einzige Tabelle ist, die in der Auflistung erscheint, ist damit die Abfrage editierbar. Schließlich ist auch der Primärschlüssel enthalten.
In Zeile 2 werden alle Beträge > 0 als Einnahmen aufgelistet. Sonst sind die Einnahmen 0. In
Zeile 3 sind entsprechend die Ausgaben alle Beträge, die < 0 sind.
Der Kontoname wird in Zeile 4 über eine korrelierende Unterabfrage eingelesen. Zu diesem
Konto wird die fortlaufende Entwicklung des Saldos in Zeile 5 ermittelt.
Auch Zeile 6 bezieht sich auf den jeweils aktuellen Datensatz, hier aber auf die Kategorie. Hierwird dann die Entwicklung des Saldos für die Kategorie in Abhängigkeit von "Datum" und "ID"ermittelt.
In Zeile 7 wird schließlich die fortlaufende Entwicklung des Saldos unabhängig von Konto undKategorie ermittelt. Dies ist dann sozusagen das Gesamtsaldo von allem, was in der Datenbank
verzeichnet ist.

Kasse_Saldo_Konto_Kategorie_Umbuch
Datenquelle
Tabelle:Kasse, Konto

Datenziel
Formular: Kasse_Umbuchung001 SELECT "a".*,002 CASE WHEN "a"."Betrag" > 0 AND "Umbuch_Konto_ID" IS NULL THEN "a"."Betrag"
ELSE 0 END AS "Einnahme",
003 CASE WHEN "a"."Betrag" < 0 AND "Umbuch_Konto_ID" IS NULL THEN "a"."Betrag"
ELSE 0 END AS "Ausgabe",004 ( SELECT "Konto" FROM "Konto" WHERE "ID" = "a"."Konto_ID" ) AS
"Kontoname",005 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Konto_ID" = "a"."Konto_ID"
AND ( "Datum" < "a"."Datum" OR ( "Datum" = "a"."Datum" AND "ID" <="a"."ID" ) ) ) COALESCE( ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" =Haushaltsbuch

206

"a"."Konto_ID" AND ( "Datum" < "a"."Datum" OR ( "Datum" = "a"."Datum" AND"ID" <= "a"."ID" ) ) ), 0 ) AS "Saldo_Konto",
006 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Kategorie_ID" ="a"."Kategorie_ID" AND "Umbuch_Konto_ID" IS NULL AND ( "Datum" <"a"."Datum" OR ( "Datum" = "a"."Datum" AND "ID" <= "a"."ID" ) ) ) AS
"Saldo_Kategorie",
007 ( SELECT "Konto" FROM "Konto" WHERE "ID" = "a"."Umbuch_Konto_ID" ) AS
"Umbuch_Kontoname",
008 COALESCE( ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Konto_ID" ="a"."Umbuch_Konto_ID" AND ( "Datum" < "a"."Datum" OR ( "Datum" ="a"."Datum" AND "ID" <= "a"."ID" ) ) ), 0 ) COALESCE( ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" ="a"."Umbuch_Konto_ID" AND ( "Datum" < "a"."Datum" OR ( "Datum" ="a"."Datum" AND "ID" <= "a"."ID" ) ) ), 0 ) AS "Saldo_Umbuchkonto",
009 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" IS NULL AND( "Datum" < "a"."Datum" OR ( "Datum" = "a"."Datum" AND "ID" <="a"."ID" ) ) ) AS "Saldo_gesamt"
010 FROM "Kasse" AS "a"011 ORDER BY "Datum", "ID" ASC
Diese Abfrage berücksichtigt im Gegensatz zu Kasse_Saldo_Konto_Kategorie auch die Umbuchungen. In Zeile 2 und 3 werden allerdings bei Umbuchungseinträgen die Beträge auf '0'
gesetzt.
Das Saldo des Kontos in Zeile 5 muss jetzt berücksichtigen, dass die Nennung des Fremdschlüssels "Umbuch_Konto_ID" dazu führt, dass das Saldo des jeweiligen Kontos um das Gegenteil
des Betrages in der Spalte "Betrag" vermindert wird. Wird z.B. vom Girokonto -200,- € auf das
Bargeldkonto umgebucht, so muss beim Bargeldkonto - -200,- € gerechnet werden. Dieser
Rechenschritt erfolgt im zweiten Teil der Rechnung von Zeile 5.
Die Kategorie wird durch die Umbuchung nicht beeinflusst. Daher wird in Zeile 6 nur die Summierung in Abhängigkeit von "Kategorie_ID", "Datum" und "ID" des aktuellen Datensatzes vorgenommen.
In Zeile 7 taucht dann die Bezeichnung für das Umbuchkonto auf. Zu diesem Konto wird jetzt in
Zeile 8 auf die gleiche Art wie in Zeile 5 berechnet, wie hoch der aktuelle Kontostand ist. Das
Saldo des Betrages, bei dem die "Umbuch_Konto_ID" mit der "Konto_ID" übereinstimmt, wird
aufsummiert. Anschließend wird das Saldo des Betrages in Abhängigkeit von der"Umbuch_Konto_ID" wird ermittelt. Der Betrag ist natürlich genau entgegengesetzt dem
Betrag, der dem Konto gutgeschrieben werden muss. Also muss diese Summe von der vorherigen abgezogen werden.

Kontoverlauf
Datenquelle
Ansicht:

Ansicht_Kasse_mit_Umbuchungen, Ansicht_Filter

Datenziel
Formular: Kategorie_Konto_Auswertung

001 SELECT "a"."Konto", "a"."Datum", "a"."Betrag",002 ( SELECT SUM( "Betrag" ) FROM "Ansicht_Kasse_mit_Umbuchungen" WHERE"Konto" = "a"."Konto" AND ( "Datum" < "a"."Datum" OR ( "Datum" ="a"."Datum" AND "ID" <= "a"."ID" ) ) ) AS "Kontoverlauf",
003 ( SELECT "StartDatum" FROM "Ansicht_Filter" ) AS "StartDatum",004 ( SELECT "EndDatum" FROM "Ansicht_Filter" ) AS "EndDatum"
005 FROM "Ansicht_Kasse_mit_Umbuchungen" AS "a"006 WHERE "a"."Datum" BETWEEN "StartDatum" AND "EndDatum"
007 ORDER BY "a"."Konto" ASC, "a"."Datum" ASC, "a"."ID" ASC
Haushaltsbuch

207

Hier wird der Verlauf der Einnahmen und Ausgaben sortiert nach "Konto", "Datum" und "ID"
(Zeile 7) mit laufender Summierung dargestellt. Dabei werden nur Datensätze berücksichtigt,
die zwischen dem in "Ansicht_Filter" stehenden "StartDatum" und "EndDatum" liegen (Zeile 3
und 4 sowie 6).
Auch hier muss wieder bei FIREBIRD nachgearbeitet werden, da FIREBIRD die Nutzung des Alias in
Zeile 6 nicht versteht:
008

WHERE "a"."Datum" BETWEEN ( SELECT "StartDatum" FROM "Ansicht_Filter" ) AND( SELECT "EndDatum" FROM "Ansicht_Filter" )Die Unterabfragen aus Zeile 3 und Zeile 4 müssen also erneut durchgeführt werden.
Die fortlaufende Summierung des Betrages erfolgt wieder über eine korrelierende Unterabfrage(Zeile 2). Die Summe wird aus den Beträgen ermittelt, die zu dem Konto des aktuellen Datensatzes "a"."Konto" passen. Sie müssen außerdem vom Datum her vor dem Datum des aktuellen
Datensatzes "a"."Datum" liegen oder, bei gleichem Datum, eine kleinere oder gleiche "a"."ID"haben. Der Bezug zur "ID" ist wichtig, weil nur so die Datensätze sicher unterschieden werden
können und bei mehreren Buchungen auf einem Konto sonst hinter jeder Buchung bereits das
Resultat für den ganzen Tag erscheinen würde.
Für die Anzeige solcher Abfragen sollte natürlich die Sortierung der Abfrage mit der Sortierungaus der Berechnung übereinstimmen. Erst dann wird der fortlaufende Charakter sichtbar.

Saldo_Kategorie
Datenquelle
Tabelle:Kasse, Kategorie

Ansicht:

Ansicht_Kasse_Kategorie

Datenziel
Formular: Konto_Salden, Konto_Salden_komplett, Konto_Salden_komplett_Umbuchung,
Buchung_Umbuchung_Salden

001 SELECT "a"."ID", "a"."Kategorie",
002 CASE WHEN "a"."ID" = 0 THEN
( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" IS NULL AND"Datum" <= :qDatum ) - COALESCE ( ( SELECT SUM( "Splitbetrag" ) FROM
"Ansicht_Kasse_Kategorie" WHERE "Kategorie_ID" > 0 AND "Datum"<= :qDatum ), 0 )ELSE( SELECT SUM( "Splitbetrag" ) FROM "Ansicht_Kasse_Kategorie" WHERE"Kategorie_ID" = "a"."ID" AND "Datum" <= :qDatum )END AS "Saldo_Kategorie",
003 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" IS NULL AND"Datum" <= :qDatum ) AS "Saldo_gesamt"
004 FROM "Kategorie" AS "a"005 WHERE "a"."ID" IN ( SELECT DISTINCT "Kategorie_ID" FROM "Kasse" WHERE"Datum" <= :qDatum )006 OR "a"."ID" = 0
007 ORDER BY "a"."Kategorie" ASC
Diese Abfrage stellt die Salden in der jeweiligen Kategorie bis zu einem über den Parameter:qDatum festgelegten Datum dar. Daneben wird noch das Gesamtsaldo aller Kategorien eingeblendet. Die Abfrage wird auf die Kategorien eingeschränkt, die in dem besagten Zeitrahmen
auch über einen Eintrag verfügen (Zeile 5). Bei FIREBIRD muss statt :qDatum in dieser Abfragejedes Mal der Parameter in den Typ VARCHAR umgewandelt werden. :qDatum wird dann zu
CAST( :qDatum AS VARCHAR(50) ) - obwohl die Eingabe selbst ja mit einem Datum verglichen
wird.

Haushaltsbuch

208

Am meisten Aufwand muss um Buchungen getrieben werden, die nicht (komplett) einer Kategorie zugewiesen wurden. Deshalb wurde in der Tabelle "Kategorie" mit "ID" = '0' die "Kategorie" = 'ohne Eintrag' erstellt. Dieser Kategorie sollen alle Beträge zugewiesen werden, die nicht
in anderen Kategorien verbucht sind.
Für die Kategorie '0' wird zuerst in Zeile 2 die Summe aller Beträge von "Kasse" berechnet, diekeinen Eintrag bei "Umbuch_Konto_ID" haben und kleiner oder gleich dem aktuellen Datumsind. Dieser Teil der Berechnung ist gleich dem in Zeile 3. Von dieser Summe soll die Summe
der Einträge aus "Splitbetrag" in "Ansicht_Kasse_Kategorie" abgezogen werden, bei denen die"Kategorie_ID" > '0' ist. Diese Beträge haben schließlich eine eindeutige Zuweisung zu einer
Kategorie. Falls hier gar keine Beträge verzeichnet sind, also der Teil dieser Unterabfrage leerist, muss für die korrekte Berechnung stattdessen über COALESCE der Wert '0' zugewiesen werden. Jede Kombination eines Feldes mit einem leeren Feld, auch eine Berechnung, bleibt sonst
immer leer. Statt also den Gesamtbetrag als 'ohne Eintrag' auszuweisen würde dann in dem
Ergebnis nichts stehen.
Ist die "ID" der Tabelle "Kategorie" ungleich '0', dann wird nur der "Splitbetrag" aus"Ansicht_Kasse_Kategorie" zu dieser "Kategorie_ID" abgefragt.
Diese Parameterabfrage funktioniert mit dem Formular problemlos. Bei der direkten Bedienungmuss aber beachtet werden, dass die Eingabe bei der HSQLDB für qDatum in der Form YYYY-MMDD, also z.B. 2020-07-24, erfolgen muss. Parameter für Datumseingaben wandelt die AbfrageGUI nicht um, sofern die Parameter nur in Unterabfragen auftauchen (https://bugs.documentfoundation.org/show_bug.cgi?id=87737). In FIREBIRD hingegen funktioniert die Umwandlung
problemlos.

Saldo_Kategorie_ungesplittet
Datenquelle
Tabelle:Kasse, Kategorie

Datenziel
Formular: Konto_Salden_oM, Konto_Salden_komplett_eM,
Konto_Salden_komplett_Umbuchung_eM

001 SELECT "a"."ID", "a"."Kategorie",
002 CASE WHEN "a"."ID" = 0 THEN ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE"Umbuch_Konto_ID" IS NULL AND "Datum" <= :qDatum ) - COALESCE ( ( SELECT
SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" IS NULL AND"Kategorie_ID" > 0 AND "Datum" <= :qDatum ), 0 )ELSE ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Kategorie_ID" = "a"."ID"AND "Umbuch_Konto_ID" IS NULL AND "Datum" <= :qDatum )END AS "Saldo_Kategorie",
003 ( SELECT SUM( "Betrag" ) FROM "Kasse" WHERE "Umbuch_Konto_ID" IS NULL AND"Datum" <= :qDatum ) AS "Saldo_gesamt"
004 FROM "Kategorie" AS "a"005 WHERE "a"."ID" IN ( SELECT DISTINCT "Kategorie_ID" FROM "Kasse" WHERE"Datum" <= :qDatum )006 OR "a"."ID" = 0
007 ORDER BY "a"."Kategorie" ASC
Die Kategorien werden zusammen mit den aufsummierten Beträgen aufgelistet. Fehlt eine
Zuweisung der Kategorie, so werden diese Beträge als 'ohne Zuweisung' angegeben.
Diese Abfrage ist vom Aufbau gleich der Abfrage Saldo_Kategorie. Allerdings wird hier die
Berechnung für die innerhalb der Tabelle "Kasse" eingetragenen Kategorien vollzogen. Jeder
Eintrag in "Betrag" hat hier maximal einen Eintrag in "Kategorie_ID".

Haushaltsbuch

209Die "Kategorie_ID" '0' erhält hier wieder grundsätzlich alle Zuweisungen, zu denen keine "Kategorie_ID" größer als '0' verzeichnet sind. Es muss also nicht unbedingt ein Eintrag '0' in "Kategorie_ID" erfolgen.
Für die Parameterabfrage gilt wie bei Saldo_Kategorie: Parameter funktionieren bei der HSQLDB
mit der SQL-Vorgabe der Datumsschreibweise, nicht aber mit der ortsüblichen Schreibweise,
weil der Parameter nur in Unterabfragen vorkommt. Dafür muss bei FIREBIRD der Parameter:qDatum über CAST( :qDatum AS VARCHAR(50) ) in einen String umgewandelt werden.

Saldo_Konto
Datenquelle
Ansicht:

Ansicht_Kasse_mit_UmbuchungenDatenziel
Formular: Konto_Salden_oM, Konto_Salden_komplett_eM,
Konto_Salden_komplett_Umbuchung_eM, Konto_Salden, Konto_Salden_komplett,Konto_Salden_komplett_Umbuchung, Buchung_Umbuchung_Salden

001 SELECT "Konto_ID", "Konto" AS "Kontoname", SUM( "Betrag" ) AS
"Saldo_Konto",( SELECT SUM( "Betrag" ) FROM "Ansicht_Kasse_mit_Umbuchungen" WHERE"Datum" <= :qDatum ) AS "Saldo_gesamt"
002 FROM "Ansicht_Kasse_mit_Umbuchungen"003 WHERE "Datum" <= :qDatum
004 GROUP BY "Konto_ID", "Konto"
005 ORDER BY "Kontoname"
Die Berechnung des Saldos beim Konto ist deutlich einfacher als bei den Kategorien. Dies liegt
daran, dass das Konto ein Pflichtfeld ist und damit immer genau ein Eintrag vorhanden ist.
Die Abfrage ist nach den Einträgen in "Konto_ID" und "Konto" gruppiert (Zeile 4). Abhängig vondieser Gruppierung wird die Summe des Betrages berechnet (Zeile 1). Zusätzlich wird übereine Unterabfrage die Summe des Gesamtbetrages aus "Ansicht_Kasse_mit_Umbuchungen"
ermittelt. Diese Abfrage ist keine korrelierende Unterabfrage, da sie ja nur ein einziges Ergebnis
liefert - den Gesamtkassenstand.
Wie bei der Abfrage Saldo_Kategorie gilt auch hier, dass die Eingabe des Parameters in der
HSQLDB bei dem direkten Start nach dem SQL-Schema für Datumsangaben erfolgen muss: YYYYMM-DD. Dafür muss bei FIREBIRD der Parameter :qDatum über CAST( :qDatum AS VARCHAR(50)) in einen String umgewandelt werden.

Haushaltsbuch

210Formulare
Formulare ohne Makros
Kasse_oM

1 MainForm (Abfrage: Kasse_Saldo_Konto_Kategorie)
Dieses Formular stellt die einfachste Variante dar. Ein einziges Tabellenkontrollfeld, in dem alle
Information der Datenquelle enthalten sind.
Eingaben in dieses Formular sind nur in den ersten 6 Spalten möglich. Die letzten 5 Spaltensind reine Berechnungen.
Die Spalten für «Konto», «Kategorie» und «Adressat» sind mit Listenfeldern versehen. Hiererfolgt eine Auswahl des Begriffes, für den dann der entsprechende Primärschlüssel als Fremdschlüssel in die Abfrage eingefügt wird. Sollen hier neue Einträge z.B. beim Adressaten erfolgen, die zur Zeit nicht auswählbar sind, so muss ein solcher Eintrag direkt in der Tabelle getätigt werden.
Die Berechnungen in den letzten Spalten erscheinen grundsätzlich erst nach der Abspeicherung des Datensatzes. Deswegen kann diese einfache Übersicht auch nur mit einem Tabellenkontrollfeld sinnvoll gelingen.

Konto_oM

Haushaltsbuch

211

1 MainForm (Tabelle: Konto)

1.1 SubForm (Abfrage: Kasse_Saldo_Konto_Kategorie)

Bei diesem Formular ist im Vergleich zu Kasse_oM lediglich ein Hauptformular hinzugekommen.
Im Hauptformular wird das Konto vorausgewählt hier kann dann auch ein neues Konto hinzugefügt werden oder die alten Konten können bearbeitet werden. Das Unterformular zeigt dann
nur Datensätze an, die zu dem Konto im Hauptformular passen. Die Salden stimmen weiterhin,da ja nur die anderen Konten ausgeblendet sind, die Salden aber für alle Konten zusammen
nach Datum und ID sortiert berechnet werden.

Konto_Salden_oM

1

MainForm(Tabelle: Konto)

1.1

SubForm (Abfrage:
Kasse_Saldo_Konto_Kategorie)

1.1.1

SaldoKonto (Abfrage:
Saldo_Konto)

1.1.2

SaldoKategorie (Abfrage:
Saldo_Kategorie_ungesplittet)

Mit diesem Formular wird das Tabellenkontrollfeld des Unterformulars 1.1 nur noch mit Feldern
bestückt, die für die Eingabe gedacht sind. Trotzdem nutzt das Formular als Datenbasis die
Abfrage mit den Summierungen. Diese Abfrage schließt nämlich gleichzeitig die Einträge aus"Kasse" aus, bei denen ein Eintrag in der "Umbuch_Konto_ID" erfolgt ist. Ohne diese Art der
Umbuchung in anderen Beispielformularen könnte also auch direkt die Tabelle "Kasse" genutzt
werden.
Die Salden werden jetzt in Abhängigkeit vom Datum des aktuellen Datensatzes in UnterUnterformularen angezeigt. Der Screenshot weist hier z.B. vom markierten Datensatz mit dem
Datum '10.01.19' auf das Saldo bis zum 10.01.19 hin. Die Salden geben also nicht den aktuellen Kontostand wieder, sondern den Kassenstand zum Ende des Buchungstages.

Haushaltsbuch

212

Konto_Salden_separat_oM

1 MainForm (Tabelle: Konto)

1.1 SubForm (Abfrage: Kasse_Saldo_Konto_Kategorie)

Ist der Platz geringer als in Konto_Salden_oM durch die Übersicht der Salden benötigt, so reichthier auch eine einfache Auswertung der Salden, die sich jetzt wieder auf die Werte bezieht, diedirekt in der entsprechenden Zeile der Abfrage des Unterformulars stehen. Die drei Felder für
die Beträge sind nur aus dem Tabellenkontrollfeld ausgelagert und als Einzelfelder unter das
Tabellenkontrollfeld eingebunden worden. Einzelfelder im gleichen Formular wie das Tabellenkontrollfeld zeigen immer den Inhalt zu dem aktuellen Datensatz an, sind aber für die Navigation mit dem Tabulator innerhalb des Tabellenkontrollfeldes nicht direkt erreichbar und damit
auch nicht direkt störend.

Haushaltsbuch

213Formulare mit einfachem Aktualisierungsmakro
Konto_Salden_komplett_eM

1 MainForm (Tabelle: Konto)
2

MaxDatum (Ansicht:
Ansicht_Filter)

1.1 SubForm (Abfrage: Kasse_Saldo_Konto_Kategorie)
2.1 SaldoKonto (Abfrage: Saldo_Konto)
2.2

SaldoKategorie (Abfrage: Saldo_Kategorie_ungesplittet)

Makros in Formulareigenschaften
1.1

Nach der Datensatzaktion

Module1.Aktualisieren_einfach

In diesem Formular wird die Anzeige der Salden nicht an das Eingabeformular 1.1 gekoppelt.
Die Abfrage zu den Formularen 2.1 und 2.2 wird stattdessen direkt beim Öffnen der Formulars 2
gestellt. Der Inhalt dort würde also bestehen bleiben, auch wenn in dem Eingabeformular etwas
geändert würde.
Vorteil dieser Konstruktion ist natürlich, dass dadurch der Kontostand unter Berücksichtigung
aller eingetragenen Buchungen angegeben wird. Das höchste Datum ist hier nicht automatischdas aktuelle Datum. Wenn bereits für einen der kommenden Tage eine Buchung mit Datum eingetragen wurde, dann wird auch die noch berücksichtigt und das Datum eben dieser Buchungals maximales Datum genommen.
Damit die Auswertung nicht nur beim Öffnen des Formulars stimmt, muss sie beim Ändern vonEingaben in dem Eingabeformular nach der Abspeicherung wieder neu eingelesen werden. Dies
wird hier durch ein kleines Makro erledigt, dass nach jeder Datensatzaktion kurz das Formular 2
und damit auch die Unterformulare 2.1 und 2.2 neu einliest.

Haushaltsbuch

214

Konto_Salden_komplett_Umbuchung_eM

1 MainForm (Tabelle: Konto)
2

MaxDatum (Ansicht:
Ansicht_Filter)

1.1 SubForm (Tabelle: Kasse)
2.1 SaldoKonto (Abfrage: Saldo_Konto)
2.2 SaldoKategorie (Abfrage: Saldo_Kategorie_ungesplittet)

Makros in Formulareigenschaften
1.1

Nach der Datensatzaktion

Module1.Aktualisieren_einfach

Gegenüber dem Formular Konto_Salden_komplett_eM wird hier lediglich noch die Möglichkeit
hinzugefügt, auch die Umbuchungsfunktion zu nutzen. Damit kann dann auch die Tabelle"Kasse" direkt benutzt werden. Allerdings muss innerhalb der Formulareigenschaft von 1.1
noch die Sortierung so eingestellt werden, dass zuerst nach dem Datum und dann nach der ID
sortiert wird. Eine solche Einstellung in den Formulareigenschaften kann gegebenenfalls auchwieder bei der Eingabe über die Navigationsleiste vorübergehend rückgängig gemacht werden.
Der Screenshot zeigt darüber hinaus, dass es zur Zeit möglich ist, dem Feld "Kategorie_ID" inder Tabelle "Kasse" gar keine Kategorie zuzuweisen. Gerade bei den Umbuchung fehlen natürlich solche Zuweisungen. Auch der Datensatz mit der "ID" = '6' hat keine Zuweisung der Kategorie. Beim Datensatz mit der "ID" = '1' ist hingegen in der Tabelle "Kasse" für die"Kategorie_ID" der Wert '0' eingetragen. Deswegen wird dort die Bezeichnung 'ohne Zuweisung' ausgegeben.

Haushaltsbuch

215

Formulare mit mehrfachem Makroeinsatz
Kasse

1

MainForm (Abfrage: Kasse_Saldo_Konto_Kategorie)

1.1

SplitKategorie (Tabelle:
rel_Kasse_Kategorie)

Makros in Formulareigenschaften1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1

Nach der Datensatzaktion

Module1.SplitRest

1.1

Nach der Datensatzaktion

Module1.SplitRest

In diesem Formular sind nur die ersten 5 Felder des Tabellenkontrollelementes im Hauptformular beschreibbar. Die anderen Feldwerte werden aus der Abfrage ausgelesen.
Im Unterformular wird die Kategorie für die jeweilige Buchung ausgesucht. Sollen Buchungenauf verschiedene Kategorien aufgeteilt werden, so können sie dort gesplittet werden. Dabei
wird beständig der Rest ausgerechnet, der noch zum Splitten zur Verfügung steht.
Die Abfrage als Grundlage des Formulars berücksichtigt nicht die Umbuchungen, die überandere Formulare in dieser Beispieldatenbank gemacht worden sind. Für eine Umbuchungmüssten grundsätzlich zwei Datensätze eingegeben werden: Die Abbuchung aus einem Konto
und die Einzahlung auf dem anderen Konto. Das ist dann fehleranfälliger, weil•

der Abbuchungswert und der Einzahlungswert eventuelle unterschiedlich eingegeben
werden oder•einer der beiden Datensätze gelöscht werden kann und damit die Abbuchung oder Einzahlung fehlt.

Haushaltsbuch

216

Aus diesem Grunde wird im folgenden Formular die Umbuchung im gleichen Datensatz vorgenommen und der Buchungsbetrag für die Umbuchung automatisch geschrieben.

Kasse_Umbuchung1

MainForm (Abfrage:
Kasse_Saldo_Konto_Kategorie_Umbuch)

1.1 SplitKategorie (Tabelle: rel_Kasse_Kategorie)

Makros in Formulareigenschaften1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1

Nach der Datensatzaktion

Module1.SplitRest

1.1

Nach der Datensatzaktion

Module1.SplitRest

In diesem Formular ist auch die Umbuchung erreichbar. Die entsprechend geänderte Abfrage
berücksichtigt die Umbuchung bei Einnahme und Ausgabe (beide bleiben 0,00 €) und auchbeim «Saldo gesamt». Lediglich das «Saldo Konto» weist diese Änderung auf, die im Feld
«Betrag» gemacht wurde. Aber eben auch nur für das entsprechende Konto. Der Betrag wird
nach der Umbuchung in dem anderen Konto mit dem entgegengesetzten Vorzeichen geführt.
Im Unterformular wird die Kategorie für die jeweilige Buchung ausgesucht. Sollen Buchungenauf verschiedene Kategorien aufgeteilt werden, so können sie dort gesplittet werden. Dabei
wird beständig der Rest ausgerechnet, der noch zum Splitten zur Verfügung steht.
Dieses Formular erfüllt alle Möglichkeiten der Umbuchung, der Splitbuchung und des gleichzeitigen Blicks auf den Kontostand mit lediglich einem Formular und einem Unterformular.

Haushaltsbuch

217

Konto1

MainForm (Tabelle:
Konto)

1.1

SubForm (Abfrage:
Kasse_Saldo_Konto_Kategorie)

1.1.1

SplitKategorie (Tabelle:
rel_Kasse_Kategorie)

Makros in Formulareigenschaften
1.1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1.1

Nach der Datensatzaktion

Module1.SplitRest

1.1.1

Nach der Datensatzaktion

Module1.SplitRest

Die ist eine Erweiterung des Formulars Kasse. Aus dem Formular wurde lediglich der Eintrag für
das Konto herausgenommen, so dass in dem Eingabeformular 1.1 als Unterformular nur die
Datensätze vorhanden sind, die zu dem im Hauptformular liegenden Formular passen.

Haushaltsbuch

218

Konto_Salden

1

MainForm(Tabelle: Konto)

SubForm (Abfrage:
1.1 Kasse_Saldo_Konto_Kategorie)

1.1.1

SplitKategorie (Tabelle: rel_Kasse_Kategorie)

1.1.2 SaldoKonto (Abfrage: Saldo_Konto)
1.1.3

SaldoKategorie (Abfrage: Saldo_Kategorie)

Makros in Formulareigenschaften
1.1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1.1

Nach der Datensatzaktion

Module1.SplitRest

1.1.1

Nach der Datensatzaktion

Module1.SplitRest

Hier wird die Angabe der Salden gegenüber dem Formular Konto aus dem Eingabeformular herausgenommen und in separate Unterformulare ausgelagert. Die Unterformulare 1.1.2 und 1.1.3
beziehen dabei das Datum, zu dem sie die Salden berechnen sollen, aus dem Formular 1.1. Es
wird also der Kontostand zum Zeitpunkt des markierten Datensatzes angegeben.

Haushaltsbuch

219

Als Datenbasis dient für 1.1 weiter eine Abfrage, weil diese Abfrage die Umbuchungen derdafür gesondert erstellten Formulare ausblendet. Umbuchungen müssten hier also, wie unter
Kasse angegeben, als zwei Datensätze geführt werden: Abbuchung und Einzahlung.

Konto_Salden_komplett

1

MainForm (Tabelle:
Konto)

2

MaxDatum (Ansicht:
Ansicht_Filter)

1.1

SubForm (Abfrage:
Kasse_Saldo_Konto_Kategorie)

SplitKategorie
1.1.1 (Tabelle:
rel_Kasse_Kategorie)

2.1 SaldoKonto (Abfrage: Saldo_Konto)
2.2 SaldoKategorie (Abfrage: Saldo_Kategorie)

Makros in Formulareigenschaften
1.1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1.1

Nach der Datensatzaktion

Module1.Aktualisieren

1.1.1

Nach der Datensatzaktion

Module1.Aktualisieren

Haushaltsbuch

220

Hier ist gegenüber Konto_Salden die Ausgabe der Salden nicht von dem ausgesuchten Datensatz abhängig. Es wird immer der Kontostand angegeben, der sich aus allen in der Datenbank
befindlichen Buchungen ergibt. Eine kleine Besonderheit im Screenshot rechts unten: Hier
wurde einfach einmal testweise aus der Splitbuchungstabelle "rel_Kasse_Kategorie" ein Betragum 3 Ct vermindert. Die Abfrage gibt dies jetzt als einen Betrag von 0,03 € der Kategorie 'ohne
Zuweisung' aus.

Konto_Salden_komplett_Umbuchung1 MainForm (Tabelle: Konto)
2

MaxDatum (Ansicht:
Ansicht_Filter)

Haushaltsbuch

1.1 SubForm (Tabelle: Kasse)

SplitKategorie
1.1.1 (Tabelle:
rel_Kasse_Kategorie)

2.1 SaldoKonto (Abfrage: Saldo_Konto)
2.2 SaldoKategorie (Abfrage: Saldo_Kategorie)

221

Makros in Formulareigenschaften
1.1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1.1

Nach der Datensatzaktion

Module1.Aktualisieren

1.1

Nach dem Datensatzwechsel

Module1.Kontofilter_Formstart

1.1.1

Nach der Datensatzaktion

Module1.Aktualisieren

Hier kommen jetzt zusätzlich zu Konto_Salden_komplett die Einträge der Umbuchung ins Spiel.
Deswegen kann hier jetzt auch direkt auf die Tabelle "Kasse" zugegriffen werden.
Zur Umbuchung stehen über ein Makro nur die Konten zur Verfügung, die nicht dem Konto im
Hauptformular 1 entsprechen.

Buchung_Umbuchung_Salden

Haushaltsbuch

222

1 MainForm (Tabelle: Kasse)
2 Filter (Tabelle: Filter)
3 MaxDatum (Ansicht: Ansicht_Filter)

1.1 SplitKategorie (Tabelle: rel_Kasse_Kategorie)
2.1 SaldoKonto (Abfrage: Saldo_Konto)
2.2 SaldoKategorie (Abfrage: Saldo_Kategorie)
3.1 SaldoKonto (Abfrage: Saldo_Konto)
3.2 SaldoKategorie (Abfrage: Saldo_Kategorie)

Makros in Formulareigenschaften1

Vor der Datensatzaktion

Module1.ZeileZwischenspeichern

1

Nach der Datensatzaktion

Module1.Aktualisieren

1Nach dem Datensatzwechsel

Module1.Aktuelles_Buchungsdatum

1.1

Nach der Datensatzaktion

Module1.Aktualisieren

Makros in Feldeigenschaften1

Schaltfläche Aktueller Monat (Aktion Ausführen)

Module1.Monat_aktuell

1

Schaltfläche Adressat hinzufügen (Aktion Ausführen)

Module1.Adressat_neu

1

Listenfeld «Konto» (Status geändert)

Module1.Kontofilter_Feldstart2

Listenfeld «Buchung aus Vorlage» (Modifiziert)

Module1.periodische_Buchung3

Schaltfläche Formularaufrufen (Aktion Ausführen)

Module1.Formular_starten

In diesem Formular werden schließlich alle Register gezogen. Schon allein die Zuordnung voneinzelnen Schaltflächen und des Listenfeldes für die Buchung aus einer Vorlage zu verschiedenen Formularen muss intensiver erklärt werden.
Doch zuerst einmal die Funktionalität:
Wird über das Listenfeld «Buchung aus Vorlage» ein vorheriger Datensatz ausgesucht, so wird
dieser Datensatz mit aktuellem Datum am Schluss der Tabelle von Formular 1 eingefügt. Das
Listenfeld speichert hierfür die ID der Vorlage in der Tabelle "Filter" ab. Deswegen auch die
Zuordnung zu Formular 2.
Ein Klick auf den Button Aktueller Monat führt dazu, dass die Datensätze in 1 nur für den aktuellen Monat angezeigt werden. Bei vielen Daten und einer Neueingabe muss sonst immer erst
nach unten gescrollt werden.
Fehlt ein Adressat in dem Listenfeld in 1, so kann dieser über den Button Adressat hinzufügenin eine einfache Input-Box eingegeben werden. Hier ist nur der Name des Adressaten vorgesehen. Hätte die Tabelle für den Adressaten mehr Felder als nur den Namen, so würde so etwasüber einen Dialog realisiert.
Die Angabe des Kontos ist jetzt wieder in das Formular integriert. In Abhängigkeit von dem
Buchungskonto wird die Liste der Umbuchungskonten mit einem Makro erstellt. Das Buchungskonto kommt in der Liste der Umbuchungskonten nicht vor.
Die Eingabe in das Formular weist jetzt auch die Möglichkeit auf, bestimmte Datensätze zu
periodisch wiederkehrenden Datensätzen zu erklären. Die Funktion bedeutet nicht, dass die
Daten automatisch eingegeben werden. Sie erscheinen vielmehr in der Auswahl des Listenfeldes «Buchung aus Vorlage».
Die Auswertungen in Formular 2 sind von dem aktuelle ausgewählten Datensatz abhängig. Sie
zeigen auch die Umbuchungen auf.
Die Auswertungen in Formular 3 beziehen sich auf alle eingetragenen Datensätze.
In Formular 2 wird auch der Anfangsmonat und der Endmonat eingegeben, für den eine Übersicht ausgegeben werden soll. Die beiden Datumsfelder gehören zur Tabelle "Filter". Die einzige
Haushaltsbuch

223

Auswertung erfolgt in Form eines Formulars. Der Button dafür wurde in das Formular 3 platziert, da durch das Verlassen von Formular 2 der Datensatz mit der den Datumsangaben
gespeichert wird.
Der Code für das Listenfeld «Buchung aus Vorlage» wird hier noch gesondert aufgeführt, weil ersich von der HSQLDB zu FIREBIRD unterscheidet.

HSQLDB:

001 SELECT LEFT("Verwendungszweck"|| SPACE((SELECTMAX(CHAR_LENGTH("Verwendungszweck")) FROM "Kasse" WHERE "periodisch" =TRUE)), (SELECT MAX(CHAR_LENGTH("Verwendungszweck")) FROM "Kasse" WHERE"periodisch" = TRUE))|| ' ' ||REPLACE( RIGHT( SPACE(8) ||"Betrag", 8),'.',',') || ' € ' ||RIGHT( '0' || EXTRACT(DAY FROM "Datum" ), 2 ) || '.' || RIGHT( '0' ||EXTRACT( MONTH FROM "Datum" ), 2 ) || '.' || EXTRACT( YEAR FROM "Datum" )AS "Periode",002 "ID"
003 FROM "Kasse" WHERE "periodisch" = TRUE
004 ORDER BY "Periode" ASC

FIREBIRD:

001 SELECT RPAD("Verwendungszweck", (SELECTMAX(CHAR_LENGTH("Verwendungszweck")) FROM "Kasse" WHERE "periodisch" =TRUE))|| ' ' ||REPLACE( LPAD("Betrag", 8),'.',',') || ' € ' ||RIGHT( '0' || EXTRACT(DAY FROM "Datum" ), 2 ) || '.' || RIGHT( '0' ||EXTRACT( MONTH FROM "Datum" ), 2 ) || '.' || EXTRACT( YEAR FROM "Datum" )AS "Periode",002 "ID"
003 FROM "Kasse" WHERE "periodisch" = TRUE
004 ORDER BY "Periode" ASC
Der Code für die HSQLDB wurde vor dem Export so weit wie möglich angepasst. Die Länge desFeldes "Verwendungszweck" wird zuerst für alle betroffenen Felder, bei denen "periodisch" =TRUE ist, ermittelt. Damit kann dann ein entsprechend breiter Platz für diesen Eintrag bei fester
Schriftart erzeugt werden. In der HSQLDB wird ein Leerzeichen so oft eingefügt, wie es diese
Länge erfordert. Danach wird der gesamte String genommen und auf die beabsichtigte Länge
abgeschnitten.
FIREBIRD kennt die Funktion SPACE nicht, hat dafür aber Funktionen wie RPAD und LPAD. Diese
Funktionen füllen auf die beabsichtigte Länge automatisch mit der entsprechenden Anzahl an
Leerzeichen von rechts aus (RPAD) oder von links aus (LPAD) auf. Es könnte sogar noch ein entsprechendes Ausfüllzeichen statt des Leerzeichens mit angegeben werden. Hierfür würde in der
HSQLDB die Funktion REPEAT benötigt.
Das Listenfeld ist unter Eigenschaften → Allgemein → Schrift auf die Schriftart «Liberation
Mono» eingestellt. Eine Schriftart mit fester Breite ist für die tabellarische Darstellung erforderlich.

Haushaltsbuch

224

Kategorie_Konto_Auswertung

1 FormKategorie (Abfrage: Kategorieverlauf)
1 Objekt1 Diagramm oben (Ansicht: Ansicht_Kategorie_Diagramm)
1 Objekt2 Diagramm unten (Ansicht: Ansicht_Konto_Diagramm)
2 FormKonto (Abfrage: Kontoverlauf)
Makros in Formulareigenschaften
1

Beim Laden

Module2.ChangeData

In dem oberen Diagramm wird der monatliche Verlauf für jede Kategorie angezeigt. Das Diagramm macht natürlich deutlich, dass es sich bei den Daten nicht um einen tatsächlichen Verlauf handelt. Die Ausgaben für die Ernährung sind so gering, dass die Person vermutlich längst
gestorben wäre. Wie sie dann aber noch etwas für das Hobby zum Jahresschluss ausgebenkann ist natürlich fraglich.
Im zweiten Diagramm wird die prozentuale Verteilung der Summen auf die verschiedenen Konten im Auswertungszeitraum angezeigt.

Haushaltsbuch

225

Beide Diagramme liegen übrigens im gleichen Formular und werden über ein Makro nacheinander mit den aktuellen Daten versorgt. In dem Formular befindet sich ein verstecktes Feld,das die Informationen über die Namen der Diagramme und die Bezeichnung für die Ansichten
enthält, die den Datenbestand für die Diagramme enthalten.
Die Auswertung der Finanzen mit Diagrammen war ursprünglich in Berichten vorgesehen. Das
Verfahren, Diagramme auch in Formulare einzubinden, ist nicht wesentlich aufwendiger, wenndas entsprechende Makro erst einmal existiert.
Vom Prinzip her geht das Ganze folgendermaßen:
4. Writer öffnen und "Diagramm einfügen" aufrufen. Dort den Diagrammtyp auswählen.
5. Das so erstellte Dummydiagramm kopieren und in das Formular einfügen.
6. Eine Ansicht zur Versorgung des Diagramms mit Daten erstellen, wie sie oben in den
Ansichten aufgezeigt wurde.
7. In einem versteckten Feld im Formular, das für das Makro die Bezeichnung "Chart"haben muss, folgende Eingaben machen: Wert → Objekt1 und Zusatzinformation →Ansicht_Kategorie_Diagramm.
8. Beim Laden des Formulars das Makro ChangeData ausführen lassen.

Berichte
Die ursprüngliche Fassung dieser Datenbank enthielt Berichte, die eine Auswertung mit Diagrammen anzeigten. Da die Diagramme aber seit LO 5.3 nicht mehr angezeigt wurden, das
Löschen der alten Diagramme sogar reproduzierbar zum Absturz von LO führt, ist statt der
Berichte jetzt ein entsprechendes Formular eingebaut worden, das hoffentlich sicherer läuft.
Lediglich ein Bericht für die Fassung ohne Makros, der «Bericht_Kategorie», ist Bestandteil derBeispieldatenbank:Haushaltsbuch

226

Benötigt
Ansicht:

Ansicht_Bericht_Kategorie

Der Bericht gibt eine Übersicht über die monatlichen Ausgaben. Die entsprechenden Summen
dazu werden im Bericht erstellt.

Der Bericht enthält einen Berichtskopf, in dem neben dem Titel das Startdatum und das Enddatum, des Berichtes aufgelistet ist. Dann folgt der Gruppenkopf, der für die jeweiligen Monatsdarstellungen gilt.

Haushaltsbuch

227

Die Sortierung erfolgt nach dem Datum. Gruppenkopf und Gruppenfuß sind vorhanden. Sobald
ein Monat zuende ist wird ein Gruppenfuß erstellt. Sobald ein neuer Monat anfängt wird ein
Gruppenkopf erstellt. Das wird durch die monatliche Gruppierung erreicht. Das Gruppenintervall mit '1' gibt an, dass es sich hier um Schritte von einem Monat handelt. Es könnte auchmehrere Monate zusammengefasst werden.
Die letzte Einstellung Zusammenhalten → Gesamte Gruppe benötigt hier ein gesondertes
Augenmerk. Dies bedeutet, dass versucht wird, sämtliche Einträge von Gruppenkopf bis einschließlich Gruppenfuß auf einer Seite zusammen zu fassen. Bei vielen Einträgen z.B. zum
Monat 'Januar 2019' kann dies dazu führen, dass die Gruppe nicht mehr komplett auf die erste
Seite passt. Der erste Lösungsweg ist hier der Beginn auf der zweiten Seite. Schließlich enthält
die zweite Seite keinen Berichtskopf und damit mehr Platz. Reicht auch die zweite Seite nichtaus, dann muss innerhalb der Gruppe ein Umbruch stattfinden. Die Folge aber ist: Auf der ersten Seite steht der Gruppenkopf dann alleine.
Da der Bericht zuerst als ein Writer-Dokument erstellt wird kann gegebenenfalls noch nachgebessert werden. Das in oben genanntem Sonderfall ungünstige Zusammenhalten könnte dannin den Tabelleneigenschaften zurückgestellt werden.
Der Inhalt vom Gruppenkopf wird über das Datumsfeld des ersten Datensatzes erstellt. Das
Datum ist hier einfach entsprechend formatiert worden, so dass nur der Monat und das Jahr
erscheinen.
Im Gruppenfuß werden die Beträge aus der gesamten Gruppe über die Funktion Summe aufsummiert. Das gleiche Verfahren, nur für den ganzen Bericht, folgt im Berichtsfuß.

Haushaltsbuch

228

Manche kleinen Fehler tauchen erst bei der Erstellung von Berichten auf. Hier z.B. die Summierung für den Monat Februar 2020. Die doppelte Unterstreichung wurde nicht angezeigt, wenn
die Farbe automatisch gesetzt werden sollte. Stattdessen musste eine Farbe vorbestimmt werden, so dass die Unterstreichung des negativen Wertes leider schwarz erfolgt.
Unter dem gesamten Bericht im Berichtsfuß taucht schließlich noch der Gesamtbetrag desBerichtes auf.
Eine Splittung des Gesamtbetrages nach Kategorien würde einen erneuten Bericht erfordern,da es nicht möglich ist, die Gruppierung einmal nach den Monaten ablaufen zu lassen und danneine separate Berechnung nach den Kategorien aufzustellen. Die einzige Form, hier eine Übersicht hinein zu bekommen, wäre das Diagramm, das aber innerhalb von Berichten immer wieder Probleme bereitet.

MakrosAktualisieren_einfach
Aufruf aus
Formular: Konto_Salden_komplett_eM, Konto_Salden_komplett_Umbuchung_eM
Makro:
001
002
003
004
005
006
007
008
009

Aktualisieren

SUB Aktualisieren_einfach
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECT
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MaxDatum")oForm.reload
END SUB

Auf dem Weg über das Dokument und die Zeichnungsoberfläche wird das Formular mit dem
Namen 'MaxDatum' aufgesucht (Zeile 5 bis 7). Das Formular wird neu geladen (Zeile 8).

Aktualisieren
Aufruf aus
Formular: Konto_Salden_komplett, Konto_Salden_komplett_Umbuchung,
Buchung_Umbuchung_Salden

Benötigt
Makro:

SplitRest, Aktualisieren_einfach

Haushaltsbuch

229

001
002
003
004

SUB Aktualisieren(oEvent AS OBJECT)
SplitRest(oEvent)Aktualisieren_einfach
END SUB

Dieses Makro ist lediglich deshalb erforderlich, weil das Nebenformular sonst von einer Änderung der Daten im Hauptformular nichts mit bekommt. Zuerst wird die eventuelle Splitbuchungnach einer Datensatzänderung durchgeführt (Zeile 2). Anschließend wird dann des Nebenformular aktualisiert.

ZeileZwischenspeichern
Aufruf aus
Formular: Kasse, Kasse_Umbuchung, Konto, Konto_Salden, Konto_Salden_komplett, Konto_Salden_komplett_Umbuchung, Buchung_Umbuchung_Salden
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

SUB ZeileZwischenspeichern(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oHiddenFeld AS OBJECT
DIM inUmbuchID AS INTEGER
DIM stUmbuch AS STRINGoForm = oEvent.Source
IF InStr(oForm.ImplementationName, "ODatabaseForm") THEN
oHiddenFeld = oForm.getByname("Hidden")inUmbuchID = oForm.findColumn("Umbuch_Konto_ID")
stUmbuch = oForm.getString(inUmbuchID)
IF oEvent.Action = 3 OR stUmbuch <>"" THEN
oHiddenFeld.HiddenValue = "no"ELSEIF oEvent.Action = 1 THEN
oHiddenFeld.HiddenValue = oForm.RowCount + 1
ELSE
oHiddenFeld.HiddenValue = oForm.Bookmark()
END IF
END IF
END SUBDie Prozedur «ZeileZwischenspeichern» ist notwendig, damit auf den Datensatz zugegriffen
werden kann, für den Eintragungen bei den Kategorien erfolgen sollen. Bei der normalen Bewegung durch ein Formular wird der Speichermodus z.B. durch das Verlassen einer Eingabezeile
im Tabellenkontrollfeld ausgelöst. Damit das Unterformular aber mit den korrekten Werten
bestückt werden kann muss nach dem Verlassen der Zeile wieder auf die Zeile zurückgesprungen werden. Erst dann sind die Formulare «SubForm» und «SubSubForm» korrekt miteinander
verbunden.
Die Prozedur wird durch das Ereignis vor der Datensatzaktion ausgelöst. Mit dem Ereignis sindzwei Implementationen verbunden, so dass das Ereignis jedes normale Makro also doppelt
durchlaufen würde. Über den Implementationsnamen wird hier die Implementation heraus
gesucht, die einen Zugriff auf die Formularfelder bietet:com.sun.star.comp.forms.ODatabaseForm (Zeile 7).
Das versteckte Feld wird bekannt gemacht (Zeile 8). Außerdem wird das Feld herausgesucht,
das in der dem Formular zugrundeliegenden Abfrage «Umbuch_Konto_ID» heißt. Dies geht hierüber die Funktion oForm.findColumn. Die Funktion gibt nur eine Zahl wieder, und zwar die Zahl
der entsprechenden Tabellenspalte. Über oForm.getString wird der Wert aus dieser Spalte als
Text ausgelesen (Zeile 9 und 10). Das Auslesen erfolgt hier als Text, da über einen Text leichter
leere Felder erkennbar sind.
Wenn das Feld «Umbuch_Konto_ID» nicht leer ist, dann handelt es sich um eine Umbuchung.
Folglich wird dort keine Zuweisung der Kategorie gebraucht. Gleiches gilt, wenn das auslösende
Ereignis eine Löschung des Datensatzes bewirken soll. Das Löschen wird über oEvent.Action= 3 abgefragt. Beim Löschen wird schließlich jede Verbindung zu den Kategorien entfernt, eineHaushaltsbuch

230

Eingabe dort ist auch gar nicht mehr möglich. Für diese beiden genannten Fälle wird in dem
versteckten Feld die Bezeichnung 'no' gespeichert (Zeile 11 und 12).
Wenn es sich bei der Eingabe um einen noch nicht existierenden neuen Datensatz handelt(oEvent.Action = 1), dann kann auf den Datensatz nicht über die Funktion Bookmark zugegriffen werden. Stattdessen wird zu der Anzahl der Zeilen der Tabelle eine Zeile hinzugezählt
und der Wert entsprechend in dem versteckten Feld gespeichert (Zeile 14). Damit kannanschließend auf die Zeile zurückgesprungen werden.
Handelt es sich um eine Datensatzänderung, dann wird über oForm.Bookmark() die entsprechende Zeile gespeichert.
Nach Ablauf dieser Prozedur erfolgt die Speicherung des aktuellen Datensatzes in dem Tabellenkontrollfeld. Nach der Datensatzaktion startet dann die Prozedur SplitRest.

SplitRest
Aufruf aus
Formular: Kasse, Kasse_Umbuchung, Konto, Konto_Salden
Makro:

Aktualisieren

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038

Kasse, rel_Kasse_Kategorie

SUB Splitrest(oEvent AS OBJECT)
DIM oDoc AS OBJECTDIM oForm AS OBJECTDIM oSubForm AS OBJECTDIM oFeld AS OBJECTDIM oHiddenFeld AS OBJECTDIM oController AS OBJECTDIM oView AS OBJECTDIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECT
DIM stSql AS STRING
DIM oSQL_Anweisung2 AS OBJECT
DIM stSql2 AS STRING
DIM oAbfrageergebnis AS OBJECT
DIM inID AS INTEGERDIM loID AS LONG
DIM inBetrag AS INTEGER
DIM doBetrag AS DOUBLE
DIM doRest AS DOUBLE
oDoc = thisComponent
IF oEvent.Source.hasByName("Hidden") THEN
oForm = oEvent.Source
ELSE
oForm = oEvent.Source.Parent
END IFoHiddenFeld = oForm.getByname("Hidden")IF oHiddenFeld.HiddenValue = "no" THEN
EXIT SUBEND IF
IF oHiddenFeld.HiddenValue <> "" THEN
oForm.absolute(oHiddenFeld.HiddenValue)oHiddenFeld.HiddenValue = ""END IFoDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IF

Haushaltsbuch

231

039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070

oVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
inID = oForm.findColumn("ID")loID = oForm.getLong(inID)
IF oHiddenFeld.Tag <> "" THEN
stSql = "SELECT ""Kategorie_ID"", ""Betrag"" FROM ""rel_Kasse_Kategorie""
WHERE ""Kasse_ID"" ='"+oHiddenFeld.Tag+"'"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
oHiddenFeld.Tag = ""WHILE oAbfrageergebnis.next
doBetrag = oAbfrageergebnis.getDouble(2)stSql2 = "INSERT INTO ""rel_Kasse_Kategorie"" (""Kasse_ID"",""Kategorie_ID"", ""Betrag"") VALUES('"+loID+"','"+oAbfrageergebnis.getShort(1)+"','"+str(doBetrag)+"')"oSQL_Anweisung2.executeUpdate(stSql2)
WEND
END IF
stSql = "SELECT ""a"".""Betrag"" - COALESCE((SELECT SUM(""Betrag"")
FROM ""rel_Kasse_Kategorie"" WHERE ""Kasse_ID"" = ""a"".""ID""),0.00)
AS ""Wert"" FROM ""Kasse"" AS ""a"" WHERE ""a"".""ID"" = '"+loID+"'"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
doRest = oAbfrageergebnis.getDouble(1)WENDoSubForm = oForm.getByname("SplitKategorie")
oFeld = oSubForm.getByName("tblKategorieBetrag")oController = oDoc.getCurrentController()oView = oController.getControl(oFeld)oView.setFocus
wait 500
IF doRest <> 0 THEN
inBetrag = oSubForm.findColumn("Betrag")oSubForm.MoveToInsertRowoSubForm.UpdateDouble(inBetrag,doRest)
END IF
END SUB

Mit dieser Prozedur soll berechnet werden, welcher Betrag in der Splittabelle für die Kategorien
zur Verfügung steht. Der Betrag soll direkt in den neuen Datensatz eingegeben werden. Es ist
also nur noch die Wahl einer Kategorie erforderlich.
Wird der Betrag nur zum Teil einer Kategorie zugeordnet, so wird das Makro aus SubSubForm
heraus erneut aufgerufen und dem nächsten Datensatz in der Splittabelle wird der verbleibende Rest zugewiesen – so lange, bis kein Rest mehr bleibt.
Da die Prozedur aus Formularen mit unterschiedliche Formularstruktur (Formular, Unterformularusw.) ausgelöst wird muss zuerst geklärt werden, ob das Formular auch das gewünschte Formular ist. In dem Formular soll sich das Feld mit der Bezeichnung «Hidden» befinden (Zeile 22 bis
26).
Ist in dem Feld «Hidden» der HiddenValue = "no", so wird die Prozedur nicht weiter ausgeführt (Zeile 28 und 29). Entweder handelt es sich in diesem Fall um eine Umbuchung oder derauslösende Datensatz aus dem Formular wurde gelöscht. Eine Zuweisung von Kategorien ist
also nicht erforderlich oder sogar unmöglich.
Der Wert aus dem versteckten Feld wird über oForm.absolute() zum Sprung auf den Datensatz aus dem Eingabeformular für die Buchung genutzt. Das versteckte Feld muss nur einmalausgelesen werden. Es sollte auf jeden Fall geleert werden, wenn die Prozedur durchlaufen
wurde (Zeile 31 bis 34). Wird grundsätzlich das Makro erst vom Formular ausgelöst und dannvom Unterformular für die Kategorien eine Eingabe getätigt, so ist die Zeile auf jeden Fall korrekt. Es könnte aber passieren, dass bei einer Auslösung des Makros direkt aus dem Tabellenkontrollfeld für die Zuweisung der Kategorie ohne vorherige Änderung des Buchungsdatensatzes ein Sprung zu einem Buchungsdatensatz erfolgt, der zuletzt geändert wurde.

Haushaltsbuch232

In den Zeilen 35 bis 39 wird erst noch einmal die Datenbankverbindung überprüft und als
Objekt oVerbindung zur Verfügung gestellt. Anschließend wird für zwei SQL-Anweisungen ein
Objekt bereitgestellt.
Im Buchungsformular wird nach dem Wert des Primärschlüsselfeldes gesucht (Zeile 42 und 43).
Die anschließende Bedingungsabfrage von Zeile 44 bis 53 trifft nur zu, wenn es sich um eine
periodische Buchung handelt. Diese Erweiterung ist für das Formular
Buchung_Umbuchung_Salden notwendig. Die Prozedur periodische_Buchung schreibt einen
Wert in oHiddenFeld.Tag.
Bei einer periodischen Buchung soll auch die Zuweisung der Kategorie automatisch erfolgen.
Eine periodische Buchung wird aber nicht vollautomatisch ausgeführt sondern liest nur zuerst
einmal die Werte der vorhergehenden Buchung in das Buchungsformular ein. Deshalb kannnicht der komplette Datensatz direkt erstellt werden. Über oHiddenFeld.Tag wird der Schlüsselwert des Datensatzes ausgelesen, aus dem die periodische Buchung erfolgt ist (Zeile 45).
Über diesen Schlüsselwert können alle Einträge aus der Tabelle «rel_Kasse_Kategorie» ausgelesen werden, die zu dem Buchungsdatensatz passen. Sie werden nacheinander (Zeile 48 bis 52)
als neue Datensätze mit der aktuellen Schlüsselnummer loID in die Tabelle «rel_Kasse_Kategorie» neu eingefügt. Dabei erweist sich das Einfügen des Betrages als etwas problematisch. Wird
der Wert nicht in einen String umgewandelt (str(doBetrag)), so gibt Basic statt eines Wertes
mit Dezimalpunkt einen Wert mit der Formatierung des eingestellten Landes der GUI wieder,
also in diesem Fall ein Dezimalkomma. Das führt unweigerlich dazu, dass der Datensatz nicht
eingefügt wird.
Anschließend wird über eine Abfrage festgestellt, wie hoch die Differenz zwischen dem Betragzu der passenden Buchungsnummer und der Summe der Beträge zu der entsprechenden
Buchungsnummer in der Tabelle «rel_Kasse_Kategorie» ist (Zeile 54 bis 58). Danach wird das
Unterformular bestimmt (Zeile 59) und der Cursor in das Tabellenkontrollfeld gesetzt (Zeile 60
bis 63).
In den Test hat es sich als notwendig erwiesen, vor weiteren Aktionen mit dem Unterformular
für die Eingabe der Kategorie eine kurze Wartezeit einzulegen (Zeile 64).
Ist nach der Abfrage noch ein Rest zu verzeichnen, so wird eine neue Zeile in dem Unterformular für die Kategorien erstellt. In diese Zeile wird der Restbetrag eingetragen. Ist hingegen kein
Rest mehr vorhanden, so erübrigt sich ein Eintrag. Die Prozedur wird beendet.

periodische_BuchungAufruf aus
Formular: Buchung_Umbuchung_Salden

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015

Kasse, Filter

SUB periodische_Buchung(oEvent AS OBJECT)
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm AS OBJECTDIM oFeld AS OBJECTDIM oHiddenFeld AS OBJECTDIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECT
DIM stSql AS STRING
DIM oAbfrageergebnis AS OBJECTDIM loID AS LONG
DIM loIDNew AS LONG
DIM inSave AS INTEGERDIM unoDate

Haushaltsbuch

233

016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063

oFeld = oEvent.Source.Model
IF oFeld.CurrentValue = "" THEN
EXIT SUBEND IFloID = oFeld.CurrentValueoDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("MainForm")oForm.moveToInsertRow
oHiddenFeld = oForm.getByName("Hidden")oHiddenFeld.Tag = loID
oDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Betrag"", ""Konto_ID"", ""Adressat_ID"", ""Verwendungszweck"",""Umbuch_Konto_ID"" FROM ""Kasse"" WHERE ""ID"" ='"+loID+"'"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
oForm.UpdateDouble(oForm.findColumn("Betrag"),oAbfrageergebnis.getDouble(1))
oForm.UpdateLong(oForm.findColumn("Konto_ID"),oAbfrageergebnis.getLong(2))
oForm.UpdateLong(oForm.findColumn("Adressat_ID"),oAbfrageergebnis.getLong(3))
IF oAbfrageergebnis.wasNull() THEN
oForm.UpdateNULL(oForm.findColumn("Adressat_ID"),NULL)
END IFoForm.UpdateString(oForm.findColumn("Verwendungszweck"),oAbfrageergebnis.getString(4))
oForm.UpdateLong(oForm.findColumn("Umbuch_Konto_ID"),
oAbfrageergebnis.getLong(5))
IF oAbfrageergebnis.wasNull() THEN
oForm.UpdateNULL(oForm.findColumn("Umbuch_Konto_ID"),NULL)
END IFWEND
inSave = MsgBox("Soll der Datensatz mit aktuellem Datum abgespeichert und
die Kategorien hinzugefügt werden?", 36 ,"Buchungsübernahme")
IF inSave = 6 THEN
unoDate = createUnoStruct("com.sun.star.util.Date")
unoDate.Year = Year(Now())
unoDate.Month = Month(Now())
unoDate.Day = Day(Now())
oForm.UpdateDate(oForm.findColumn("Datum"),unoDate)
IF oForm.isNew THEN
oForm.insertRow()
ELSEoForm.updateRow()
END IF
END IFoSQL_Anweisung.executeUpdate("UPDATE ""Filter"" SET ""Kasse_ID"" = NULL WHERE""ID"" = TRUE")
oFeld.Parent.reload
END SUBIm Hauptformular des Formulars Buchung_Umbuchung_Salden können Datensätze als periodische Datensätze gekennzeichnet werden. Diese Datensätze werden dann als Vorlage fürandere Datensätze über das Listenfeld ganz oben im Formular abrufbar. Der Inhalt dieser
Datensätze wird, bis auf das Buchungsdatum, als neuer Datensatz eingefügt und muss nur
noch abgespeichert werden.
Der Wert des Listenfeldes wird ermittelt (Zeile 16 bis 20). Es handelt sich dabei nicht um denangezeigten Wert, sondern um den Inhalt des Schlüsselfeldes, das damit verbunden ist. In diesem Fall ist das der Primärschlüsselinhalt der Tabelle «Kasse».

Haushaltsbuch

234

Dieser Wert wird zur weiteren Nutzung in dem bereits in der Prozedur SplitRest vorgestellten
versteckten Feld, in diesem Fall in den Zusatzinformationen (Tag), zwischengelagert (Zeile 26).
Er wird dort später für die Splitbuchungen bei den Kategorien benötigt.
Die Datenbankverbindung wird überprüft und die Verbindung zur als Objekt zur Verfügung
gestellt (Zeile 27 bis 31). Der Datensatz zu dem entsprechenden Primärschlüssel aus derTabelle "Kasse" wird ausgelesen (Zeile 33 und 34). Die Felder "ID" und "Datum" werden nicht
benötigt. "Datum" wird sowieso neu eingestellt und "ID" ist bekannt – außerdem soll ja ein
neuer Datensatz erstellt wird, so dass "ID" über den Autowert neu festgelegt wird.
Sämtliche Werte werden direkt als neue Werte in das Formular übertragen (Zeile 35 bis 49).
Dabei wäre es nicht notwendig, eine WHILE - WEND - Schleife zu verwenden, da die Abfragesowieso nur einen Datensatz als Ergebnis hat. oAbfrageergbnis.next würde für das eine
Ergebnis in Zeile 35 ausreichen. Mit wasNull() wird das letzte Ergebnis jeweils getestet. Damit
wird ausgeschlossen, dass stattdessen '0' für die "Adressat_ID" weiter gegeben wird, obwohl
dort vielleicht kein Wert enthalten ist. "Betrag" und "Konto_ID" hingegen sind Felder, die nachTabellendefinition nie NULL sein dürfen.
Der folgende "Verwendungszweck" darf zwar auch NULL sein. Wenn dort allerdings ein leerer
String eingetragen wird, so schadet dies nicht der Integrität der Dateneingabe. Bei der"Umbuch_Konto_ID" muss hingegen wieder darauf geachtet werden, dass tatsächlich NULLübermittelt wird.
In Zeile 50 wird eine Messagebox ausgegeben. Die Zahl '36' besagt hier, dass ein Ja- und ein
Nein-Button angezeigt werden (Wert: 4) und ein Fragezeichen-Symbol erscheint (Wert: 32).
Wird auf die Nachfrage mit Ja geantwortet, so gibt die Messagebox den Wert 6 zurück. Nurdann wird in den Zeile 52 bis 55 das aktuelle Datum zusammengestellt und in Zeile 56 in das
entsprechende Formularfeld geschrieben.
Zeile 57 bis 61 dienen der Speicherung der eingegebenen Elemente. Hier wurde die sichere
Variante genutzt, die zuerst abklärt, ob die Datenzeile neu ist, um dann den entsprechenden
Befehl abzusetzen. Da in dem Formular gleichzeitig andere Makros mitlaufen kann es sein, dassdie Zeile schon teilweise abgespeichert wurde und deswegen ein insertRow() zu einem Fehler
in der Funktionsreihenfolge führt.
Nach dem Abspeichern wird automatisch die Prozedur SplitRest aufgerufen, die dann aus dem
alten Eintrag der Tabelle "Kasse" den Primärschlüssel in dem versteckten Feld (Zeile 26) vorfindet, den neuen Primärschlüssel aus dem Formular ausliest und damit auch die Inhalte für die
Kategorien von der einen Buchung zu der anderen Buchung überträgt.
Zum Schluss wird der Wert des Listenfeldes über den entsprechenden Wert in der Tabelle "Filter" wieder zurück gesetzt (Zeile 64).

Adressat_neu
Aufruf aus
Formular: Buchung_Umbuchung_Salden

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009

Adressat

SUB Adressat_neu(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oListbox AS OBJECT
DIM stInput AS STRING
DIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oSQL_Anweisung2 AS OBJECT
DIM stSql AS STRING

Haushaltsbuch

235

010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034

DIM oAbfrageergebnis AS OBJECT
stInput = InputBox("Bitte den Namen des Adressaten eingeben","Neuer Adressat")
stInput = Trim(stInput)
IF stInput = "" THEN
msgbox "Der Eintrag für den Adressaten ist leer."
EXIT SUBEND IFoDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
stSql = "SELECT ""Name"" FROM ""Adressat"" WHERE ""Name"" ='"+stInput+"'"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
IF oAbfrageergebnis.Next THEN
msgbox "Der Adressat """+stInput+""" ist schon vorhanden."ELSEoSQL_Anweisung2.executeUpdate("INSERT INTO ""Adressat"" (""Name"") VALUES('" + stInput + "')")
oForm = oEvent.Source.Model.Parent
oListbox = oForm.getByName("MainForm_Grid").getByName("lboAdressat")oListbox.refresh
END IF
END SUB

Am Anfang fehlen noch viele Adressaten. Hier soll auf einfach Weise wenigstens ein Name fürden Adressaten abgefragt werden, damit eine Eingabe zügig erfolgen kann.

Solch ein Eingabefeld wird in der Zeilen 11 gebildet. Der Inhalt wird eingelesen, auf mögliche
gleiche Inhalte hin überprüft und ansonsten in die Datenbank übertragen. Der Inhalt derInputBox wird als Text in stInput gespeichert. Um unnötige Leertasten auszuschließen wird
stInput mit Trim(stInput) von Leertasten vor und nach der sichtbaren Eingabe befreit (Zeile
12).
Ist der Inhalt leer, so wird das Makro nach einer kurzen Meldung beendet (Zeile 13 bis 16).
Ansonsten wird eine Verbindung zur Datenquelle aufgebaut, sofern noch nicht vorhanden. Daeine SQL-Anweisung abgesetzt wird, während die andere noch im Speicher liegt, werden für die
SQL-Anweisungen zwei Objekte erstellt (Zeile 17 bis 23).
In der Datenbank wird jetzt nachgefragt, ob der "Name" in genau der Schreibweise schon vorhanden ist. Bei dieser Nachfrage wird nach Groß- und Kleinschreibung unterschieden. Wenn
klar ist, dass der "Name" nicht bereits vor kommt (Zeile 26 und 27) wird die zweite Anweisung,nämlich das Einfügen des neuen Datensatzes, abgearbeitet.
Nachdem ein neuer Datensatz eingefügt wurde ist es erforderlich, dass das Listenfeld seinen
Datenbestand neu einliest. Da der Button, der das Makro ausgelöst hat, genau im gleichen Formular liegt wie das Tabellenkontrollfeld für die Buchungseingabe, kann das Formular überoEvent.Source.Model.Parent erreicht werden (Zeile 30). Das Listenfeld liegt im Tabellenkontrollfeld. Das Tabellenkontrollfeld hat den Namen «MainForm_Grid», das Listenfeld den Namen«lboAdressat». Über oListbox.refresh wird der Inhalt des Listenfeldes erneuert.

Haushaltsbuch

236

Monat_aktuell
Aufruf aus
Formular: Buchung_Umbuchung_Salden
001
002
003
004
005
006
007

SUB Monat_aktuell(oEvent AS OBJECT)
DIM oForm AS OBJECT
oForm = oEvent.Source.Model.Parent
oForm.Filter = "EXTRACT( YEAR FROM ""Datum"") =EXTRACT( YEAR FROM CURRENT_DATE )AND EXTRACT( MONTH FROM ""Datum"") = EXTRACT( MONTH FROM CURRENT_DATE )"oForm.ApplyFilter = TrueoForm.reload
END SUB

Der Button für den Filter liegt in dem Formular, dessen Filter entsprechend eingestellt werdensoll. Der Filtertext in Zeile 4 enthält den SQL-Befehl, mit dem auch eine Tabelle entsprechend
gefiltert würde. Er wird über den Formularfilter an SELECT * FROM "Kasse" mit WHERE angehängt.
Der Filter wird mit oForm.Filter zugewiesen. Mit oForm.ApplyFilter wird der Filter eingestellt. Er kann also auch später über die Navigationsleiste wieder abgestellt und erneut eingestellt werden. Zum Schluss wird das Formular neu eingelesen.

Formular_starten
Aufruf aus
Formular: Buchung_Umbuchung_Salden
001
002
003
004
005

SUB Formular_starten(oEvent AS OBJECT)
DIM stFormular AS STRING
stFormular = oEvent.Source.Model.Tag
ThisDatabaseDocument.FormDocuments.getByName(stFormular).open
END SUB

Diese Prozedur startet über einen Button ein Formulardokument. Der Name des entsprechenden Formulardokuments ist in den Zusatzinformationen (Tag) des Buttons enthalten.

Aktuelles_Buchungsdatum
Aufruf aus
Formular: Buchung_Umbuchung_Salden

Benötigt
Tabelle:FilterMakro:

Kontoliste_filtern

001
002
003
004
005
006
007
008
009
010
011
012

SUB Aktuelles_Buchungsdatum(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oDatenquelle AS OBJECTDIM oVerbindung AS OBJECTDIM oSQL_Anweisung AS OBJECT
DIM stSql AS STRING
DIM oDoc AS OBJECTDIM oDrawpage AS OBJECTDIM oForm2 AS OBJECTDIM oFeld AS OBJECT
DIM inID AS INTEGER
oForm = oEvent.Source

Haushaltsbuch

237

013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032

IF oForm.IsNew THEN
stSql = "UPDATE ""Filter"" SET ""DatensatzDatum"" = CURRENT_DATEWHERE ""ID"" = TRUE"
ELSE
stSql = "UPDATE ""Filter"" SET ""DatensatzDatum"" = '" +oForm.getString(oForm.findColumn("Datum")) + "' WHERE ""ID"" = TRUE"END IFoDatenquelle = ThisComponent.Parent.CurrentController
If NOT (oDatenquelle.isConnected()) THEN
oDatenquelle.connect()
END IFoVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung.executeUpdate(stSql)
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm2 = oDrawpage.forms.getByName("Filter")oForm2.reload
inID = oForm.getInt(oForm.findColumn("Konto_ID"))oFeld = oForm.getByname("Kasse_Grid").getByName("lboUmbuchID")
Kontoliste_filtern(inID,oFeld)
END SUB

Das Makro sorgt dafür, dass die Salden für das Konto und die Kategorie abhängig von dem
aktuellen Datensatz ermittelt werden können und, wenn gerade eine Neueingabe gemacht
wird, abhängig vom aktuellen Datum dargestellt werden.
Nach dem Datensatzwechsel im Hauptformular wird ermittelt, ob ein neuer Datensatz eingefügt werden soll: oForm.IsNew (Zeile 13). Handelt es sich um einen neuen Datensatz, so wird in
der Tabelle "Filter" das Feld "DatensatzDatum" auf das aktuelle Datum CURRENT_DATE eingestellt. Handelt es sich nicht um ein einen neuen Datensatz, so wird stattdessen in der Tabelle"Filter" das Feld "DatensatzDatum" auf den Wert des aktuellen Datums des gerade erreichten
Datensatzes eingestellt: oForm.getString(oForm.findColumn("Datum")).
Nachdem die Verbindung zur Datenbank überprüft und Die SQL-Anweisung vorbereitet wurde(Zeile 18 bis 23) wird der SQL-Befehl an die Datenbank verschickt und das Nebenformular «Filter» auf den aktuellen Wert entsprechend eingestellt (Zeile 27 und 28).
Nach dem Datensatzwechsel muss außerdem das Listenfeld für das Umbuchkonto neu eingestellt werden. Dies erfolgt in den Zeilen 29 bis 31.

Kontofilter_Formstart
Aufruf aus
Formular: Konto_Salden_komplett_Umbuchung

Benötigt
Makro:
001
002
003
004
005
006
007
008
009

Kontoliste_filtern

SUB Kontofilter_Formstart(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECT
DIM inID AS INTEGER
oForm = oEvent.Source
inID = oForm.getInt(oForm.findColumn("Konto_ID"))oFeld = oForm.getByname("Kasse_Grid").getByName("lboUmbuchID")
Kontoliste_filtern(inID,oFeld)
END SUB

Wenn der Datensatz im Formular gewechselt wird soll das Listenfeld für das Umbuchungs-Konto
neu eingelesen werden. So kann, zusätzlich zu der Beschränkung in der Tabelle Kasse, ausgeschlossen werden, dass das abgebende Konto und das Umbuchungs-Konto gleich sein können.

Haushaltsbuch

238

In Zeile 6 wird der aktuelle Wert für die "Konto_ID" über das Formular ausgelesen. Zeile 7 ermittelt anschließend das Objekt des Listenfeldes für das Umbuchungs-Konto. Hier sind zwei Ebenen mit getByName eingebaut, da dieses Feld innerhalb eines Tabellenkontrollfeldes liegt. Derermittelte Wert für das abgebende Konto wird zusammen mit dem Objekt für das Listenfeld desUmbuchungs-Kontos an das Makro «Kontoliste_Filtern» weitergegeben.

Kontofilter_Feldstart
Aufruf aus
Formular: Buchung_Umbuchung_Salden

Benötigt
Makro:
001
002
003
004
005
006
007
008
009

Kontoliste_filtern

SUB Kontofilter_Feldstart(oEvent AS OBJECT)
DIM oFeldStart AS OBJECTDIM oFeld AS OBJECT
DIM inID AS INTEGER
oFeldStart = oEvent.Source.ModelinID = oFeldStart.ValueItemList(oEvent.Selected)
oFeld = oFeldStart.Parent.getByName("lboUmbuchID")
Kontoliste_filtern(inID,oFeld)
END SUB

Das Makro soll dann den korrekten Wert für das Listenfeld auslesen, wenn das Listenfeld
gerade geändert wurde. Der Wert ist also noch nicht in dem Formular gespeichert und auchnicht über CurrentValue erreichbar.
In Zeile 6 wird deswegen der Wert über die Position in der Werteliste bestimmt. Direkt nachdem Auslösen wird unter oEvent.Selected diese Position angegeben. Hier könnte auch stattdessen oFeldStart.SelectedItemPos stehen.
Über das auslösende Feld oFeldStart wird in Zeile 7 das Feld für das Umbuchung-Konto ermittelt. Dabei wird einfach eine Ebene nach oben gegangen und von dort aus der Name desUmbuchung-Kontos aufgerufen.
Der ermittelte Wert für das ausgewählte abgebende Konto wird zusammen mit dem Objekt fürdas Listenfeld des Umbuchungs-Kontos an das Makro «Kontoliste_Filtern» weitergegeben.

Kontoliste_filtern
Aufruf aus
Makro:
001
002
003
004
005
006

Aktuelles_Buchungsdatum, Kontofilter_Formstart, Kontofilter_Feldstart

SUB Kontoliste_filtern(inID,oFeld)
DIM stSql(0) AS STRING
stSql(0) = "SELECT ""Konto"", ""ID"" FROM ""Konto"" WHERE ""ID"" <> "+inID+"
ORDER BY ""Konto"""oFeld.ListSource = stSql
oFeld.refresh
END SUB

Der SQL-Code für die Liste der Umbuchungskonten wird neu zusammengestellt. In dem Listenfeld soll der Wert nicht mehr angezeigt werden, der bereits für das Konto vergeben ist.
Das Listenfeld wird mit einem Array bestückt. Dieses Array enthält lediglich einen Wert. Deshalb die entsprechende Definition in Zeile 2. In Zeile 3 wird dann der aktuelle Wert des abgebenden Kontos von der Liste ausgeschlossen. Anschließend wird dieses Array dem Listenfeld
zugewiesen und das Listenfeld neu eingelesen.

Haushaltsbuch

239

ChangeData
Aufruf aus
Formular: Kategorie_Konto_Auswertung
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060

SUB ChangeData(oEvent AS OBJECT)
DIM oDiag AS OBJECTDIM oDatasource AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Command AS OBJECTDIM oResult AS OBJECTDIM oForm AS OBJECTDIM oHiddenControl AS OBJECT
DIM stSql AS STRING
DIM stRow AS STRING
DIM stType AS STRING
DIM i AS INTEGERDIM k AS INTEGER
DIM x AS INTEGERDIM n AS INTEGER
DIM aNewData(0)DIM aNewRowDescription(0)DIM aTmp() AS DOUBLE
DIM aNewColumnDescription()
DIM aType()
DIM arView()
DIM arDiag()oForm = oEvent.Source
oHiddenControl = oForm.getByName("Chart")
arView = Split(oHiddenControl.Tag,",")
arDiag = Split(oHiddenControl.HiddenValue,",")FOR n = LBound(arView()) TO UBound(arView())
stSql = "SELECT * FROM """+arView(n)+""""
oDatasource = ThisComponent.Parent.CurrentController
IF NOT (oDatasource.isConnected()) THEN
oDatasource.connect()
END IFoConnection = oDatasource.ActiveConnection()oSQL_Command = oConnection.createStatement()oResult = oSQL_Command.executeQuery(stSql)
i = 0
k = 0
x = 0
WHILE oResult.next
stRow = oResult.getString(1)stType = oResult.getString(4)IF aNewRowDescription(i) = stRow THEN
ReDim Preserve aNewColumnDescription(k)ReDim Preserve aTmp(k)ELSE
IF x > 0 THENi = i + 1
ELSE
x = 1
END IF
ReDim Preserve aNewRowDescription(i)ReDim Preserve aNewData(i)k = 0ReDim Preserve aNewColumnDescription(k)ReDim aTmp(k)END IF
aNewRowDescription(i) = stRow
aNewColumnDescription(k) = oResult.getString(2)aTmp(k) = oResult.getDouble(3)aNewData(i) = aTmp()

Haushaltsbuch

240

061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076

k = k + 1
WEND
oDiag = thisComponent.EmbeddedObjects.getByName(arDiag(n))
oXCOEO = oDiag.ExtendedControlOverEmbeddedObject
oXCOEO.changeState(4)IF stType <> "XY" THEN
oDiag.model.Data.setData(aNewData)
END IFoDiag.model.DataProvider.setData(aNewData)oDiag.model.DataProvider.setRowDescriptions(aNewRowDescription)oDiag.model.DataProvider.setColumnDescriptions(aNewColumnDescription)oDiag.Component.setmodified(true)
oDiag.Component.update()
oXCOEO.changeState(1)NEXT
END SUBNach der Deklaration der Variablen wird zuerst aus den Zusatzinformationen (Tag) des versteckten Kontrollfeldes der Name für die Ansicht ausgelesen (Zeile 25). Da hier mehrere Namen
für mehrere Diagramme gespeichert werden können werden die Namen durch Komma getrennt
und über Split in ein Array eingelesen. Gleiches gilt für die Namen der Diagramme, die im
HiddenValue in der entsprechenden Reihenfolge eingetragen sind (Zeile 26). Anschließend
läuft eine Schleife (Zeile 27 bis 74) über das erste Array ab , das ja genau so viele Elemente
enthält wie das zweite Array. Der erforderliche SQL-Code zum Auslesen der gesamten Ansicht
wird formuliert (Zeile 28), die Verbindung zur Datenbank, falls erforderlich, hergestellt (Zeile 29
bis 32) und der SQL-Code an die Datenbank weitergeleitet (Zeile 33 bis 35). In oResult wird
das Ergebnis der Abfrage gespeichert.
Vor dem Start der Schleife zur Ermittlung des Inhaltes aus oResult werden noch einige Zahlenvariablen auf '0' gesetzt, die im Laufe der Schleife verändert werden sollen.
Innerhalb der Schleife (Zeile 39 bis 62) werden die Inhalte der verschiedenen Spalten der
Ansicht ausgelesen. Die Inhalte werden in unterschiedliche Arrays zur Weitergabe an den
DataProvider abgespeichert. Hier werden wieder Daten (Data), Zeilenbeschriftungen(RowDescriptions) und Spaltenbeschriftungen (ColumnDescriptions) unterschieden.
Mit oResult.getString(1) wird das Ergebnis zum jeweiligen Datensatz aus der ersten Spalte
als Text ausgelesen (Zeile 40). Hier handelt es sich um Zeilenbeschriftungen, die eben einfach
Text sind, in der vorliegenden Datenbank z.B. die Bezeichnungen der Kategorien. Solange die
Abfrage nacheinander gleiche Zeilenbeschriftungen liefert, werden alle weitere Inhalte dem
gleichen Datenbestand zugeordnet. Um dies zu gewährleisten, erfolgt zuerst eine Abfrage, ob
der entsprechende Eintrag von stRow bereits als letzter Eintrag von aNewRowDescriptions()
vorhanden ist (Zeile 42 bis 44).
Ist dies nicht der Fall, wie z.B. direkt beim Einlesen des ersten Datensatzes, dann erfolgt das
Vorgehen, das unter ELSE beschrieben ist. Nur wenn der Zähler x größer als '0' ist, wird derZähler für i heraufgesetzt (Zeile 46 und 47). Dies soll vermeiden, dass der erste Eintrag des zu
erzeugenden Arrays später leer ist. Für alle späteren Eintritte in diese Schleife wird allerdings
dann x auf '1' gesetzt, so dass i heraufgesetzt wird und die Arrays mit einem weiteren Datensatz beschrieben werden können.
Redim Preserve sichert den bisherigen Inhalt eines Arrays und eröffnet gleichzeitig die Möglichkeit, einen zusätzlichen Eintrag ans Ende des Arrays anzufügen.
aNewData und aNewRowDescriptions werden immer zusammen abgespeichert. Deswegenhaben die Arrays den gemeinsamen Zähler i (Zeile 51 und 52). aNewColumnDescriptions
sowie die in einem temporären Array aTmp gespeicherten Dateninhalte werden bei jedem
neuen Durchgang durch die WHILE-NEXT–Schleife mit einem zusätzlichen Datensatz versehen.
Auch sie haben deshalb einen gemeinsamen Zähler, hier k (Zeile 54 und 55). Dieser Zähler
wird bei jeder Änderung von aNewRowDescriptions neu mit '0' gestartet. Dabei wird das temporäre Array nicht gesichert, da es zwischenzeitig in dem Array aNewData abgespeichert wurde.

Haushaltsbuch

241Die Inhalte für das Array aTmp werden immer als Dezimalzahlen aus der Abfrage ausgelesen.
Daher der Eintrag oResult.getDouble(3) (Zeile 59).
Damit ein Diagramm im Formular kontinuierlich geändert werden kann, muss es zuerst einmal
aktiviert werden. Das Diagramm selbst ist dem Formular sonst völlig unbekannt. Das Diagramm wird über den Namen ausgesucht (Zeile 63). Anschließend wird der Controller für das
eingebettete Diagramm angesprochen. Zuerst wird mit '4' die UI aktiviert. Die möglichen Parameter sind unter com.sun.star.embed.EmbedStates: LOADED = 0, RUNNING = 1, ACTIVE = 2,
INPLACE_ACTIVE = 3, UI_ACTIVE = 4.
Nachdem alle Daten ausgelesen wurden, werden diese in den DataProvider übertragen(Zeile 66 bis 69). Mit oDiag.model.Data.setData(aNewData) werden nicht nur die Daten neu
geschrieben, sondern z.B. auch die Anzahl der Säulen in einem Spaltendiagramm aktualisiert.
Dieser Eintrag führt allerdings bei einem XY-Diagramm dazu, dass hier die Grundstruktur desDiagramms zerstört und der erste Dateneintrag nicht als X-Achsenwert gesehen wird. Deshalb
ist dieser Eintrag für XY-Diagramme ausgeschlossen.
Nach der Änderung wird der Status des Diagramms auf '1' gesetzt, da ansonsten das Diagramm die ganze Zeit im Bearbeitungsmodus erscheint.
Eine Übersicht über die möglichen Diagrammtypen liefert: https://api.libreoffice.org/docs/idl/
ref/servicecom_1_1sun_1_1star_1_1chart_1_1Diagram.html

Haushaltsbuch

242

Termine

Termine243

Einführung
Die Datenbank «Termine»9 dient nicht dazu, einen einfachen Terminplaner zu ersetzen. Terminplaner sind für die Verwaltung privater Termine wesentlich besser geeignet. Die Datenbank ist
entstanden, um an einer Schule die über das ganze Schuljahr verstreuten Termine zusammenzufassen und in Form von verschiedenen Berichten auf unterschiedliche Art verfügbar zu
machen. So gibt es einen Bericht, aus dem das schuljährliche Terminheft erstellt wird (jedeSeite ein kompletter Monat mit allen Tagen außer Samstag und Sonntag), einen anderen
Bericht, der unter den Terminen viele Einträge ermöglicht und so für Organisationszwecke wie
Vertretungsunterricht herangezogen werden kann.

Ausdrucke über Berichten sind die zentrale Anwendung dieser Termindatenbank.
Diese Datenbank ist vor allem auch deswegen entstanden, weil es eben im Schulalltag vieleverschiedene Ausgabemöglichkeiten für Termine gibt und es immer problematisch ist, Termine
per Hand von einem Formular in ein anderes einzutippen. Fehler beim Abgleich sind vorprogrammiert.
Diese Datenbank wird beständig erweitert. So ist ein Feld für eine Vielfachgruppe hinzu gekommen. Es bestand plötzlich das Problem, dass doch die Jahresübersichten für Klassenarbeiten
abhängig von den Terminen für bestimmte Jahrgänge erstellt werden sollten. Eine Eingabe fürbestimmte Klassen war aber gar nicht vorgesehen.
Aktuellste Erweiterung ist die Möglichkeit, Termine für einen bestimmten Zeitraum in eine iCalendar-Datei (*.ics) zu exportieren, so dass die Termine für KollegInnen nicht nur in gedruckter
Form, sondern auch direkt in digitaler Form in deren Kalender übernommen werden können.
Auch der Import aus iCalendar-Daten ist in einem Formular mit integriert.

9

Beispieldatenbank Beispiel_Termine.odb

Termine244

Tabellen
Die Datenbank besteht aus insgesamt 3 Tabellen, die miteinander verknüpft sind. Die restlichen
Tabellen sind temporär durch Makros erstellte Tabellen, eine Filtertabelle und eine Tabelle, inder die Termine der Ferien direkt eingetragen werden. Daneben tauchen in dem Tabellenordnernoch einige Ansichten auf.

Tabellen zur Dateneingabe über Formulare oder direkt

TermineDatenziel
Ansicht:

Ansicht_Kalender_Export, Ansicht_Kalender_ohne_Ferien_Export,
Ansicht_Termine_Monat_Jahre

Abfrage:

Termine_Gruppe_multi, Terminuebersicht

Makro:

Kalender, Zeitplan, Monat, GruppeMultiUebersicht, Termindaten_uebertragen, Tabellenstart, Ical_Import

Formular, Bericht::keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.
Basis für iCalendar-Eintrag UID

Bezeichnung

Text

Terminbeschreibung in Kurzform
Eingabe erforderlich: Ja
Basis für iCalendar-Eintrag SUMMARY

Ort

Text

Wo findet der Termin statt?
Eingabe erforderlich: Nein
Basis für iCalendar-Eintrag LOCATION

Beginn_Datum

Datum

Starttag des Termins
Eingabe erforderlich: Ja
Datumsbasis für iCalendar-Eintrag DTSTART

Beginn_Uhrzeit

Zeit

Startuhrzeit des Termins
Eingabe erforderlich: Nein
Zeitbasis für iCalendar-Eintrag DTSTART

Termine245FeldnameFeldtyp

Beschreibung

Ende_Datum

Datum

Endtag des Termins, kann auch frei bleiben. Dann ist der Starttag
und der Endtag in der Auswertung gleichgesetzt.
Eingabe erforderlich: Nein
Datumsbasis für iCalendar-Eintrag DTEND

Ende_Uhrzeit

Zeit

Enduhrzeit des Termins, kann auch frei bleiben. Dann ist der Termin für einen ganzen Tag ausgelegt.
Eingabe erforderlich: Nein
Zeitbasis für iCalendar-Eintrag DTEND

Bemerkung

Text

Detailliertere Beschreibung
Eingabe erforderlich: Nein
Basis für iCalendar-Eintrag DESCRITION

artID

Integer

Fremdschlüssel für die Art des Termins, bei schulischen Belangenso etwas wie Fachkonferenz, Informationsveranstaltung usw.
Eingabe erforderlich: Nein
Wird nicht nach iCalendar exportiert

zieID

Integer

Fremdschlüssel für die Zielgruppe des Termins, bei schulischen
Belangen so etwas wie LehrerInnen, Eltern, SchülerInnen oderÖffentlichkeit.
Eingabe erforderlich: Ja
Wird nicht nach iCalendar exportiert

Gruppe_multi

Integer

Mehrfacheintrag für gleichartige Gruppen, bei schulischen Belangen alle Jahrgänge der Schule. Der Eintrag erfolgt hier Bitweise,d.h. den Gruppen werden feste Werte im Zweiersystem zugeordnet: 20 = 1 | 21 = 2 | 22 = 4 | 23 = 8 usw. So können mehrere
Gruppen in einer Zahl wiedergegeben werden. Wird z.B '7' eingetragen, so sind die ersten 3 Gruppen betroffen, weil 1 + 2 + 4 =7.
Solch ein Eintrag kann auch durch eine n:m-Beziehung von Tabellen erreicht werden. Dieses Feld wurde aber im Nachhinein hinzugefügt und konnte einfacher in die komplette Datenbank mit allen
Abfragen usw. übernommen werden.
Eingabe erforderlich: Nein

Terminart
Datenziel
Formular: Terminart_Termin (MainForm)
Makro:

Monat, Tabellenstart

Ansicht, Abfrage, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.
Wird mit der Tabelle "Termine" verknüpft

Terminart

Text

Art des Termins, bei schulischen Belangen so etwas wie Fachkonferenz, Informationsveranstaltung usw.
Eingabe erforderlich: Ja

Die Tabelle "Terminart" wird nur in einem der Formulare zum direkten Editieren genutzt. Die
anderen Formulare greifen auf diese Tabelle nur lesend zu.

Termine246

Zielgruppe
Datenziel
Makro:

Bericht_Start, Tabellenstart

Ansicht, Abfrage, Formular, Bericht: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.
Wird mit der Tabelle "Termine" verknüpft

Zielgruppe

Text

Zielgruppe des Termins, bei schulischen Belangen so etwas wie
LehrerInnen, Eltern, SchülerInnen oder Öffentlichkeit.
Eingabe erforderlich: Ja

Termingruppe

Text

Auf welche Termine soll die Gruppe Zugriff haben. Lehrerkonferenzen z.B. brauchen nicht in der Terminübersicht für die Eltern zu
erscheinen. Hier stehen, durch Kommas getrennt, die eigene ID
sowie die IDs anderer Zielgruppen
Eingabe erforderlich: Nein

Die Tabelle "Zielgruppe" wird nur direkt mit Daten gefüllt. Die Formulare greifen auf die Zielgruppe mit Listenfeldern zu. Die Termingruppe entscheidet, welche Termine später in den
Berichten enthalten sind. Wenn eine Termingruppe mit allen Einträgen aus "Termingruppe"."ID"durch Komma getrennt bestückt ist, dann werden alle Termine aus dem Terminkalender beim
Ausdruck für diese Gruppe berücksichtigt.
Diese oben genannten 3 Tabellen "Termine", "Terminart" und "Zielgruppe" wurden über Extras→ Beziehungen miteinander verknüpft.

FerienDatenziel
Ansicht:

Ansicht_Gruppe_multi, Ansicht_Kalender_Export, Ansicht_Termine_Monat_Jahre

Makro:

Kalender, GruppeMultiUebersicht, Tabellenstart

Abfrage, Formular, Bericht: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Beginn_Datum

Datum

Beginn von Ferien. Kann auch für den Beginn anderer Zeiten
genutzt werden, bei denen kein Eintrag eines Termins erfolgt. Hiereingetragene Termine werden im Kalender als Ferien gekennzeichnet.
Eingabe erforderlich: Ja

Ende_Datum

Datum

Ende der Ferien, bei einem einzelnen Ferientag gleich dem
Beginndatum.
Eingabe erforderlich: Nein

Diese Tabelle wird direkt editiert. Sie wird über Formulare nicht beeinflusst und nur bei den
Berichten berücksichtigt.

Termine247

Filtertabelle
Datenziel
Ansicht:

Ansicht_Gruppe_multi, Ansicht_Kalender_Export,
Ansicht_Kalender_ohne_Ferien_Export, Ansicht_Termine_Monat_Jahre

Formular: Terminart_Termin (Berichte, Export), Termine_Kalenderübersicht (Monat, Berichte,
Export), Termine_Monat_Terminart (Berichte, Export), Termine_Tabelleneingabe(Monat, Berichte, Export)
Makro:

Bericht_Start, Monatsansicht, Monatsansicht_aktualisieren, DialogEnde, Ical_Import

Abfrage, Bericht:

keiner direkt

In der Tabelle "Filter" werden folgende Felder definiert:
Feldname

Feldtyp

Beschreibung

ID

Ja/NeinPrimärschlüssel, erster Datensatz 'Ja' bereits eingegeben.

StartDatum

Datum

Startdatum für den Export nach iCalendar
Eingabe erforderlich: Nein

EndDatum

Datum

Enddatum für den Export nach iCalendar
Eingabe erforderlich: Nein

Jahr_Monat

Text

Einhält eine Kombination aus Jahreszahl und Monatszahl,
durch Komma getrennt (z.B. '2020,04')
Diese Eingabe ist erforderlich, damit die Monatsdarstellung einwandfrei funktioniert. Zur Not kann hier ein
Dummy wie oben genannt eingesetzt werden.
Eingabe erforderlich: Ja

Startjahr

Small Integer

Enthält die Jahreszahl, mit der der Kalenderausdruck
startetEingabe erforderlich: Nein

Halbjahrende

Datum

Nur für Schulen: Das Ende des ersten Halbjahres, damit
Ausdrucke auch halbjahresweise erfolgen können.
Eingabe erforderlich: Nein

FerienJa/Nein

Sollen Ferien beim Export mit exportiert werden?
Eingabe erforderlich: Nein

BerichtZielgruppeID

Integer

Fremdschlüsselfeld für die Zielgruppe beim Berichtsausdruck
Nur zur Speicherung der Formulareingabe
Eingabe erforderlich: Nein

BerichtStartDat

Datum

Startdatum für den Berichtsausdruck
Nur zur Speicherung der Formulareingabe
Eingabe erforderlich: Nein

BerichtEndDat

Datum

Enddatum für den Berichtsausdruck
Nur zur Speicherung der Formulareingabe
Eingabe erforderlich: Nein

BerichtName

Text

Berichtsname aus dem Listenfeld des Formulars – nicht
unbedingt identisch mit dem Berichtsname in BaseselbstNur zur Speicherung der Formulareingabe
Eingabe erforderlich: Nein

Termine248FeldnameFeldtyp

Beschreibung

BerichtGuppeMulti

Small Integer

Kennziffer für die gewünschte Gruppe beim Bericht"Liste_Gruppe_multi"
Nur zur Speicherung der Formulareingabe
Eingabe erforderlich: Nein

StartIDImport

Integer

Beim Import von iCalendar-Daten wird hier die erste
neue ID aus der Tabelle "Termine" gespeichert. Dies
dient zum Anzeigen der neu importierten Inhalte im Formular.
Eingabe erforderlich: nein

EndIDImport

Integer

Beim Import von iCalendar-Daten wird hier die letzte
neue ID aus der Tabelle "Termine" gespeichert. Wird nur
ein Datensatz eingefügt, so ist die EndIDImport gleich
der StartIDImport. Dies dient zum Anzeigen der neu
importierten Inhalte im Formular.
Eingabe erforderlich: nein

Diese Tabelle wird zum Filtern für den Export, für die Erstellung der Tabellen für die Berichte
usw. genutzt. Sie enthält nur einen Datensatz. Der Datensatz muss durch die Eingabe des Primärschlüssels 'Ja' erstellt sein, bevor die Tabelle im Formular genutzt werden kann.

Durch Makros gefüllt Tabellen
Die weiteren Tabellen sind Tabellen, die jeweils durch Makros für den folgenden Ausdruck
gefüllt werden. Sie starten alle mit der Benennung "tmp_".

tmp_Halbjahreskalender
Datenziel
Bericht:

Halbjahreskalender_A4_quer

Makro:

Halbjahreskalender

Ansicht, Abfrage, Formular: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Jahr

Small Integer

Jahreszahl

T1 … T6

Text

Insgesamt 6 Felder für Termine, siehe "Termine"."Terminbezeichnung" – 250 Zeichen

D1 … D6

Datum

Zu den 6 Terminfeldern passende 6 Datumsfelder

Sort_Zahl

Tiny Integer

Sortierzahl, '1' für das erste Halbjahr, '2' für das 2. Halbjahr

F1 … F6

Text

Ferienanzeige, wenn '1', dann wird dies als "Ferien" im Kalendervermerkt. 1 Zeichen

Die Termine werde in dieser Tabelle so aufgenommen, dass jeweils 6 Monatsverläufe nebeneinander präsentiert werden. In D1 beginnt die Zählung z.B. mit dem '1.1.2020', in D2 mit dem'1.2.2020' usw.
Da ein Monat maximal 31 Tage hat enthält diese Tabelle insgesamt für ein Jahr 62 Tabellenzeilen – 31 Tabellenzeilen für das erste Halbjahr und 31 Tabellenzeilen für das zweite Halbjahr.

Termine249

tmp_Gruppe_multi
Datenziel
Ansicht:

Ansicht_Gruppe_multi

Makro:

GruppeMultiUebersicht

Abfrage, Formular, Bericht:: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Datum

Datum

Komplette Datumsreihenfolge für ein (Schul)jahr

Bezeichnung

Text

Siehe "Termine"."Terminbezeichnung", auch Feiertage ..

FormatText

Ferienanzeige, wenn '1', dann wird dies als "Ferien" im Kalendervermerkt. 1 Zeichen

Gruppe_multi

Text

Zuordnung des Termins zu einer Gruppe, siehe"Termine"."Gruppe_multi". 5 Zeichen

tmp_Kalender
Datenziel
Ansicht:

Ansicht_Wochentage

Makro:

Kalender, Monatskalender, Halbjahreskalender

Abfrage, Formular, Bericht: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Datum

Datum

Komplette Datumsreihenfolge für ein (Schul)jahr

Bezeichnung

Text

Siehe "Termine"."Terminbezeichnung", auch Feiertage ..

FormatText

Ferienanzeige, wenn '1', dann wird dies als "Ferien" im Kalendervermerkt. 1 Zeichen

tmp_Monat
Datenziel
Formular: Termine_Kalenderübersicht (Ansicht)
Makro:

Monat

Ansicht, Abfrage, Bericht: keine direkt

Termine250

FeldnameFeldtyp

Beschreibung

ID

Tiny
IntegerPrimärschlüssel

T1 … T7

Text

Insgesamt 7 Felder für Termine (7 Tage), siehe"Termine"."Beginn_Datum", "Termine"."Terminbezeichnung" Alle
Termine für einen Tag des Monats in einem Feld, durch Zeilenumbrüche voneinander abgesetzt, nach Wochentagen sortiert. – 500
Zeichen

tmp_Terminuebersicht
Datenziel
Bericht:

Zeitplan

Makro:

Zeitplan

Ansicht, Abfrage, Formular: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Beginn_Datum

Datum

Startdatum für den Termin. Wenn der Termin nur an einem
Tag liegt ist dies der einzige Datumseintrag.

Bezeichnung

Text

Siehe "Termine"."Terminbezeichnung"Terminart

Text

Siehe "Terminart"."Terminart"

Monat

Tiny Integer

Monatszahl

Jahr

Small Integer

Jahreszahl

D2 … D7

Text

Wiederholungstermine oder Terminfortführung bis zu dem
entsprechenden Datum.

tmp_Wochenkalender
Datenziel
Ansicht:

Ansicht_Wochen_einzeln

Abfrage:

Wochenkalender_Mo_Fr

Bericht:

Monatskalender_Mo_So

Makro:

Monatskalender

Formular: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Jahr

Small Integer

Jahreszahl

Monat_Text

Text

Monatsname, 20 Zeichen

Woche

Tiny Integer

Wochenzahl

T1 … T7

Text

250 Zeichen

D1 … D7

Datum

Monat_Zahl

Tiny Integer

TermineMonat, Zahlwert

251FeldnameFeldtyp

Beschreibung

F1 … F7

Text

Formatierung (Ferien, Feiertage) , 1 Zeichen

Monat_Sort

Tiny Integer

Sortierung bei Jahresübergängen sicherstellen

tmp_Wochenkalender_halb
Datenziel
Bericht:

Wochenkalender_halb

Makro:

Wochenkalender_halb

Ansicht, Abfrage, Formular: keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, als Autowert ausgelegt.

Jahr

Small
Integer

Jahreszahl

Monat_Text

Text

Monatsname, 20 Zeichen

Woche

Tiny
Integer

Wochenzahl

T1 … T4

Text

Siehe "Termine"."Terminbezeichnung", Termine der Tage Montag –Mittwoch und Donnerstag – Sonntag werden im Wochenkalenderin jeweils einer Zeile zusammengefasst – 250 Zeichen

D1 … D4

Datum

Datum zu den Terminen

Monat_Zahl

Tiny
IntegerMonat, Zahlwert

F1 … F4

Text

Formatierung (Ferien, Feiertage) , 1 Zeichen

Ansichten
Ansicht_Gruppe_multi
Datenquelle
Tabelle:tmp_Gruppe_multi, Filter, Ferien

Datenziel
Abfrage:

Liste_Gruppe_Multi

Ansicht, Formular, Bericht: keine direkt

001
002
003
004
005
006
007
008

SELECT "Datum" AS "Sort",
CASE WHEN DAYOFWEEK("Datum")=2 THEN 'Mo'
WHEN DAYOFWEEK("Datum")=3 THEN 'Di'
WHEN DAYOFWEEK("Datum")=4 THEN 'Mi'
WHEN DAYOFWEEK("Datum")=5 THEN 'Do'
WHEN DAYOFWEEK("Datum")=6 THEN 'Fr'
END AS "Tag",RIGHT('0'||EXTRACT(DAY FROM "Datum"),2)||'.'||RIGHT('0'||EXTRACT (MONTHFROM "Datum"),2)||'.' AS "Dat",

Termine252

009 "Bezeichnung", "Format", EXTRACT (YEAR FROM "Datum")||RIGHT(0||WEEK("Datum"),2) AS "W1", "Gruppe_multi"
010 FROM "tmp_Gruppe_multi"
011 WHERE "Datum" BETWEEN ( SELECT "Startjahr" FROM "Filter" WHERE "ID" = TRUE
) || '-08-01' AND (SELECT "Startjahr" FROM "Filter" WHERE "ID" = TRUE ) +1 || '-08-01'
012 AND DAYOFWEEK("Datum") BETWEEN 2 AND 6
013 AND ( NOT "Bezeichnung" = 'Ferien' OR "Bezeichnung" IS NULL )014 UNION
015 SELECT "Beginn_Datum",
016 NULL AS "Tag",
017 NULL AS "Dat",
018 'Ferien vom '||RIGHT('0'||EXTRACT (DAY FROM "Beginn_Datum"),2)||'.'||
RIGHT('0'||EXTRACT (MONTH FROM "Beginn_Datum"),2)||'.'||' bis '||RIGHT('0'||EXTRACT(DAY FROM "Ende_Datum"),2)||'.'||RIGHT('0'||EXTRACT(MONTH FROM "Ende_Datum"),2)||'.',
019 '1',020 NULL,
021 (SELECT DISTINCT "Gruppe_multi" FROM "tmp_Gruppe_multi")
022 FROM "Ferien"
023 WHERE ("Beginn_Datum" BETWEEN ( SELECT "Startjahr" FROM "Filter" WHERE"ID" = TRUE ) || '-08-01' AND ( SELECT "Startjahr" FROM "Filter" WHERE"ID" = TRUE ) + 1 || '-08-01' )024 OR ("Ende_Datum" BETWEEN ( SELECT "Startjahr" FROM "Filter" WHERE "ID"=TRUE ) || '-08-01' AND ( SELECT "Startjahr" FROM "Filter" WHERE "ID" =TRUE ) + 1 || '-08-01' )Die Ansicht dient als Basis für einen Listenausdruck, der nach den Gruppen sortiert angefertigt
wird. Dieser Listenausdruck wird nach Erstellung der Tabelle "tmp_Gruppe_multi" ermöglicht.
Die erste Spalte zeigt das Datum als Sortierelement an (Zeile 1), die zweite Spalte bezeichnet
den Wochentag (Zeile 2 bis 7). Hier sind nur 5 Wochentage aufgeführt, da die Liste nur fürmontags bis freitags erstellt wird.
In Zeile 8 wird das Datum in Kurzform übernommen: '21.04', also zweistellige Tageszahl,
getrennt mit einem Punkt von der zweistelligen Monatszahl. Die Zweistelligkeit wird erreicht,
indem einfach eine Null mit der jeweiligen Zahl kombiniert wird und dann die beiden hinteren
Zeichen durch RIGHT('String', 2 ) übernommen werden.
Zeile 9 speichert dann noch das Jahr, gekoppelt mit der Kalenderwoche, so dass auch hierüber
eine Sortierung erfolgen kann. Aus diesem Wert werden nachher in einer Abfrage die Schulwochen abgeleitet, die ja mitten im Jahr beginnen und auch die Ferien nicht berücksichtigen.
Die ausgelesenen Werte werden nach den folgenden Bedingungen zusammengestellt:
In Zeile 11 wird nach dem Startjahr aus der Tabelle "Filter" geschaut. Die Datumswerte für die
Liste werden danach auf die Zeit vom 1.8. des Startjahres bis zum 31.7. des Folgejahres
erlaubt. Der Schuljahresbeginn findet immer Anfang August statt, auch wenn dort z.B. nochFerien sind.
Zeile 12 begrenzt die ausgelesenen Datumseinträge auf die Wochentage 2 bis 6, was den
Wochentagen von Montag bis Freitag entspricht.
Zeile 13 und Zeile schließen dann noch aus, dass die "Bezeichnung" der Tabelle"tmp_Gruppe_multi" den Begriff 'Ferien' enthält. Diese Bedingung alleine würde aber die Felder,
bei denen die "Bezeichnung" leer ist, auch mit aus der Ansicht heraus werfen. Sie müssen deshalb noch extra wieder mit ausgenommen werden. Entweder also nicht 'Ferien' oder ein leeres
Feld.
Daran angehängt wird jetzt noch ein Auszug der Tabelle "Ferien". Für jeden Eintrag in derTabelle "Ferien" wird hier ein Eintrag eingefügt, der dann anschließend in der auszudruckendenÜbersicht als eine eingefärbte Zeile mit dem Verweis darauf existiert, dass die Tage fehlen, weilda eben Ferien liegen. Dieser Verweis wird in Zeile 18 zusammengestellt, die spezielle Formatierung verbirgt sich hinter der Ziffer '1', die in Zeile 19 hinzugefügt wird.

Termine253

Ansicht_Kalender_Export
Datenquelle
Tabelle:Termine, Filter, Ferien

Datenziel
Makro:

Export_Start

Ansicht, Abfrage, Formular, Bericht: keine direkt

001 SELECT RIGHT('0000000'||"ID",8) AS "UID",
002 "Ort" AS "Location", "Bezeichnung" AS "Summary", "Bemerkung" AS"Description",
003 COALESCE(
004 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 ) || 'T' || RIGHT( '0' || EXTRACT( HOUR FROM "Beginn_Uhrzeit" ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM "Beginn_Uhrzeit" ), 2 ) || RIGHT( '0'|| EXTRACT( SECOND FROM "Beginn_Uhrzeit" ), 2 ),005 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 )) AS "DtStart",
006 COALESCE (007 EXTRACT( YEAR FROM "Ende_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Ende_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Ende_Datum" ), 2 )|| 'T' || RIGHT( '0' || EXTRACT( HOUR FROM "Ende_Uhrzeit" ), 2 ) || RIGHT('0' || EXTRACT( MINUTE FROM "Ende_Uhrzeit" ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM "Ende_Uhrzeit" ), 2 ),008 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 ) || 'T' || RIGHT( '0' || EXTRACT( HOUR FROM "Ende_Uhrzeit" ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM "Ende_Uhrzeit" ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM "Ende_Uhrzeit" ), 2 ),009 EXTRACT( YEAR FROM "Ende_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Ende_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Ende_Datum" )+1, 2),
010 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" )+1, 2 )) AS "DtEnd",
011 EXTRACT( YEAR FROM CURRENT_DATE ) || RIGHT( '0' || EXTRACT( MONTH FROMCURRENT_DATE ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM CURRENT_DATE ), 2 )|| 'T' || RIGHT( '0' || EXTRACT( HOUR FROM CURRENT_TIME ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM CURRENT_TIME ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM CURRENT_TIME ), 2 ) AS "DtStamp"
012 FROM "Termine"
013 WHERE "Beginn_Datum" BETWEEN COALESCE((SELECT "StartDatum" FROM "Filter"
WHERE "ID" = TRUE),"Beginn_Datum") AND COALESCE((SELECT "EndDatum" FROM
"Filter" WHERE "ID" = TRUE),"Beginn_Datum")
014 OR "Ende_Datum" BETWEEN COALESCE((SELECT "StartDatum" FROM "Filter" WHERE"ID" = TRUE),"Ende_Datum") AND COALESCE((SELECT "EndDatum" FROM "Filter"
WHERE "ID" = TRUE),"Ende_Datum")
015 UNION
016 SELECT RIGHT('000000'||"ID"||'F',8) AS "UID", '','Ferien','',
017 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 ) AS "DtStart",
018 COALESCE(

Termine254

019 EXTRACT( YEAR FROM "Ende_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Ende_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Ende_Datum" )+1, 2) ,020 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" )+1, 2 ) ) AS "DtEnd",
021 EXTRACT( YEAR FROM CURRENT_DATE ) || RIGHT( '0' || EXTRACT( MONTH FROMCURRENT_DATE ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM CURRENT_DATE ), 2 )|| 'T' || RIGHT( '0' || EXTRACT( HOUR FROM CURRENT_TIME ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM CURRENT_TIME ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM CURRENT_TIME ), 2 ) AS "DtStamp"
022 FROM "Ferien"
023 WHERE "Beginn_Datum" BETWEEN COALESCE((SELECT "StartDatum" FROM "Filter"
WHERE "ID" = TRUE),"Beginn_Datum") AND COALESCE((SELECT "EndDatum" FROM
"Filter" WHERE "ID" = TRUE),"Beginn_Datum")
024 OR "Ende_Datum" BETWEEN COALESCE((SELECT "StartDatum" FROM "Filter" WHERE"ID" = TRUE),"Ende_Datum") AND COALESCE((SELECT "EndDatum" FROM "Filter"
WHERE "ID" = TRUE),"Ende_Datum")
025 ORDER BY "DtStart"
Aus dieser Ansicht findet der Export nach iCalendar statt. Die verschiedenen Felder sind durcheinen Alias auch für den Nutzer sichtbar mit den Bezeichnungen des Exportformats vorbelegt.
Die Ansicht verbindet die Dateneinträge aus der Tabelle "Termine" (Zeile 11) mit denen aus derTabelle "Ferien" (Zeile 21), so dass alle Termine exportiert werden. Die Termine werden lediglich
durch die Filtertabelle vom Datum her eingegrenzt (Zeile 12 und 13 sowie Zeile 22 und 23).
Die UID aus iCalendar wird über den Primärschlüssel als eindeutiges Kennzeichen erstellt.
Dabei wird die Integer-Zahl des Primärschlüssels mit führenden Nullen zu einem achtstelligen
Text umgewandelt (Zeile 1).
Ort, Bezeichnung und Bemerkung werden hier durch den Alias nur schon einmal so gekennzeichnet, dass klar ist, auf welcher Felder des iCalendar-Exports sie sich beziehen.
Die Datums- und Zeitbehandlung erfordert hier ein besonderes Vorgehen. Reine Datumswerte
werden im Format YYYYMMDD, also z.B. '20200421', erwartet. Kommt ein Zeitfeld hinzu, sowird dieses über ein 'T' angehängt. Zeiten werden in dem Format HHMMSS angegeben, alsoz.B. '113559'. Ein kompletter Zeitstempel wäre also '20200421T113549'.
Zeile 3 weist bereist mit dem Befehl COALESCE darauf hin, dass eventuell ein Inhalt so, wie erzusammengebaut wird, auch leer sein kann. Zuerst wird die Kombination von Datum und Zeit
versucht. Ist diese Leer, so wird nur das Datum in den erforderlichen String umgewandelt.
Die Monatszahl und die Tageszahl können beim Auslesen einstellig sein. Deswegen wird immer
eine führende '0' mit diesen Zahlen kombiniert und von rechts aus die beiden Zeichen weiterbenutzt. Zweistellige Zahlen sind also von der Konstruktion RIGHT ( '0' || EXTRACT(MONTHFROM "Beginn_Datum"), 2 ) (Zeile 4 und folgende) nicht berührt.
Die Kombination von "DtStart" ist damit abgeschlossen. Bei der Kombination zu "DtEnd" sindhingegen 4 verschiedene Kombinationen möglich (Zeile 7 bis Zeile 10). Zuerst wird die Kombination von "Ende_Datum" und "Ende_Uhrzeit" getestet. Klappt die nicht, dann wird geprüft, ob
zwar eine "Ende_Uhrzeit" existiert, aber eben keine "Ende_Datum". Dann wird die Uhrzeit mit
dem "Beginn_Datum" kombiniert, weil davon ausgegangen wird, dass der Termin genau am
gleichen Tag zu Ende ist. Existiert zwar ein "Ende_Datum" aber keine "Ende_Uhrzeit", so wird
nur das Datum übernommen, aber um einen Tag erhöht, da so der gesamte Tag erfasst wird.
Existiert weder ein "Ende_Datum" noch eine "Ende_Uhrzeit", so wird nur das "Beginn_Datum"
für diesen Zeitstempel übernommen, ebenfalls um einen Tag erhöht. Die iCalendar-Spezifikation sieht bei einer Angabe eines reinen Datumswertes für "DtStart" den Termin als Termin füreinen ganzen Tag an, wenn der Tag für "DtEnd" auf dem Folgetag liegt.
In Zeile 11 wird schließlich der Zeitstempel für die Erstellung des Termins über das aktuelle
Datum und die aktuelle Zeit zusammengestellt.

Termine255

Die Daten aus "Termine" werden über den UNION-Befehl mit den Daten aus "Ferien" verbunden.
Bei der Zusammenstellung der Ferien ab Zeile 16 ist das Verfahren etwas einfacher, da dort die
Zeitfelder nicht vorkommen. Das "Beginn_Datum" wird hier direkt übernommen, für das"Ende_Datum" muss wieder ein Tag addiert werden, damit in iCalendar klar ist, dass der Termin
für einen ganzen Tag gilt. Fehlt ein "Ende_Datum", so wird stattdessen das "Beginn_Datum" umeinen Tag erhöht. Es wird dann von lediglich einem Ferientag ausgegangen.

Ansicht_Kalender_ohne_Ferien_Export
Datenquelle
Tabelle:Termine, Filter

Datenziel
Makro:

Export_Start

Ansicht, Abfrage, Formular, Bericht: keine direkt

001 SELECT RIGHT('0000000'||"ID",8) AS "UID",
002 "Ort" AS "Location", "Bezeichnung" AS "Summary", "Bemerkung" AS"Description",
003 COALESCE(
004 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 ) || 'T' || RIGHT( '0' || EXTRACT( HOUR FROM "Beginn_Uhrzeit" ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM "Beginn_Uhrzeit" ), 2 ) || RIGHT( '0'|| EXTRACT( SECOND FROM "Beginn_Uhrzeit" ), 2 ),005 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 )) AS "DtStart",
006 COALESCE (007 EXTRACT( YEAR FROM "Ende_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Ende_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Ende_Datum" ), 2 )|| 'T' || RIGHT( '0' || EXTRACT( HOUR FROM "Ende_Uhrzeit" ), 2 ) || RIGHT('0' || EXTRACT( MINUTE FROM "Ende_Uhrzeit" ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM "Ende_Uhrzeit" ), 2 ),008 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" ),2 ) || 'T' || RIGHT( '0' || EXTRACT( HOUR FROM "Ende_Uhrzeit" ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM "Ende_Uhrzeit" ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM "Ende_Uhrzeit" ), 2 ),009 EXTRACT( YEAR FROM "Beginn_Datum" ) || RIGHT( '0' || EXTRACT( MONTH FROM
"Beginn_Datum" ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM "Beginn_Datum" )+1, 2 )) AS "DtEnd",
010 EXTRACT( YEAR FROM CURRENT_DATE ) || RIGHT( '0' || EXTRACT( MONTH FROMCURRENT_DATE ), 2 ) || RIGHT( '0' || EXTRACT( DAY FROM CURRENT_DATE ), 2 )|| 'T' || RIGHT( '0' || EXTRACT( HOUR FROM CURRENT_TIME ), 2 ) ||RIGHT( '0' || EXTRACT( MINUTE FROM CURRENT_TIME ), 2 ) || RIGHT( '0' ||EXTRACT( SECOND FROM CURRENT_TIME ), 2 ) AS "DtStamp"
011 FROM "Termine"
012 WHERE "Beginn_Datum" BETWEEN COALESCE((SELECT "StartDatum" FROM "Filter"
WHERE "ID" = TRUE),"Beginn_Datum") AND COALESCE((SELECT "EndDatum" FROM
"Filter" WHERE "ID" = TRUE),"Beginn_Datum")
013 OR "Ende_Datum" BETWEEN COALESCE((SELECT "StartDatum" FROM "Filter" WHERE"ID" = TRUE),"Ende_Datum") AND COALESCE((SELECT "EndDatum" FROM "Filter"
WHERE "ID" = TRUE),"Ende_Datum")
014 ORDER BY "DtStart"

Termine256

Diese Ansicht unterscheidet sich von der Ansicht "Ansicht_Kalender_Export" nur darin, dassnicht mit dem Befehl UNION eine zweite Abfrage zu den Ferien an die erste Abfrage angehängt
wird.
Diese Ansicht dient also zum Export der Daten nach iCalendar ohne den gleichzeitigen Export
der Ferien.

Ansicht_Termine_Monat_Jahre
Datenquelle
Tabelle:Termine, Filter, Ferien

Datenziel
Ansicht, Abfrage, Formular, Bericht: keine direkt

001 SELECT DISTINCT "Jahr", "Jahr"||','||RIGHT('0'||"Monat_Z",2) AS"Monat_Zahl",
002 CASE WHEN "Monat_Z"='1' THEN 'Januar' WHEN "Monat_Z"='2' THEN 'Februar'
WHEN "Monat_Z"='3' THEN 'März' WHEN "Monat_Z"='4' THEN 'April'
WHEN "Monat_Z"='5' THEN 'Mai' WHEN "Monat_Z"='6' THEN 'Juni'
WHEN "Monat_Z"='7' THEN 'Juli' WHEN "Monat_Z"='8' THEN 'August'
WHEN "Monat_Z"='9' THEN 'September' WHEN "Monat_Z"='10' THEN 'Oktober'
WHEN "Monat_Z"='11' THEN 'November' WHEN "Monat_Z"='12' THEN 'Dezember'
END AS "Monat_Text",
003 "Monat_Z" FROM
004 (SELECT EXTRACT (YEAR FROM "Beginn_Datum" ) AS "Jahr", EXTRACT (MONTH FROM
"Beginn_Datum" ) AS "Monat_Z" FROM "Termine"
005 UNION ALL
006 SELECT EXTRACT (YEAR FROM "Ende_Datum" ) AS "Jahr", EXTRACT (MONTH FROM
"Ende_Datum" ) AS "Monat_Z" FROM "Termine" WHERE NOT "Ende_Datum" IS NULL)
007 AS "Jahre_Monate" ORDER BY "Jahr" ASC, "Monat_Z" ASC
Diese Ansicht dient dazu, Listenfelder zur Auswahl der Monate zu beschicken. Nach dieser Auswahl wird in Formularen anschließend der Inhalt gefiltert (Formulare "Termine_Kalenderübersicht" und "Termine_Tabelleneingabe").

Ansicht_Wochen_einzeln
Datenquelle
Tabelle:tmp_Wochenkalender

Datenziel
Makro:

Wochenkalender_halb

Ansicht, Abfrage, Formular, Bericht: keine direkt

001 SELECT "tmp_Wochenkalender".*002 FROM ( SELECT "Woche", MIN( "ID" ) AS "ID_einzeln" FROM
"tmp_Wochenkalender" AS "tmp_Wochenkalender" GROUP BY "Woche", "Jahr" ) AS
"Woche_ID", "tmp_Wochenkalender"
003 WHERE "Woche_ID"."ID_einzeln" = "tmp_Wochenkalender"."ID"In der Tabelle "tmp_Wochenkalender" sind für die entsprechenden Berichte Wiederholungen anden Monatsenden eingebaut, sofern eine Woche nicht genau mit dem Monat endet. Dadurchwerden Wochen einmal im Vormonat und dann im aktuellen Monat angezeigt.
Für die Erstellung von "tmp_Wochenkalender_halb" müssen aus "tmp_Wochenkalender" alle
Datensätze entfernt werden, die eine Verdoppelung der Wochen darstellen. Hier wird in der
Termine257Unterabfrage gruppiert nach "Woche" und "Jahr" der kleinere von den Primärschlüsselwerten"ID" über MIN("ID") herausgesucht. Wenn also eine Woche doppelt vorkommt, so wird derjeweils zweite Datensatz mit der größeren "ID" nicht mehr in dieser Ansicht angezeigt.

Ansicht_Wochentage
Datenquelle
Tabelle:tmp_Kalender

Datenziel
Bericht:

Wochentage_Mensaabo

Ansicht, Abfrage, Formular: keine direkt

001 SELECT COUNT( "ID" ) AS "MoDiMiDoFr",
002 ( SELECT COUNT( "ID" ) FROM "tmp_Kalender" WHERE "Format" IS NULL AND
DAYOFWEEK( "Datum" ) IN ( 2, 4, 5 ) AND EXTRACT (YEAR FROM "Datum" ) =EXTRACT (YEAR FROM "a"."Datum" ) AND EXTRACT (MONTH FROM "Datum" ) =EXTRACT (MONTH FROM "a"."Datum" ) ) AS "MoMiDo",003 ( SELECT COUNT( "ID" ) FROM "tmp_Kalender" WHERE "Format" IS NULL AND
DAYOFWEEK( "Datum" ) IN ( 2, 3, 4, 5 ) AND EXTRACT (YEAR FROM "Datum" ) =EXTRACT (YEAR FROM "a"."Datum" ) AND EXTRACT (MONTH FROM "Datum" ) =EXTRACT (MONTH FROM "a"."Datum" ) ) AS "MoDiMiDo",004 ( SELECT COUNT( "ID" ) FROM "tmp_Kalender" WHERE "Format" IS NULL AND
DAYOFWEEK( "Datum" ) IN ( 2, 4, 5, 6 ) AND EXTRACT (YEAR FROM "Datum" ) =EXTRACT (YEAR FROM "a"."Datum" ) AND EXTRACT (MONTH FROM "Datum" ) =EXTRACT (MONTH FROM "a"."Datum" ) ) AS "MoMiDoFr",
005 EXTRACT (YEAR FROM "Datum" ) AS "Jahr", EXTRACT (MONTH FROM "Datum" ) AS
"Monat",006 CASEWHEN( EXTRACT (MONTH FROM "Datum" ) < 8 ,EXTRACT (YEAR FROM "Datum" )1||'/'|| EXTRACT (YEAR FROM "Datum" ),EXTRACT (YEAR FROM "Datum" )||'/'||EXTRACT (YEAR FROM "Datum" )+1) AS "Schuljahr"
007 FROM "tmp_Kalender" AS "a"008 WHERE "Format" IS NULL AND DAYOFWEEK( "Datum" ) BETWEEN 2 AND 6 GROUP BY
EXTRACT (YEAR FROM "Datum" ), EXTRACT (MONTH FROM "Datum" )Mit dieser Ansicht wird die Anzahl bestimmter Wochentagskombinationen in einzelnen Monaten
ermittelt. Danach soll in einem Bericht ermittelt werden, für wie viele Tage für ein entsprechendes Essensabo in der Schulmensa gezahlt werden muss. Zeile 1 zählt alle Wochentage vonMontag bis Freitag, die in der Tabelle "tmp_Kalender" verzeichnet sind (Zeile 7). Allerdings dürfen diese Tage keinen Formatvermerk haben (Zeile 8). Dieser Formatvermerk wird bei allen
Tagen gemacht, die Feiertage sind oder in den eingetragenen Ferien liegen. Die Wochentage
Montag bis Freitag haben bei der internen HSQLDB die Nummern 2 bis 6. Die Ausgabe derWochentage wird nach der Jahreszahl und der Monatszahl gruppiert.
Die Abfrage für die Wochentage von Montag bis Freitag ist die äußere Abfrage. In den Unterabfragen können nicht mehr Datenzeilen enthalten sein als in den äußeren Abfrage. Da die Unterabfragen nur einen Teil dieser äußeren Tage enthalten wird dies auch nicht passieren. Wennvon Montag bis Freitag kein einziger Tag regulärer Schultag war, dann ist dies auch nicht bei
einem einzelnen der Tage der Fall.
In der äußeren Abfrage wird auch noch die Jahreszahl und die Monatszahl abgespeichert.
Außerdem ist dort auch noch das Schuljahr in der entsprechenden Schreibweise '2020/2021'
angegeben. Die Monate 1 – 7 gehören zum alten Schuljahr. Deswegen wird in Zeile 6 für dieseder Jahreswert das Startjahr um ein Jahr heruntergesetzt. Der Monat 'April 2020' gehört schließlich zum Schuljahr '2019/2020'. Bei allen anderen Monaten wird stattdessen das Zieljahr desSchuljahres um 1 Jahr heraufgesetzt.

Termine258

Auf diese äußere Abfrage greifen jetzt die korrelierenden Unterabfragen zu. Sie richten sichnach den Jahres- und Monatsangaben der äußeren Abfrage. Dazu wurde in der äußeren Abfrage
dem Tabellennamen "tmp_Kalender" ein Alias "a" zugewiesen (Zeile 7). Die Unterabfrage inZeile 2 z.B. ermittelt nur die Anzahl der Tage Montag, Mittwoch und Donnerstag. Nach diesen
Tagen wird über den COUNT-Befehl gesucht. Das Jahr der Unterabfrage soll mit dem Jahr deräußeren Abfrage übereinstimmen. Der Verweis auf das aktuelle Jahr der äußeren Abfrage, dasgerade in dieser Zeile stehen wird, ist das "a" vor dem Datum, aus dem das Jahr ermittelt wird.
Auch der Monat der Unterabfrage soll mit dem Monat der äußeren Abfrage übereinstimmen.
Auch hier wieder das "a" als Verweis auf die äußere Abfrage.
In Zeile 3 und Zeile 4 sind entsprechende korrelierende Unterabfragen für andere Tageskombinationen enthalten.

Abfragen
Liste_Gruppe_Multi
Datenquelle
Ansicht:

Ansicht_Gruppe_multi

Datenziel
Bericht:

Liste_Gruppe_multi, Liste_Gruppe_multi_1HJ, Liste_Gruppe_multi_2HJ

001 SELECT "a".*,002 ( SELECT COUNT( DISTINCT ( "W1" ) ) FROM "Ansicht_Gruppe_multi" WHERE "W1"<= "a"."W1" ) AS "Woche"
003 FROM "Ansicht_Gruppe_multi" AS "a"Der "Ansicht_Gruppe_multi" wird hier lediglich ein Zähler für die Wochen hinzugefügt. Damit
können in der Übersicht der Berichte auch die jeweiligen Schulwochen gekennzeichnet werden,
die ja unabhängig von Kalenderwochen sind. Zum einen beginnen die Schulwochen mitten im
Jahr, zum anderen werden die Wochen, die komplett in den Ferien liegen, nicht mitgezählt.
Der laufende Zähler wird durch eine korrelierende Unterabfrage bewerkstelligt. Die"Ansicht_Gruppe_multi" wird mit einem Alias "a" versehen (Zeile 3). Dadurch kann sich derZähler der Unterabfrage immer nach dem aktuellen Datensatz der äußeren Abfrage richten.
"W1" in der "Ansicht_Gruppe_multi" ist dabei eine Kombination aus Jahreszahl und Wochenzahl.
Über diese Kombination sind Wochen, die zum Anfang eines Folgejahres liegen, für den Zähler
größer als Wochen, die im aktuellen Jahr liegen.

Termine_Gruppe_multi
Datenquelle
Tabelle:TermineDatenziel
Formular: Terminart_Termin (SubTermine), Termine_Kalenderübersicht (MainForm),
Termine_Monat_Terminart (MainForm), Termine_Tabelleneingabe (MainForm)

001 SELECT "Termine".*,002 BITAND( "Gruppe_multi", 1 ) "Jg5", BITAND( "Gruppe_multi", 2 ) "Jg6",
BITAND( "Gruppe_multi", 4 ) "Jg7", BITAND( "Gruppe_multi", 8 ) "Jg8",
BITAND( "Gruppe_multi", 16 ) "Jg9", BITAND( "Gruppe_multi", 32 ) "Jg10",

Termine259

BITAND( "Gruppe_multi", 64 ) "Jg11", BITAND( "Gruppe_multi", 128 ) "Jg12",
BITAND( "Gruppe_multi", 256 ) "Jg13",003 EXTRACT( YEAR FROM "Beginn_Datum" ) AS "Jahr", EXTRACT( MONTH FROM
"Beginn_Datum" ) AS "Monat"
004 FROM "Termine" ORDER BY "Beginn_Datum" ASC
Die einzelnen Checkboxen in den Formularen sind nicht direkt mit einem Feld in der Tabelle"Termine" verbunden. Stattdessen sind ihre Werte nach dem Verfahren 20, 21, 22, 23 usw. als
Einserwerte des Binärsystems festgelegt. Alle Werte zusammen werden in einem Feld
"Gruppe_multi" als Summe gespeichert. Die Abfrage soll dazu dienen, dass die Werte aber auchnach dem Abspeichern anschließend von den Checkboxen wieder angezeigt werden können.
Angenommen in dem Feld "Gruppe_multi" befindet sich die Zahl '7', dann soll ja hier klar werden, dass nur 1, 2 und 4 in diese Zahl hinein passen – die Zahlen also, die den Jahrgängen 5, 6
und 7 in den Formularen zugeordnet waren. Die Zahl 7 hat in der Binärschreibweise die Binärfolge 0111. Wird jetzt nach dem 7 Jahrgang gesucht, so muss die Funktion BITAND bei der Addition von 0111 + 0100 genau den Wert für den Jahrgang 7 ermitteln. 0100 ist dann genau 22.
Und damit wird eben die Checkbox für den Jahrgang 7 als ausgewählt angezeigt.

Terminuebersicht
Datenquelle
Tabelle: Termine

Datenziel
Formular: Terminart_Termin (SubTerminMonat, SubTerminOrt), Termine_Monat_Terminart (SubTermineMonat, SubTerminart)

001 SELECT "Termine".*,002 EXTRACT( MONTH FROM "Beginn_Datum" ) AS "Monat",003 EXTRACT( YEAR FROM "Beginn_Datum" ) AS "Jahr",
004 FROM "Termine"
005 ORDER BY "Beginn_Datum" ASC, "Beginn_Uhrzeit" ASC
Die Verbindung zu den Unterformularen, vor allem dem Unterformular, das die Termine in
Abhängigkeit vom Monat zeigt, wird über den Monatswert und den Jahreswert festgelegt. Deswegen wird hier neben allen Werten aus der Tabelle "Termine" die Monatszahl (Zeile 2) und die
Jahreszahl (Zeile 3) ermittelt.

Wochenkalender_Mo_Fr
Datenquelle
Tabelle:tmp_Wochenkalender

Datenziel
Bericht:

Monatskalender_Mo_Fr

Der Bericht soll nur die Wochentage Montag bis Freitag darstellen. Fällt der Monatsanfang auf
einen Samstag oder Sonntag, so soll die entsprechende Woche nicht in dem neuen Monat auftauchen. Deshalb ist eine Abfrage nötig, die die Monatsübergänge anders regelt als beim
Bericht für ganze Wochen.
001
002
003
004
005

SELECT "tmp_Wochenkalender".*
FROM "tmp_Wochenkalender"
WHERE EXTRACT( MONTH FROM "D_1" ) = "Monat_Zahl"
OR EXTRACT( MONTH FROM "D_2" ) = "Monat_Zahl"
OR EXTRACT( MONTH FROM "D_3" ) = "Monat_Zahl"

Termine260

006 OR EXTRACT( MONTH FROM "D_4" ) = "Monat_Zahl"
007 OR EXTRACT( MONTH FROM "D_5" ) = "Monat_Zahl"

Formulare
Die Formulare erfüllen grundsätzlich alle den gleichen Zweck: Befüllen der Tabelle "Termine"
mit Inhalten, Übersicht über die Termine sowie Ausgabe der Termine in den verschiedenen
Berichten. Sie zeigen nur, wie unterschiedlich Formulare für gleiche Zwecke gestaltet werden
können.

Terminart_Termin

1

2

MainForm(Tabelle: Terminart)

1.1

SubTermine (Abfrage:
Termine_Gruppe_multi)

1.1.1

SubTerminMonat (Abfrage:
Terminuebersicht)

1.1.2

SubTerminOrt (Abfrage: Terminuebersicht)

Berichte (Tabelle: Filter)

3 Export (Tabelle: Filter)
Makros in Formulareigenschaftenkeine

Termine261

Makros in Feldeigenschaften
1.1

Beginn Dat. (Text modifiziert)

Eingabe.DatenAktualisieren_Terminort_T
ermine1.1

Checkboxen "Jahrgänge" (Status geändert)

Eingabe.GruppeMultiSpeichern

2

Schaltfläche OK (Aktion ausführen)

Abfragen.Bericht_Start

3

Schaltfläche Exportieren (Aktion ausführen)

Export.Export_Start

Das Formular ist so organisiert, dass die Terminart vorgewählt wird. Es zeigt im Unterformular"SubTermine" die zu der Terminart passenden Einträge an. Im Unterformular werden auch inder Hauptsache die Daten eingegeben. Die gruppierten Checkboxen "Jahrgänge" sind ebenfalls
Teil dieses Unterformulars.
Die Formulare "SubTerminMonat" und "SubTerminOrt" zeigen zu dem ausgewählten Datensatz
eine entsprechende Übersicht an.
Die Formulare "Berichte" und "Export" sind nicht für die Eingabe von Daten bestimmt. Sie starten lediglich die Berichtsausführung bzw. den Export der Daten.

Termine_Kalenderübersicht

1 MainForm (Abfrage: Termine_Gruppe_multi)
2 Monat (Tabelle: Filter)

2.1 Ansicht (Tabelle: tmp_Monat)

3 Berichte (Tabelle: Filter)
4 Export (Tabelle: Filter)

Termine262

Makros in Formulareigenschaften
1

Beim Laden

Eingabe.LetzterDatensatz_Termine_Kalender1

Beim erneuten Laden

Eingabe.Monatsansicht_aktualisieren

1Nach dem Datensatzwechsel

Eingabe.Monatsansicht_aktualisieren

Makros in Feldeigenschaften
1

Kombinationsfeld "Bezeichnung" (Fokusverlust)

Eingabe.Termindaten_uebertragen

1

Beginn Datum (Fokuserhalt)

Eingabe.Anfangswert_Datum

1

Beginn Datum (Fokusverlust)

Eingabe.Monatsansicht_aktualisieren

1

Checkboxen Jahrgänge (Status geändert)

Eingabe.GruppeMultiSpeichern

2

Listenfeld Monat (Modifiziert)

Eingabe.Monat_aktualisieren

3

Schaltfläche OK (Aktion ausführen)

Abfragen.Bericht_Start

4

Schaltfläche Exportieren (Aktion ausführen)

Export.Export_Start

Dieses Formular bietet eine Eingabemöglichkeit in einfachen Eingabefeldern. Dadurch geht dieÜbersicht auf die Daten verloren. Deswegen ist neben den Eingabefeldern eine Übersicht fürden jeweils aktuellen Monat eingeblendet. An der entsprechenden Makroübersicht wird deutlich, dass diese Darstellung ohne Makros nicht zu bewältigen ist. Schließlich soll der aktuelle
Monat während der Eingabe nach der Auswahl des Datums zum Terminbeginn gegebenenfalls
neu eingestellt werden. Ansonsten würde ja die Übersicht nur passen, wenn durch die Termine
im Hauptformular gescrollt wird, nicht aber, wenn ein Termin in einem neuen Monat eingegeben wird.
Die Formulare "Berichte" und "Export" sind nicht für die Eingabe von Daten bestimmt. Sie starten lediglich die Berichtsausführung bzw. den Export der Daten.

Termine263

Termine_Monat_Terminart

1

MainForm (Abfrage: Termine_Gruppe_multi)

1.1 SubTermineMonat (Abfrage: Terminuebersicht)
1.2 SubTerminart (Abfrage: Terminuebersicht)

2 Berichte (Tabelle: Filter)
3 Export (Tabelle: Filter)
Makros in Formulareigenschaften
1

Beim Laden

Eingabe.LetzterDatensatz

Makros in Feldeigenschaften
1

Beginn Datum (Text modifiziert)

Eingabe.DatenAktualisieren_Termine

1

Listenfeld "Terminart" (Modifiziert)

Eingabe.DatenAktualisieren_Termine

1

Checkboxen Jahrgänge (Status geändert)

Eingabe.GruppeMultiSpeichern

2

Schaltfläche OK (Aktion ausführen)

Abfragen.Bericht_Start

3

Schaltfläche Exportieren (Aktion ausführen)

Export.Export_Start

Das Hauptformular "MainForm" dient hier wieder zur Dateneingabe in einfache Formularfelder.
Die Übersicht zu den aktuell eingegebenen Daten vermitteln die Unterformulare "SubTermineMonat" und "SubTerminart". Auch hier muss mehr mit Makros gearbeitet werden, da während
der Eingabe in die Formularfelder des Hauptformular die Unterformulare ggf. neu eingestellt
werden müssen. Sonst würde die Übersicht auf die Daten nur etwas bringen, wenn durch die
Daten gescrollt wird, nicht aber, wenn neue Daten eingegeben werden.
Die Formulare "Berichte" und "Export" sind nicht für die Eingabe von Daten bestimmt. Sie starten lediglich die Berichtsausführung bzw. den Export der Daten.

Termine264

Termine_Tabelleneingabe

1 Monat (Tabelle: Filter)
2 MainForm (Abfrage: Termine_Gruppe_multi)
3 Berichte (Tabelle: Filter)
4 Export (Tabelle: Filter)
Makros in Formulareigenschaftenkeine

Makros in Feldeigenschaften
1

Listenfeld Monat (Modifiziert)

Filter.Filter_Abgleich

2

Checkboxen Jahrgänge (Status geändert)

Eingabe.GruppeMultiSpeichern

3

Schaltfläche OK (Aktion ausführen)

Abfragen.Bericht_Start

4

Schaltfläche Exportieren (Aktion ausführen)

Export.Export_Start

5

Schaltfläche Importieren (Aktion ausführen)

Import.Import_Start

Dieses Formular hat sich in der Praxis als das herausgestellt, was überwiegend genutzt wird.
Nach einer Vorauswahl des Monats zeigt das Formular "MainForm" alle Daten zu dem aktuellen
Monat an. Die Daten können gegebenenfalls über eine kleine Navigationsleiste sortiert undneben der Filterung nach dem Monat noch zusätzlich gefiltert werden.
Die Filterung nach dem Monat erfolgt über den Formularfilter, der in den Eigenschaften von"MainForm" steckt:
001 (EXTRACT( YEAR FROM "Beginn_Datum" )||','|| RIGHT('0'||EXTRACT( MONTH FROM
"Beginn_Datum" ),2) >= COALESCE((SELECT "Jahr_Monat" FROM "Filter" WHERE"ID" = TRUE),EXTRACT( YEAR FROM "Beginn_Datum" )||','|| RIGHT('0'||
EXTRACT( MONTH FROM "Beginn_Datum" ),2)))

Termine265

Wenn in der Tabelle "Filter" der Kombinationswert aus Jahr und Monat steht, dann wird dieser
mit dem entsprechenden Eintrag in der Abfrage verglichen. Ist der Eintrag leer, so werden alle
Datensätze angezeigt, die einen Eintrag für das "Beginn_Datum" haben. Und da dieser Eintrag
nicht leer sein darf sind das dann tatsächlich alle Datensätze.
Der Einsatz dieses Formularfilters hat den Vorteil, dass der Filter gegebenenfalls auch ausgeschaltet werden kann. Ist dies nicht erforderlich, so kann der Filter auch direkt in die entsprechende Abfrage eingebaut werden.
Die Formulare "Berichte" und "Export" sind nicht für die Eingabe von Daten bestimmt. Sie starten lediglich die Berichtsausführung bzw. den Export sowie in diesem Formular auch den Importder Daten.
Werden Daten importiert, so werden sie anschließend über den durch das Makro erstellten Formularfilter direkt in der Tabellenansicht des Formulars zum weiteren bearbeiten angezeigt.

Berichte
Die Berichte stellen den eigentlichen Einsatzpunkt dieser Datenbank dar. Es geht hier weniger
um eine Terminverwaltung wie im privaten Terminkalender. Da sind die speziellen Programme
weit bedienungsfreundlicher. Hier geht es darum, dass zu unterschiedlichen Anlässen unterschiedliche Kalenderlayouts benötigt und ausgedruckt werden können.

Halbjahreskalender_A4_quer

Datenquelle
Tabelle tmp_Halbjahreskalender

Termine266

Der Halbjahreskalender besteht aus 2 DIN A4-Blättern, auf denen jeweils ein halbes Kalenderjahr abgebildet wird. Unabhängig von der Vorauswahl des Datums beim Druck wird zu dem
gewählten Startdatum der entsprechende Jahreskalender ausgegeben.
Auf dem Ausdruck ist zu erkennen, dass der Platz nicht allzu viele Einträge zulässt. Da aber die
Berichte ja sowieso im Writer dargestellt werden kann z.B. der Textüberlauf am 17. April
anschließend noch bearbeitet werden. Hier kann entweder der Text gekürzt oder die Schriftart
verkleiner werden. Auf keinen Fall kann die Zeilenhöhe vergrößert werden, da sonst der Ausdruck von der ersten Seite auf der 2. Seite fortgesetzt würde.
In gelber Farbe sind die freien Tage gekennzeichnet. Dies sind entweder Ferientage oder Feiertage.

Liste_Gruppe_multi
Diese Liste bietet Platz für Termineinträge, die nicht in den Ferien liegen. Die Ferien werden nur
als Anhaltspunkt vermerkt.
In der Praxis werden in dieser Liste die Klassenarbeiten innerhalb eines Jahrgangs koordiniert,
so dass nicht zu viele Klassenarbeiten in einer Woche oder an direkt aufeinander folgenden
Tagen geschrieben werden.

Datenquelle
Abfrage Liste_Gruppe_Multi

Zusätzlich zu dieser Liste können noch separate Listen erstellt werden, die durch die Eingabe
eines Datums getrennt werden. Der Grund für die Trennung war rein praktischer Natur: Wurden
die Ausdrucke für die Listen untereinander gehängt, so war gerade einmal genug Platz für ein
Halbjahr. Die Listen eines ganzen Schuljahres brauchten den Platz von 7 Seiten, was einer
Gesamtlänge von ca. 210 cm entspricht.

Liste_Gruppe_multi_1HJ
Hiermit wird eine Liste für das erste Halbjahr ausgegeben. Der Trenner dafür wird aus"Filter"."Halbjahrende" ausgelesen.
Termine267

Datenquelle
Abfrage: Liste_Gruppe_Multi
Filter: "Sort" <= (SELECT "Halbjahrende" FROM "Filter" WHERE "ID" = TRUE)

Liste_Gruppe_multi_2HJ
Hiermit wird eine Liste für das zweite Halbjahr ausgegeben. Der Trenner dafür wird aus"Filter"."Halbjahrende" ausgelesen.
Datenquelle
Abfrage: Liste_Gruppe_Multi
Filter: "Sort" > (SELECT "Halbjahrende" FROM "Filter" WHERE "ID" = TRUE)

Monatskalender_Mo_Fr

Datenquelle
Abfrage: Wochenkalender_Mo_Fr

Dieser Kalender war der ursprüngliche Kalenderausdruck des Projekts. Die doch recht dunkele
Farbgebung für den Feiertag war dem Drucker geschuldet. Die Drucke des Kalenders erfolgen
nämlich über einen sogenannten Risographen, der von der Qualität der Graustufendarstellung
nicht an Laserdrucker heran kommt.
Jedes Feld hat genügend Platz für mehrere Einträge am Tag. Durchgehende Einträge werden
dabei immer nach oben gesetzt, so wie das hier bei den Einträgen ab dem 11.05. zu sehen ist.
Sollte einmal der Platz nicht ausreichen, so besteht nach der Erstellung durch den Report Bilder
noch die Möglichkeit, die Einträge entsprechend anzupassen. Vielleicht wird etwas an den For-

Termine268

mulierungen geändert oder einfach die Einträge für ein Feld mit einer etwas kleineren Schriftart
ausgedruckt.

Monatskalender_Mo_So

Datenquelle
Tabelle: tmp_Wochenkalender

Dieser Monatskalender stellt nur eine Erweiterung des vorherigen dar, falls eben auch die Sicht
auf den Samstag und den Sonntag gewünscht sind. Dafür steht dann aber eben etwas weniger
Platz für den Text zur Verfügung.

Wochenkalender_halb

Termine269

Jede Seite in diesem Kalender stellt eine halbe Woche dar. Unterhalb der Datumsangabe werden in dem großen Feld die Termine eingetragen, die in der Datenbank stehen. Wie in deruntenstehenden Ansicht zu sehen werden dort Feiertage und Wochenenden farbig noch besonders hervorgehoben.
Unterhalb der Termineinträge sind dann viele Zeilen zum Eintragen von irgendwelchen Ereignissen, die tagesaktuell verarbeitet werden müssen. In der täglichen Anwendung dient dieser Planzur Vertretungs- und Raumplanung an der Schule. Die Einträge werden hier handschriftlich
gemacht und anschließend in das Vertretungs- und Planungsprogramm übernommen.

Datenquelle
Tabelle: tmp_Wochenkalender_halb

Termine270

Wochentage_Mensaabo

Datenquelle
Ansicht: Ansicht_Wochentage

Die Mensa der Schule bietet unterschiedliche Abos an. Um den Preis vernünftig bestimmen zu
können wird mit dem Programm einfach geschaut, wie viele Tage im ganzen Jahr denn in dieser
Abozeit liegen. Dabei werden die Ferientage, Feiertage und Wochenenden nicht mit gezählt.

ZeitplanDatenquelle
Tabelle: tmp_Terminuebersicht

Dieser Zeitplan ist eine Tischvorlage für Konferenzen. Da wird noch einmal kurz zusammengefasst, was denn wann im Jahr geplant ist.

Makros
AA_Variablen
In diesem Modul sind neben globalen Variablen, die hier nicht einzeln aufgeführt werden, einige
allgemeine Funktionen enthalten.

Termine271

Datenbankstart
Aufruf aus
Datenbankdatei: Extras → Anpassen → Ereignisse → Dokument öffnen
oder: → Ansicht wurde erzeugt

Benötigt
Makro:

Datenbankbackup

Globale Variablen sind in allen Modulen verfügbar. Damit die Verbindung zur Datenbank nicht inallen möglichen Prozeduren erneut überprüft werden muss wird sie einmal für die gesamte
Laufzeit in einer globalen Variablen abgespeichert. Bei unterschiedlichen LO-Versionen hat sichgezeigt, dass die Verbindung mit den Ereignissen des Datenbankdokumentes manchmal problematisch ist. Ansicht wurde erzeugt fand z.B. in LO 6.1.5.2 unter Kubuntu gar nicht statt.
Stattdessen startete das Makro sehr wohl mit Dokument öffnen.
Vor der Definition der Datenquelle wird noch in Zeile 3 eine Kopie der Datenbank in den Backup-Ordner geschrieben.
Außerdem wird noch global das Startdatum mit der ersten Formularöffnung festgelegt.
001
002

GLOBAL oVerbindung AS OBJECTGLOBAL daStartDat AS DATE

001
002
003
004
005
006
007

SUB Datenbankstart
DIM oDatenquelle AS OBJECT
Datenbankbackup(10)oDatenquelle = thisDatabaseDocument.CurrentController
IF NOT (oDatenquelle.isConnected()) THEN oDatenquelle.connect()oVerbindung = oDatenquelle.ActiveConnection()
END SUB

OfficeVersion
Aufruf aus
Makro:

Datumswert, Listenfeld_Text, ID_Ermittlung

Manche Funktionen ändern sich im Laufe der Zeit. So wird z.B. seit LO 4.1 anders auf die Werte
eines Listenfeldes zugegriffen oder seit 4.1.2 Datumsfelder anders ausgelesen. Um die Datenbank kompatibel zu älteren Versionen zu halten muss also die Office-Version gegebenenfalls
bestimmt werden können.
001
002
003
004
005
006
007
008
009
010
011
012

FUNCTION OfficeVersion()
DIM aSettings, aConfigProvider
DIM aParams2(0) AS NEW com.sun.star.beans.PropertyValueDIM sProvider$, sAccess$sProvider = "com.sun.star.configuration.ConfigurationProvider"sAccess = "com.sun.star.configuration.ConfigurationAccess"aConfigProvider = createUnoService(sProvider)aParams2(0).Name = "nodepath"
aParams2(0).Value = "/org.openoffice.Setup/Product"aSettings = aConfigProvider.createInstanceWithArguments(sAccess, aParams2())
OfficeVersion() = array(aSettings.ooName,aSettings.ooSetupVersionAboutBox)
END FUNCTION

Der nodepath in Zeile 8 stellt den Einstieg in Extras → Optionen → LibreOffice → Erweitert →Experteneinstellungen dar. Der Wert aus Zeile 9 ist dann der Zugriff auf die in Zeile 11 genauer
bezeichneten Elemente.
Mit ooName wird der Unterschied zwischen LO und AOO ermittelt. Der String, der über diesen
Parameter ermittelt wird, heißt bei LO "LibreOffice".

Termine272

Mit ooSetupVersionAboutBox wird der Wert zu der jeweiligen Version ermittelt, also z.B.
6.4.3.2 bei LibreOffice

Datumswert
Aufruf aus
Makro:

Bericht_Start, Anfangswert_Datum, Monatsansicht_aktualisieren

Benötigt
Makro:

OfficeVersion

Die Behandlung des Datumswertes hat sich in Abhängigkeit von der LO-Version geändert.
001
002
003
004
005
006
007
008
009
010
011
012
013
014

FUNCTION Datumswert(oFeld AS OBJECT) AS DATE
DIM b()
a() = OfficeVersion()b = Split(a(1),".")
IF a(0) = "LibreOffice" AND (b(0) = 4 AND b(1) > 0) OR b(0) > 4 THEN
DIM stMonat AS STRING
DIM stTag AS STRING
stMonat = Right("0" & Trim(Str(oFeld.CurrentValue.Month)),2)
stTag = Right("0" & Trim(Str(oFeld.CurrentValue.Day)),2)
Datumswert = CDateFromIso(oFeld.CurrentValue.Year & stMonat & stTag)
ELSE
Datumswert = CDateFromIso(oFeld.getCurrentValue)
END IF
END FUNCTION

In Zeile 4 wird der zweite Eintrag aus der Funktion OfficeVersion() in seine Bestandteile zerlegt. Die Zahlen für die Version sind durch Punkte getrennt und mit den einzelnen Ziffern kanndie Version bestimmt werden.
In Zeile 5 wird zuerst nachgeschaut, ob bei dem aus der Funktion OfficeVersion() zurückgegebenen ersten Eintrag um "LibreOffice" handelt. "AOO" würde also direkt auf die Zeile 10 weiter verwiesen.
Anschließend wird in Zeile 5 nach dem zweiten Eintrag geschaut. Der erste Eintag des neuen
Arrays b muss eine 4 und der zweite Eintrag eine Zahl größer als 0 enthalten. Damit sind alle
Versionen LO 4.* abgedeckt. Kommt es zu höheren Versionen, so wird dies durch die Bedingung
hinter dem OR abgedeckt. Der erste Wert des Arrays ist dann größer als 4.
Sind diese Bedingungen erfüllt, so wird das Datum nicht über den CurrentValue ausgelesen,sondern stattdessen als Struct aus einzelnen Feldern dieses Wertes. Ab LO 4.1 existiert hier der
Eintag Year, Month und Day.

Listenfeld_Text
Aufruf aus
Makro:

Bericht_Start

Benötigt
Makro:
001
002
003
004
005
006
007

OfficeVersion

FUNCTION Listenfeld_Text(oFeld AS OBJECT) AS STRING
DIM b()
a() = OfficeVersion()b = Split(a(1),".")
IF a(0) = "LibreOffice" AND (b(0) = 4 AND b(1) > 0) OR b(0) > 4 THEN
Listenfeld_Text = oFeld.StringItemList(oFeld.SelectedItems(0))
ELSE

Termine

273

008
009
010

Listenfeld_Text = oFeld.CurrentValue()
END IF
END FUNCTION

Der angezeigte Text eines Listenfeldes muss abhängig von der LO-Version ermittelt werden.
Während er früher über CurrentValue wie in Zeile 8 zu erreichen war muss jetzt in Zeile 6 inder Liste des Listenfeldes der gerade angezeigte Wert ermittelt werden. Als CurrentValue wird
in den aktuellen Fassungen der Wert gesehen, der schließlich in der Datenbank gespeichert
werden soll.

ID_Ermittlung
Aufruf aus
Makro:

Bericht_Start

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016

OfficeVersion

FUNCTION ID_Ermittlung(oFeld AS OBJECT) AS INTEGER
DIM stInhalt AS STRING
DIM b()
a() = OfficeVersion()b = Split(a(1),".")
IF a(0) = "LibreOffice" AND (b(0) = 4 AND b(1) > 0) OR b(0) > 4 THEN
stInhalt = oFeld.CurrentValueELSE
stInhalt = oFeld.ValueItemList(oFeld.SelectedItems(0))
END IFIF IsEmpty(stInhalt) THEN
ID_Ermittlung = -1
ELSE
ID_Ermittlung = Cint(stInhalt)
END IF
END FUNCTION

Die Ermittlung des abzuspeichernden Inhaltes aus dem Listenfeld, meist der Primärschlüsselwertes des angezeigten Inhaltes, erfolgt neu über den CurrentValue in Zeile 6. Die ID-Felder in
den Tabellen dieser Datenbank haben alle einen Zahlenwert. Deswegen gibt diese Funktion den
Wert auch als Integer-Wert aus. Das hat bei einem leeren Feld allerdings den Nachteil, dasseine Zahlenvariable daraus dann '0' machen würde. Deshalb wird in Zeile 12 in diesem Fall der
Wert '-1' als Funktionswert weitergegeben.

Abfragen
KalenderAufruf aus
Makro:

Bericht_Start

Benötigt
Makro:

FeiertageArray, Date_2_SQLDate

Tabelle:tmp_Kalender, Termine, Ferien

001
002
003
004
005

SUB Kalender(inJahr AS INTEGER, inFerien AS INTEGER, stTermin AS STRING)
DIM stDatum AS STRING
DIM stDat_Beginn AS STRING
DIM stDat_Ende AS STRING
DIM stUpdate AS STRING

Termine

274

006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060

061

TermineDIM stZeit AS STRING
DIM i AS INTEGER
DIM d AS DATEDIM d_neu AS DATEDIM d_end AS DATEDIM stBezeichnung AS STRING
DIM stBemerkung AS STRING
inJahresschleife = inJahr
oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Kalender"""oSQL_Anweisung.executeUpdate(stSql)
oSQL_Anweisung.executeQuery("ALTER TABLE ""tmp_Kalender"" ALTER COLUMN ""ID""RESTART WITH 1")
FOR inJahr=inJahresschleife TO inJahresschleife+1
Feiertage = FeiertageArray(inJahr)d = "01.01."+inJahr
d_end = "01.01."+(inJahr+1)
stDatum = Date_2_SQLDate(d)d_neu=d
WHILE d_neu < d_end
stSql = "INSERT INTO ""tmp_Kalender"" (""Datum"") VALUES ('"+stDatum+"') "oSQL_Anweisung.executeUpdate(stSql)
d_neu=DateAdd("d",1,d_neu)
stDatum = Date_2_SQLDate(d_neu)
WEND
stSql = "SELECT ""Beginn_Datum"", ""Ende_Datum"" FROM ""Ferien"" WHERE
YEAR(""Beginn_Datum"") ='"+inJahr+"' OR YEAR(""Ende_Datum"")='"+inJahr+"'
ORDER BY ""Beginn_Datum"" ASC, ""Ende_Datum"" DESC"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stDat_Beginn = oAbfrageergebnis.getString(1)stDat_Ende = oAbfrageergebnis.getString(2)IF inFerien = 1 THEN
stUpdate = """Bezeichnung"" ='Ferien', ""Format"" = '1'"
ELSE
stUpdate = """Format"" = '1'"
END IF
stSql1 = "UPDATE ""tmp_Kalender"" SET "+stUpdate+" WHERE ""Datum"" ='"+stDat_Beginn+"'"oSQL_Anweisung1.executeUpdate(stSql1)
IF stDat_Ende <>"" AND stDat_Ende <> stDat_Beginn THENd_neu=DateAdd("d",1,CDate(stDat_Beginn))
WHILE d_neu <= CDate(stDat_Ende)
stDatum = Date_2_SQLDate(d_neu)
stSql1 = "UPDATE ""tmp_Kalender"" SET "+stUpdate+" WHERE ""Datum"" ='"+stDatum+"'"oSQL_Anweisung1.executeUpdate(stSql1)
d_neu=DateAdd("d",1,d_neu)
WEND
END IFWEND
FOR i=0 TO 14
stDatum = Feiertage(i,1)
stBezeichnung = Feiertage(i,0)
stSql = "UPDATE ""tmp_Kalender"" SET ""Bezeichnung"" ='"+stBezeichnung+"',""Format"" = '1' WHERE ""Datum"" = '"+stDatum+"'"oSQL_Anweisung.executeUpdate(stSql)
NEXT
stSql = "SELECT ""Bezeichnung"", ""Beginn_Datum"", ""Ende_Datum"",""Bemerkung"", ""Beginn_Uhrzeit"" FROM ""Termine"" WHERE ""zieID"" IN("+stTermin+") AND (YEAR(""Beginn_Datum"")='"+inJahr+"' OR
YEAR(""Ende_Datum"")='"+inJahr+"') ORDER BY ""Beginn_Datum"" ASC,""Ende_Datum"" DESC, ""Beginn_Uhrzeit"" ASC"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)

275

062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107

WHILE oAbfrageergebnis.next
stBezeichnung = oAbfrageergebnis.getString(1)stBezeichnung_Dat_Ende = oAbfrageergebnis.getString(1)stDat_Beginn = oAbfrageergebnis.getString(2)stDat_Ende = oAbfrageergebnis.getString(3)stBemerkung = oAbfrageergebnis.getString(4)stZeit = oAbfrageergebnis.getString(5)stZeit = left(stZeit,5)
IF stBemerkung <> "" THEN
stBezeichnung = stBezeichnung + " " + stBemerkung
stBezeichnung_Dat_Ende = stBezeichnung_Dat_Ende + " " + stBemerkung
END IFIF stZeit <> "" THEN
stBezeichnung = stZeit + " " + stBezeichnung
stBezeichnung_Dat_Ende = stZeit + " " + stBezeichnung_Dat_Ende
END IF
stSql1 = "SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE ""Datum"" ='"+stDat_Beginn+"'"oAbfrageergebnis1 = oSQL_Anweisung1.executeQuery(stSql1)
WHILE oAbfrageergebnis1.next
IF oAbfrageergebnis1.getString(1) <>"" THEN
stBezeichnung = oAbfrageergebnis1.getString(1) + CHR(13) +stBezeichnung
END IF
WEND
stSql1 = "UPDATE ""tmp_Kalender"" SET ""Bezeichnung"" ='"+stBezeichnung+"'
WHERE ""Datum"" = '"+stDat_Beginn+"'"oSQL_Anweisung1.executeUpdate(stSql1)
IF stDat_Ende <>"" AND stDat_Ende <> stDat_Beginn THENd_neu=DateAdd("d",1,CDate(stDat_Beginn))
WHILE d_neu <= CDate(stDat_Ende)
stDatum = Date_2_SQLDate(d_neu)
stSql2 = "SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE""Datum"" = '"+stDatum+"'"oAbfrageergebnis2 = oSQL_Anweisung2.executeQuery(stSql2)
WHILE oAbfrageergebnis2.next
IF oAbfrageergebnis2.getString(1) <>"" THEN
stBezeichnung = oAbfrageergebnis2.getString(1) + CHR(13) +stBezeichnung_Dat_Ende
ELSE
stBezeichnung = stBezeichnung_Dat_Ende
END IF
WEND
stSql1 = "UPDATE ""tmp_Kalender"" SET ""Bezeichnung"" ='"+stBezeichnung+ "' WHERE ""Datum"" = '"+stDatum+"'"oSQL_Anweisung1.executeUpdate(stSql1)
d_neu=DateAdd("d",1,d_neu)
WEND
END IFWENDNEXT
END SUB

Diese Prozedur erwartet beim Aufruf eine Jahreszahl das Startjahr (inJahr), eine 1 oder 0 fürden Schriftzug "Ferien" in den Feldern für die Terminbezeichnung (inFerien) sowie eine Benennung der Gruppe aus der Tabelle "Zielgruppe"."Termingruppe", für die der Termin gedacht ist(stTermin).
Die Kalenderzusammenstellungen verlaufen vom Prinzip her nach dem gleichen Schema.
9. Zuerst wird die bestehende Tabelle, hier "tmp_Kalender", von allen Daten geleert (Zeile17).
10. Danach wird der automatisch hoch zählende Primärschlüsselwert wird auf den Startwert'1' eingestellt (Zeile 19).

Termine276

11. Anschließend wird in einer Schleife für das angegebene Startjahr und das Folgejahr der
geforderte Inhalt ausgelesen (ab Zeile 20 bis Zeile 106). Diese Schleife ist erforderlich,da nicht Jahreskalender für ein Kalenderjahr sondern Jahreskalender für ein Schuljahr
erstellt werden.
12. In einer hierin enthaltenen Schleife wird zuerst die Tabelle komplett mit Datumswerten
für das ganze Jahr versehen (Zeile 26 bis Zeile 31)
13. Abhängig von der Kalenderart werden zuerst die Ferien (Zeile 32 bis 53) oder die Feiertage (Zeile 54 bis Zeile 59) in die Tabelle übertragen. Die zweite Schleife überschreibt
dabei gegebenenfalls Werte der ersten Schleife. Es steht dann eben der Name des Feiertags und nicht, falls gewünscht (Zeile 38), "Ferien" in den Feldern. Es gibt Berichte wie
den Bericht "Liste_Gruppe_multi", bei dem die Auflistung von Feiertagen innerhalb vonFerien zu unerwünschten Nebeneffekten führt. Dort sind dann die Schleifen vertauscht.
14. Zum Schluss, hier ab Zeile 60, wird dann die jeweilige Tabelle mit Daten gefüllt.
Die Datenabfrage ab Zeile 60 hier im Einzelnen:
Die Abfrage in Zeile 60 wird eingegrenzt durch die Zielgruppe, für die die Daten zusammengestellt werden, sowie das "Beginn_Datum" bzw. das "Ende_Datum", das in dem entsprechenden
Jahr liegen soll. Das Abfrageergebnis wird für das "Beginn_Datum" und die "Beginn_Uhrzeit"
aufsteigend sortiert, für das "Ende_Datum" allerdings absteigend. Damit soll erreicht werden,
dass bei gleichem "Beginn_Datum" der Datensatz zuerst ausgelesen wird, der über einen längeren Zeitraum läuft. Dadurch können Einträge z.B. im Wochenkalender auf gleicher Höhe
positioniert werden, wenn sie mehrmals hintereinander vorkommen.
Damit auch die Bezeichnung für das "Ende_Datum" erhalten bleibt, wird das Feld "Bezeichnung" doppelt ausgelesen (Zeile 63 und Zeile 64). Alle anderen Felder werden als Strings ausgelesen, da die Formatierung direkt so wieder für das Einfügen in die Zieltabelle geeignet ist.
Die ausgelesene Zeit wird dabei in Zeile 69 auf die Angabe von Stunden und Minuten zusammengekürzt.
Kommt eine Bemerkung vor, so wird sie mit der Bezeichnung gekoppelt (Zeile 70 bis Zeile 73).
Der Eintrag für eine Zeit wird gegebenenfalls der Bemerkung vorangestellt (Zeile 74 bis Zeile
77).
Steht bereits ein Eintrag zu dem Datum in der Tabelle "tmp_Kalender", so wird dieser Eintrag
ausgelesen und der neue Eintrag an den vorhergehenden mit einem Zeilenumbruch angehängt(Zeile 76 bis Zeile 86).
Nur wenn das "Ende_Datum" nicht leer und nicht gleich dem "Beginn_Datum" ist wird mit weiteren Einträgen für jedes dazwischenliegende Datum die Tabelle "tmp_Kalender" weiter aufgefüllt. In Zeile 88 muss dafür zuerst das Startdatum in eine für die Funktion DateAdd verarbeitbare Date-Variable mit der Funktion CDate umgewandelt werden. Dann können auch einzelne
Tage addiert werden.
Die Datumsschleife läuft dann von Zeile 89 bis Zeile 103. Wieder muss zuerst geklärt werden,
ob in "tmp_Kalender" schon ein Eintrag für das gewünschte Datum existiert. Unter den Umständen muss der neue Eintrag mit einem Zeilenumbruch an den vorhergehenden angehängt werden (Zeile 91 bis Zeile 99). Ansonsten wird nur einfach der aktuelle Eintrag mit einem Update
in die Tabelle "tmp_Kalender" eingefügt (Zeile 100 bis Zeile 101). In Zeile 95 muss dann auch
zum ersten Mal die in Zeile 64 erstellte zweite Variable stBezeichnung_Dat_Ende eingesetzt
werden, da die Variable stBezeichnung laufend überschrieben wird.

Monatskalender
Aufruf aus
Makro:

TermineBericht_Start

277

Benötigt
Makro:

Date_2_Monat, Date_2_SQLDate

Tabelle:tmp_Wochenkalender, tmp_Kalender

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043

044
045
046
047
048
049
050
051
052
053
054
055
056

SUB Monatskalender(daStart AS DATE, daEnde AS DATE)DIM i AS INTEGER
DIM inMonat AS INTEGER
DIM inWoche AS INTEGER
DIM inMonat_Sort AS INTEGER
DIM inTag AS INTEGER
DIM inDd AS INTEGER
DIM inMd AS INTEGER
DIM inYd AS INTEGER
DIM inW AS INTEGER
DIM inID AS INTEGER
DIM inWcount AS INTEGER
DIM inArrayCount AS INTEGER
DIM inMonat_2 AS INTEGER
DIM inJahr AS INTEGER
DIM d AS DATEDIM d_neu AS DATEDIM d_end AS DATEDIM stBezeichnung AS STRING
DIM stBezeichnung_2 AS STRING
DIM stDatum AS STRING
DIM stDat AS STRING
DIM stDat_2 AS STRING
DIM stMonat AS STRING
DIM stForm AS STRING
d = YEAR(daStart) & "-" & MONTH(daStart) & "-01"inJahr = Year(d)d_end = daEnde
d_neu = DateAdd("d",1,d)
stDatum = Date_2_SQLDate(d)oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Wochenkalender"""oSQL_Anweisung.executeUpdate(stSql)
oSQL_Anweisung.executeQuery("ALTER TABLE ""tmp_Wochenkalender"" ALTER COLUMN""ID"" RESTART WITH 1")
stMonat = Date_2_Monat(d)inTag = DatePart("w",d,2)
inWoche = DatePart("ww",d,2,2)
inMonat = Trim(Month(d))
inMonatSort = inMonat
WHILE d_neu < d_end
stSql1 = "INSERT INTO ""tmp_Wochenkalender"" (""Jahr"",""Woche"",""D_"+inTag+""", ""Monat_Text"", ""Monat_Zahl"", ""Monat_Sort"") VALUES('"+inJahr+"', '"+inWoche+"', '"+stDatum+"', '"+stMonat+"', '"+inMonat+"','"+inMonatSort+"')"oSQL_Anweisung1.executeUpdate(stSql1)
stSql = "CALL IDENTITY()"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
inID = oAbfrageergebnis.getInt(1)WEND
FOR i = inTag+1 TO 7
stDatum = Date_2_SQLDate(d_neu)
stSql2 = "UPDATE ""tmp_Wochenkalender"" SET ""D_"+i+""" ='"+stDatum+"'
WHERE ""Woche"" = '"+inWoche+"' AND ""Jahr"" = '"+inJahr+"'"oSQL_Anweisung2.executeUpdate(stSql2)
d_neu=DateAdd("d",1,d_neu)NEXT
stDatum = Date_2_SQLDate(d_neu)

Termine

278

057
058
059
060
061
062
063
064
065
066

067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095

stMonat = Date_2_Monat(d_neu)
inTag = 1
inMonat_2 = Trim(Month(d_neu))
inMonatSort = inMonat_2
inJahr = Year(d_neu)
IF Year(d) < inJahr THEN
inMonatSort = inMonat_2+12
END IFIF inMonat_2 <> inMonat AND d_neu < d_end THEN
stSql = "INSERT INTO ""tmp_Wochenkalender"" (""Jahr"",""Woche"",""Monat_Text"",""Monat_Zahl"",""Monat_Sort"",""D_1"",""D_2"",""D_3"",""D_4"",""D_5"",""D_6"",""D_7"") VALUES ('"+inJahr+"','"+inWoche+"','"+stMonat+"','"+inMonat_2+"','"+inMonatSort+"',"
FOR i = 1 TO 7
stSql = stSql + "(SELECT ""D_"+i+""" FROM ""tmp_Wochenkalender"" WHERE
ID = '"+inID+"'),"
NEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
END IF
inWoche = DatePart("ww",d_neu,2,2)
inMonat = Trim(Month(d_neu))
inMonatSort = inMonat
IF Year(d) < inJahr THEN
inMonatSort = inMonat + 12
END IFd_neu=DateAdd("d",1,d_neu)
WEND
stSql = "SELECT ""Bezeichnung"", DAYOFWEEK( ""Datum""), ""Datum"", ""Format""
FROM ""tmp_Kalender"""oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stBezeichnung = oAbfrageergebnis.getString(1)inTag = oAbfrageergebnis.getInt(2)inID = inTag - 1
IF inID = 0 THEN
inID = 7
END IF
stDat = oAbfrageergebnis.getString(3)stForm = oAbfrageergebnis.getString(4)stSql1 = "UPDATE ""tmp_Wochenkalender"" SET ""T_"+inID+"""='"+stBezeichnung+"', ""F_"+inID+""" ='"+stForm+"' WHERE ""D_"+inID+""" ='"+stDat+"'"oSQL_Anweisung1.executeUpdate(stSql1)
WEND
END SUB

Diese Prozedur erwartet beim Aufruf lediglich eine Angabe des Startdatums daStart und eine
Angabe des Enddatums daEnde.
In Zeile 26 wird direkt aus daStart lediglich Monat und Jahr herausgezogen und der Beginn auf
den Monatsersten gelegt. Es ist also ohne Belang, ob da nun der 15.4.20 steht – daraus wird
dann einfach der 1.4.20 zusammengesetzt – natürlich in der für Datenbanken üblichen Schreibweise '2020-04-01'.
Nach dem Leeren der Tabelle "tmp_Wochenkalender" werden zunächst einmal verschiedene
zusätzliche Variablen ermittelt. Die ist in Zeile 37 der deutschsprachige Monatsname(stMonat), in Zeile 38 die Tageszahl (inTag mit Montag als dem ersten Wochentag (Parameter2)), in Zeile 39 die Wochenzahl (inWoche mit Montag als dem ersten Wochentag und der ersten
gezählten Woche, die 4 Tage im aktuellen Jahr hat, (Parameter 2,2)) und in Zeile 40 die Monatszahl (inMonat).
Von Zeile 42 bis Zeile 80 verläuft eine Schleife, die so lange neue Datumseinträge in die Tabelle"tmp_Wochenkalender" einfügt, bis das Enddatum erreicht wird. Dabei wird zum Startdatumzum Schluss der Schleife immer ein Tag addiert.

Termine279

Zuerst wird in der Schleife ein Datensatz eingefügt, der alle Daten für den ersten Tag desMonats enthält (Zeile 43 und Zeile 44), der nicht unbedingt auch erster Tag der Woche ist.
Neben dem Datum sind dies auch das Jahr, die Woche, der Monatsname und der Monat als
Zahl. Die bei diesem Insert automatisch erstellte ID wird ermittelt (Zeile 45 bis Zeile 49) und im
Nachfolgenden für die weiteren Dateneinträge der Wochentage bis zum 7. Wochentag genutzt(Zeile 50 bis Zeile 55). Damit ist die erste Zeile für den Monat erstellt.
Die Variablen werden auf die neuen Werte eingestellt (Zeile 56 bis Zeile 61), die sich aus derDatumsänderung ergeben haben. Die Sortiervariable wird dabei um 12 erhöht, wenn die Jahreszahl auf ein neues Jahr wechselt (Zeile 63). Nur bei einem Monatswechsel während derWoche wird die nächste Zeile durch eine SQL-Anweisung hinzugefügt, (Zeile 65 bis Zeile 72),
deren SQL-Code teilweise durch eine Schleife für alle Tage der Woche erstellt wird (Zeile 67 bis
Zeile 69). Diese Schleife liest einfach die vorher erstellten Werte für die Tage erneut ein undsorgt dafür, dass die Werte auch in der Folgezeile eingefügt werden. Dies ist erforderlich, damit
eine Zeile einmal beim Auslesen der Werte in einem Monat und dann gleichlautend bei Auslesen im folgenden Monat erscheinen kann. So erscheinen für manche Wochen bei Monatsübergängen die Wochenzahlen doppelt.
Ansonsten werden von Zeile 73 bis Zeile 79 die Variablen auf die Werte für die nächste Woche
eingestellt und die Schleife beginnt wieder von vorne.
Nachdem so die Datumswerte eingefügt wurden wird anschließend der Inhalt aus der Tabelle"tmp_Kalender" ausgelesen. In einer Schleife (Zeile 83 bis Zeile 94) werden die Datensätze
ermittelt. Da die Funktion DAYOFWEEK (Zeile 81) bei der HSQLDB den Beginn der Woche mit dem
Wert '1' auf den Sonntag legt, hier aber der Montag als erster Tag benutzt wird, muss der Wert
nach der Abfrage erst einmal korrigiert werden (Zeile 86 bis Zeile 89). Ansonsten werden die
Werte zum jeweiligen Datum direkt übernommen und in Zeile 92 und Zeile 93 in die Tabelle"tmp_Wochenkalender" übertragen.

Wochenkalender_halb
Aufruf aus
Makro:

Bericht_Start

Benötigt
Makro:

keins

Tabelle:tmp_Wochenkalender_halb

Ansicht:

Ansicht_Wochen_einzeln

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017

SUB Wochenkalender_halb(daStart AS DATE, daEnde AS DATE)DIM inWoche AS INTEGER
DIM inJahr AS INTEGER
DIM i AS INTEGER
DIM d AS DATEDIM d_end AS DATE
DIM arRow1()
DIM arRow2()
d = daStart
inJahr = Year(d)d_end = daEnde
oSQL_Anweisung = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Wochenkalender_halb"""oSQL_Anweisung.executeUpdate(stSql)
oSQL_Anweisung.executeQuery("ALTER TABLE ""tmp_Wochenkalender_halb""
ALTER COLUMN ""ID"" RESTART WITH 1")
inWoche = DatePart("ww",d,2,2)
arRow1 = array("Monat_Text","Monat_Zahl","D_1","D_2","D_3","T_1","T_2","T_3","F_1", "F_2","F_3")

Termine

280

018
019
020
021
022
023
024
025
026

027
028
029
030
031
032
033
034
035
036

arRow2 = array("Monat_Text","Monat_Zahl","D_4","D_5","D_6","D_7","T_4","T_5","T_6","T_7","F_4","F_5","F_6","F_7")
WHILE d < d_end
stSql = "INSERT INTO ""tmp_Wochenkalender_halb"" (""Jahr"",""Woche"",""Monat_Text"",""Monat_Zahl"",""D_1"",""D_2"",""D_3"",""T_1"",""T_2"",""T_3"",""F_1"",""F_2"",""F_3"") VALUES ('"+inJahr+"','"+inWoche+"',"
FOR i = LBound(arRow1()) TO UBound(arRow1())
stSql = stSql + "(SELECT """+arRow1(i)+""" FROM ""Ansicht_Wochen_einzeln""
WHERE ""Woche"" = '"+inWoche+"' AND ""Jahr"" = '"+inJahr+"'),"
NEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
stSql = "INSERT INTO ""tmp_Wochenkalender_halb"" (""Jahr"",""Woche"",""Monat_Text"",""Monat_Zahl"",""D_1"",""D_2"",""D_3"",""D_4"",""T_1"",""T_2"",""T_3"",""T_4"",""F_1"",""F_2"",""F_3"",""F_4"") VALUES('"+inJahr+"','"+inWoche+"',"
FOR i = LBound(arRow2()) TO UBound(arRow2())
stSql = stSql + "(SELECT """+arRow2(i)+""" FROM ""Ansicht_Wochen_einzeln""
WHERE ""Woche"" = '"+inWoche+"' AND ""Jahr"" = '"+inJahr+"'),"
NEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
d = DateAdd("d",7,d)inJahr = Year(d)inWoche = DatePart("ww",d,2,2)
WEND
END SUB

Für diese Prozedur bereitet die "Ansicht_Wochen_einzeln" bereits die Daten so weit auf, dasssie vom Prinzip her nur noch einmal neu sortiert in die Tabelle "tmp_Wochenkalender_halb"übertragen werden müssen.
Zuerst wird die Tabelle "tmp_Wochenkalender" geleert (Zeile 13 und 14) und der Primärschlüsselwert auf 1 zurückgestellt (Zeile 15). Anschließend wird von der Startwoche die Wochenzahl
ermittelt (Zeile 16).
Die Inhalte aus "Ansicht_Wochen_einzeln" sollen in dem Kalender so verteilt werden, dass die
Tage "D_1" bis "D_3" (Montag bis Mittwoch) auf einer (linken) Seite erscheinen, die Tage "D_4"
bis "D_7" auf der anderen Seite erscheinen. Das Berichtsdesign packt hier den Sonntag in die
gleiche Spalte wie den Samstag, da am Samstag und Sonntag sowieso nicht so viele dienstliche
Termine vorkommen. In arRow1 werden die Felder für die Tage Montag bis Mittwoch zusammengefasst, in arRow2 die Felder für Donnerstag bis Sonntag (Zeile 17 und 18).
Von Zeile 19 bis Zeile 35 läuft die Schleife so lange ab, bis das vorgegebene Enddatum erreichtist.
Der Code für die Inserts wird in 2 Etappen zusammengestellt. Zuerst werden die Felder aufgeführt, in die die Daten geschrieben werden sollen, ergänzt um die Werte für Jahr und Woche.
Anschließend wird in einer Schleife abgefragt, was an Daten zu dem angegebenen Jahr und derangegebenen Woche in den jeweiligen Feldern aus dem Array arRow1 in der Ansicht"Ansicht_Wochen_einzeln" enthalten ist. Die Schleife wird solange durchlaufen, bis das komplette Array abgearbeitet ist (Zeile 21 bis 23). Anschließend endet der Code wegen der Schleife
mit einer Klammer und einem Komma. Das Komma wird ersetzt durch die korrekte 2. schließende Klammer (Zeile 24).
Nach dem Insert für die erste Seite erfolgt der Insert für die zweite Seite mit den gleichen
Schritten. Nachdem die Schritte durchlaufen sind wird das Datum um eine Woche heraufgesetzt (Zeile 32) und anschließend die Jahreszahl und die Wochenzahl aus diesem Datum für den
nächsten Schleifendurchlauf ermittelt.

Termine281

Halbjahreskalender
Aufruf aus
Makro:

Bericht_Start

Benötigt
Makro:

Date_2_SQLDate

Tabelle:tmp_Halbjahreskalender, tmp_Kalender

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049

SUB Halbjahreskalender(inJahr AS INTEGER)
DIM i AS INTEGER
DIM inMonat AS INTEGER
DIM inWoche AS INTEGER
DIM inTag AS INTEGER
DIM inDd AS INTEGER
DIM inMd AS INTEGER
DIM inYd AS INTEGER
DIM inW AS INTEGER
DIM inID AS INTEGER
DIM inWcount AS INTEGER
DIM inArrayCount AS INTEGER
DIM inMonat_2 AS INTEGERDIM k AS INTEGER
DIM d AS DATEDIM d_neu AS DATEDIM d_end AS DATEDIM stBezeichnung AS STRING
DIM stBezeichnung_2 AS STRING
DIM stDatum AS STRING
DIM stDat AS STRING
DIM stDat_2 AS STRING
DIM stMonat AS STRING
DIM stTag AS STRING
DIM stJahr AS STRING
DIM arMon()d = "01.01."+inJahr
d_end = "01.01."+(inJahr+1)
d_neu=DateAdd("d",1,d)
stDatum = Date_2_SQLDate(d)stJahr = Trim(Str(inJahr))oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Halbjahreskalender"""oSQL_Anweisung.executeUpdate(stSql)
oSQL_Anweisung.executeQuery("ALTER TABLE ""tmp_Halbjahreskalender""ALTER COLUMN ""ID"" RESTART WITH 1")
arMon = array("-01-","-02-","-03-","-04-","-05-","-06-","-07-","-08-","-09-","-10-","-11-","-12-")
inID = 28
IF isDate("29.02."+stJahr) THEN
inID = 29
END IF
FOR i=1 TO inID
IF i < 10 THEN
stTag = "0"+Trim(Str(i))
ELSE
stTag = Trim(Str(i))
END IF
stSql = "INSERT INTO ""tmp_Halbjahreskalender"" (""Jahr"",""Sort_Zahl"",""T_1"",""D_1"",""F_1"",""T_2"",""D_2"",""F_2"",""T_3"",""D_3"",""F_3"",""T_4"",""D_4"",""F_4"",""T_5"",""D_5"",""F_5"",""T_6"",""D_6"",""F_6"")
VALUES ('"+inJahr+"','1',"

Termine

282

050
051

052
053
054
055
056
057
058
059

060
061
062
063

064
065
066
067
068
069
070
071
072

073
074
075
076
077
078
079
080
081
082

083
084

085
086
087
088
089
090
091
092
093

Termine

FOR k = 0 TO 5
stSql = stSql + " (SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE""Datum"" = '"+stJahr+arMon(k)+stTag+"'),'"+stJahr+arMon(k)+stTag+"',(SELECT ""Format"" FROM ""tmp_Kalender"" WHERE ""Datum"" ='"+stJahr+arMon(k)+stTag+"'),"
NEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
NEXT
inID = inID + 1
FOR i = inID TO 30
stTag = Trim(Str(i))
stSql = "INSERT INTO ""tmp_Halbjahreskalender"" (""Jahr"",""Sort_Zahl"",""T_1"",""D_1"",""F_1"",""T_3"",""D_3"",""F_3"",""T_4"",""D_4"",""F_4"",""T_5"",""D_5"",""F_5"",""T_6"",""D_6"",""F_6"")
VALUES ('"+inJahr+"','1',"
FOR k = 0 TO 5
IF k = 1 THEN
ELSE
stSql = stSql + " (SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE""Datum""= '"+stJahr+arMon(k)+stTag+"'),'"+stJahr+arMon(k)+stTag+"',(SELECT ""Format"" FROM ""tmp_Kalender"" WHERE ""Datum"" ='"+stJahr+arMon(k)+stTag+"'),"END IFNEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
NEXT
stTag = "31"
stSql = "INSERT INTO ""tmp_Halbjahreskalender"" (""Jahr"",""Sort_Zahl"",""T_1"",""D_1"",""F_1"",""T_3"",""D_3"",""F_3"",""T_5"",""D_5"",""F_5"") VALUES('"+inJahr+"','1',"
FOR k = 0 TO 4 STEP 2
stSql = stSql + " (SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE""Datum"" = '"+stJahr+arMon(k)+stTag+"'),'"+stJahr+arMon(k)+stTag+"',(SELECT ""Format"" FROM ""tmp_Kalender"" WHERE ""Datum"" ='"+stJahr+arMon(k)+stTag+"'),"
NEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
FOR i=1 TO 30
IF i < 10 THEN
stTag = "0"+Trim(Str(i))
ELSE
stTag = Trim(Str(i))
END IF
stSql = "INSERT INTO ""tmp_Halbjahreskalender"" (""Jahr"",""Sort_Zahl"",""T_1"",""D_1"",""F_1"",""T_2"",""D_2"",""F_2"",""T_3"",""D_3"",""F_3"",""T_4"",""D_4"",""F_4"",""T_5"",""D_5"",""F_5"",""T_6"",""D_6"",""F_6"")
VALUES ('"+inJahr+"','2',"
FOR k = 6 TO 11
stSql = stSql + " (SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE""Datum"" = '"+stJahr+arMon(k)+stTag+"'),'"+stJahr+arMon(k)+stTag+"',(SELECT ""Format"" FROM ""tmp_Kalender"" WHERE ""Datum"" ='"+stJahr+arMon(k)+stTag+"'),"
NEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
NEXT
stTag = "31"
stSql = "INSERT INTO ""tmp_Halbjahreskalender"" (""Jahr"",""Sort_Zahl"",""T_1"",""D_1"",""F_1"",""T_2"",""D_2"",""F_2"",""T_4"",""D_4"",""F_4"",""T_6"",""D_6"",""F_6"") VALUES ('"+inJahr+"','2',"
FOR k = 6 TO 11
IF k = 8 OR k = 10 THEN
ELSE

283

094

095
096
097
098
099

stSql = stSql + " (SELECT ""Bezeichnung"" FROM ""tmp_Kalender"" WHERE""Datum"" = '"+stJahr+arMon(k)+stTag+"'),'"+stJahr+arMon(k)+stTag+"',(SELECT ""Format"" FROM ""tmp_Kalender"" WHERE ""Datum"" ='"+stJahr+arMon(k)+stTag+"'),"END IFNEXT
stSql = Left(stSql , Len(stSql) -1 ) &")"oSQL_Anweisung.executeUpdate(stSql)
END SUBDie Prozedur "Halbjahreskalender" soll die Tabelle "tmp_Halbjahreskalender" mit Daten versehen, die anschließend den Bericht "Halbjahreskalender_A4_quer" versorgen. Dieser Halbjahreskalender stellt jeweils 6 komplette Monate auf einer Seite dar. Das bedeutet auch, dass füreinen Bericht 6 Spalten für Datumswerte und 6 Spalten für Inhalte vorgehalten werden müssen. Und da die Ferien auch noch in den Bericht einfließen sollen sind hier für den Formatierungscode wieder 6 Felder notwendig. Zusammen mit der Jahreszahl, einer Sortierziffer für das
1. und 2. Halbjahr und dem Primärschlüssel sind das insgesamt 21 Felder.
Zuerst wird die Tabelle "tmp_Halbjahreskalender" geleert (Zeile 35) und der AutoWert wieder
auf den Startwert 1 gesetzt (Zeile 37). In Zeile 38 werden dann noch die Monatszahlen vorformatiert für die Eingabe in ein Datum in ein Array geschrieben. In Zeile 40 bis Zeile 42 wird mit
der Funktion isDate überprüft, ob in dem Jahr ein 29. Februar existiert. Ist dies der Fall, so wird
der Variablen "inID" der Wert 29, sonst der Wert 28 zugeschrieben.
Die erste Schleife von Zeile 43 bis 54 läuft so lange, bis dieser Wert von "inID" erreicht ist.
Dabei wird zuerst die Tageszahl als Text erstellt, indem den einstelligen Zahlen für "inID" eine'0' vorangestellt wird oder aber einfach aus der Zahl eine Textvariable erstellt wird (Zeilen 44
bis 48).
Der SQL-Code für den anschließenden Insert wird in zwei Teilen zusammengestellt. Zuerst
erfolgt die Zusammenstellung der Felder, in die die Daten eingefügt werden. Außerdem steht inder Basis bereits die Variable für das Jahr und die Sortierzahl, die in diesem Fall für das erste
Halbjahr eine '1' einsetzt (Zeile 49). Von Zeile 50 bis Zeile 52 erfolgt dann eine Schleife, diedurch alle Monate hindurchgeht, die im ersten Halbjahr liegen. Das jeweilige Datum wird dabei
mit dem bereits aus Zeile 38 beschriebenen Array variabel erstellt. Datenbasis für die Inhalte
des Halbjahreskalenders ist die Tabelle "tmp_Kalender", die also vorher erstellt werden muss.
Da die Schleife zum Schluss mit einer Klammer gefolgt von einem Komma endet muss hiernoch nachgebessert werden. Das Komma wird entfernt und eine schließende Klammer gesetzt(Zeile 53). Anschließend wird die Datenzeile erstellt und die Schleife beginnt für die ersten 28(oder 29) Tage wieder von vorne.
Von Zeile 57 bis Zeile 68 erfolgt die gleiche Schleife noch einmal, dieses Mal aber ohne den
Februar. Die Schleife wird also maximal 2 Mal durchlaufen, denn dann sind auch die anderen 5
Monate des ersten Halbjahres auf 30 Tage aufgestockt.
Schließlich muss noch einmal ein Insert für all die Monate stattfinden, die 31 Tage haben. Dies
wird in Zeile 69 bis Zeile 73 erledigt Die Schleife kann praktischerweise in Zweierschritten
durchlaufen werden (Zeile 69), da im ersten Halbjahr jeder 2. Monat 31 Tage hat.
Ab Zeile 76 erfolgt dann ein entsprechendes Vorgehen für das 2. Halbjahr. Hier kann direkt bis
zu 30 Tagen aufgefüllt werden, weil eben alle Monate mindestens 30 Tage haben. Nur die Reihenfolge der Monate für die 31 Tage ist nicht so günstig gelegen wie im ersten Halbjahr, so
dass hier mit k=6 (Monat '-07-') bis k=11 (Monat '-12-') in Einserschritten gearbeitet wird (Zeile91). Dafür werden dann in Zeile 92 die Monate September und November nicht mehr im SQLBefehl berücksichtigt.

ZeitplanAufruf aus
Makro:

TermineBericht_Start

284

Benötigt
Makro:

SQLDate_2_Datum, Date_2_SQLDate

Tabelle:tmp_Terminuebersicht, Termine

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022

023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042

SUB Zeitplan(daStart AS DATE, daEnde AS DATE, stTermin AS STRING)
DIM stStart AS STRING
DIM stEnde AS STRING
DIM stDat AS STRING
DIM stDatum AS STRING
DIM stBemerkung AS STRING
DIM stBezeichnung AS STRING
DIM stBezeichnung_alt AS STRING
DIM stBezeichNeu AS STRING
DIM stSpaltengrenze AS STRING
DIM inID AS INTEGER
DIM inID_alt AS INTEGER
DIM i AS INTEGERDIM k AS INTEGER
stStart = Date_2_SQLDate(daStart)
stEnde = Date_2_SQLDate(daEnde)oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Terminuebersicht"""oSQL_Anweisung.executeUpdate(stSql)
stSql = "INSERT INTO ""tmp_Terminuebersicht"" (SELECT ""Termine"".""ID"",""Termine"".""Beginn_Datum"",""Termine"".""Bezeichnung"",""Terminart"".""Terminart"", EXTRACT(MONTH FROM ""Beginn_Datum"" ),EXTRACT(YEAR FROM ""Beginn_Datum"" ), NULL,NULL,NULL,NULL,NULL,NULL FROM""Termine"" LEFT JOIN ""Terminart"" ON ""Termine"".""artID"" =""Terminart"".""ID"" WHERE ""Termine"".""zieID"" IN ("+stTermin+")AND((""Termine"".""Beginn_Datum"" > '"+stStart+"' AND""Termine"".""Beginn_Datum"" < '"+stEnde+"') OR (""Termine"".""Ende_Datum"" >'"+stStart+"' AND ""Termine"".""Ende_Datum"" < '"+stEnde+"')) ORDER BY""Termine"".""Beginn_Datum"" ASC, ""Termine"".""Beginn_Uhrzeit"" ASC)"oSql_Anweisung.executeUpdate(stSql)
stSql = "SELECT ""ID"", ""Ende_Datum"", ""Bemerkung"", ""Bezeichnung"" FROM""Termine"" WHERE ""zieID"" IN ("+stTermin+") AND NOT((""Ende_Datum"" IS NULL
OR ""Ende_Datum"" = ""Beginn_Datum"") AND ""Bemerkung"" IS NULL) "oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
inID = oAbfrageergebnis.getInt(1)stDat = oAbfrageergebnis.getString(2)stBemerkung = oAbfrageergebnis.getString(3)stBezeichnung = oAbfrageergebnis.getString(4)IF stDat <> "" THEN
stDatum = "- "+SQLDate_2_Datum(stDat)
stSql1 = "UPDATE ""tmp_Terminuebersicht"" SET ""D_2"" ='"+stDatum+"' WHERE""ID"" = '"+inID+"'"oSQL_Anweisung1.executeUpdate(stSql1)
END IFIF stBemerkung <> "" THEN
stBezeichnung = stBezeichnung + " | " + stBemerkung
stSql1 = "UPDATE ""tmp_Terminuebersicht"" SET ""Bezeichnung""='"+stBezeichnung+"' WHERE ""ID"" = '"+inID+"'"oSQL_Anweisung1.executeUpdate(stSql1)
END IF
WEND
stSql = "SELECT ""ID"", ""Beginn_Datum"", ""Ende_Datum"", ""Bemerkung"",""Bezeichnung"" FROM ""Termine"" WHERE ""zieID"" IN ("+stTermin+") AND((""Beginn_Datum"" > '"+stStart+"' AND ""Beginn_Datum"" < '"+stEnde+"') OR(""Ende_Datum"" > '"+stStart+"' AND ""Ende_Datum"" < '"+stEnde+"')) AND""Bezeichnung"" IN (SELECT ""Bezeichnung"" FROM ""Termine"" GROUP BY""Bezeichnung"" HAVING((COUNT(""ID"")>1))) ORDER BY ""Bezeichnung"",""Beginn_Datum"""

Termine

285

043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095

oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
inID_alt = -1
stBezeichnung_alt = ""k = 1
stSpaltengrenze = "Zu viele Termine in einer Zeile. Nicht alle Termine wurdenübertragen."
WHILE oAbfrageergebnis.next
inID = oAbfrageergebnis.getInt(1)stDatum = SQLDate_2_Datum(oAbfrageergebnis.getString(2))
stEnde = oAbfrageergebnis.getString(3)stBemerkung = oAbfrageergebnis.getString(4)stBezeichnung = oAbfrageergebnis.getString(5)IF stBezeichnung = stBezeichnung_alt THEN
IF i>7 THEN
msgbox stSpaltengrenze
ELSE
stSql1 = "UPDATE ""tmp_Terminuebersicht"" SET ""D_"+i+"" ='"+stDatum+"'
WHERE ""ID"" = '"+inID_alt+"'"oSQL_Anweisung1.executeUpdate(stSql1)
i = i + 1
IF stEnde <> "" THEN
IF i>7 THEN
msgbox stSpaltengrenze
ELSE
stEnde = "- "+SQLDate_2_Datum(stEnde)
stSql1 = "UPDATE ""tmp_Terminuebersicht"" SET ""D_"+i+"""='"+stEnde+"' WHERE ""ID"" = '"+inID_alt+"'"oSQL_Anweisung1.executeUpdate(stSql1)
i = i + 1
END IF
END IFIF stBemerkung <> "" THEN
stSql2 = "SELECT ""Bezeichnung"" FROM ""tmp_Terminuebersicht"" WHERE""ID"" = '"+inID_alt+"'"oAbfrageergebnis2 = oSQL_Anweisung2.executeQuery(stSql2)
WHILE oAbfrageergebnis2.next
stBezeichNeu = oAbfrageergebnis2.getString(1)WEND
stBezeichNeu = stBezeichNeu + " | " + stBemerkung
stSql1 = "UPDATE ""tmp_Terminuebersicht"" SET ""Bezeichnung""='"+stBezeichNeu+"' WHERE ""ID"" = '"+inID_alt+"'"oSQL_Anweisung1.executeUpdate(stSql1)
END IF
stSql1 = "DELETE FROM ""tmp_Terminuebersicht"" WHERE ""ID"" ='"+inID+"'"oSQL_Anweisung1.executeUpdate(stSql1)
END IFELSE
i = 2
stSql2 = "SELECT ""D_2"" FROM ""tmp_Terminuebersicht"" WHERE ""ID"" ='"+inID+"' AND NOT ""D_2"" IS NULL"
oAbfrageergebnis2 = oSQL_Anweisung2.executeQuery(stSql2)
WHILE oAbfrageergebnis2.next
i = 3
WEND
stBezeichnung_alt = stBezeichnung
inID_alt = inID
END IFWENDEND SUB

Mit dieser Prozedur wird der Zeitplan über die Tabelle "tmp_Terminuebersicht" mit Daten versorgt. Nachdem die Tabelle "tmp_Terminübersicht" geleert worden ist (Zeile 20 und 21) wird sie
direkt über einen SQL-Befehl mit Daten aus der Tabelle "Termine" versorgt (Zeile 22 und 23).
Da die Tabelle "tmp_Terminübersicht" mehr Felder hat als in diesem Insert direkt mit Inhalt
gefüllt werden sollen, werden die anderen Felder einfach mit einem leeren Inhalt NULL gefüllt.
Termine286

In den Bedingungen für die Inhalte des Inserts steht für das Feld "zieID" IN ("+stTermin+"). Die
Variable stTermin besteht aus Ganzzahlen, getrennt durch Komma, die einen Verweis auf die
Primärschlüssel der Tabelle "Zielgruppe" darstellen. So kann bei den Zielgruppen geregelt werden, welche Inhalte aus anderen Gruppen auch Kalender für die eigentliche Zielgruppe aufzunehmen sind.
In Zeile 24 und 25 werden die Inhalte aus der Tabelle "Termine" gesucht, die die Zielgruppe in
ihrem Kalender finden soll und die ein Enddatum haben, das sich von dem Startdatum unterschiedet oder einen Eintrag in der Spalte "Bemerkung" vorweisen können. In der Abfrage ist die
Bedingung so gestellt, dass nicht beide gleichzeitig NULL sein dürfen. Von Zeile 26 bis Zeile 41
wird dann an jedes Feld, zu dem ein Enddatum existiert, das Enddatum an das Anfangsdatum
angehängt und zu jeder Zeile, zu der eine Bemerkung existiert, die Bemerkung an die Bezeichnung angehängt.
Mit der Abfrage in Zeile 42 werden die Termine gesucht, bei denen die Bezeichnung mehr als
einmal vorkommt ("Bezeichnung" HAVING((COUNT("ID")>1))). Diese Termine sollen an die
vorherigen gleichlautenden Termine mit Datumsvermerk angehängt werden. Da die Spaltenanzahl für Termine in dem Bericht vom Platz her begrenzt ist (das letzte Feld ist D7 - also maximal 7 Datumseinträge) wird schon einmal für eine Meldung zur Spaltengrenze in Zeile 47 der
Text in eine Variable geschrieben.
Die in Zeile 48 beginnende Schleife springt nach dem Auslesen der Werte aus der ersten Abfragezeile zuerst nach Zeile 84, da die Bedingung von Zeile 54 nicht erfüllt ist (stBezeichnung_alt
startet leer – Zeile 45). Die Variable i wird hier zuerst auf '2' gesetzt (für "D_2"). Dann wird
durch Abfrage nachgesehen, ob in der Spalte D_2 bereits ein Eintrag ist. Ist dort ein Eintrag, sosoll mit i=3 weiter fortgefahren werden. Ansonsten bleibt i=2 bestehen. In den Zeilen 91 und
92 werden die aktuellen Variablen für die Bezeichnung und die ID auf die Variablen mit dem
Zusatz "_alt" kopiert, damit später klar ist, wann eine Änderung des Datensatzes erfolgen
muss.
Anschließend geht es für den nächsten Datensatz weiter mit Zeile 54, da die Variablen für die
Bezeichnung jetzt gleich sind und auch die anschließende Bedingung erfüllt ist, dass i nicht größer als 7 ist. In Zeile 58 bis Zeile 60 wird das Beginndatum in die aktuelle Datenzeile übertragen und der Zähler von i um eins erhöht, damit der folgende Eintrag im nächsten Tabellenfeld
erfolgt. Ist i zu groß (Zeile 62), so wird kein weiterer Datumseintrag vorgenommen (Zeile 65 bis
68).
Enthält die Bemerkung Text (Zeile 71), so wird dieser Text an die bisherige Bezeichnung in derTabelle mit angehängt. So könnte z.B. die ursprüngliche Bezeichnung "Lehrerkonferenz" lauten
und in den Bemerkungen dann '1.', '2.' usw stehen. Dann werden alle Lehrerkonferenzen ineine Zeile geschrieben und zu Beginn steht dann 'Lehrerkonferenz | 1. | 2. | …'
Die Tabelle "tmp_Terminuebersicht" enthält nach dem Insert alle Daten, auch wiederholendeWerte für die Bezeichnung. Nachdem der Datensatz mit der gleichen Bezeichnung in den vorherigen Datensatz überführt wurde muss er gelöscht werden. Hierzu wird die aktuelle ID
genutzt (Zeile 81).

GruppeMultiUebersicht
Aufruf aus
Makro:

Bericht_Start

Benötigt
Makro:

FeiertageArray, Date_2_SQLDate

Tabelle:tmp_Gruppe_multi, Ferien, Termine

001
002

SUB GruppeMultiUebersicht(inJahr AS INTEGER, inFerien AS INTEGER, stTermin AS
STRING, inJahrgang AS INTEGER, stJahrgang AS STRING)
DIM stDatum AS STRING

Termine

287

003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060

TermineDIM stDat_Beginn AS STRING
DIM stDat_Ende AS STRING
DIM stUpdate AS STRING
DIM stZeit AS STRING
DIM i AS INTEGER
DIM d AS DATEDIM d_neu AS DATEDIM d_end AS DATEDIM stBezeichnung AS STRING
DIM stBemerkung AS STRING
inJahresschleife = inJahr
oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung2 = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Gruppe_multi"""oSQL_Anweisung.executeUpdate(stSql)
oSQL_Anweisung.executeQuery("ALTER TABLE ""tmp_Gruppe_multi""
ALTER COLUMN ""ID"" RESTART WITH 1")
FOR inJahr=inJahresschleife TO inJahresschleife+1
Feiertage = FeiertageArray(inJahr)d = "01.01."+inJahr
d_end = "01.01."+(inJahr+1)
stDatum = Date_2_SQLDate(d)d_neu=d
WHILE d_neu < d_end
stSql = "INSERT INTO ""tmp_Gruppe_multi"" (""Datum"") VALUES('"+stDatum+"') "oSQL_Anweisung.executeUpdate(stSql)
d_neu=DateAdd("d",1,d_neu)
stDatum = Date_2_SQLDate(d_neu)
WEND
FOR i=0 TO 14
stDatum = Feiertage(i,1)
stBezeichnung = Feiertage(i,0)
stSql = "UPDATE ""tmp_Gruppe_multi"" SET ""Bezeichnung""='"+stBezeichnung+"', ""Format"" = '1' WHERE ""Datum"" = '"+stDatum+"'"oSQL_Anweisung.executeUpdate(stSql)
NEXT
stSql = "SELECT ""Beginn_Datum"", ""Ende_Datum"" FROM ""Ferien"" WHERE
YEAR(""Beginn_Datum"")='"+inJahr+"' OR YEAR(""Ende_Datum"")='"+inJahr+"'
ORDER BY ""Beginn_Datum"" ASC, ""Ende_Datum"" DESC"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stDat_Beginn = oAbfrageergebnis.getString(1)stDat_Ende = oAbfrageergebnis.getString(2)IF inFerien = 1 THEN
stUpdate = """Bezeichnung"" ='Ferien', ""Format"" = '1'"
ELSE
stUpdate = """Format"" = '1'"
END IF
stSql1 = "UPDATE ""tmp_Gruppe_multi"" SET "+stUpdate+" WHERE ""Datum"" ='"+stDat_Beginn+"'"oSQL_Anweisung1.executeUpdate(stSql1)
IF stDat_Ende <>"" AND stDat_Ende <> stDat_Beginn THENd_neu=DateAdd("d",1,CDate(stDat_Beginn))
WHILE d_neu <= CDate(stDat_Ende)
stDatum = Date_2_SQLDate(d_neu)
stSql1 = "UPDATE ""tmp_Gruppe_multi"" SET "+stUpdate+" WHERE""Datum"" = '"+stDatum+"'"oSQL_Anweisung1.executeUpdate(stSql1)
d_neu=DateAdd("d",1,d_neu)
WEND
END IF
WEND
stSql = "SELECT ""Bezeichnung"", ""Beginn_Datum"", ""Ende_Datum"",""Bemerkung"", ""Beginn_Uhrzeit"", ""Gruppe_multi"" FROM ""Termine"" WHERE

288

061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109

BITAND(""Gruppe_multi"",'"+inJahrgang+"') > 0 AND ""zieID"" IN("+stTermin+") AND (YEAR(""Beginn_Datum"")='"+inJahr+"' OR
YEAR(""Ende_Datum"")='"+inJahr+"') ORDER BY ""Beginn_Datum"" ASC,""Ende_Datum"" DESC, ""Beginn_Uhrzeit"" ASC"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stBezeichnung = oAbfrageergebnis.getString(1)stBezeichnung_Dat_Ende = oAbfrageergebnis.getString(1)stDat_Beginn = oAbfrageergebnis.getString(2)stDat_Ende = oAbfrageergebnis.getString(3)stBemerkung = oAbfrageergebnis.getString(4)stZeit = oAbfrageergebnis.getString(5)stZeit = left(stZeit,5)
IF stBemerkung <> "" THEN
stBezeichnung = stBezeichnung + " " + stBemerkung
stBezeichnung_Dat_Ende = stBezeichnung_Dat_Ende + " " + stBemerkung
END IFIF stZeit <> "" THEN
stBezeichnung = stZeit + " " + stBezeichnung
stBezeichnung_Dat_Ende = stZeit + " " + stBezeichnung_Dat_Ende
END IF
stSql1 = "SELECT ""Bezeichnung"" FROM ""tmp_Gruppe_multi"" WHERE ""Datum""= '"+stDat_Beginn+"'"oAbfrageergebnis1 = oSQL_Anweisung1.executeQuery(stSql1)
WHILE oAbfrageergebnis1.next
IF oAbfrageergebnis1.getString(1) <>"" THEN
stBezeichnung = oAbfrageergebnis1.getString(1) + CHR(13) +stBezeichnung
END IF
WEND
stSql1 = "UPDATE ""tmp_Gruppe_multi"" SET ""Bezeichnung""='"+stBezeichnung+"' WHERE ""Datum"" = '"+stDat_Beginn+"'"oSQL_Anweisung1.executeUpdate(stSql1)
IF stDat_Ende <>"" AND stDat_Ende <> stDat_Beginn THENd_neu=DateAdd("d",1,CDate(stDat_Beginn))
WHILE d_neu <= CDate(stDat_Ende)
stDatum = Date_2_SQLDate(d_neu)
stSql2 = "SELECT ""Bezeichnung"" FROM ""tmp_Gruppe_multi"" WHERE""Datum"" = '"+stDatum+"'"oAbfrageergebnis2 = oSQL_Anweisung2.executeQuery(stSql2)
WHILE oAbfrageergebnis2.next
IF oAbfrageergebnis2.getString(1) <>"" THEN
stBezeichnung = oAbfrageergebnis2.getString(1) + CHR(13) +stBezeichnung_Dat_Ende
ELSE
stBezeichnung = stBezeichnung_Dat_Ende
END IF
WEND
stSql1 = "UPDATE ""tmp_Gruppe_multi"" SET ""Bezeichnung""='"+stBezeichnung+"' WHERE ""Datum"" = '"+stDatum+"'"oSQL_Anweisung1.executeUpdate(stSql1)
d_neu=DateAdd("d",1,d_neu)
WEND
END IF
WEND
NEXT
stSql = "UPDATE ""tmp_Gruppe_multi"" SET ""Gruppe_multi"" ='"+stJahrgang+"'"oSQL_Anweisung.executeUpdate(stSql)
END SUB

Der prinzipielle Ablauf ist wie der bei ähnlichen Prozeduren zur Erstellung von Tabellen, dieanschließend als Basis für einen der berichte dienen. In Zeile 17 bis Zeile 19 wird die Tabelle
von vorherigem Inhalt geleert und der Autowert auf '1' neu eingestellt. Von Zeile 20 bis Zeile
106 erfolgt dann eine Schleife, die für 2 Jahre durchlaufen wird. Erst ganz zum Schluss dieser
Prozedur wird dann zu allen in der Tabelle eingetragenen Datensätzen der Jahrgang hinzugefügt (Zeile 107 und Zeile 108).
Termine289

Die Datumseinträge in die Tabelle erfolgen von Zeile 26 bis Zeile 31. Anschließend werden vonZeile 32 bis Zeile 37 die Feiertage hinzugefügt. Dies geschieht hier vor den Ferien, weil die
Ferien in dem Bericht nur mit einer Zeile erscheinen sollen und die Feiertage in den Ferien nicht
angezeigt werden sollen.
In Zeile 38 und Zeile 39 werden die Ferien ausgelesen und anschließend ab Zeile 40 bis Zeile
59 in der Tabelle den jeweiligen Datumswerten zugeordnet.
In der Abfrage Zeile 60 werden jetzt die Termine herausgesucht, die zu dem entsprechenden
Jahrgang passen. Wichtig ist hier die Funktion BITAND("Gruppe_multi", Jahrgangswert) > 0.
Angenommen in dem Feld "Gruppe_multi" befindet sich die Zahl '7', dann soll ja hier klar werden, dass nur 1, 2 und 4 in diese Zahl hinein passen – die Zahlen also, die den Jahrgängen 5, 6
und 7 in den Formularen zugeordnet waren. Die Zahl 7 hat in der Binärschreibweise die Binärfolge 0111. Wird jetzt nach dem 7 Jahrgang gesucht, so muss die Funktion bei der Addition von0111 + 0100 einen Wert größer als 0 ermitteln. Beim binären Addieren zählen nur die Felder als
1, bei denen beide binären Codes eine 1 stehen haben. 0111 + 0100 = 0100 und ist also größer als 0.
Alle Daten, die jetzt für diesen Jahrgang gelten, werden wie bei der Prozedur "Kalender", nachund nach abgearbeitet, gegebenenfalls mit der Zeit und den Bemerkungen ergänzt und bei
mehreren Terminen am gleichen Tag auch noch mit einem Zeilenumbruch versehen in die
Tabelle "tmp-Gruppe_multi" eingetragen.

Monat
Aufruf aus
Makro:

Monatsansicht

Benötigt
Makro:

Date_2_Datum, Date_2_SQLDate

Tabelle:tmp_Monat, Termine, Terminart

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023

024

SUB Monat(inMonat AS INTEGER, inJahr AS INTEGER)
DIM d AS DATEDIM d_neu AS DATEDIM d_end AS DATEDIM stDatum AS STRING
DIM stDatumSql AS STRING
DIM inID AS INTEGER
DIM i AS INTEGERoSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung1 = oVerbindung.createStatement()
stSql = "DELETE FROM ""tmp_Monat"""oSQL_Anweisung.executeUpdate(stSql)
d = CDate("01."+inMonat+"."+inJahr)
d_end = DateAdd("m",1,d)inID = 1
FOR d_neu = d TO d_end-1
stDatumSql = Date_2_SQLDate(d_neu)
stDatum = Date_2_Datum(d_neu)
inWochentag = DatePart("w",d_neu,2,2)
stSql = "INSERT INTO ""tmp_Monat"" (""ID"", ""T_"+inWochentag+""") VALUES('"+inID+"','"+stDatum+"')"
oSql_Anweisung.executeUpdate(stSql)
stBezeichnung = stDatum
stSql ="SELECT ""Terminart"".""Terminart""||': '||""Termine"".""Bezeichnung""
FROM ""Termine"" LEFT JOIN ""Terminart"" ON ""Terminart"".""ID"" =""Termine"".""artID"" WHERE ""Termine"".""Beginn_Datum"" ='"+stDatumSql+"'
OR (""Termine"".""Beginn_Datum"" < '"+stDatumSql+"' AND""Termine"".""Ende_Datum"" >= '"+stDatumSql+"')"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)

Termine

290

025
026
027
028
029
030
031
032
033
034
035
036
037
038

039
040
041
042
043
044
045
046
047
048
049

WHILE oAbfrageergebnis.next
stBezeichnung = stBezeichnung + CHR(13) + oAbfrageergebnis.getString(1)stSql1 = "UPDATE ""tmp_Monat"" SET ""T_"+inWochentag+"""='"+stBezeichnung+"' WHERE ""ID""='"+inID+"'"oSQL_Anweisung1.executeUpdate(stSql1)
WEND
FOR i = inWochentag+1 TO 7
d_neu = DateAdd("d",1,d_neu)
IF d_neu < d_end THEN
stDatumSql = Date_2_SQLDate(d_neu)
stDatum = Date_2_Datum(d_neu)
stBezeichnung = stDatum
stSql1 = "UPDATE ""tmp_Monat"" SET ""T_"+i+""" ='"+stBezeichnung+"'
WHERE ""ID""='"+inID+"'"oSQL_Anweisung1.executeUpdate(stSql1)
stSql = "SELECT ""Terminart"".""Terminart""||':'||""Termine"".""Bezeichnung"" FROM ""Termine"" LEFT JOIN""Terminart"" ON ""Terminart"".""ID"" = ""Termine"".""artID"" WHERE""Termine"".""Beginn_Datum"" = '"+stDatumSql+"' OR(""Termine"".""Beginn_Datum"" < '"+stDatumSql+"' AND""Termine"".""Ende_Datum"" >= '"+stDatumSql+"')"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stBezeichnung = stBezeichnung + CHR(13) +oAbfrageergebnis.getString(1)stSql1 = "UPDATE ""tmp_Monat"" SET ""T_"+i+""" ='"+stBezeichnung+"'
WHERE ""ID""='"+inID+"'"oSQL_Anweisung1.executeUpdate(stSql1)
WEND
END IF
NEXT
inID = inID + 1
NEXT
END SUBDie Prozedur "Monat" soll die Monatsübersicht für das Formular "Termine_Kalenderübersicht"
erstellen.
Zuerst wird die alte Tabelle in Zeile 11 und 12 geleert. Eine Zurückstellung des AutoWertes ist
nicht erforderlich, da der Primärschlüssel direkt eingegeben wird. Das Startdatum für die
Tabelle wird auf den Monatsanfang gesetzt (Zeile 13), das Enddatum auf den Anfang des folgenden Monats (Zeile 14). Die anschließende Schleife wird so lange durchlaufen, bis durch
Addition von Tagen zu dem Startdatum das Enddatum erreicht wird. Die Zunahme des Datums
um jeweils einen Tag erfolgt in Zeile 31.
In Zeile 19 wird der Tag der Woche als Zahl ermittelt. Dieser Tag ist Bestandteil der Feldbezeichnung für die folgende Eingabe in die Tabelle. Dadurch wird gewährleistet, dass der erste Eintag
nicht einfach in das erste Feld geschrieben wird und jeder Monat scheinbar mit einem Montag
beginnt. Über das Schlüsselfeld und das Datum (als Text in deutschsprachiger Schreibweise)
wird die erste Zeile in die Tabelle eingefügt (Zeile 20 und 21). Anschließend wird zu dem Datum
nachgesehen, ob ein Eintrag in der Tabelle "Termine" vorhanden ist. Dieser wird dann gegebenenfalls an das bereits eingefügte Datum angehängt (Zeile 22 bis Zeile 29).
Von Zeile 30 bis Zeile 46 wird der gleiche Vorgang für alle folgenden Tage der Woche erneut
durchgeführt. Anschließend wird in Zeile 47 der Wert des Schlüsselfeldes um 1 erhöht und die
Eingabe für die nächste Woche kann ab Zeile 16 erfolgen.

Monatsansicht
Aufruf aus
Makro:

TermineMonatsansicht_aktualisieren, Monat_aktualisieren

291

Benötigt
Makro:

Monat

Tabelle:Filter

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

SUB Monatsansicht
DIM oForm AS OBJECTDIM oSubForm AS OBJECT
DIM stBezeichnung AS STRING
DIM a()
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("Monat")oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Jahr_Monat"" FROM ""Filter"" WHERE ""ID"" = TRUE "oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stBezeichnung = oAbfrageergebnis.getString(1)WEND
a = Split(stBezeichnung,",")
Monat(CInt(a(1)),CInt(a(0)))
oSubForm = oForm.getByName("Ansicht")
oSubForm.reload()
END SUB

Diese Prozedur ist Teil der Prozedur, die die Monatsübersicht in dem Formular "Termine_Kalenderübersicht" darstellen soll.
Aus der Filtertabelle wird die Kombination "Jahr_Monat" ausgelesen. Hier sind die Jahreszahl
und die Monatszahl durch ein Komma getrennt. Diese Trennung wird in Zeile 15 dazu genutzt,
beide Zahlen in ein Array zu speichern. In Zeile 16 werden so die Monatszahl und die Jahreszahl
an die Prozedur "Monat" weitergeleitet. Ist die Prozedur "Monat" abgelaufen, so wird das Formular, das die Ansicht beinhaltet, neu geladen.

Date_2_SQLDate
Aufruf aus
Makro:

Kalender, Monatskalender, Halbjahreskalender, Zeitplan, Monat, Bericht_Start, GruppeMultiUebersicht, FeiertageArray

Benötigt
Makro:
001
002
003
004
005
006
007

keins

FUNCTION Date_2_SQLDate(d AS DATE) AS STRING
DIM stMonth AS STRING
DIM stDay AS STRING
stMonth = Right("0" + Trim(Str(Month(d))),2)
stDay = Right("0" + Trim(Str(Day(d))),2)
Date_2_SQLDate = Trim(Str(Year(d))) +"-"+ stMonth +"-"+ stDay
END FUNCTION

Aus der internen Basic-Datumsangabe wird ein Datum in SQL-Schreibweise erzeugt. Sind die
Monatsangaben oder Tagesangaben einstellig, so wird ihnen eine '0' vorangestellt. Diesgeschieht einfach, indem eine '0' grundsätzlich vorangestellt wird, schließlich aber nur die letzten beiden Ziffern weitergegeben werden.

Date_2_Datum
Aufruf aus
Makro:

Termine

Monat

292

Benötigt
Makro:
001
002
003
004
005
006
007

keins

FUNCTION Date_2_Datum(d AS DATE) AS STRING
DIM stMonth AS STRING
DIM stDay AS STRING
stMonth = Right("0" + Trim(Str(Month(d))),2)
stDay = Right("0" + Trim(Str(Day(d))),2)
Date_2_Datum = stDay +"."+ stMonth +"."+ Trim(Str(Year(d)))
END FUNCTION

Aus der internen Basic-Datumsangabe wird ein Datum in deutschsprachiger Schreibweise mit
vierstelliger Jahreszahl erzeugt. Sind die Monatsangaben oder Tagesangaben einstellig, so wird
ihnen eine '0' vorangestellt. Dies geschieht einfach, indem eine '0' grundsätzlich vorangestellt
wird, schließlich aber nur die letzten beiden Ziffern weitergegeben werden.

SQLDate_2_Datum
Aufruf aus
Makro:

Zeitplan

Benötigt
Makro:
001
002
003
004
005
006
007

keins

FUNCTION SQLDate_2_Datum(d AS STRING) AS STRING
DIM a()
DIM stDatum AS STRINGa = split(d,"-")
stDatum = a(2)+"."+a(1)+"."+Right(a(0),2)
SQLDate_2_Datum = stDatum
END FUNCTION

Das SQL-Datum hat den Aufbau YYYY-MM-DD. Für das Datum in deutschsprachiger Schreibweise wird hier eine Vertauschung von Jahreszahl und Tageszahl vorgenommen. Dann werden
noch die Punkte als Trenner gesetzt und die Jahreszahl in zweistelliger Form ausgegeben.

Date_2_Monat
Aufruf aus
Makro:

Monatskalender

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016

keins

FUNCTION Date_2_Monat(d AS DATE) AS STRING
IF Month(d) = 1 THEN
Date_2_Monat = "Januar"
ELSEIF Month(d) = 2 THEN
Date_2_Monat = "Februar"
ELSEIF Month(d) = 3 THEN
Date_2_Monat = "März"
ELSEIF Month(d) = 4 THEN
Date_2_Monat = "April"
ELSEIF Month(d) = 5 THEN
Date_2_Monat = "Mai"ELSEIF Month(d) = 6 THEN
Date_2_Monat = "Juni"
ELSEIF Month(d) = 7 THEN
Date_2_Monat = "Juli"ELSEIF Month(d) = 8 THEN

Termine

293

017
018
019
020
021
022
023
024
025
026
027

Date_2_Monat = "August"
ELSEIF Month(d) = 9 THEN
Date_2_Monat = "September"
ELSEIF Month(d) = 10 THEN
Date_2_Monat = "Oktober"
ELSEIF Month(d) = 11 THEN
Date_2_Monat = "November"
ELSE
Date_2_Monat = "Dezember"
END IF
END FUNCTION

Mit Hilfe der Monatszahl wird hier jeweils ein entsprechender Monatsname ausgegeben. Diese
Funktion könnte genauso gut durch eine globale Variable in Form eines Arrays ersetzt werden.

Datum_Ostersonntag
Aufruf aus
Makro:

FeiertageArray

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036

keins

FUNCTION Datum_Ostersonntag(inJahr AS INTEGER) AS LONGREM Spencers Osterformel mit Ganzzahldivision \DIM a AS INTEGER
DIM b AS INTEGER
DIM c AS INTEGER
DIM d AS INTEGERDIM e AS INTEGER
DIM f AS INTEGERDIM g AS INTEGERDIM h AS INTEGER
DIM i AS INTEGERDIM k AS INTEGERDIM l AS INTEGER
DIM m AS INTEGERDIM n AS INTEGER
DIM p AS INTEGERDIM OS AS INTEGERDIM loDatum AS LONG
a = inJahr MOD 19
b = inJahr \ 100
c = inJahr MOD 100
d = b \ 4
e = b MOD 4
f = (b + 8) \ 25
g = (b - f + 1) \ 3
h = (19*a + b - d - g + 15) MOD 30
i = c \ 4k = c MOD 4
l = ( 32 + 2*e + 2*i - h - k ) MOD 7
m = ( a + 11*h + 22*l ) \ 451
n = ( h + l - 7*m + 114 ) \ 31 'Monatp = ( h + l - 7*m + 114 ) MOD 31
Os = p + 1 'Tag
loDatum = DateSerial(inJahr, n, Os)
Datum_Ostersonntag() = loDatum
END FUNCTION

Diese Funktion ist eine direkte Umsetzung von https://de.wikipedia.org/wiki/Spencers_Osterformel. Aus der Jahresvorgabe wird so ein Datum für den Ostersonntag ermittelt, mit dem innerhalb von Basic aus weiter gerechnet werden kann. So können die Feiertage für ein Jahr
bestimmt werden, die von der Lage des Ostersonntags abhängig sind.
Termine294

FeiertageArray
Aufruf aus
Makro:

Kalender, GruppeMultiUebersicht

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036

Datum_Ostersonntag, Date_2_SQLDate

FUNCTION FeiertageArray(inJahr AS INTEGER) AS OBJECT
DIM d_Ostersonntag AS DATEDIM Feiertage(14,1)
d_Ostersonntag = CDate(Datum_Ostersonntag(inJahr))
Feiertage(0,0) = "Neujahr"
Feiertage(0,1) = Trim(Str(inJahr)) + "-01-01"
Feiertage(1,0) = "Rosenmontag"
Feiertage(1,1) = Date_2_SQLDate(DateAdd("d",-48,d_Ostersonntag))Feiertage(2,0) = "Karfreitag"
Feiertage(2,1) = Date_2_SQLDate(DateAdd("d",-2,d_Ostersonntag))Feiertage(3,0) = "Ostersonntag"
Feiertage(3,1) = Date_2_SQLDate(d_Ostersonntag)
Feiertage(4,0) = "Ostermontag"
Feiertage(4,1) = Date_2_SQLDate(DateAdd("d",1,d_Ostersonntag))Feiertage(5,0) = "Chr.Himmelfahrt"
Feiertage(5,1) = Date_2_SQLDate(DateAdd("d",39,d_Ostersonntag))Feiertage(6,0) = "Pfingstsonntag"
Feiertage(6,1) = Date_2_SQLDate(DateAdd("d",49,d_Ostersonntag))Feiertage(7,0) = "Pfingstmontag"
Feiertage(7,1) = Date_2_SQLDate(DateAdd("d",50,d_Ostersonntag))Feiertage(8,0) = "Fronleichnam"
Feiertage(8,1) = Date_2_SQLDate(DateAdd("d",60,d_Ostersonntag))Feiertage(9,0) = "Tag der dt. Einheit"
Feiertage(9,1) = Trim(Str(inJahr)) + "-10-03"
Feiertage(10,0) = "Allerheiligen"Feiertage(10,1) = Trim(Str(inJahr)) + "-11-01"
Feiertage(11,0) = "Heiligabend"
Feiertage(11,1) = Trim(Str(inJahr)) + "-12-24"
Feiertage(12,0) = "1. Weihnachtstag"
Feiertage(12,1) = Trim(Str(inJahr)) + "-12-25"
Feiertage(13,0) = "2. Weihnachtstag"
Feiertage(13,1) = Trim(Str(inJahr)) + "-12-26"
Feiertage(14,0) = "Tag der Arbeit"
Feiertage(14,1) = Trim(Str(inJahr)) + "-05-01"
FeiertageArray = Feiertage
END FUNCTION

In Abhängigkeit vom Ostersonntag werden viele Feiertage berechnet. Die Feiertage werden mit
Datum und Bezeichnung in einem Array gespeichert. Das Datum wird dabei in der für SQL notwendigen amerikanischen Schreibweise zwischengespeichert.

BerichtZeilenhoeheAuto
Aufruf aus
Makro:

Bericht_Start

Benötigt
Makro:
001
002
003
004

keins

SUB BerichtZeilenhoeheAuto(oReport AS OBJECT)
DIM oTables AS OBJECTDIM oTable AS OBJECT
DIM inT AS INTEGER

Termine

295

005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

DIM inI AS INTEGERDIM oRows AS OBJECTDIM oRow AS OBJECT
oTables = oReport.getTextTables()FOR inT = 0 TO oTables.count() - 1
oTable = oTables.getByIndex(inT)
IF Left$(oTable.name, 6) = "Detail" THEN
oRows = oTable.Rows
FOR inI = 0 TO oRows.count - 1
oRow = oRows.getByIndex(inI)oRow.IsAutoHeight = True
NEXT inI
ENDIF
NEXT inT
END SUB

Diese Prozedur sucht in dem fertigen Bericht alle Tabellen auf, die die Namensbezeichnung"Detail" haben. Die Zeilen in diesem Bereich werden dann auf automatische Höhe eingestellt.
Ab LO 6.4 wäre dies nicht mehr nötig, da dort die AutoGrow-Eigenschaft in den ReportBuilder
aufgenommen wurde.

Bericht_Start
Aufruf aus
Formular: Terminart_Termin, Termine_Kalenderübersicht, Termine_Monat_Terminart,
Termine_Tabelleneingabe

Benötigt
Makro:

Kalender, Monatskalender, Wochenkalender_halb, Halbjahreskalender, Zeitplan, GruppeMultiUebersicht, BerichtZeilenhoeheAuto, DialogStart, Date_2_SQLDate

Tabelle:Zielgruppe, Filter

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029

SUB Bericht_Start(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld_1 AS OBJECTDIM oFeld_2 AS OBJECTDIM oFeld_3 AS OBJECTDIM oFeld_4 AS OBJECTDIM oReport AS OBJECT
DIM daStart AS DATEDIM daEnde AS DATEDIM stKalender AS STRING
DIM inZielgruppe AS INTEGER
DIM stTermin AS STRING
DIM stGruppeMulti AS STRING
DIM stHalbjahr AS STRING
DIM inGruppeMulti AS INTEGER
oForm = oEvent.Source.Model.Parent
oForm.updateRow
Wait 1000
oFeld_1 = oForm.getByName("datStart")oFeld_2 = oForm.getByName("datEnde")oFeld_3 = oForm.getByName("lboKalenderart")oFeld_4 = oForm.getByName("lboZielgruppe")
oFeld_5 = oForm.getByName("lboGruppeMulti")
stKalender = Listenfeld_Text(oFeld_3)
inZielgruppe = ID_Ermittlung(oFeld_4)
oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Termingruppe"" FROM ""Zielgruppe"" WHERE ""ID""='"+inZielgruppe+"' "oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next

Termine

296

030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088

TerminestTermin = oAbfrageergebnis.getString(1)WEND
IF stKalender <>"" THEN
IF NOT isEmpty(oFeld_1.currentValue) THEN
daStart = Datumswert(oFeld_1)
IF NOT isEmpty(oFeld_2.currentValue) THEN
daEnde = Datumswert(oFeld_2)
ELSE
daEnde = "1900-01-01"
END IFIF daEnde < daStart THEN
daEnde = DateAdd("yyyy",1,daStart)
END IF
ELSE
daStart = now
daEnde = DateAdd("yyyy",1,daStart)
END IFIF stKalender = "Terminübersicht" THEN
Zeitplan(daStart,daEnde,stTermin)oReport = ThisDatabaseDocument.ReportDocuments.getByName("Zeitplan").open
BerichtZeilenhoeheAuto(oReport)
ELSEIF stKalender = "Halbjahreskalender" THEN
Kalender(YEAR(daStart),0,stTermin)
Halbjahreskalender(YEAR(daStart))
ThisDatabaseDocument.ReportDocuments.getByName("Halbjahreskalender_A4_quer").open
ELSEIF stKalender = "Monate Montag - Sonntag" THEN
Kalender(YEAR(daStart),1,stTermin)
Monatskalender(daStart,daEnde)
ThisDatabaseDocument.ReportDocuments.getByName("Monatskalender_Mo_So").open
ELSEIF stKalender = "Monate Montag - Freitag" THEN
Kalender(YEAR(daStart),1,stTermin)
Monatskalender(daStart,daEnde)
ThisDatabaseDocument.ReportDocuments.getByName("Monatskalender_Mo_Fr").open
ELSEIF stKalender = "Halbwochenkalender als Notizbuch" THEN
Kalender(YEAR(daStart),1,stTermin)
Monatskalender(daStart,daEnde)
Wochenkalender_halb(daStart,daEnde)
ThisDatabaseDocument.ReportDocuments.getByName("Wochenkalender_halb").open
ELSEIF stKalender = "Wochentage Mensaabo" THEN
Kalender(YEAR(daStart),1,stTermin)
ThisDatabaseDocument.ReportDocuments.getByName("Wochentage_Mensaabo").open
ELSEIF stKalender = "Klassenarbeitenübersicht" THEN
stSql = "UPDATE ""Filter"" SET ""Startjahr"" = '"+Year(daStart)+"' WHERE""ID"" = TRUE "oSQL_Anweisung.executeUpdate(stSql)
IF NOT isEmpty(oFeld_5.currentValue) THEN
stGruppeMulti = Listenfeld_Text(oFeld_5)
inGruppeMulti = ID_Ermittlung(oFeld_5)
GruppeMultiUebersicht(YEAR(daStart),1,stTermin,inGruppeMulti,
stGruppeMulti)
ThisDatabaseDocument.ReportDocuments.getByName("Liste_Gruppe_multi").open
ELSE
msgbox "Ein Jahrgang muss ausgewählt werden."
END IFELSEIF stKalender = "Klassenarbeitenübersicht 1. HJ" THEN
stSql = "UPDATE ""Filter"" SET ""Startjahr"" = '"+Year(daStart)+"' WHERE""ID"" = TRUE "oSQL_Anweisung.executeUpdate(stSql)
stSql = "SELECT ""Halbjahrende"" FROM ""Filter"" WHERE ""ID"" = TRUE AND""Halbjahrende"" BETWEEN '"+Date_2_SQLDate(daStart)+"' AND'"+Date_2_SQLDate(daEnde)+"'"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stHalbjahr = oAbfrageergebnis.getString(1)

297

089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

WEND
IF stHalbjahr = "" THEN
DialogStart
END IFIF NOT isEmpty(oFeld_5.currentValue) THEN
stGruppeMulti = Listenfeld_Text(oFeld_5)
inGruppeMulti = ID_Ermittlung(oFeld_5)
GruppeMultiUebersicht(YEAR(daStart),1,stTermin,inGruppeMulti,
stGruppeMulti)
ThisDatabaseDocument.ReportDocuments.getByName("Liste_Gruppe_multi_1HJ").open
ELSE
msgbox "Ein Jahrgang muss ausgewählt werden."
END IFELSEIF stKalender = "Klassenarbeitenübersicht 2. HJ" THEN
stSql = "UPDATE ""Filter"" SET ""Startjahr"" = '"+Year(daStart)+"' WHERE""ID"" = TRUE "oSQL_Anweisung.executeUpdate(stSql)
stSql = "SELECT ""Halbjahrende"" FROM ""Filter"" WHERE ""ID"" = TRUE AND""Halbjahrende"" BETWEEN '"+Date_2_SQLDate(daStart)+"' AND'"+Date_2_SQLDate(daEnd)+"'"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stHalbjahr = oAbfrageergebnis.getString(1)WEND
IF stHalbjahr = "" THEN
DialogStart
END IFIF NOT isEmpty(oFeld_5.currentValue) THEN
stGruppeMulti = Listenfeld_Text(oFeld_5)
inGruppeMulti = ID_Ermittlung(oFeld_5)
GruppeMultiUebersicht(YEAR(daStart),1,stTermin,inGruppeMulti,
stGruppeMulti)
ThisDatabaseDocument.ReportDocuments.getByName("Liste_Gruppe_multi_2HJ").open
ELSE
msgbox "Ein Jahrgang muss ausgewählt werden."
END IFELSE
END IF
END IF
END SUB

Von der Prozedur "Bericht_Start" aus werden alle Berichte gestartet. Kein Bericht kann mit aktuellen Daten direkt gestartet werden, da die Daten erst einmal aus den Eingabetabellen so aufgearbeitet werden müssen, dass sie für den Bericht verwertbar sind. Das geschieht, indem inverschiedenen Prozeduren die mit "tmp_" beginnenden Tabelle mit Inhalten befüllt werden.
Zu Beginn wird der Inhalt der Formularfelder in die Tabelle "Filter" abgespeichert (Zeile 17).
Hier ist anschließend eine Pause von 1 Sekunde eingebaut (Zeile 18), damit diese Speicherungauch tatsächlich einwandfrei funktioniert.
Von Zeile 19 bis Zeile 23 werden die Objekte für die verschiedenen Formularfelder erstellt. In
Zeile 24 und Zeile 25 werden mittels bereits definierter Funktionen die entscheidenden Werte
der Listenfelder ermittelt. Von Zeile 27 bis Zeile 31 wird die zu der Zielgruppe passende Termingruppe gesucht und in der Variablen stTermin abgespeichert.
Die weiteren Schritte werden nur durchlaufen, wenn in dem Listenfeld für den gewünschten
Kalender auch ein Eintrag vorhanden war (Zeile 32).
Die Zeilen 33 bis 46 dienen dazu, auf jeden Fall ein brauchbares Datum zur weiteren Verarbeitung zu übermitteln. Wenn das Startdatum nicht leer ist wird noch nachgeschaut, ob das Enddatum auch nicht leer ist. Liegt das Enddatum vor dem Startdatum, dann wird der Wert ignoriert und einfach ein Jahr zu dem Startdatum hinzugezählt. Enthält das Startdatum keinen Wert,
so wird hier einfach von dem aktuellen Datum ausgegangen und das Enddatum wieder ein Jahr
später gesetzt.
Termine298

In den folgenden Schritten wird jeweils überprüft, welcher Eintrag in stKalender steht. Zu dem
jeweiligen Eintrag werden dann die verschiedenen Prozeduren aufgerufen bevor zum Schlussauch der dazu passende Bericht gestartet wird. Hier nur zwei Beispiele:•

Terminübersicht (Zeile 47 bis 50): Zuerst wird die Prozedur "Kalender" gestartet.
Anschließend wird der Bericht aufgerufen, hier allerdings so, dass er für die Prozedur
weiter als Variable zur Verfügung steht. Zum Schluss werden die Zeilen in dem Detailbereich des Berichtes auf automatische Höhe eingestellt.

•

Klassenarbeiten Übersicht 2. HJ. (Zeilen 101 bis 119): Zuerst wird das Startjahr in die
Tabelle "Filter" eingetragen. Danach wird nachgeschaut, ob in der Tabelle "Filter" ein Eintrag für das "Halbjahrende" existiert, der außerdem auch noch zwischen dem Startdatum und dem Enddatum liegt. Ist das nicht der Fall, dann wird der Dialog für die Eingabe
des Halbjahresdatums gestartet. Als letzte Hürde wird überprüft, ob denn eine Auswahleines Jahrgangs erfolgt ist. Ist dies der Fall, dann startet die Prozedur "GruppeMultiUebersicht" und anschließend dann der Bericht "Liste_Gruppe_multi_2HJ". Wurde kein Jahrgang ausgewählt, so erfolgt die Meldung, dass noch ein Jahrgang ausgewählt werdenmuss bevor der Bericht zusammengestellt werden kann.

Backup
Die Absicherung von Datenbankdateien sollte regelmäßig erfolgen. Eine Sicherheitskopie
erstellt LibreOffice hiervon nicht automatisch, so dass dafür etwas nachgeholfen werden sollte.

Datenbankbackup
Aufruf aus
Makro:

Datenbankstart, Backup_sofort

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028

keins

SUB Datenbankbackup(inMax AS INTEGER)
DIM oPath AS OBJECTDIM oDoc AS OBJECT
DIM sTitel AS STRING
DIM sUrl_Ziel AS STRING
DIM sUrl_Start AS STRING
DIM i AS INTEGERDIM k AS INTEGER
oDoc = ThisComponentsTitel = oDoc.Title
sUrl_Start = oDoc.URL
DO WHILE sUrl_Start = ""oDoc = oDoc.ParentsTitel = oDoc.Title
sUrl_Start = oDoc.URL
LOOP
oPath = createUnoService("com.sun.star.util.PathSettings")
FOR i = 1 TO inMax + 1
IF NOT FileExists(oPath.Backup & "/" & i & "_" & sTitel) THEN
IF i > inMax THEN
FOR k = inMax - 1 TO 1 STEP -1
IF FileDateTime(oPath.Backup & "/" & k & "_" & sTitel) <=FileDateTime(oPath.Backup & "/" & k+1 & "_" & sTitel) THEN
IF k = 1 THENi = k
EXIT FOR
END IF
ELSE
i = k+1

Termine

299

029
030
031
032
033
034
035
036
037
038

EXIT FOR
END IFNEXT
END IF
EXIT FOR
END IFNEXT
sUrl_Ziel = oPath.Backup & "/" & i &"_" & sTitel
FileCopy(sUrl_Start,sUrl_Ziel)
END SUB

Das Prinzip der Makros ist erst einmal, lediglich eine Kopie der Datenbankdatei in das BackupVerzeichnis von LibreOffice zu befördern. Dort soll nicht sofort die vorhergehende Kopie
gelöscht und die neue Kopie eingefügt werden. Stattdessen soll eine vorher festgelegte Anzahl
an Kopien angelegt werden bevor die älteste dieser Kopien entfernt wird.
Zuerst wird ein Bezug zur Datenbankdatei hergestellt (Zeile 9). Von der Datei wird der Pfad
ausgelesen. Falls das Makro nicht direkt von der Base-Datei aus gestartet wird, sondern vielleicht aus einem Formular ausgelöst wird, muss der Pfad aus dem nächst höheren Element
parent ausgelesen werden (Zeile 12 bis Zeile 16). Anschließend wird der Titel der Datei (derhier nicht vorher durch ein Makro geändert werden darf) ausgelesen und in einer Schleife vonZeile 18 bis Zeile 35 laufend als neue Datei im Backupverzeichnis mit einem Zähler versehenausgetestet (Zeile 19).
Nur wenn die Backupdatei nicht existiert und wenn der Zähler größer ist als das vorgegebene
Maximum an Dateien geht es in die weitere Verschachtelung. Ist der Zähler kleiner und existiert die Datei nicht, so wird die Schleife mit EXIT FOR (Zeile 33) beendet.
Solange noch nicht geklärt ist, wie die Datei denn nun heißen darf, erfolgt rückwärts zählend
ab der Kopie mit der höchsten Nummer ein Vergleich. Ist die Datei mit der aktuellen Nummer
vom Zeitstempel her kleiner oder gleich der Datei, die die nächst höhere Nummer hat. Solange
das der Fall ist wird weiter gesucht. Wird stattdessen eine neuere Datei mit größerem Zeitstempel gefunden, so muss die neu einzufügende Datei die Nummer bekommen, die um 1 größer ist
als diese neuere Datei (Zeile 27 bis Zeile 30). Ist dies nie der Fall, so wird die Datei mit der
Nummer 1 ersetzt (Zeile 23 bis Zeile 26).
Damit ist klar, welchen Zähler die Backupdatei erhalten soll. Die Ziel-URL wird erstellt (Zeile 36)
und die Ausgangsdatei zur Ziel-URL hin kopiert (Zeile 37).

Backup_sofort
Aufruf aus
Datenbankdatei, direkt in die Symbolleiste neben dem Speicherbutton integriert

Benötigt
Makro:
001
002
003
004

Datenbankbackup

SUB Backup_sofort
oVerbindung.flush
Datenbankbackup(10)END SUB

Über einen Button z.B. in der Symbolleiste der Base-Datei kann diese Prozedur ausgelöst werden. Zuerst werden die Daten mit einem flush in die Tabelle übertragen, die in der Datenbankdatei steckt. Danach wird die Prozedur zum Backup der Datenbankdatei gestartet. Die '10'
sagt hier lediglich, dass bis zu 10 Backups aufbewahrt werden und danach das älteste Backupüberschrieben wird.

Eingabe
Die Prozeduren in diesem Modul beeinflussen die Eingabe von Daten über die Formulare.
Termine300

DatenSpeichern
Aufruf aus
Makro:

DatenAktualisieren_Termine, DatenAktualisieren_Terminort_Termine

Benötigt
Makro:
001
002
003
004
005
006
007

keins

SUB DatenSpeichern
DIM oDocument AS OBJECTDIM oDispatcher AS OBJECT
oDocument = ThisComponent.CurrentController.FrameoDispatcher = createUnoService("com.sun.star.frame.DispatchHelper")oDispatcher.executeDispatch(oDocument, ".uno:RecSave", "", 0, Array())
END SUB

Die ist ein allgemeiner Aufruf zum Speichern innerhalb eines Formulars. UNO (Universal
Network Object) dient hier dazu, über den Dispatcher schnell an eine komplexere Funktion zu
kommen. Der direkte Weg zu dieser Funktionalität würde bedeuten, dass zuerst abgeklärt werden muss, ob die Daten bereits abgespeichert sind. Dann muss ein "Update" erfolgen. Ansonsten ist ein "Insert" notwendig. .uno:RecSave fasst diese beiden Möglichkeiten in einem Objekt
zusammen.

DatenAktualisieren_Termine
Aufruf aus
Formular: Termine_Monat_Terminart

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013

DatenSpeichern

SUB DatenAktualisieren_Termine
DIM oForm AS OBJECTDIM oSubForm_1 AS OBJECTDIM oSubForm_2 AS OBJECT
DatenSpeichern
oDoc=thisComponent
oDrawpage=oDoc.drawpage
oForm = oDrawpage.Forms.getByName("MainForm")oSubForm_1 = oForm.getByName("SubForm")oSubForm_2 = oForm.getByName("SubTerminart")oSubForm_1.reload()
oSubForm_2.reload()
END SUB

Abhängig von der Eingabe im Feld "BeginnDatum" oder im Feld "TerminArt" sollen die Inhalte
der Unterformulare neu eingelesen werden.
In Zeile 5 wird die Prozedur "DatenSpeichern" aufgerufen, die unabhängig davon funktioniert,
ob es sich um ein Update oder um einen Insert handelt. Anschließend werden die Unterformulare über den Formularstrang aufgerufen. Beide Formulare werden nacheinander (Zeile 11 und
Zeile 12) neu geladen.

DatenAktualisieren_Terminort_Termine
Aufruf aus
Formular: Terminart_Termin

Termine301

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015

DatenSpeichern

SUB DatenAktualisieren_Terminart_Termine
DIM oForm AS OBJECTDIM oSubForm AS OBJECTDIM oSubSubForm_1 AS OBJECTDIM oSubSubForm_2 AS OBJECT
DatenSpeichern
oDoc=thisComponent
oDrawpage=oDoc.drawpage
oForm = oDrawpage.Forms.getByName("MainForm")oSubForm = oForm.getByName("SubTermine")oSubSubForm_1 = oSubForm.getByName("SubTerminMonat")oSubSubForm_2 = oSubForm.getByName("SubTerminOrt")oSubSubForm_1.reload()
oSubSubForm_2.reload()
END SUBNach einer Datensatzänderung im Unterformular in dem Feld für das "BeginnDatum "sollen die
UnterUnterformulare auf diese Änderung entsprechend eingestellt werden. Da zu dem Zeitpunkt auch der Ort bereits eingegeben wurde können sich Zeit und Ort, also die Parameter für
die beiden Unterformulare, geändert haben.
In Zeile 6 wird die Prozedur "DatenSpeichern" aufgerufen, die unabhängig davon funktioniert,
ob es sich um ein Update oder um einen Insert handelt. Anschließend werden die UnterUnterformulare über den gesamten Formularstrang aufgerufen. Beide Formulare werden nacheinander (Zeile 13 und Zeile 14) neu geladen.

LetzterDatensatz
Aufruf aus
Formular: Termine_Monat_Terminart
Makro:

LetzterDatensatz_Termine_Kalender

Benötigt
Makro:
001
002
003
004
005

keins

SUB LetzterDatensatz(oEvent AS OBJECT)
DIM oForm AS OBJECT
oForm = oEvent.Source
oForm.last()
END SUB

Beim Laden des Formulars wird das Formular direkt auf den letzten Datensatz gesetzt, damit
anschließend mit einer Neueingabe begonnen werden kann.
Das Formular ist die Quelle für das auslösende Ereignis, oEvent.Source. Mit last() wird derletzte Datensatz aufgesucht.

LetzterDatensatz_Termine_Kalender
Aufruf aus
Formular: Termine_Kalenderübersicht

Benötigt
Makro:
001

LetzterDatensatz, Monatsansicht_aktualisieren

SUB LetzterDatensatz_Termine_Kalender(oEvent AS OBJECT)

Termine

302

002
003
004

LetzterDatensatz(oEvent)
Monatsansicht_aktualisieren
END SUB

Beim Laden des Formulars soll der Nutzer nicht erst umständlich zum letzten Datensatz navigieren. Das Formular hat hier schließlich kein übersichtliches Tabellenkontrollfeld, wo das nur
ein Klick mit der Maus ist. Deswegen wird zuerst das Formular mit der Prozedur "Letzter Datensatz" auf den letzten Datensatz eingestellt und danach die Prozedur "Monatsansicht_aktualisieren" aufgerufen, damit das Datum des letzten Datensatzes auch in der Monatsansicht zu finden
ist.

Anfangswert_Datum
Aufruf aus
Formular: Termine_Kalenderübersicht

Benötigt
Makro:
001
002
003
004
005
006
007

Datumswert

SUB Anfangswert_Datum(oEvent AS OBJECT)
DIM oFeld AS OBJECT
oFeld = oEvent.Source.Model
IF NOT ISEMPTY(oFeld.getCurrentValue()) THEN
daStartDat = Datumswert(oFeld)
END IF
END SUB

Diese Prozedur wird ausgelöst, wenn auf das Datumsfeld in dem Formular zugegriffen wird, dasdie komplette Monatsübersicht darstellt.
Zuerst wird lediglich das Feld aus dem auslösenden Ereignis abgeleitet (Zeile 3). Ist dieses Feld
nicht leer (Zeile 4), so wird aus dem Feld der entsprechende Inhalt für das Datum ausgelesen,
durch die Funktion Datumswert in eine Cdate-Variable überführt und in der globalen Variablen
daStartDat abgespeichert (Zeile 5). Diese Abspeicherung ist für die Prozedur
Monatsansicht_aktualisieren wichtig. Hier muss erkannt werden, ob beim Verlassen des Feldes
eine Datumsänderung erfolgt ist oder nur mit dem Cursor einmal das Feld betreten und wieder
verlassen wurde.

Monatsansicht_aktualisieren
Aufruf aus
Formular: Termine_Kalenderübersicht
Makro:

LetzterDatensatz_Termine_Kalender

Benötigt
Makro:

Datumswert, MonatsansichtTabelle:Filter

001
002
003
004
005
006
007
008
009
010

SUB Monatsansicht_aktualisieren
DIM oForm AS OBJECTDIM oForm_2 AS OBJECTDIM oFeld_0 AS OBJECTDIM oFeld_1 AS OBJECTDIM oFeld_2 AS OBJECT
DIM daStart AS DATEDIM stMonat AS STRING
DIM stBezeichnung AS STRING
oDoc = thisComponent

Termine

303

011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041

oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("MainForm")oFeld_1 = oForm.getByName("datBeginn_Datum")
IF NOT ISEMPTY(oFeld_1.getCurrentValue()) THEN
IF oForm.getString(oForm.findColumn("ID")) = "" THEN
oForm.insertRow()
ELSEoForm.updateRow()
END IFoSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Jahr_Monat"" FROM ""Filter"" WHERE ""ID"" = TRUE "oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stBezeichnung = oAbfrageergebnis.getString(1)WEND
daStart = Datumswert(oFeld_1)
IF daStartDat <> daStart OR stBezeichnung = "" THEN
IF isDate(daStart) THEN
stMonat = Trim(str(Year(daStart)))+","+
Right("0"+Trim(str(Month(daStart))),2)
stSql = "UPDATE ""Filter"" SET ""Jahr_Monat"" = '"+stMonat+"' WHERE""ID"" = TRUE "oSQL_Anweisung.executeUpdate(stSql)
oForm_2 = oDrawpage.Forms.getByName("Monat")oFeld_2=oForm_2.getByName("lboMonat_Jahr")oFeld_2.refresh()oForm_2.reload()
Monatsansicht
daStartDat = daStart
END IF
END IF
END IF
END SUBIn dem Formular mit der Monatsübersicht soll die Übersicht in dem Moment aktualisiert werden,
wenn bei einer Neueingabe oder einer Datensatzänderung das Feld mit dem "BeginnDatum"
verlassen wird. Bei einem Monatswechsel kann sonst während der Eingabe der Termin nicht
richtig in seinem Umfeld eingeordnet werden.
In Zeile 13 wird das Monatsfeld aufgesucht und dann in Zeile 14 überprüft, ob das Feld nicht
zufällig leer ist. Dann würde eine Neueinstellung ja keinen Sinn ergeben. Ist das Feld nicht leer,
so läuft die Prozedur weiter. Ansonsten endet sie hier.
Zeile 15 dient zur Überprüfung, ob der Datensatz schon vorhanden ist. Enthält das Feld "ID" derzugrundeliegenden Datenquelle keinen Eintrag, dann muss der Datensatz neu eingefügt werden: insertRow(). Andernfalls ist ein updateRow() erforderlich. Das Feld "ID" wird hier mit der
Methode getString abgefragt, damit auf einen leeren String hin überprüft werden kann. Wenn
das Feld mit einem getLong oder getInt abgefragt würde, dann wäre das Ergebnis selbst bei
einem leeren Feld '0' – schlecht zu überprüfen, wenn doch eine '0' auch als Primärschlüssel vorkommen darf.
Von Zeile 20 bis Zeile 25 wird ermittelt, welcher Eintrag in der Filtertabelle für das Feld
"Jahr_Monat" vorhanden ist. Nach diesem Feld wird schließlich die Monatsansicht gefiltert.
Der Basic-Interne Datumswert für das auslösende Datumsfeld wird ermittelt. Anschließend wird
dieser Wert mit der zu Beginn abgespeicherten globalen Variable daStartDat verglichen (Zeile27). Wenn sich die Werte unterscheiden oder wenn in der Filter-Tabelle kein Eintrag für das Feld
"Jahr_Monat" zu finden ist, dann läuft die Prozedur weiter. Ansonsten endet sie hier. Das Neueinlesen macht ja keinen Sinn, wenn die Übersicht bereits auf den aktuellen Monat eingestellt
ist.
Das neu eingegebene Datum wird jetzt zu einer Kombination von Monat und Jahr umgeschrieben (Zeile 29) und in die Filtertabelle eingetragen (Zeile 30 und Zeile 31).

Termine304

Das Listenfeld über der Datumsansicht, das dazu dient, die Datumsansicht umzustellen, wird
neu eingelesen (Zeile 34). Es könnte ja sein, dass der neue Eintrag noch nicht mit vorherigen
Einträgen in der Terminübersicht übereinstimmt und deshalb im Listenfeld noch nicht vorhanden ist. Anschließend wird das Formular, in dem sich das Listenfeld befindet, neu geladen(Zeile 35).
Anschließend wird die Prozedur "Monatsansicht" ausgelöst, die die Gesamtansicht bei derMonatsübersicht im Formular steuert.
Zum Abschluss wird die globale Variable daStartDat mit dem neuen Wert daStart überschieben, damit beim nächsten Eintrag wieder davon ausgegangen werden kann, dass die Datumsübersicht auf daStartDat eingestellt ist.

Monat_aktualisieren
Aufruf aus
Formular: Termine_Kalenderübersicht

Benötigt
Makro:
001
002
003
004
005
006
007
008
009

Monatsansicht

SUB Monat_aktualisieren(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oFeld AS OBJECT
oFeld = oEvent.Source.ModeloForm = oFeld.Parent
oFeld.commit()oForm.updateRow()
Monatsansicht
END SUB

Diese Prozedur ist in recht allgemeingültiger Weise geschrieben. Sie wird ausgelöst durch ein
Listenfeld im Formular. Das Feld wird über das auslösende Ereignis in Zeile 4 ermittelt. Das Feld
steckt in einem Formular. Das Formular ist Parent für das Feld (Zeile 5).
Bei Änderung des Feldes wird der Inhalt an die darunterliegende Tabelle übertragen und überdas Formular mit einem updateRow() abgespeichert. Hier gilt nur ein updateRow(), weil es sichum die Filtertabelle handelt, bei der der Datensatz immer nur ausgetauscht wird.
Anschließend wird die Prozedur "Monatsansicht" ausgelöst, die die Gesamtansicht bei derMonatsübersicht im Formular steuert.

Termindaten_uebertragen
Aufruf aus
Formular: Termine_Kalenderübersicht

Benötigt
Makro:

keins

Tabelle:Termine

001
002
003
004
005
006
007
008

SUB Termindaten_uebertragen
DIM oForm AS OBJECTDIM oFeld_0 AS OBJECTDIM oFeld_1 AS OBJECTDIM oFeld_2 AS OBJECTDIM oFeld_3 AS OBJECTDIM oFeld_4 AS OBJECTDIM oFeld_5 AS OBJECT

Termine

305

009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053

DIM oFeld_6 AS OBJECT
DIM stBezeichnung AS STRING
DIM stBeginn AS STRING
DIM stEnde AS STRING
DIM stOrt AS STRING
DIM stBemerkung AS STRING
DIM startID AS STRING
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName("MainForm")
IF oForm.getString(oForm.findColumn("ID")) = "" THEN
oFeld_1=oForm.getByName("comBezeichnung")
stBezeichnung = oFeld_1.getCurrentValue()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT ""Beginn_Uhrzeit"", ""Ende_Uhrzeit"", ""Ort"", ""Bemerkung"",""artID"" FROM ""Termine"" WHERE ""ID"" = ( SELCET MAX(""ID"") FROM""Termine"" WHERE ""Bezeichnung"" = '"+stBezeichnung+"' )"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stBeginn = oAbfrageergebnis.getString(1)stEnde = oAbfrageergebnis.getString(2)stOrt = oAbfrageergebnis.getString(3)stBemerkung = oAbfrageergebnis.getString(4)startID = oAbfrageergebnis.getString(5)WEND
oFeld_2=oForm.getByName("timBeginn_Uhrzeit")oFeld_3=oForm.getByName("timEnde_Uhrzeit")oFeld_4=oForm.getByName("comOrt")oFeld_5=oForm.getByName("txtBemerkung")
oFeld_6=oForm.getByName("lboartID")
IF stBeginn <> "" THEN
oFeld_2.BoundField.updateString(stBeginn)
END IFIF stEnde <> "" THEN
oFeld_3.BoundField.updateString(stEnde)END IFIF stOrt <> "" THEN
oFeld_4.BoundField.updateString(stOrt)
END IFIF stBemerkung <> "" THEN
oFeld_5.BoundField.updateString(stBemerkung)END IFIF startID <> "" THEN
oFeld_6.BoundField.updateString(startID)
END IF
END IF
END SUB

Dieses Makro läuft nur ab, wenn ein neuer Datensatz erstellt wird, das Feld "ID" also noch keinen Wert enthält (Zeile 19). Es wird von dem Feld "comBezeichnung" (Zeile 20) ausgelöst undsoll dazu führen, dass der letzte Eintrag mit der gleichen Bezeichnung herausgesucht wird undder Inhalt der anderen Felder ausgelesen und in die Formularfelder übertragen wird. Die
Abfrage in Zeile 23 klärt in einer Unterabfrage den letzten Eintrag mit der gleichen "Bezeichnung" ab und übergibt an die äußere Abfrage nur einen einzigen Wert: die ID.
Die Inhalte werden als Strings ausgelesen (Zeile 26 bis Zeile 30). Leere Felder werden so zu
NULL in den Eingabefeldern. Eine Umwandlung der Inhalte in Datumswerte, Zeitwerte oder
Zahlenwerte sollte vermieden werden, da dies nur zu Formatierungsproblemen und fehlerhaften Einträgen führt. Eine leere Uhrzeit gibt es nicht, ebenso wenig eine leere Zahl und ein leeres Datum. Hier würden dann Default-Werte genommen – bei den Zahlen eben die '0', bei Uhrzeiten wohl ebenfalls '00:00:00' und bei Zeiten dann das Startdatum 01.01.1900.
In den Zeilen 37 bis 51 werden die Inhalte, sofern sie nicht leer sind, in die entsprechenden Formularfelder mit den entsprechenden BoundField.updateString – Kommandos geschrieben.

Termine306

GruppeMultiSpeichern
Aufruf aus
Formular: Terminart_Termin, Termine_Kalenderübersicht, Termine_Monat_Terminart,
Termine_Tabelleneingabe

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015

keins

SUB GruppeMultiSpeichern(oEvent AS OBJECT)
DIM aFields()
DIM oForm AS OBJECT
DIM inValue AS INTEGER
DIM i AS INTEGER
oForm = oEvent.Source.Model.Parent
aFields = Array("Check1","Check2","Check3","Check4","Check5","Check6","Check7","Check8","Check9")
inValue = 0
FOR i = LBound(aFields()) TO UBound(aFields())
IF oForm.getByName(aFields(i)).State = 1 THEN
inValue = inValue + CInt(oForm.getByName(aFields(i)).refValue)
END IFNEXT
oForm.UpdateInt(oForm.findColumn("Gruppe_multi"),inValue)
END SUBIn den Formularen befinden sich 9 Checkboxen, die nicht direkt mit einem Feld der zugrundeliegenden Tabelle verbunden sind. Stattdessen existiert in der Tabelle das Feld mit der Bezeichnung "Gruppe_multi" (Zeile 14). Die Checkboxen werden in einem Array zusammengefasst (7).
Anschließend wird eine Schleife durch alle Checkboxen gestartet (9) und die Werte der Checkboxen, die ausgewählt wurden (State = 1, Zeile 10), zusammen addiert . Der Gesamtwert wird
in das Datenfeld "Gruppe_multi" übertragen.

DialogStart
Aufruf aus
Makro:

Bericht_Start

Benötigt
Dialog:

Dialog Datumseingabe001

DIM oDialogDat AS OBJECT

001
002
003
004
005

SUB DialogStart
DialogLibraries.LoadLibrary("Standard")
oDialogDat = createUnoDialog(DialogLibraries.Standard.Datumseingabe)
oDialogDat.Execute()
END SUB

Für die Berichte, die eine Halbjahresübersicht anfertigen sollen, ist eine Datumseingabe desHalbjahres erforderlich. Statt diese Datumseingabe über ein Input-Feld zu erledigen und nachher erst einmal auf die korrekte Eingabe überprüfen zu müssen wird hier ein Dialog genommen
und mit dieser Prozedur gestartet.
Der Dialog wird vorher als Variable für das ganze Modul deklariert, da er ja auch in der Prozedur
zur Beendigung des Dialogs benötigt wird.

Termine307

DialogEnde
Aufruf aus
Dialog:

Dialog Datumseingabe

Benötigt
Makro:

DialogStart

Tabelle:Filter

001
002
003
004
005
006
007
008
009

SUB DialogEnde
DIM stDatum AS STRING
oField = oDialogDat.getControl("datDatum")
stDatum = oField.Date.Year & "-" & Right("0" & Trim(Str(oField.Date.Month)),2) &"-" & Right("0" & Trim(Str(oField.Date.Day)),2)
oSQL_Anweisung = oVerbindung.createStatement()
stSql = "UPDATE ""Filter"" SET ""Halbjahrende"" = '"+stDatum+"' WHERE ""ID"" =TRUE "oSQL_Anweisung.executeUpdate(stSql)
oDialogDat.EndExecute()
END SUB

Der Dialog besteht nur aus einem Datumsfeld und einem Button. Wird der Dialog über den Button beendet, so wird diese Prozedur aufgerufen.
Aus dem Datumsfeld heraus wird der Wert direkt so formatiert, dass er über ein SQL-Kommando als Datum erkannt wird (Zeile 4). Anschließend wird der Wert in die Filtertabelle
geschrieben und danach der Dialog beendet.

ExportIcal_Export
Aufruf aus
Makro:

Export_Start

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021

keins

SUB Ical_Export(stView AS STRING)DIM oUcb AS OBJECTDIM oOutputStream AS OBJECTDIM oFile AS OBJECTDIM oSQL_Anweisung AS OBJECTDIM oAbfrageergebnis AS OBJECT
DIM stBaseUrl AS STRING
DIM stFileUrl AS STRING
DIM stString AS STRING
DIM stSql AS STRING
DIM stUID AS STRING
DIM stLocation AS STRING
DIM stSummary AS STRING
DIM stDescription AS STRING
DIM stDtStart AS STRING
DIM stDtEnd AS STRING
DIM stDtStamp AS STRING
DIM i AS INTEGER
DIM ar()oUcb = createUnoService("com.sun.star.ucb.SimpleFileAccess")
oOutputStream = createUnoService("com.sun.star.io.TextOutputStream")

Termine

308

022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075

ar = split(ThisDatabaseDocument.URL, "/")
stBaseUrl = ""FOR i = LBound(ar()) TO UBound(ar()) - 1
stBaseUrl = stBaseUrl & ar(i) & "/"NEXT
stBaseFile = ar(i)stFileUrl = stBaseUrl & "BaseTermine.ics"
IF oUcb.exists(stFileUrl) THENoUcb.kill(stFileUrl)
END IFoFile = oUcb.OpenFileReadWrite(stFileUrl)oOutputStream.SetOutputStream(oFile.getOutputStream)
stString = "BEGIN:VCALENDAR" & CHR(13) & CHR(10)stString = stString & "VERSION:2.0" & CHR(13) & CHR(10)stString = stString & "https://www.familiegrosskopf.de/robert/" & CHR(13) &CHR(10)stString = stString & "METHOD:PUBLISH" & CHR(13) & CHR(10)oOutputStream.writeString(stString)oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT * FROM """+stView+""""
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stUID = oAbfrageergebnis.getString(1)stLocation = oAbfrageergebnis.getString(2)stSummary = oAbfrageergebnis.getString(3)stDescription = oAbfrageergebnis.getString(4)stDtStart = oAbfrageergebnis.getString(5)IF Len(stDtStart) = 8 THEN
stDtStart = ";VALUE=DATE:"&stDtStart
ELSE
stDtStart = ":"&stDtStart
END IF
stDtEnd = oAbfrageergebnis.getString(6)IF Len(stDtEnd) = 8 THEN
stDtEnd = ";VALUE=DATE:"&stDtEnd
ELSE
stDtEnd = ":"&stDtEnd
END IF
stDtStamp = oAbfrageergebnis.getString(7)stString = "BEGIN:VEVENT" & CHR(13) & CHR(10)stString = stString & "UID:" & stUID & stBaseFile & CHR(13) & CHR(10)stString = stString & "LOCATION:" & stLocation & CHR(13) & CHR(10)stString = stString & "SUMMARY:" & stSummary & CHR(13) & CHR(10)stString = stString & "DESCRIPTION:" & stDescription & CHR(13) & CHR(10)stString = stString & "CLASS:PUBLIC" & CHR(13) & CHR(10)stString = stString & "DTSTART" & stDtStart & CHR(13) & CHR(10)stString = stString & "DTEND" & stDtEnd & CHR(13) & CHR(10)stString = stString & "DTSTAMP:" & stDtStamp & CHR(13) & CHR(10)stString = stString & "END:VEVENT" & CHR(13) & CHR(10)oOutputStream.writeString(stString)
WEND
stString = "END:VCALENDAR"
oOutputStream.writeString(stString)oOutputStream.closeOutput()
END SUBIn Zeile 22 bis Zeile 25 wird die BasisURL der aktuellen Base-Datei ermittelt. Der Dateiname
wird separat in der Variablen stBaseFile gespeichert (27). Anschließend wird der Pfad und Namefür die neu anzulegende Datei festgelegt. Existiert diese Datei bereits, so wird sie in Zeile 30
erst einmal gelöscht. Dies ist notwendig, da beim Schreiben eines Strings in die Datei neu am
Anfang begonnen wird. Bestehende Zeilen werden überschrieben, doch wenn der neue Inhalt
weniger Zeilen einnimmt als der alte Inhalt, dann enthält die Datei anschließend neben dem
neuen Inhalt auch noch alten Inhalt.

Termine309

Von Zeile 35 bis Zeile 37 werden die Kopfzeilen für die iCalendar-Datei zusammengestellt. Jede
Zeile endet dabei mit einem Zeilenumbruch, der sowohl für Linux als auch für Windows gilt.
Diese Kopfzeilen werden in Zeile 38 dann in die neue Datei geschrieben.
In Zeile 40 und Zeile 41 werden die einzufügende Inhalte aus der vorgewählten Ansicht ausgelesen. Die Inhalte werden Zeile für Zeile abgearbeitet und in das Schema für die iCalendar-Datei übertragen. Ist in den Zeitstempelfeldern nur ein Eintrag, der eine Länge von 8 Zeichen hat,dann handelt es sich um einen Eintrag, der nur aus einem Datum ohne Zeit besteht. Dies muss
entsprechend in der iCalendar-Datei vermerkt werden. Deshalb werden diese Variablen in den
Zeilen 48 bis 58 entsprechend ergänzt. Dadurch werden Einträge als Einträge für den ganzen
Tag gesehen und erscheinen nicht in der Terminübersicht, die sich auf Termine mit Startzeit undEndzeit bezieht.
Besondere Aufmerksamkeit erfordert die korrekte Zeitangabe. Hier gibt es unterschiedliche
Herangehensweisen, die davon geprägt sind, dass zwei Personen, die einen Termin zu einem
Zeitpunkt ausmachen, sich auch zu dieser zeit tatsächlich treffen können, wenn so ein Kalenderausschnitt der anderen Person zugeschickt wird. Innerhalb eines Ortes ist das kein Problem,so dass in dieser Prozedur auch auf alle Angaben zu Zeitzonen verzichtet wird (sogenannte
floatende Zeitangabe). Sobald die Personen aber in unterschiedlichen Zeitzonen arbeiten mussder Termin je nach Zeitzone unterschiedlich ausgelesen werden. Bei dem Makro führten am
Anfang Einträge wie DTSTART;TZID=Europe/Berlin: oder ein Z am Ende des Zeitstempels
dazu, dass die korrekt aus der Datenbank ausgelesenen Werte hier in unter Berücksichtigung
der Sommerzeit um 2 Stunden versetzt in den Kalender eingelesen wurden.
Nachdem alle Datensätze ausgelesen wurden wird in Zeile 72 der Endeintrag vorgenommen
und anschließend als letzter Eintrag in die Datei übertragen. Anschließend wird die Dateiausgabe geschlossen.
Die erstelle iCalendar-Datei kann jetzt in andere Kalenderprogramme übertragen werden. Sie
sollte dazu aber vorher aus dem Verzeichnis der Base-Datei in ein anderes Verzeichnis kopiertwerde, da sonst die Ausgabe neuer Daten in eine Datei mit dem gleichen Namen nicht möglichist. Die Kalenderdatei blockiert dann den Schreibvorgang vom Makro aus.

Export_Start
Aufruf aus
Formular: Terminart_Termin, Termine_Kalenderübersicht, Termine_Monat_Terminart,
Termine_Tabelleneingabe

Benötigt
Makro:

Ical_Export

Ansicht:

Ansicht_Kalender_Export, Ansicht_Kalender_ohne_Ferien_Export

001
002
003
004
005
006
007
008
009
010
011

SUB Export_Start(oEvent AS OBJECT)
DIM stView AS STRINGoForm = oEvent.Source.Model.Parent
oForm.updateRow
IF oForm.getByName("chkFerien").State = 1 THEN
stView = "Ansicht_Kalender_Export"
ELSE
stView = "Ansicht_Kalender_ohne_Ferien_Export"
END IF
Ical_Export(stView)
END SUB

Es kann zwischen den beiden Exportmöglichkeiten mit und ohne Ferien gewählt werden. Der
Unterschied besteht lediglich darin, welche Ansicht (View) als Datengrundlage genutzt wird.

Termine310

ImportDieses Modul sollte mit entsprechender Vorsicht gehandhabt werden. Die Zusammenstellung
der Prozeduren beruht nur auf Tests mit einem einfachen Export eines Termins aus einer Kalendersoftware, der über Mail versendet wurde.
Mit den Prozeduren in diesem Modul ließ sich der Termin einwandfrei importieren. Ein doppelter
Import wurde ausgeschlossen, da vorher nach gleichlautender Terminbezeichnung zusammen
mit gleichlautendem Datum gesucht wird.

Ical_ImportAufruf aus
Makro:

Import_Start

Benötigt
Makro:

Ical_Timestamp, LetzterSonntag

Tabelle:Termine, Filter

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041

FUNCTION Ical_Import(stFileUrl AS STRING) AS INTEGERDIM oSQLAnweisung AS OBJECTDIM oAbfrageergebnis AS OBJECT
DIM stSql AS STRING
DIM stRow AS STRING
DIM stBez AS STRING
DIM stBezeichnung AS STRING
DIM stBemerkung AS STRING
DIM stOrt AS STRING
DIM stDat AS STRING
DIM stStartDat AS STRING
DIM stStartZeit AS STRING
DIM stEndDat AS STRING
DIM stEndZeit AS STRING
DIM inBegin AS INTEGER
DIM inCount AS INTEGER
DIM daStart AS DATEDIM daEnd AS DATEDIM fn AS INTEGER
DIM ink AS INTEGERDIM loID AS LONG
fn = freeFile
OPEN stFileUrl FOR INPUT AS #fn
DO WHILE NOT eof(#fn)
LINE INPUT #fn, stRow
IF stRow = "BEGIN:VEVENT" THEN
stBemerkung = "NULL,"
stOrt = "NULL,"
stEndDat = "NULL,"
stEndZeit = "NULL"
inBegin = 1
END IFIF stRow = "END:VEVENT" THENoSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT COUNT(""ID"") FROM ""Termine"" WHERE ""Bezeichnung"" ='"+stBez+"' AND ""Beginn_Datum"" = '"+stDat+"'"oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
inCount = oAbfrageergebnis.getInt(1)WEND
IF inCount = 0 THEN
stSql = "INSERT INTO ""Termine"" (""zieID"",""Bezeichnung"",""Bemerkung"",""Ort"",""Beginn_Datum"",""Beginn_Uhrzeit"",

Termine

311

042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094

Termine""Ende_Datum"",""Ende_Uhrzeit"") VALUES ('0',"+stBezeichnung+stBemerkung+stOrt+stStartDat+stStartZeit+stEndDat+stEndZeit+") "oSQL_Anweisung.executeUpdate(stSql)
stSql = "CALL IDENTITY()"
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
loID = oAbfrageergebnis.getLong(1)WEND
IF ink = 0 THEN
stSql = "UPDATE ""Filter"" SET ""StartIDImport"" = '"+loID+"',""EndIDImport"" = '"+loID+"' WHERE ""ID"" = TRUE"
ELSE
stSql = "UPDATE ""Filter"" SET ""EndIDImport"" = '"+loID+"' WHERE""ID"" = TRUE"END IFoSQL_Anweisung.executeUpdate(stSql)
ink = ink + 1
END IF
inBegin = 0
END IFIF inBegin = 1 THEN
IF InStr(stRow,"SUMMARY") THEN
stBez = Mid(stRow, inStr(stRow,":")+1)
stBezeichnung = "'" & stBez & "',"ELSEIF InStr(stRow,"DESCRIPTION:") THEN
stBemerkung = "'" & Mid(stRow, inStr(stRow,":")+1) & "',"ELSEIF InStr(stRow,"LOCATION:") THEN
stOrt = "'" & Mid(stRow, inStr(stRow,":")+1) & "',"ELSEIF InStr(stRow,"DTSTART") THEN
daStart = Ical_Timestamp(Mid(stRow, inStr(stRow,":")+1))
IF InStr(stRow,"UTC") OR Right(stRow,1) = "Z" THEN
daStart = DateAdd("h",1,daStart)
IF daStart > LetzterSonntag(DateValue(Year(daStart) & "-04-01")) AND
daStart < LetzterSonntag(DateValue(Year(daStart) & "-11-01"))
THEN
daStart = DateAdd("h",1,daStart)
END IF
END IF
stDat = Year(daStart) & "-" & Right("0" & Month(daStart),2) & "-" &Right("0" & Day(daStart),2)
stStartDat = "'" & stDat & "',"IF InStr(Mid(stRow, inStr(stRow,":")+1),"T") THEN
stStartZeit = "'" & Right("0" & Hour(daStart),2) & ":" &Right("0" & Minute(daStart),2) & ":" &Right("0" & Second(daStart),2) & "',"ELSE
stStartZeit = "NULL,"END IFELSEIF InStr(stRow,"DTEND") THEN
daEnd = Ical_Timestamp(Mid(stRow, inStr(stRow,":")+1))
IF InStr(stRow,"UTC") OR Right(stRow,1) = "Z" THEN
daEnd = DateAdd("h",1,daEnd)
IF daEnd > LetzterSonntag(DateValue(Year(daEnd) & "-04-01")) AND
daEnd < LetzterSonntag(DateValue(Year(daEnd) & "-11-01")) THEN
daEnd = DateAdd("h",1,daEnd)
END IF
END IF
stEndDat = "'" & Year(daEnd) & "-" & Right("0" & Month(daEnd),2)& "-" & Right("0" & Day(daEnd),2) & "',"IF InStr(Mid(stRow, inStr(stRow,":")+1),"T") THEN
stEndZeit = "'" & Right("0" & Hour(daEnd),2) & ":" &Right("0" & Minute(daEnd),2) & ":" &Right("0" & Second(daEnd),2) & "'"ELSE
stEndZeit = "NULL"
END IF

312

095
096
097
098
099
100
101

ELSE
END IF
END IFLOOP
CLOSE #fn
Ical_Import = ink
END FUNCTION

In Zeile 22 wird die nächste freie Datenkanalnummer nachgeschlagen und direkt danach
genutzt. Die Datei wird für den INPUT geöffnet. Etwas verwirrend, aber es ist der Input in das
ausführende Programm gemeint - also Lesezugriff. Solange der Inhalt der Datei nicht zu Ende
ist wird der Inhalt Zeilenweise ausgelesen. Dies findet in der Schleife von Zeile 24 bis Zeile 98
statt.
Jeder Termin beginnt mit BEGIN:VEVENT. Das ist sozusagen der Start, jetzt darauf zu achten,
wann die anderen Schlüsselbegriffe erscheinen (Zeile 26). Mit dem ersten Auftauchen desSchlüsselbegriffs werden noch 2 Variablen gesetzt. Zum einen die Variablen stBemerkung,stOrt, stStartZeit, stEndDat und stEndZeit, die für die Datenbank vordefiniert wird als NULL.
Dieses Feld kann eventuell beim Export aus einem Kalenderprogramm fehlen. Und dann noch
die Variable inBegin, die mit dem Wert '1' ermöglichen soll, dass mit dieser Zeile die Verzweigung ab Zeile 58 beschritten werden kann.
Ab Zeile 54 wird nach den Schlüsselbegriffen SUMMARY, DESCRITION, LOCATION, DTSTART und
DTEND geschaut. Taucht einer dieser Begriffe auf, so wird die entsprechende Verzweigung
betreten.
SUMMARY (Zeile 59): Hier wird die Bezeichnung des Termins ausgelesen. Daraus werden 2 Variablen gebildet. StBez ohne irgendeine zusätzliche Formatierung, damit nach der Bezeichnung
in den bestehenden Daten gesucht werden kann. StBezeichnung wird gleich so formatiert, dasssie für den SQL-Code des INSERT geeignet ist. SUMMARY kann auch noch Hinweise auf die Sprache enthalten, so dass der Doppelpunkt hier nicht in den Suchbegriff aufgenommen wurde.
DESCRIPTION (Zeile 61) und LOCATION (Zeile 64): Die Bemerkung und der Ort sind so vorformatiert, dass sie direkt in den INSERT übernommen werden können. Dabei wird davon ausgegangen, dass immer eine Ortsangabe aus der Datenquelle vorliegt.
Komplizierter wird es mit der Umwandlung von Zeitstempeln aus iCalendar zu einer Trennung
von Datum und Zeit, die zudem noch eventuelle Abweichungen zu der Sommerzeit berücksichtigt. Der Zeitstempel wird hierfür durch die Funktion Ical_Timestamp in eine in Basic verarbeitbare Datumsvariable mit Zeitanteil umgewandelt (Zeile 67). Enthält DTSTAT zusätzlich einen
Vermerk wie 'UTC' oder ein 'Z' am Ende des Zeitstempels, so muss die Zeit auf die in dieser
Datenbank gebräuchlichen Normalzeit umgewandelt werden Zeile 68 bis 73). Diese Umwandlung wird hier nur für den deutschsprachigen Raum nach Zeitzone Berlin gemacht (1 Stunde
addieren bei Normalzeit (Zeile 69), 2 Stunden addieren bei Sommerzeit (Zeile 71 eine zusätzlich Stunde). Als Basis für die Unterschiede wird der letzte Sonntag vor dem 1.4 und der letzte
Sonntag vor dem 1.11. herausgesucht. An diesen Sonntagen erfolgt momentan der Wechsel
von Winterzeit zu Sommerzeit und umgekehrt.
Das Datum aus DTSTRAT wird in Zeile 74 zu einem in SQL gebräuchlichen Datumsstring umgewandelt. Dabei wird auf die zweistellige Monatszahl und die zweistellige Tageszahl Rücksicht
genommen. In Zeile 75 wird nur der reine SQL-String erstellt (für die Abfrage nach Duplikaten),in Zeile 76 dann der String, der direkt im INSERT weiter verwendet werden kann. Enthält die
Zeile innerhalb des Zeitstempels, also nicht in dem führenden DTSTART, ein 'T', dann liegt aucheine Zeitangabe vor. Diese Zeitangabe wird dann mit zweistelliger Stunden-, Minuten- und
Sekundenangabe erstellt (Zeile 77). Ist kein 'T' vorhanden, so wird der Wert hier auf für den
SQL-String auf NULL gesetzt.
Die Umwandlung von DTEND läuft identisch zu der von DTSTART ab. Einzige Ausnahme ist hierder SQL-Code für den INSERT, der für das letzte Element nicht mit einem Komma endet (Zeile
91 bzw. Zeile 93).
Taucht END:VEVENT auf, dann wird es zeit zum Abspeichern des Datensatzes (Zeile 33 bis Zeile57). Von Zeile 35 bis Zeile 39 wird überprüft, ob ein Datensatz mit gleicher Bezeichnung und
Termine313

gleichem Datum vorkommt. So ein Datensatz soll erst einmal nicht importiert werden. Erst
wenn die Zählung hier '0' ergibt geht es weiter zum Abspeichern.
In Zeile 41 wird der durch die Variablen bereits gut vorbereitete Code zum Einfügen der Daten
erstellt und abgeschickt. Zeile 43 ermittelt die durch den INSERT erstellten Primärschlüsselwert. Dieser wird in die Felder StartIDImport und EndIDImport übertragen (Zeile 49), wenn es
der erste Datensatz dieses Importes ist. Beim zweiten Datensatz ist ink=1 (Zeile 54) und damit
wird nur EndIDImport auf den neuen Wert gesetzt (Zeile 51).
Der Wert für inBegin wird jetzt auf 0 gesetzt, damit die ganze folgende Untergliederung nicht
noch durchsucht wird.
Mit Zeile 98 startet der Import für den nächsten Datensatz. Ist der Import zu Ende, dann wird in
Zeile 99 der Zugriff auf die Datei wieder freigegeben und in Zeile 100 mit der Funktion
Ical_Import die Anzahl der importierten Datensätze weitergegeben.

Ical_Timestamp
Aufruf aus
Makro:

Ical_Import

Benötigt
Makro:
001
002
003
004
005
006
007
008
009

keins

FUNCTION IcalTimestamp(d AS STRING) AS DATEDIM daT AS DATEIF InStr(d,"T") THEN
daT = DateSerial(Left(d,4), Mid(d,5,2), Mid(d,7,2)) + TimeSerial(Mid(d,10,2),Mid(d,12,2), Mid(d,14,2))
ELSE
daT = DateSerial(Left(d,4), Mid(d,5,2), Mid(d,7,2))
END IF
IcalTimestamp = daT
END FUNCTION

Der Timestamp in ICalendar ist nach dem Prinzip Jahreszahl, Monatszahl, Tageszahl, Trenner 'T',
Stunden, Minuten und Sekunden angelegt. Es kann auch vorkommen, dass keine Zeit mit angegeben ist, dann entfällt der Inhalt ab 'T'. Da der Zeitstempel angepasst werden muss, wenn die
Zeitzone berücksichtigt wird, muss der Timestamp in einen internen Zeitstempel umgewandelt
werden.
In der Bedingung Zeile 3 wird nach diesem 'T' gefragt. Ist es vorhanden, so wird ein Zeitstempel aus Datum und Zeit angefertigt, mit dem weiter gerechnet werden kann. Während das Jahr
noch über Left ausgelesen wird kommt bei den anderen Teilen des Strings die Funktion Mid
zum Einsatz. Mit ihr lassen sich genau bemessene Teilstrings auslesen, mit Mid(d,7,2) sinddies die Zeichen ab dem 7. von der Variablen d und von dort aus 2 Zeichen. Aus dem Timestamp '20200423T154809' wird so '23' für den Tag ausgelesen.
Enthält der Timestamp aus Icalendar kein 'T', so wird nur ein Datum ausgelesen (Zeile 6).
Der in Basic verwertbare Zeitstempel wird dann an die Funktion als Rückgabewert übergeben(Zeile 8).

Import_Start
Aufruf aus
Formular: Terminart_Termin, Termine_Kalenderübersicht, Termine_Monat_Terminart,
Termine_Tabelleneingabe

Termine314

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021

Ical_ImportSUB Import_Start(oEvent AS OBJECT)
DIM oFeld AS OBJECTDIM oFeld1 AS OBJECT
DIM stFileUrl AS STRING
DIM inImport AS INTEGER
oFeld = oEvent.Source.ModeloFeld1 = oFeld.parent.getByName("Dateiauswahl")
IF oFeld1.Text <> "" THEN
stFileUrl = ConvertToUrl(oFeld1.Text)
inImport = Ical_Import(stFileUrl)
END IFIF inImport > 0 THEN
oForm = oFeld.Parent.Parent.getByName("MainForm")oForm.filter ="""ID"" BETWEEN (SELECT ""StartIDImport"" FROM ""Filter"" WHERE""ID"" = True) AND (SELECT ""EndIDImport"" FROM ""Filter"" WHERE ""ID"" =True) "
oForm.ApplyFilter = TRUE
oForm.reload()
msgbox "Die importierten Datensätze werden im Formular angezeigt."
ELSE
msgbox "Es wurden keine Datensätze importiert."
END IF
END SUB

Es wird nicht direkt nach der Auswahl der Datei importiert, da dies bei eine eventuell falschen
Auswahl dann sofort zu importieren Daten führt. Stattdessen wird über einen gesonderten Button der Import angestoßen.Das Feld für die Dateiauswahl wird über die Lage des auslösenden
Buttons im Formular bestimmt (Zeile 6 und 7). Danach wird aus der Dateiauswahl der Pfad ausgelesen und in eine verwertbare URL umgewandelt. Diese URL wird an die Funktion Ical_Importweitergegeben Zeile 10). Diese Funktion gibt mit inImport zurück, wie viele Datensätze importiert wurden.
Wenn überhaupt ein Datensatz importiert wurde, so soll das Ergebnis des Imports vom Formular aus angezeigt werden. Dies wird dadurch erreicht, dass ein Formularfilter gesetzt wird (Zeile
14 und 15). Der Formularfilter liest dabei die in der Tabelle "Filter" abgespeicherten Werte für
die erste und die letzte ID der anzuzeigenden Werte aus. Diese Werte wurden in der Funktion
Ical_Import dort abgespeichert.
Sind Datensätze importiert worden, so kommt die Meldung, dass die importierten Datensätze
jetzt angezeigt werden (Zeile 17). Worden keine importiert, so wird auch dies vermeldet (Zeile
19).

LetzterSonntag
Aufruf aus
Makro:

Ical_Import

Benötigt
Makro:
001
002
003
004
005
006
007
008

keins

FUNCTION LetzterSonntagVormonat(d AS DATE)
d = DateAdd("d", -1, d)
DO WHILE DatePart("w",d) <> 1
d = DateAdd("d", -1, d)LOOP
d = DateValue(d) + TimeValue("02:00:00")
LetzterSonntag = d
END FUNCTION

Termine315

Für den Wechsel von Winterzeit zu Sommerzeit und umgekehrt ist es notwendig, den letzten
Sonntag im März und im Oktober zu ermitteln. Diese Funktion übernimmt einfach das Datum,
was ihr mitgegeben wurde, zählt davon einen Tag herunter (Zeile 2), damit nicht bei der Weitergabe eines Sonntags als Datumswert das gleiche Datum zurückgegeben wird, und zählt vondort aus so lange je einen Tag herunter (Zeile 4), bis es wieder bei dem Wochentag '1' angelangt ist (Zeile 3). Das Datum wird dann mit dem Zeitwert von 2 Uhr ergänzt, da dies die
Grenze für den Wechsel von Sommerzeit zu Winterzeit und umgekehrt darstellt. Das Datum
wird an die aufrufende Prozedur zurückgegeben.

FilterAufruf aus
Formular: Termine_Tabelleneingabe

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018

keins

SUB Filter_Abgleich(oEvent AS OBJECT)
DIM oFeld AS OBJECTDIM oForm1 AS OBJECTDIM oForm2 AS OBJECT
DIM stTag AS String
DIM atTag()oFeld = oEvent.Source.Model
stTag = oFeld.Tag
oForm1 = oFeld.parent
oFeld.commit()oForm1.updateRow()oForm2 = oForm1.parent.getByName(aTag(0))
IF UBound(aTag) > 0 THEN
oForm2.filter = aTag(1)oForm2.ApplyFilter = TRUEEND IFoForm2.reload()
END SUBIn den Zusatzinformationen (8) des Listenfeldes für die Monatsvorgabe ist enthalten, wie das
zweite Formular heißen soll, das nach Betätigung des Listenfeldes aktualisiert werden soll.
Außerdem steht doch, durch einen '→' getrennt, gegebenenfalls eine Filterinformation für das
zu aktualisierende Formular. Alle anderen Elemente, die angesprochen werden sollen, werden
durch das auslösende Ereignis ermittel.
Das Listenfeld gibt in Zeile 10 den Wert an das Formular weiter, in dem es sich befindet.
Anschließend wird das Formular abgespeichert und schließlich das Formular, das dadurch gefiltert werden soll, neu eingelesen. Dabei wird in Zeile 14 und 15 gegebenenfalls der Filter wieder
zugewiesen, falls er durch andere Aktionen, z.B. den Import von Daten, entfernt wurde.

Wartung
Tabellenindex_runter
Aufruf aus
Makro:

Tabellenstart

Benötigt
Makro:

Termine

keins

316

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019

Sub Tabellenindex_runter(stTabelle AS STRING)
DIM stAnzahl AS INTEGER
DIM inAnzahl AS INTEGER
DIM inSequence_Value AS INTEGERoSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT MAX(""ID"") FROM """+stTabelle+""""
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
WHILE oAbfrageergebnis.next
stAnzahl = oAbfrageergebnis.getString(1)WEND
IF stAnzahl = "" THEN
inAnzahl = -1
ELSE
inAnzahl = cInt(stAnzahl)
END IF
inSequence_Value = inAnzahl+1
oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung1.executeQuery("ALTER TABLE """+tablename+""" ALTER COLUMN ""ID""RESTART WITH "+inSequence_Value+"")End Sub

Mit dieser Prozedur wird das automatisch hoch geschriebene Primärschlüsselfeld mit der vorgegebenen Bezeichnung "ID" auf den niedrigst möglichen Wert eingestellt.
In Zeile 6 und Zeile 7 wird der höchste eingegebenen Wert für das Feld "ID" ermittelt. Der Wert
wird als Text in Zeile 9 ausgelesen. Die Textform ist hier notwendig, weil sonst selbst bei einer
leeren Tabelle nicht ein leerer Wert ausgegeben wird sondern die Integer-Zahl 0.
Ist das Ergebnis ein leerer Text (11), dann wird die Integer-Wert 1 gesetzt. Ansonsten wird derInteger-Wert angenommen, der als höchster Wert in der Tabelle steht (14). Zu diesem Wert wird
jetzt 1 addiert (16) und dieser Wert als nächster einzufügende Wert für die Sequenz in die
Tabellenbeschreibung eingefügt (18).

Tabellenstart
Aufruf aus
Makro:

keins, wird direkt aufgerufen, wenn Daten zurückgesetzt wurden.

Benötigt
Makro:

Tabellenindex_runter

Tabelle:Termine, Terminart, Zielgruppe, Ferien

001
002
003
004
005
006

Sub Tabellenstart
Tabellenindex_runter("Termine")
Tabellenindex_runter("Terminart")
Tabellenindex_runter("Zielgruppe")
Tabellenindex_runter("Ferien")
End Sub

Die Prozedur gibt lediglich alle benannten Tabellen nacheinander an die Prozedur "Tabellenindex_runter" weiter. Dort wird dann die jeweilige Tabelle so eingestellt, dass der Auto-Wert mit
der nächstfolgenden Zahl weiter zählt.

Dialog Datumseingabe
Aufruf aus
Makro:

TermineDialogStart

317

Benötigt
Makro:

DialogEnde

Dieser Dialog ist notwendig, damit noch nach dem Aufruf eines Berichtes über das Formular die
Eingabe eines Halbjahresdatums erledigt werden kann. Natürlich könnte das auch über ein Feld
im Formular erfolgen. Nur ist die Eingabe für lediglich zwei der verschiedenen Berichte notwendig.
Alternativ dazu wäre auch über den Befehl InputBox eine Eingabe möglich gewesen. Da hätte
dann aber anschließend wieder abgesichert werden müssen, dass die Eingabe wirklich einem
Datum entspricht. Hier ist das Datumsfeld in einem Dialog, noch dazu mit übersichtlich aufklappbarer Monatsübersicht, klar im Vorteil.

Termine318

Vereinsverwaltung

Vereinsverwaltung

319

Einführung
Vereine benötigen eine Mitgliedsverwaltung, eine Übersicht über die aktuellen Vorstände unddarüber hinaus oft noch verschiedene Module wie z.B. eine Kontoverwaltung, eine Beitragsverwaltung usw.
Die Datenbank «LO Verein»10 stellt eine überarbeitete Fassung einer älteren Vereinssoftware
dar, die für einen Kanusportverein entwickelt wurde. Neben der Mitgliedsverwaltung sind dort
auch Beitragsverwaltungen, Abteilungsverwaltungen und die Verwaltung der Liegeplätze von
Booten zu finden.

Der besondere Zusatz der aktuellen Version: Die verschiedenen Anwendungsgebiete sind einzeln in einer Einstellungstabelle zuschaltbar. Nicht jeder Verein braucht eine Liegeplatzverwaltung für Boote. Die Beitragsverwaltung wird manchmal auch lieber über eine Buchungssoftware
der Banken erledigt. Da stören dann diese Module nur, wenn sie als Eintragungspunkte in den
Formularen auftauchen.

Tabellen
In der Datenbank sind grundsätzlich die Tabellen enthalten, die für alle Module erforderlichsind. So befinden sich dort eben unabhängig davon, ob sie gebraucht werden, z. B. Tabellen zur
Verwaltung von Bootsliegeplätzen («Bootsständerverwaltung»). Diese Tabellen sollten, auchwenn sie nicht benötigt werden, nicht gelöscht werden. Sie könnten stattdessen über Extras →Tabellenfilter unsichtbar geschaltet werden.

10 Beispieldatenbank Beispiel_LO_Verein202211.odb

Vereinsverwaltung

320

Zentrale Mitgliedstabelle

In der "tbl_Mitglied" werden die Daten verwaltet, die sich auf die einzelne Person beziehen. Gibt
es mehrere Personen, die z. B. eine gemeinsame Adresse haben, so wird in dem Feld
"Gruppe_mitID" die Primärschlüsselnummer der Person gespeichert, für die zuerst diese
Adresse eingegeben wurde. Deswegen in dem obigen Bild die Verbindung des Primärschlüssels"ID" mit dem Fremdschlüssel "Gruppe_mitID" innerhalb der Tabelle.

Vereinsverwaltung321

Abbildung 1: Die Beziehung ist so geregelt, dass wedereine Aktualisierung noch eine Löschung eines Mitglieds
möglich ist, das mit anderen Mitgliedern über"Gruppe_mitID" verbunden ist.

Vereinsverwaltung

322

Datenziel
Formular: Einzelmessung_System, frm_Berichte, frm_Boot, frm_Mitglied, frm_Schluessel,
frm_Vorstand
Makro:

SelectGroup, NewBoatAllowed, Group

Abfrage:

Abfrage_Start, qry_Beitraege, qry_Bootsstaender, qry_Filter_Mitglied, qry_Jubilaeum,
qry_Mitglied_Konto, qry_Mitgliedsdauer, qry_Schluessel_Uebersicht

Ansicht:

viw_Adresse_Num_Namen, viw_Beitrag, viw_Filter_Mitglieder_komplett_AusEin,
viw_Gruppe, viw_Mitglieder_komplett, viw_Statistik_Alter_Detail

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Nachname

Text

Nachname des Mitglieds
Eingabe erforderlich: Ja

Vorname

Text

Vorname des Mitglieds

Geschlecht

Text

Kürzel ('d', 'm', 'w'), maximal ein Zeichen

GebDatDatum

Geburtsdatum

AufDat

Datum

Aufnahmedatum

Schwimmer

Ja/NeinSchwimmausweis liegt vor – wichtig für einen wassersporttreibenden Verein

AusDatDatum

Austrittsdatum – Mitglieder werden beim Austritt nicht(direkt) gelöscht.

Nichtdruck

Ja/Nein

Bei Rundschreiben kein gedrucktes Schreiben an diese Person. Stimmt die Adresse nicht, so ist die Post unzustellbar.
Das Porto kann hier gespart werden.

Telefon_mobil

Text

Mobiltelefonnummer wird direkt ans Mitglied gekoppelt

E-MailText

E-Mail-Adresse z. B. Für Rundschreiben per Mail.

Gruppe_mitID

Integer

Zuweisung zu einem anderen Mitglied, bei dem die Adresse
und die Kontoverbindung gespeichert wurde. Fremdschlüssel
auf "tbl_Mitglied"."ID"

Photo

Text

Bilder werden in einem gesonderten Verzeichnis gespeichert.
In der Datenbank selbst liegt nur der Pfad dazu. Im Formularkann das Bild so trotzdem angezeigt werden.

Zur Absicherung des Austrittsdatums wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Mitglied"
002 ADD CONSTRAINT "Austrittsdatum >= Aufnahmedatum"
003 CHECK ( COALESCE ("AusDat", "AufDat")>="AufDat" )Hierdurch wird das Feld "AusDat" so festgelegt, dass es auf jeden Fall nicht vor dem Feld "AufDat" liegt. Ein Austritt vor dem Beitritt ist nicht möglich. Bei Fehlversuchen wird ausgegeben:«Check constraint violation Austrittsdatum => Aufnahmedatum table: tbl_Mitglied…»
Zur Absicherung des Geburtsdatums wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Mitglied"
002 ADD CONSTRAINT "Aktuelles Datum >= Geburtsdatum"
003 CHECK ("GebDat"<=CURRENT_DATE)
Hierdurch wird das Feld "GebDat" so festgelegt, dass es auf jeden Fall nicht in der Zukunft liegt.
Ein Beitritt vor der Geburt ist nicht möglich. Bei Fehlversuchen wird ausgegeben:«Check constraint violation Aktuelles Datum >= Geburtsdatum table: tbl_Mitglied…»

Vereinsverwaltung

323

Adresse

Die Adresse wird nicht direkt beim Mitglied eingetragen. Sie kann über das Feld "Gruppe_mitID"
auch anderen Mitgliedern zugeordnet werden, obwohl so die Beziehung zwischen "tbl_Mitglied"und "tbl_Adresse" eine 1:1-Beziehung ist.
Postleitzahl und Ort sind aus der Tabelle "tbl_Adresse" ausgelagert, da sie gerade bei Vereinen
in der Regel sehr häufig mehrfach vorkommen.

tbl_Adresse

Datenziel
Formular: frm_Mitglied
Makro:

SelectGroup, Group

Abfrage:

Abfrage_Start

Ansicht:

viw_Adresse_Num_Namen, viw_Anschrift, viw_Filter_Mitglieder_komplett_AusEin,
viw_Mitglieder_komplett

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

mitID

IntegerPrimärschlüssel und gleichzeitig Fremdschlüssel nach"tbl_Mitglied"."ID"

Vereinsverwaltung

324

FeldnameFeldtyp

Beschreibung

Straße_Nr

Text

Straße und Hausnummer zusammen in einem Feld

ortID

Integer

Fremdschlüssel nach "tbl_Ort"."ID"

Nichtdruck

Ja/Nein

Diese Adresse soll vom Druck ausgenommen werden, da dort
niemand aus der Gruppe mehr wohnt.

Telefon_fest

Text

Festnetztelefonnummer, wird mit der Adresse gespeichert,
da in der Regel max. eine Festnetznummer angegeben wird.

Abbildung 2: Die Beziehung ist so geregelt, dass bei einerÄnderung der "ID" aus "tbl_Mitglied" die "mitID" aus"tbl_Adresse" angepasst wird: Kaskadierend aktualisieren. Wird das mit der Adresse verbundene Mitglied
aus der Tabelle gelöscht, so wird auch der Datensatz inder Adresse gelöscht: Kaskadierend löschen.
In der Tabelle "tbl_Adresse" ist (bis auf den Primärschlüssel) keins der Felder als erforderlichgesetzt worden. Es ist also möglich, dort auch nur die Telefonnummer oder nur die Straße zu
speichern.

tbl_Ort

Vereinsverwaltung

325

Datenziel
Formular: frm_Mitglied
Makro:

SelectGroup

Ansicht:

viw_Anschrift, viw_Filter_Mitglieder_komplett_AusEin, viw_Mitglieder_komplett

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Postleitzahl

Text

Postleitzahl

Ort

Text

Ort
Eingabe erforderlich: Ja

Land

Text

Landeskürzel, 2 Zeichen, beide groß geschrieben

Abbildung 3: Die Beziehung ist so geregelt, dass bei einerÄnderung der "ID" aus "tbl_Ort" die "ortID" aus"tbl_Adresse" angepasst wird: Kaskadierend aktualisieren. Wird der mit der Adresse verbundene Ort aus derTabelle gelöscht, so wird das Feld ortID als leeres Feld
gesetzt: NULL setzen. Es kann also auch (vorübergehend) Adressen ohne Ortszuweisung geben.

Vereinsverwaltung

326

KontoDas Konto wird nicht direkt beim Mitglied eingetragen. Es kann über das Feld "Gruppe_mitID"
auch anderen Mitgliedern zugeordnet werden, obwohl so die Beziehung zwischen "tbl_Mitglied"und "tbl_Konto" eine 1:1-Beziehung ist.
Bank und BIC sind aus der Tabelle "tbl_Konto" ausgelagert, da sie gerade bei Vereinen in derRegel sehr häufig mehrfach vorkommen.
Die Kontoverwaltung kann durch die separaten Tabellen mit Hilfe der Einstellungen des Programms leichter abgewählt werden.

tbl_Konto
Datenziel
Formular: frm_Mitglied
Makro:

SelectGroup

Abfrage:qry_Beitraege, qry_Mitglied_Konto

Ansicht:

viw_Filter_Mitglieder_komplett_AusEin, viw_Mitglieder_komplett

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

mitID

IntegerPrimärschlüssel und gleichzeitig Fremdschlüssel nach"tbl_Mitglied"."ID"Nachname

Text

Nachname des Kontoinhabers / der Kontoinhaberin, falls
nicht identisch mit dem aus "tbl_Mitglied"."ID"

Vorname

Text

Vorname des Kontoinhabers / der Kontoinhaberin, falls nicht
identisch mit dem aus "tbl_Mitglied"."ID"

IBAN

Text

IBAN wird mit einem Leerzeichen nach 4 Zeichen eingegeben. Das erledigt ein maskiertes Feld, Länge des Feldes fürmaximale IBAN von 34 Zeichen deshalb 42 Zeichen.
Eingabe erforderlich: Ja

Vereinsverwaltung

327

FeldnameFeldtyp

Beschreibung

banID

Integer

Fremdschlüssel nach "tbl_Bank"."ID"
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_Konto" und "tbl_Mitglied" ist wie in Abbildung 2 so geregelt,
dass bei einer Änderung des Primärschlüssels von "tbl_Mitglied" der Fremdschlüssel angepasst
wird. Bei einer Löschung in der "tbl_Mitglied" wird auch der Datensatz in "tbl_Konto" gelöscht.

tbl_Bank

Datenziel
Formular: frm_Mitglied
Makro:

SelectGroup

Abfrage:qry_Beitraege, qry_Mitglied_Konto

Ansicht:

viw_Filter_Mitglieder_komplett_AusEin, viw_Mitglieder_komplett

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Bank

Text

Name der Bank
Eingabe erforderlich: Ja

BIC

Text

BIC der Bank
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_Konto" und "tbl_Bank" ist wie in Abbildung 3 so geregelt, dassbei einer Änderung des Primärschlüssels von "tbl_Bank" der Fremdschlüssel angepasst wird. Bei
einer Löschung in der "tbl_Bank" wird der Fremdschlüssel "banID" in "tbl_Konto" auf NULL
gesetzt.

Vereinsverwaltung

328

Vorstand

Mitglieder können unterschiedliche Vorstandsposten besetzen. Gleiche Vorstandsposten werden nacheinander von unterschiedlichen Mitgliedern wahr genommen. Die 1:n-Beziehung zwischen "tbl_Mitglied" und "tbl_Mitglied_VorstandPosten" sowie die 1:n-Beziehung zwischen"tbl_VorstandPosten" und "tbl_Mitglied_VorstandPosten" ergeben so zusammen eine n:m-Beziehung zwischen "tbl_Mitglied" und "tbl_VorstandPosten".

tbl_Mitglied_VorstandPosten

Datenziel
Formular: frm_Berichte
Makro:

SelectBoard

Abfrage:qry_Filter_Vorstand, qry_Jubilaeum, qry_Vorstand_aktuell, qry_Vorstand_aktuell_lt_Satzung

Ansicht, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

mitID

Integer

Fremdschlüssel nach "tbl_Mitglied"."ID"
Eingabe erforderlich: Ja

vorID

Integer

Fremdschlüssel nach "tbl_VorstandPosten"."ID"
Eingabe erforderlich: Ja

Vereinsverwaltung

329

FeldnameFeldtyp

Beschreibung

WahlDat

Datum

Wahldatum
Eingabe erforderlich: Ja

EndDatDaum

Beendigung der Vorstandstätigkeit.

Die Verknüpfung zwischen "tbl_Mitglied_VorstandPosten" und "tbl_Mitglied" ist wie in Abbildung
3 so geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Mitglied" der Fremdschlüssel angepasst wird. Bei einer Löschung in der "tbl_Mitglied" wird der Fremdschlüssel "mitID"in "tbl_Mitglied_VorstandPosten" auf NULL gesetzt. So bleibt zumindest die Belegung des Postens erhalten, auch wenn das entsprechende Mitglied nicht mehr in der Datenbank verzeichnet
ist. Über Berichte sollte deswegen der Vorstand auch außerhalb der Datenbank gesichert werden.
Zur Absicherung des Enddatums/Rücktrittsdatums wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Mitglied_VorstandPosten"
002 ADD CONSTRAINT "Rücktrittsdatum >= Wahldatum"
003 CHECK ( COALESCE("EndDat","WahlDat")>="WahlDat" )Hierdurch wird das Feld "EndDat" so festgelegt, dass es auf jeden Fall nicht vor dem "WahlDat"
liegt. Ein Rücktritt vor der Wahl ist nicht möglich. Bei Fehlversuchen wird ausgegeben:«Check constraint violation Rücktrittsdatum >= Wahldatum table:
tbl_Mitglied_VorstandPosten…»

tbl_VorstandPosten

Datenziel
Formular: frm_Berichte, frm_Vorstand
Makro:

SelectBoard

Abfrage:qry_Jubilaeum, qry_Vorstand_aktuell, qry_Vorstand_aktuell_lt_Satzung

Ansicht, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

PostenText

Beschreibung des Postens
Eingabe erforderlich: Ja

Zyklus

Text

Wahlen in geraden ('g') oder ungeraden ('u') Jahren? Wenn
nicht durch Satzung vorgegeben: '-'Vereinsverwaltung

330FeldnameFeldtyp

Beschreibung

Posten_laut_S
atzungJa/NeinIst der Posten in der Satzung festgelegt? Dann hier: Ja

Posten_w

Text

Für Unterschriftsfelder: Beschreibung des Postens in weiblicher Form

Posten_m

Text

Für Unterschriftsfelder: Beschreibung des Postens in männlicher Form

Die Verknüpfung zwischen "tbl_Mitglied_VorstandPosten" und "tbl_VorstandPosten" ist wie in
Abbildung 3 so geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_VorstandPosten" der Fremdschlüssel angepasst wird. Bei einer Löschung in der "tbl_VorstandPosten" wird
der Fremdschlüssel "vorID" in "tbl_Mitglied_VorstandPosten" auf NULL gesetzt.

Schlüssel

Mitglieder können unterschiedliche Schlüssel entleihen. Schlüssel für gleiche Schließbereichekönnen von mehreren Mitgliedern entliehen werden. Die 1:n-Beziehung zwischen "tbl_Mitglied"und "tbl_Mitglied_Schluessel" sowie die 1:n-Beziehung zwischen "tbl_Schluessel" und "tbl_Mitglied_MitgliedSchluessel" ergeben so zusammen eine n:m-Beziehung zwischen "tbl_Mitglied"und "tbl_Schluessel".
Manchmal sind zu wenig Schlüssel verfügbar. So muss beständig der Bestand kontrollierbar
sein und ggf. neue Schlüssel nachgekauft werden. Deshalb die Bestandserfassung in "tbl_SchluesselAnzahl".
Die Schlüsselverwaltung kann mit Hilfe der Einstellungen des Programms abgewählt werden.

Vereinsverwaltung

331tbl_MitgliedSchluessel

Datenziel
Formular: frm_Schluessel
Makro:

SelectKey

Abfrage:qry_Schluessel_Uebersicht

Ansicht:

viw_Schluessel_Mitglied

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

SNr

Text

Fremdschlüssel zur Tabelle "tbl_Schluessel", wie dort ein Textfeld mit maximal 5 Zeichen
Eingabe erforderlich: Ja

mitID

Integer

Fremdschlüssel zur Tabelle "tbl_Mitglied"
Eingabe erforderlich: Ja

LeihDat

Datum

Ausleihdatum
Eingabe erforderlich: Ja

Pfand

DezimalPfand für die Ausleihe
Eingabe erforderlich: Ja

RueckDat

Datum

Rückgabedatum

RueckPfand

DezimalRückgabe des Pfandes, eventuell bei verbogenem Schlüssel
mit Abzug

Bemerkung

Text

Grund für die Zuweisung zu einem besonderen Schließkreiso. ä.

VerlustJa/NeinIst der Schlüssel verloren, so wird hier mit 'Ja' der Schlüssel
komplett aus dem System genommen. Der Pfand wird nicht
zurück gezahlt.

Die Verknüpfung zwischen "tbl_MitgliedSchluessel" und "tbl_Mitglied" ist wie in Abbildung 3 so
geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Mitglied" der Fremdschlüssel
angepasst wird. Bei einer Löschung in der "tbl_Mitglied" wird der Fremdschlüssel "mitID" in"tbl_MitgliedSchluessel" auf NULL gesetzt.
Zur Absicherung des Rückgabedatums wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Mitglied_Schluessel"
002 ADD CONSTRAINT "Rückgabedatum >= Ausleihdatum"
003 CHECK ( COALESCE("RueckDat","LeihDat")>="LeihDat" )Hierdurch wird das Feld "RueckDat" so festgelegt, dass es auf jeden Fall nicht vor dem"LeihDat" liegt. Eine Rückgabe vor der Ausleihe ist nicht möglich. Bei Fehlversuchen wird ausgegeben:«Check constraint violation Rückgabedatum >= Ausleihdatum table: tbl_Mitglied_Schluessel…»
Vereinsverwaltung

332

Zur Absicherung des Rückgabepfandes wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Mitglied_Schluessel"
002 ADD CONSTRAINT "Rückgabepfand <= Pfand"
003 CHECK ( COALESCE("RueckPfand","Pfand")<="Pfand" )Hierdurch wird das Feld "RueckPfand" so festgelegt, dass es auf jeden Fall nicht größer als"Pfand" ist. Es soll nicht mehr Pfand zurückgezahlt werden als tatsächlich gezahlt wurde. Bei
Fehlversuchen wird ausgegeben:«Check constraint violation Rückgabepfand <= Pfand table: tbl_Mitglied_Schluessel…»

tbl_Schluessel

Datenziel
Formular: frm_Schluessel
Makro:

SelectKey

Ansicht, Bericht:

keine direkt

Feldname

Feldtyp

Beschreibung

SNr

Text

Primärschlüssel, Text mit 5 Zeichen

BereichText

Schließbereich des Schlüssels
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_MitgliedSchluessel" und "tbl_Schluessel" ist wie in Abbildung 3
so geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Schluessel" der Fremdschlüssel angepasst wird. Bei einer Löschung in der "tbl_Schluessel" wird der Fremdschlüssel "SNr"in "tbl_MitgliedSchluessel" auf NULL gesetzt.

tbl_SchluesselAnzahl

Datenziel
Makro:

SelectKey

Ansicht, Bericht:

Vereinsverwaltung

keine direkt

333FeldnameFeldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

SNr

Text

Fremdschlüssel zur "tbl_Schluessel", wie dort Text mit 5 Zeichen
Eingabe erforderlich: Ja

KaufDat

Datum

Kaufdatum für den Schlüssel
Eingabe erforderlich: Ja

AnzahlSmall Integer

Anzahl der gekauften Schlüssel für den entsprechenden
Schließbereich. Small Integer wurde hier als Feldtyp gewählt,da direkt bei der Einrichtung eventuell mehr Schlüssel benötigt werden als mit Tiny Integer (bis +127 bei der HSQLDB)
möglich wären.
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_SchluesselAnzahl" und "tbl_Schluessel" ist wie in Abbildung 3 so
geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Schluessel" der Fremdschlüssel angepasst wird. Bei einer Löschung in der "tbl_Schluessel" wird der Fremdschlüssel "SNr" in"tbl_SchluesselAnzahl" auf NULL gesetzt.

Abteilung

Ein Mitglied kann in mehreren Abteilungen sein. In jeder Abteilung sind mehrere Mitglieder. Die
Beziehung zwischen "tbl_Mitglied" und "tbl_Abteilung" ist also eine n:m-Beziehung. Diese
Beziehung wird bei der Tabellenkonstruktion durch eine Tabelle gelöst, in der die Primärschlüssel beider betroffener Tabellen enthalten sind. Hier ist das die "tbl_Mitglied_Abteilung", dielediglich die beiden Fremdschlüssel als gemeinsame Primärschlüssel enthält.
Da Abteilungen häufig mit separaten Beiträgen verbunden sind, ist in "tbl_Abteilung" eine Verknüpfung zu der Beitragstabelle enthalten.
Die Abteilungsverwaltung kann mit Hilfe der Einstellungen des Programms abgewählt werden.

Vereinsverwaltung

334

tbl_Mitglied_Abteilung

Datenziel
Formular: frm_Mitglied
Ansicht:

Ansicht_Datum, viw_Beitrag

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

mitID

IntegerPrimärschlüssel zusammen mit "abtID", Fremdschlüssel zu"tbl_Mitglied"abtID

IntegerPrimärschlüssel zusammen mit "mitID", Fremdschlüssel zu"tbl_Abteilung"

Die Verknüpfung zwischen "tbl_Mitglied_Abteilung" und "tbl_Mitglied" ist wie in Abbildung 2 sogeregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Mitglied" der Fremdschlüssel
angepasst wird. Bei einer Löschung in der "tbl_Mitglied" wird auch der Datensatz in "tbl_Mitglied_Abteilung" gelöscht.

tbl_Abteilung

Datenziel
Formular: frm_Einstellungen, frm_Mitglied
Ansicht:

Ansicht_Datum, viw_Beitrag

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Abteilung

Text

Beschreibung der Abteilung
Eingabe erforderlich: Ja

Vereinsverwaltung

335

FeldnameFeldtyp

Beschreibung

beiID

Integer

Fremdschlüssel zur Tabelle "tbl_Beitrag"
Hier ist keine Eingabe erforderlich, weil es Abteilungen ohne
Beitragsbezug geben kann und außerdem auch die Möglichkeit bestehen soll, die gesamte Datenbank ohne das Beitragsmodul zu betreiben.

Die Verknüpfung zwischen "tbl_Mitglied_Abteilung" und "tbl_Abteilung" ist wie in Abbildung 2 sogeregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Abteilung" der Fremdschlüssel
angepasst wird. Bei einer Löschung in der "tbl_Abteilung" wird auch der Datensatz in "tbl_Mitglied_Abteilung" gelöscht.

Beitrag

Der Beitrag wird bei dem Mitglied eingetragen, das über die Gruppierung von Mitgliedern(anhand der Adresse oder des Kontos) als «Gruppenleiter(in)» vorgesehen ist. Ein Mitglied kannfür verschiedene Beiträge zahlen müssen. Ein bestimmter Beitrag ist aber wiederum bei verschiedenen Mitgliedern vorgesehen. Die n:m-Beziehung zwischen "tbl_Mitglied" und "tbl_Beitrag" wird über zwei 1:n-Beziehungen zur "tbl_Gruppe_Beitrag" geregelt.
Bei manchen Beitragsarten kann der Beitrag gestaffelt sein. So zahlen in dem Verein, der als
Ausgangspunkt für diese Datenbank diente, ein zweites oder drittes Mitglied einer Gruppe
einen immer niedrigeren Abteilungsbeitrag für die «Rennabteilung». Das entspricht dann einer
Gruppenermäßigung. Bei den Booten hingegen steigt der Preis pro Boot mit der Anzahl dergelagerten Boote. Dies ist entsprechend in "tbl_Beitrag_Staffel" hinterlegt.
Die Beitragsverwaltung kann mit Hilfe der Einstellungen des Programms abgewählt werden.

Vereinsverwaltung

336

tbl_Gruppe_Beitrag

Datenziel
Formular: Einzelmessung_System
Makro:

SubscriptionSave

Ansicht:

viw_Beitrag

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

mitID

IntegerPrimärschlüssel zusammen mit "beiID", Fremdschlüssel zu"tbl_Mitglied"beiID

IntegerPrimärschlüssel zusammen mit "mitID", Fremdschlüssel zu"tbl_Beitrag"

AnzahlTiny Integer

Wie viele Mitglieder in der Gruppe müssen den entsprechenden Beitrag zahlen?

Die Verknüpfung zwischen "tbl_Gruppe_Beitrag" und "tbl_Mitglied" ist wie in Abbildung 2 sogeregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Mitglied" der Fremdschlüssel
angepasst wird. Bei einer Löschung in der "tbl_Mitglied" wird auch der Datensatz in"tbl_Gruppe_Beitrag" gelöscht.

tbl_Beitrag

Datenziel
Formular: Einzelmessung_System, frm_EinstellungenAnsicht:

viw_Beitrag, viw_Beitrag_Gruppe_kalkuliert

Bericht:keine direkt

Vereinsverwaltung

337

FeldnameFeldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

BeitrArt

Text

Beschreibung des Beitrags
Eingabe erforderlich: Ja

BeitrHoehe

Dezimal

Nicht alle Felder sind mit einem entsprechenden Betrag versehen. Hier gibt es Beiträge, die nach einer entsprechenden
Staffel nicht einfach aufsummiert werden. Siehe dazu die folgende Tabelle "tbl_Beitrag_Staffel"

BeitrMonate

Tiny Integer

Anzahl der Monate, für die der angegebenen Beitrag gilt
Eingabe erforderlich: Ja

BeitrBezug

Text

Maximal 10 Zeichen, hier:'Mit' → einfacher Mitgliedsbeitrag'MitG' → Gruppenbeitrag, z. B. Für Paare, Familien'Abt' → Abteilungsbeitrag'Boot' → Bootsständermiete
Eingabe erforderlich: Ja

MinAlterTiny Integer

Mindestalter, sofern nicht vorgegeben '0'
Eingabe erforderlich: Ja

MaxAlterTiny Integer

Maximalalter, sofern nicht vorgegeben '127' (Tiny Integer)
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_Gruppe_Beitrag" und "tbl_Beitrag" ist wie in Abbildung 2 sogeregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Beitrag" der Fremdschlüssel
angepasst wird. Bei einer Löschung in der "tbl_Beitrag" wird auch der Datensatz in"tbl_Gruppe_Beitrag" gelöscht.
Zur Absicherung der Monatszahl wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Beitrag"
002 ADD CONSTRAINT "Monatszahl 1 bis 12"003 CHECK ("BeitrMonate" BETWEEN 1 AND 12)
Hierdurch wird das Feld "BeitrMonate" auf Eingaben von 1 bis 12 begrenzt. Bei Fehlversuchen
wird ausgegeben:«Check constraint violation Monatszahl 1 bis 12 table: tbl_Beitrag…»

tbl_Beitrag_Staffel

Vereinsverwaltung

338

Datenziel
Formular: frm_EinstellungenAnsicht:

viw_Beitrag

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, kein Auto-Wert, da übersichtliche Anzahl an
EinrägenbeiID

Integer

Fremdschlüssel zu Tabelle "tbl_Beitrag"
Eingabe erforderlich: Ja

Anzahl

TinyInteger

Beitrag für 1, 2, 3 … Personen/Booten
Eingabe erforderlich: Ja

BeitrHoehe

DezimalHöhe des Beitrags bei 1, 2, 3 … Personen/Booten
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_Beitrag_Staffel" und "tbl_Beitrag" ist wie in Abbildung 2 so geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_Beitrag" der Fremdschlüssel angepasst wird. Bei einer Löschung in der "tbl_Beitrag" wird auch der Datensatz in"tbl_Beitrag_Staffel" gelöscht.

Boot

Jedes Mitglied kann verschiedene Boote in Bootsständern des Vereins lagern. Die Boote sind in"tbl_Boot" verzeichnet.
Boote werden nach verfügbaren Bootsarten ("tbl_BootArt") eingeteilt und haben auch einen
entsprechenden Beitragsbezug, weil sie unterschiedlich viel Platz einnehmen. Hier sind dies
Canadier (mit einem Stechpaddel zu fahren) und Kajaks. Canadier sind als Wanderboot deutlich
breiter und deshalb nur an bestimmten Stellen lagerbar und mit einem besonderen Beitrag versehen.
Die Bootsständer aus "tbl_BootStaender" befinden sich in unterschiedlichen Hallen. Sie können
in verschiedenen Hallen gleiche Bezeichnungen haben, sind nur zusammen mit der Hallenbezeichnung aus "tbl_BootHalle" schließlich eindeutig zu erkennen.

Vereinsverwaltung

339

Die Bootsständerverwaltung kann mit Hilfe der Einstellungen des Programms abgewählt werden.

tbl_Boot

Datenziel
Formular: frm_Boot
Makro:

SelectBoatRack, NewBoatAllowed

Abfrage:qry_Bootsstaender

Ansicht:

viw_Beitrag, viw_Boote_Mitglied

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Bootsname

Text

Name des Bootes
Eingabe erforderlich: Ja

bo_aID

Integer

Fremdschlüssel zur Tabelle "tbl_BootArt". Ohne die BootArt ist
die Ermittlung einer eindeutigen Bootsständermiete nicht
möglich.
Eingabe erforderlich: Ja

mitID

Integer

Fremdschlüssel zur Tabelle "tbl_Mitglied"
Auch Boote, die bei einer Aufräumaktion keinen Mitglied
zugeordnet werden können, sollen verzeichnet werden können. Deswegen ist hier die Eingabe auf nicht erforderlichbelassen worden.

bo_sID

Integer

Fremdschlüssel zur Tabelle "tbl_BootStaender"
Eingabe erforderlich: Ja

Bemerkung

Text

Allgemeine Bemerkungen

Vereinsverwaltung340

Abbildung 4: Bei einer Änderung des Primärschlüssels in"tbl_BootArt" soll der Fremdschlüssel angepasst werden.
Ein Löschen des Schlüssels ist nicht möglich, da das Feldin "tbl_Boot" als Eingabe erforderlich → 'Ja' definiert
ist.
Die Verknüpfung zwischen "tbl_Boot" und "tbl_Mitglied" ist wie in Abbildung 3 so geregelt, dassbei einer Änderung des Primärschlüssels von "tbl_Mitglied" der Fremdschlüssel angepasst wird.
Bei einer Löschung in der "tbl_Mitglied" wird der Fremdschlüssel "mitID" in "tbl_Boot" auf NULL
gesetzt. So könnte ein Boot z. B. von einem Mitglied zurückgelassen werden und in das Eigentum des Vereins übergehen.
Neben der "bo_aID" ist auch die "bo_sID" zu den Bootsständern so verknüpft, dass ein Löschen
des Bootsständers nicht möglich ist, solange ein Boot dort liegt. Siehe: Abbildung 4.

Vereinsverwaltung

341

tbl_BootArt

Datenziel
Formular: frm_Boot, frm_Einstellungen
Makro:

SelectBoat, NewBoatAllowed

Abfrage:qry_Bootsstaender

Ansicht:

viw_Beitrag, viw_Boote_Mitglied

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Bootsart

Text

Kurze Beschreibung der Bootsart, max. 10 Zeichen, hier: 'K1','K2', 'C4' …
Eingabe erforderlich: Ja

beiID

Integer

Fremdschlüssel zur Tabelle "tbl_Beitrag". Eingabe nicht erforderlich, da der Beitrag zu den abwählbaren Modulen gehört.

Die Verknüpfung zwischen "tbl_Boot" und "tbl_BootArt" ist wie in Abbildung 3 so geregelt, dassbei einer Änderung des Primärschlüssels von "tbl_BootArt" der Fremdschlüssel angepasst wird.
Bei einer Löschung in der "tbl_BootArt" ist nicht möglich

tbl_BootStaender

Vereinsverwaltung

342

Datenziel
Formular: frm_Boot
Makro:

SelectBoatRack

Abfrage:qry_Bootsstaender

Ansicht:

viw_Boote_Mitglied

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

BootStaend

Text

Kurzer Text, max. 10 Zeichen, hier: '1.1', '1.2' …
Eingabe erforderlich: Ja

bo_hID

Integer

Fremdschlüssel zur Tabelle "tbl_BootHalle"
Eingabe erforderlich: Ja

Die Verknüpfung zwischen "tbl_Boot" und "tbl_BootStaender" ist wie in Abbildung 3 so geregelt,
dass bei einer Änderung des Primärschlüssels von "tbl_BootStaender" der Fremdschlüssel angepasst wird. Eine Löschung in der "tbl_BootStaender" ist nicht möglich, sofern in "tbl_Boot" dieser Ständer benutzt wird.

tbl_BootHalle

Datenziel
Formular: frm_Boot
Makro:

SelectBoatRack

Abfrage:qry_Bootsstaender

Ansicht:

viw_Boote_Mitglied

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel der Tabelle. Der Primärschlüssel muss eindeutig sein. Das Feld ist als Auto-Wert-Feld gesetzt.

Halle_kurz

Text

Kurzbeschreibung der Halle, max. 10 Zeichen
Eingabe erforderlich: Ja

Halle_lang

Text

Längerer Text zur Verwendung in Berichten

Die Verknüpfung zwischen "tbl_BootStaender" und "tbl_BootHalle" ist wie in Abbildung 4 so
geregelt, dass bei einer Änderung des Primärschlüssels von "tbl_BootHalle" der Fremdschlüssel
Vereinsverwaltung

343

angepasst wird. Eine Löschung in der "tbl_BootHalle" ist nicht möglich, wenn der Fremdschlüssel "bo_hID" in "tbl_BootStaender" benutzt wird.

Tabellen ohne Relationen
Einige Tabellen haben keine direkte Beziehung zu anderen Tabellen. Sie werden für Abfragen,Formulare und Berichte genutzt.

tbl_Altergruppen

Datenziel
Abfrage:qry_Filter_Austritt

Ansicht:

viw_Statistik_Alter_Detail

Bericht:keine direkt

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, wegen der übersichtlichen Tabelle kein AutoWert

MinAlterTiny Integer

Untere Altersgrenze für die entsprechende Gruppe
Eingabe erforderlich: Ja

MaxAlterTiny Integer

Obere Altersgrenze für die entsprechende Gruppe
Eingabe erforderlich: Ja

tbl_Einstellungen

Vereinsverwaltung

344

Datenziel
Formular: frm_Einstellungen, frm_Mitglied
Abfrage:qry_Forms, qry_Jubilaeum

Ansicht:

viw_Reports

Bericht:keine direkt

Makro:

Preferences, SelectGroup

Feldname

Feldtyp

Beschreibung

ID

Ja/NeinPrimärschlüssel, soll nur einen einzigen Datensatz enthalten,
deshalb Ja/Nein

Vereinsname

Text

Name des Vereins

Anschrift

Text

Komplette Anschrift für Absender von Briefen

Ort

Text

Ortsbezeichnung für Datumskombinationen («Rheine, den…»)

Homepage

Text

Homepage des Vereins

EMail

Text

Mailkontakt

IBAN

Text

Wieder 42 Zeichen maximal, auch hier mit 4 Zeichen gefolgt
von einem Leerzeichen formatiert

BIC

Text

BIC

Bank

Text

Name der Bank

Kontoverwaltung

Ja/Nein

Wenn nicht gewünscht: Nein

Beitragsverwaltung

Ja/Nein

Wenn nicht gewünscht: Nein

Schluesselverwaltung

Ja/Nein

Wenn nicht gewünscht: Nein

Bootstaenderverwaltung

Ja/Nein

Wenn nicht gewünscht: Nein

Abteilungsverwaltung

Ja/Nein

Wenn nicht gewünscht: Nein

Logo

Bild

Logo wird eingelesen und z. B. Für Berichte genutzt.

Zur Absicherung des Feldes "ID", damit wirklich nur ein Datensatz gespeichert werden kann,wird unter Extras → SQL eingegeben:
001 ALTER TABLE "tbl_Einstellungen"
002 ADD CONSTRAINT "Nur ein Datensatz"
003 CHECK ("ID" = TRUE)
Hierdurch wird das Feld "ID" auf die Eingabe TRUE (ausgewählt). Bei Fehlversuchen wird ausgegeben:«Check constraint violation Nur ein Datensatz table: tbl_Einstellungen…»

tbl_Filter

Vereinsverwaltung

345

Datenziel
Abfrage:qry_Filter, qry_Filter_Austritt, qry_Filter_Gruppe, qry_Filter_Mitglied, qry_Filter_VorstandAnsicht:

viw_Filter_Mitglieder_komplett_AusEin, viw_Gruppe, viw_Mitglieder_komplett, viw_Statistik_Alter_Detail

Bericht:keine direkt

Makro:

FilterIDSet, FormStart

Feldname

Feldtyp

Beschreibung

ID

IntegerPrimärschlüssel, Entspricht der VerbindungsID zur Datenbank. Damit kann bei Serverdatenbanken auch mit dieser
Tabelle gefiltert werden. Schlüsselwert wird beim Aufruf derDatei über Makro geschrieben.

mitID

IntegerSchlüsselwert von "tbl_Mitglied"

Formular

Text

Formularname (zur Anzeige des aktuellen Formulars in einem
Listenfeld)

WahlDat

Datum

Wahldatum für den Vorstand

Bericht

Text

Berichtsname (zur Anzeige des zu startenden Berichtes ineinem Listenfeld)

StartDat

Datum

Startdatum z. B. Für die Berichtausgabe

EndDatDatum

Enddatum für den Zeitraum beginnend mit "StartDat"

Austritte

Ja/Nein

Bericht: Es existiert ein Bericht, der entweder alle Austritteoder alle Beitritte anzeigt. Sollen die Austritte angezeigt werden, so wird das über 'Ja' hier geregelt.

Unterschrift1ID

Integer

Erste Unterschrift z. B. unter Urkunden. Hier wird der Primärschlüssel aus "tbl_VorstandPosten" gespeichert

Unterschrift2ID

Integer

Zweite Unterschrift, ebenfalls über den Primärschlüssel
"tbl_VorstandPosten" zu erreichen

Statistik_Stich
tag

Datum

Zu diesem Datum soll die Statistik ausgegeben werden

Beitrag_Stichtag

Datum

Zu diesem Datum soll der Beitrag erhoben werden.

Vereinsverwaltung

346

Ansichten
viw_Abteilungen_Mitglied

Datenquelle
Tabelle:tbl_Abteilung, tbl_Mitglied_Abteilung

Datenziel
Ansicht:

viw_Anschrift, viw_Filter_Mitglieder_komplett_AusEin, viw_Mitglieder_komplett

001 SELECT DISTINCT "c"."mitID",
002( SELECT "Abteilung"
003
FROM ( SELECT "a"."mitID", "a"."abtID",
004( SELECT COUNT( "mitID" ) FROM "tbl_Mitglied_Abteilung"
WHERE "mitID" = "a"."mitID" AND "abtID" <= "a"."abtID" ) AS "Nr"005
FROM "tbl_Mitglied_Abteilung" AS "a" ) AS "b", "tbl_Abteilung"
006
WHERE "tbl_Abteilung"."ID" = "b"."abtID" AND "b"."Nr" = 1 AND "mitID" ="c"."mitID" ) ||007
COALESCE ( ', ' || ( SELECT "Abteilung"
008
FROM ( SELECT "a"."mitID", "a"."abtID",
009( SELECT COUNT( "mitID" ) FROM "tbl_Mitglied_Abteilung"
WHERE "mitID" = "a"."mitID" AND "abtID" <= "a"."abtID" ) AS "Nr"010
FROM "tbl_Mitglied_Abteilung" AS "a" ) AS "b", "tbl_Abteilung"
011
WHERE "tbl_Abteilung"."ID" = "b"."abtID" AND "b"."Nr" = 2 AND "mitID" ="c"."mitID" ), '' ) ||
...
022
023
024

COALESCE ( ', ' || ( SELECT "Abteilung"
FROM ( SELECT "a"."mitID", "a"."abtID",( SELECT COUNT( "mitID" ) FROM "tbl_Mitglied_Abteilung"
WHERE "mitID" = "a"."mitID" AND "abtID" <= "a"."abtID" ) AS "Nr"025
FROM "tbl_Mitglied_Abteilung" AS "a" ) AS "b", "tbl_Abteilung"
026
WHERE "tbl_Abteilung"."ID" = "b"."abtID" AND "b"."Nr" = 5 AND "mitID" ="c"."mitID" ), '' )027 AS "Abteilungen"028 FROM "tbl_Mitglied_Abteilung" AS "c"
Mit dieser Ansicht werden dem Mitglied in einem Feld alle Abteilungen zugewiesen, die es ausgewählt hat. Eine derartig aufwendige Abfrage ist nur bei der internen HSQLDB notwendig. Die
interne FIREBIRD Datenbank hat dafür die Funktion LIST(), MYSQL/MARIADB die Funktion
GROUP_CONCAT(), POSTGRESQL die Funktion STRING_AGG() usw.
Die Unterabfrage von Zeile 2 bis Zeile 6 wiederholt sich hier nahezu identisch fünf mal. Die einzigen Unterschiede sind die im Code mit rot hervorgehobenen Zahlen sowie ab der ersten Wiederholung die Verknüpfung über COALESCE( ', '|| … , '' ).
Vereinsverwaltung347

In der am weitesten innen liegenden Abfrage von Zeile 4 wird der Abteilung, die einem einzelnen Mitglied zugeordnet wurde, eine laufende "Nr" zugewiesen. Dadurch kann aus der Abfrageanschließend die Abteilung mit der "Nr" = 1 herausgefiltert werden.
Zeile 3 und 5 umschließen diese Abfrage und geben zuerst einmal eine "mitID", eine "abtID"und eine "Nr" aus.
Zeile 2 und 6 dienen dann dazu, diese Unterabfrage mit der "tbl_Abteilung" zu kombinieren und
die Abteilung auszulesen, die in der Unterabfrage die "Nr" = 1 zugewiesen bekommen hat.
Über die folgenden gleichlautenden Abfragen werden die Abteilungen mit den Nummern 2 bis 5
zugeordnet. Es können hier also höchstens 5 Abteilungen hintereinander, durch ein Komma
getrennt, in das Feld "Abteilungen" geschrieben werden. In der Praxis dürfte das aber völlig
genügen.

viw_Adresse_Num_Namen

Datenquelle
Tabelle:tbl_Mitglied, tbl_Adresse

Datenziel
Ansicht:

viw_Anschrift

001
002
003
004
005
006

SELECT "c"."Geschlecht","c"."Vorname","c"."Nachname","c"."gruID","c"."adrID",( SELECT COUNT( "ID" ) FROM "tbl_Mitglied" WHERE "ID" <= "c"."ID" AND
COALESCE ( "Gruppe_mitID", "ID" ) = "c"."gruID" ) AS "GruppenNr",
007 ( SELECT COUNT( "ID" ) FROM "tbl_Mitglied" WHERE "ID" <= "c"."ID" AND( COALESCE ( "Gruppe_mitID", "ID" ) = "c"."adrID" OR "ID" ="c"."adrID" ) ) AS "GruppenAdressNr",008 ( SELECT COUNT( "gruID" )FROM ( SELECT DISTINCT "Nachname",
CASE WHEN "tbl_Adresse"."mitID" = "tbl_Mitglied"."ID" THEN
"tbl_Mitglied"."ID" ELSE "tbl_Mitglied"."Gruppe_mitID" END AS "adrID"
FROM "tbl_Mitglied" LEFT JOIN "tbl_Adresse" ON "tbl_Mitglied"."ID" ="tbl_Adresse"."mitID" )WHERE "Nachname" <= "c"."Nachname" AND "adrID" = "c"."adrID" )AS "NachnAdressNr"
Vereinsverwaltung

348

009 FROM ( SELECT "a"."ID","a"."Geschlecht","a"."Vorname","a"."Nachname",
COALESCE ( "Gruppe_mitID", "ID" ) AS "gruID",
CASE WHEN "b"."mitID" = "a"."ID" THEN "a"."ID" ELSE "a"."Gruppe_mitID" END
AS "adrID"
FROM "tbl_Mitglied" AS "a"LEFT JOIN "tbl_Adresse" AS "b" ON "a"."ID" = "b"."mitID" ) AS "c"
In Zeile 9 werden die ersten vier Felder direkt aus der Tabelle "tbl_Mitglied" ausgelesen. Dasvierte Feld gibt lediglich dann statt des Feldes "Gruppe_mitID" das Feld "ID" wieder, wenn das
Feld "Gruppe_mitID" leer ist. Dies ist bei allen Mitgliedern der Fall, die nicht einem vorher eingegebenem Mitglied zugeordnet werden konnten. Neben der so ermittelten Gruppenzugehörigkeit
kann es auch in einer Gruppe unterschiedliche Adressen geben. Deswegen wird eine separate"adrID" ermittelt, die dann anders ist als die "gruID", wenn "mitID" in der Tabelle "tbl_Adresse"
gleich der "ID" in der Tabelle "tbl_Mitglied" ist, obwohl dieses Mitglied unter "Gruppe_mitID"(über das Konto) einer Gruppe zugeordnet wurde. Im Screenshot ist das beim 5. Datensatz('Jolantha Schulze') zu sehen.
Damit die Mitglieder einer Adress-Gruppe zusammen im Adressfeld einer Adresse erscheinenkönnen, ist bei der internen HSQLDB ein Umweg über die Durchnummerierung aller Mitglieder
der Gruppe notwendig. Die GruppenNr gibt hier die Durchnummerierung nach der Gruppe wieder, die unterschiedliche Adressen nicht berücksichtigt. Die "GruppenAdressNr" ist die Nummerierung, die die gleiche Adresse einer Gruppe betrifft. Damit werden in der Adresse die Vornamen zusammengefasst. Auch bei den Nachnamen kann es vorkommen, dass zu einer Gruppe
verschiedene Nachnamen gehören. Der Screenshot zeigt so einen Fall für die Gruppe "gruID" ='3'. Auch die unterschiedlichen Nachnamen müssen für jede Gruppe durchnummeriert werden.
Dies geschieht im Feld "NachnAdressNr". Bei der Nutzung anderer Datenbanken wie z. B. derinternen FIREBIRD Datenbank ist dieser Klimmzug mit der Durchnummerierung nicht notwendig.
Dort erledigt die Gruppierung der Vornamen und Nachnamen die Funktion LIST() direkt.

viw_Anschrift

Datenquelle
Tabelle:tbl_Adresse, tbl_Ort

Ansicht:

viw_Adresse_Num_Namen

Datenziel
Abfrage:qry_Anschriften_2spaltig, qry_Anschriften_3spaltig

Bericht:

Ergebnisliste

001 SELECT DISTINCT "a"."adrID",
002 CASE WHEN (SELECT MAX("GruppenAdressNr") FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID") = 1 THEN
( SELECT CASE WHEN "Geschlecht"='m' THEN 'Herrn' WHEN "Geschlecht"='w'
THEN 'Frau' ELSE '' END FROM "tbl_Mitglied" WHERE "ID" =Vereinsverwaltung349

"a"."adrID")
ELSE '' END AS "Anrede",003 ( SELECT "Vorname" FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID" AND "GruppenAdressNr" = 1 ) ||004 COALESCE( CASE WHEN (SELECT MAX("GruppenAdressNr") FROM
"viw_Adresse_Num_Namen" WHERE "adrID" = "a"."adrID") = 2 THEN ' und 'ELSE ', ' END||
005( SELECT "Vorname" FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID" AND "GruppenAdressNr" = 2 ), '' ) ||...
016 COALESCE( CASE WHEN (SELECT MAX("GruppenAdressNr") FROM
"viw_Adresse_Num_Namen" WHERE "adrID" = "a"."adrID") = 8 THEN ' und 'ELSE ', ' END||
017( SELECT "Vorname" FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID" AND "GruppenAdressNr" = 8 ), '' )AS "Vornamen",018 ( SELECT DISTINCT "Nachname" FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID" AND "NachnAdressNr" = 1 ) ||019 COALESCE( CASE WHEN (SELECT MAX("NachnAdressNr") FROM
"viw_Adresse_Num_Namen" WHERE "adrID" = "a"."adrID") = 2 THEN ' und 'ELSE ', ' END||
020( SELECT DISTINCT "Nachname" FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID" AND "NachnAdressNr" = 2 ), '' ) ||...
025 COALESCE( CASE WHEN (SELECT MAX("NachnAdressNr") FROM
"viw_Adresse_Num_Namen" WHERE "adrID" = "a"."adrID") = 5 THEN ' und 'ELSE ', ' END||
026( SELECT DISTINCT "Nachname" FROM "viw_Adresse_Num_Namen"
WHERE "adrID" = "a"."adrID" AND "NachnAdressNr" = 5 ), '' )027 AS "Nachnamen",028 "tbl_Adresse"."Straße_Nr",029 "tbl_Ort"."Postleitzahl",
030 "tbl_Ort"."Ort",
031 "tbl_Adresse"."Nichtdruck"
032 FROM "viw_Adresse_Num_Namen" AS "a"033 LEFT JOIN "tbl_Adresse" ON "a"."adrID" = "tbl_Adresse"."mitID"
034 LEFT JOIN "tbl_Ort" ON "tbl_Adresse"."ortID" = "tbl_Ort"."ID"035 WHERE ("Nichtdruck" = False OR "Nichtdruck" IS NULL)
AND "adrID" IS NOT NULL
036 ORDER BY "Nachnamen" ASC
Diese Ansicht baut direkt auf der Ansicht viw_Adresse_Num_Namen auf. Sie ist wieder nur für
die interne HSQLDB so umfangreich. Bei der internen FIREBIRD würde die Zusammenstellung derGruppierung wesentlich komfortabler durch LIST() ablaufen.
Nach dem Auslesen der "adrID" aus "viw_Adresse_Num_Namen" wird anhand der Geschlechtsangaben und der Anzahl der Personen der Gruppe eine Anrede erstellt (Zeile 2). Nur wenn nur
eine Person in der Gruppe ist wird nachgesehen, welches Geschlecht der Person zugeordnet
wurde. Für männliche Personen wird dann 'Herrn', für weibliche Personen 'Frau' gewählt.
Ansonsten bleibt die "Anrede" leer.
Ab Zeile 3 wird zuerst der Vorname für die Person heraus gesucht, die innerhalb der Gruppe die"GruppenAdressNr" = '1' zugewiesen bekommen hat. Anschließend wird für jede weiter "GruppenAdressNr" nachgeschaut, ob zu der "GruppenAdressNr" auch ein Datensatz existiert. Existiert kein Datensatz, so wird einfach leerer Text angehängt. Existiert ein Datensatz, so wird
nachgesehen, ob die "GruppenAdressNr" gleichzeitig die höchste GruppenAdressnummer
Max("GruppenAdressNr") ist. Ist dies der Fall, so wird der letzte Vorname mit der Bindung 'Vereinsverwaltung

350

und ' an den vorherigen Namen angehängt. Existieren noch weitere Einträge, so wird der Vorname mit ', ' angehängt.
Diese Prozedur wird in dem obigen Beispiel bis zum 8. Vornamen durchgeführt. Der Code fürdie Vornamen 3 bis 7 wurde hier nicht noch aufgeführt.
Das Verfahren für den Nachnamen ab Zeile 18 verläuft identisch zu dem des Vornamens. Hierwird aber die Prozedur lediglich bis zum 5. unterschiedlichen Nachnamen in der Gruppe durchgeführt. In der Regel dürften bei Gruppen ja deutlich weniger unterschiedliche Nachnamen als
unterschiedliche Vornamen zu finden sein.
Bei den Nachnamen wird extra mit DISTINCT vermieden, dass anschließend gleiche Namen
mehrmals auftauchen.
Anschließend werden noch die Straße mit Nummer, die Postleitzahl und der Ort aus den entsprechenden Tabellen zusammengesucht (Zeile 28 bis 30). Beim Nichtdruck wird darauf geachtet, dass er nicht NULL ist sondern als nicht angewählt, also False erscheint, wenn er nicht tatsächlich ausgewählt wurde. «Nichtdruck» bedeutet hier, dass an die Adresse nichts verschickt
wird. Es kommt schließlich immer wieder vor, dass Post nach Umzügen unzustellbar zurück
kommt. Da kann dann das Porto auch gleich gespart werden.
Die "tbl_Adresse" und die "tbl_Ort" sind beide über einen LEFT JOIN mit der"viw_Adresse_Gruppe" verknüpft. Dadurch werden auf jeden Fall alle Datensätze aus"viw_Adresse_Gruppe" übernommen, auch wenn sie, wie aus dem entsprechenden Screenshot
ersichtlich, nicht unbedingt eine Adresse angegeben haben.

viw_Beitrag

Abbildung 5: Beiträge gefiltert für die Gruppe "gruID" = '3'. So
erscheint das Ergebnis der Ansicht in Formularen und Berichten.
Datenquelle
Tabelle:tbl_Mitglied, tbl_Beitrag, tbl_Gruppe_Beitrag, tbl_Beitrag_Staffel, tbl_Boot, tbl_BootArt,

Vereinsverwaltung

351

tbl_Abteilung, tbl_Mitglied_Abteilung

Datenziel
Abfrage:qry_Beitraege

Formular: Einzelmessung_System

Hier sind viele Abfragen mit Hilfe von UNION untereinander zusammengefasst. Jede dieser
Abfragen ergibt maximal einen Datensatz für eine Gruppe ("gruID"). Jede Abfrage enthält
neben der "gruID" auch die Spalte "Sort" zur Sortierung der Ergebnisse in der Übersicht. Jede
Abfrage enthält außerdem genau 4 Spalten, die vom Typ des Inhaltes (Text, Dezimalzahl) von
Abfrage zu Abfrage übereinstimmen müssen.
Zeile 1: Die Abfrage beginnt mit einer festen Spalte "Sort". Der Wert ist '1', da der Inhalt dieser
Abfrage ganz oben in der Liste erscheinen soll. In der folgenden Spalte wird aus verschiedenen
Feldern der "tbl_Beitrag" die Information für die "Beitragsart" zusammengesetzt. Die Höhe desBeitrags wird aus einer Summe der Multiplikation von "BeitrHoehe" * "Anzahl" für die jeweilige
Gruppe ermittelt.
001 /* Mitgliedsbeitrag ohne andere Beitraege */SELECT '1' AS "Sort",'einfacher Beitrag für ' || "tbl_Beitrag"."BeitrMonate" || CASE WHEN
"tbl_Beitrag"."BeitrMonate" > 1 THEN ' Monate' ELSE ' Monat' END AS"Beitragsart",
SUM( "tbl_Beitrag"."BeitrHoehe" * "tbl_Gruppe_Beitrag"."Anzahl" ) AS
"Beitrag","tbl_Gruppe_Beitrag"."mitID" AS "gruID"
FROM "tbl_Beitrag", "tbl_Gruppe_Beitrag"
WHERE "tbl_Gruppe_Beitrag"."beiID" = "tbl_Beitrag"."ID"GROUP BY "gruID", "Beitragsart"
002 UNION
In Zeile 3 wird statt des reinen Mitgliedsbeitrages die Bootsständermiete erfasst. Hier muss auf
eine ausführlichere Unterabfrage zugegriffen werden, da die Informationen zu den Booten
gleich aus mehreren Tabellen geholt werden müssen, so dass alle Boote erfasst werden, die zu
einer Gruppe gehören und dann auch noch der gestaffelte Preis berücksichtigt werden kann.
In der innersten Abfrage wird zuerst einmal die Anzahl der Boote und Bootstypen ermittelt, dieden jeweiligen Gruppenmitgliedern zugeordnet werden können. Dann wird in der auf dieseAbfrage zugreifenden Abfrage in der Tabelle "tbl_Beitrag_Staffel" nachgeschaut, welcher Beitrag für diese Anzahl an Booten fällig ist.
Da nur jeweils ein Datensatz pro Gruppe existiert (entweder hat die Gruppe einen Canadieroder bis zu drei Kajaks im Bootshaus liegen) muss nicht extra noch einmal das Ergebnis derAbfrage bei der Bootsständermiete summiert und gruppiert werden.
003 /* Bootsstaendermiete */SELECT '2' AS "Sort",'Bootsständer für '||"tbl_Beitrag"."BeitrMonate"|| CASE WHEN
"tbl_Beitrag"."BeitrMonate" > 1 THEN ' Monate' ELSE ' Monat' END AS"Beitragsart",
COALESCE( "tbl_Beitrag"."BeitrHoehe", "tbl_Beitrag_Staffel"."BeitrHoehe")
AS "Beitrag","a"."gruID"
FROM (SELECT COUNT( "tbl_Boot"."ID" ) AS "Anzahl","tbl_BootArt"."beiID", COALESCE ( "tbl_Mitglied"."Gruppe_mitID","tbl_Mitglied"."ID" ) AS "gruID"
FROM "tbl_Boot", "tbl_BootArt", "tbl_Mitglied"
WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID" AND"tbl_Boot"."mitID" = "tbl_Mitglied"."ID" GROUP BY"tbl_BootArt"."beiID", "gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
Vereinsverwaltung

352

LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID" WHERE"tbl_Beitrag_Staffel"."Anzahl" = "a"."Anzahl" OR"tbl_Beitrag_Staffel"."Anzahl" IS NULL
004 UNION
Beim Abteilungsbeitrag können Beiträge für verschiedene Abteilungen anfallen. Hier muss also
im Gegensatz zur Bootsständermiete zuerst der Einzelbeitrag pro Abteilung ausgerechnet werden, um dann in der äußersten Abfrage wieder gruppiert und summiert zu werden.
005 /* Abteilungsbeitrag */SELECT '3' AS "Sort",'Abteilungsbeitrag für '||"tbl_Beitrag"."BeitrMonate"|| CASE WHEN
"tbl_Beitrag"."BeitrMonate" > 1 THEN ' Monate' ELSE ' Monat' END AS"Beitragsart",
SUM("b"."BeitrHoehe") AS "Beitrag","b"."gruID"
FROM (SELECT "a"."Anzahl", "a"."beiID", "a"."gruID",
COALESCE( "tbl_Beitrag"."BeitrHoehe","tbl_Beitrag_Staffel"."BeitrHoehe") AS "BeitrHoehe"
FROM (SELECT COUNT( "tbl_Mitglied_Abteilung"."mitID" ) AS "Anzahl","tbl_Abteilung"."beiID", COALESCE ( "tbl_Mitglied"."Gruppe_mitID","tbl_Mitglied"."ID" ) AS "gruID" FROM "tbl_Mitglied_Abteilung","tbl_Abteilung", "tbl_Mitglied" WHERE"tbl_Mitglied_Abteilung"."abtID" = "tbl_Abteilung"."ID" AND"tbl_Mitglied_Abteilung"."mitID" = "tbl_Mitglied"."ID" AND NOT"tbl_Abteilung"."beiID" IS NULL GROUP BY "tbl_Abteilung"."beiID","gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID" WHERE "tbl_Beitrag_Staffel". "Anzahl" ="a"."Anzahl" OR "tbl_Beitrag_Staffel". "Anzahl" IS NULL) AS "b","tbl_Beitrag"
WHERE "b"."beiID" = "tbl_Beitrag"."ID"GROUP BY "gruID", "Beitragsart"
006 UNION
Beim einfachen Quartalsbeitrag werden nur die Zahlungen berücksichtigt, die innerhalb eines
Quartals erfolgen. Dabei handelt es sich um die Mitgliedsbeiträge und die Bootsständermiete.
Die Abfragen für den Mitgliedsbeitrag 8 und die Bootsständermiete 10 sind gleich den Abfragen
in Zeile 1 und Zeile 3. Der einzige Unterschied besteht darin, dass der Beitrag durch die Beitragsmonate geteilt wird (hier bei beiden Beträgen ist das eine Division durch 1 – könnte ja
auch bereits ein Vierteljahresbeitrag sein …). Anschließend wird dieser Beitrag für einen Monat
für das Quartal mit 3 multipliziert.
Diese beiden Beiträge werden über die Abfrage in Zeile 7 und die Verbindungen mit UNION
zusammengefasst
007 /* Quartal ohne Ganzjahreszahlungen, Quartal 2-3 je nach Abteilung */SELECT '4' AS "Sort",'Gesamt Quartal' AS "Beitragsart",
SUM("Beitrag"),"gruID" FROM (008 -- Mitgliedsbeitrag
SELECT SUM( "tbl_Beitrag"."BeitrHoehe" *"tbl_Gruppe_Beitrag"."Anzahl" / "tbl_Beitrag"."BeitrMonate" ) * 3 AS
"Beitrag","tbl_Gruppe_Beitrag"."mitID" AS "gruID"
FROM "tbl_Beitrag", "tbl_Gruppe_Beitrag"
WHERE "tbl_Gruppe_Beitrag"."beiID" = "tbl_Beitrag"."ID"GROUP BY "gruID"

Vereinsverwaltung

353

009
UNION
010 -- Bootstaendermiete
SELECT ("b"."BeitrHoehe"/"tbl_Beitrag"."BeitrMonate" ) * 3 AS
"Beitrag","b"."gruID"
FROM (SELECT "a"."Anzahl", "a"."beiID", "a"."gruID",
COALESCE( "tbl_Beitrag"."BeitrHoehe","tbl_Beitrag_Staffel"."BeitrHoehe") AS "BeitrHoehe"
FROM (SELECT COUNT( "tbl_Boot"."ID" ) AS "Anzahl","tbl_BootArt"."beiID", COALESCE ( "tbl_Mitglied"."Gruppe_mitID","tbl_Mitglied"."ID" ) AS "gruID" FROM "tbl_Boot", "tbl_BootArt","tbl_Mitglied" WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID" AND"tbl_Boot"."mitID" = "tbl_Mitglied"."ID" GROUP BY"tbl_BootArt"."beiID", "gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID" WHERE "tbl_Beitrag_Staffel". "Anzahl"= "a"."Anzahl" OR "tbl_Beitrag_Staffel". "Anzahl" IS NULL) AS "b","tbl_Beitrag"
WHERE "b"."beiID" = "tbl_Beitrag"."ID") GROUP BY "gruID"
011 UNION
Für das Quartal mit den Ganzjahreszahlungen wird die gleiche Abfrage durchgeführt wie für das
einfache Quartal. Es wird lediglich die Ganzjahreszahlung für die Abteilungsbeiträge hinzugefügt.
012 /* 1. Quartal, mit Ganzjahreszahlungen */SELECT '5' AS "Sort",'Gesamt 1. Quartal' AS "Beitragsart",
SUM("Beitrag"),"gruID" FROM (013 -- Mitgliedsbeitrag
SELECT SUM( "tbl_Beitrag"."BeitrHoehe" *"tbl_Gruppe_Beitrag"."Anzahl" / "tbl_Beitrag"."BeitrMonate" ) * 3 AS
"Beitrag","tbl_Gruppe_Beitrag"."mitID" AS "gruID"
FROM "tbl_Beitrag", "tbl_Gruppe_Beitrag"
WHERE "tbl_Gruppe_Beitrag"."beiID" = "tbl_Beitrag"."ID"GROUP BY "gruID"
014
UNION
015 -- Bootstaendermiete
SELECT ("b"."BeitrHoehe"/"tbl_Beitrag"."BeitrMonate" ) * 3 AS
"Beitrag","b"."gruID"
FROM (SELECT "a"."Anzahl", "a"."beiID", "a"."gruID",
COALESCE( "tbl_Beitrag"."BeitrHoehe","tbl_Beitrag_Staffel"."BeitrHoehe") AS "BeitrHoehe"
FROM (SELECT COUNT( "tbl_Boot"."ID" ) AS "Anzahl","tbl_BootArt"."beiID", COALESCE ( "tbl_Mitglied"."Gruppe_mitID","tbl_Mitglied"."ID" ) AS "gruID" FROM "tbl_Boot", "tbl_BootArt","tbl_Mitglied" WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID" AND"tbl_Boot"."mitID" = "tbl_Mitglied"."ID" GROUP BY"tbl_BootArt"."beiID", "gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID"
WHERE "tbl_Beitrag_Staffel". "Anzahl" = "a"."Anzahl" OR"tbl_Beitrag_Staffel". "Anzahl" IS NULL) AS "b","tbl_Beitrag"
WHERE "b"."beiID" = "tbl_Beitrag"."ID"
Vereinsverwaltung

354

016
UNION
017 -- Abteilungsbeitrag
SELECT SUM("b"."BeitrHoehe") AS "Beitrag","b"."gruID"
FROM (SELECT "a"."Anzahl", "a"."beiID", "a"."gruID",
COALESCE( "tbl_Beitrag"."BeitrHoehe","tbl_Beitrag_Staffel"."BeitrHoehe") AS "BeitrHoehe"
FROM (SELECT COUNT( "tbl_Mitglied_Abteilung"."mitID" ) AS "Anzahl","tbl_Abteilung"."beiID", COALESCE( "tbl_Mitglied"."Gruppe_mitID", "tbl_Mitglied"."ID" ) AS "gruID"
FROM "tbl_Mitglied_Abteilung", "tbl_Abteilung", "tbl_Mitglied"
WHERE "tbl_Mitglied_Abteilung"."abtID" = "tbl_Abteilung"."ID"AND "tbl_Mitglied_Abteilung"."mitID" = "tbl_Mitglied"."ID"AND NOT "tbl_Abteilung"."beiID" IS NULLGROUP BY "tbl_Abteilung"."beiID", "gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID"
WHERE "tbl_Beitrag_Staffel". "Anzahl" = "a"."Anzahl" OR"tbl_Beitrag_Staffel". "Anzahl" IS NULL) AS "b","tbl_Beitrag"
WHERE "b"."beiID" = "tbl_Beitrag"."ID" GROUP BY "gruID") GROUP BY "gruID"
018 UNION
Die Ganzjahreszahlung unterscheidet sich von der Zahlung für das erste Quartal nur dadurch,
dass jetzt die Mitgliedsbeiträge und die Bootsständermieten, die für einen Monat berechnet
wurden, eben vor dem Zusammenfassen mit 12 multipliziert werden.
019 /* Jahreszahlungen gesamt */SELECT '6' AS "Sort",'Gesamt Jahr' AS "Beitragsart",
SUM("Beitrag"),"gruID" FROM (020 -- Mitgliedsbeitrag
SELECT SUM( "tbl_Beitrag"."BeitrHoehe" *"tbl_Gruppe_Beitrag"."Anzahl" / "tbl_Beitrag"."BeitrMonate" ) * 12 AS"Beitrag","tbl_Gruppe_Beitrag"."mitID" AS "gruID"
FROM "tbl_Beitrag", "tbl_Gruppe_Beitrag"
WHERE "tbl_Gruppe_Beitrag"."beiID" = "tbl_Beitrag"."ID"GROUP BY "gruID"
021 UNION
022 -- Bootstaendermiete
SELECT ("b"."BeitrHoehe"/"tbl_Beitrag"."BeitrMonate" ) * 12 AS"Beitrag","b"."gruID"
FROM (SELECT "a"."Anzahl", "a"."beiID", "a"."gruID",
COALESCE( "tbl_Beitrag"."BeitrHoehe","tbl_Beitrag_Staffel"."BeitrHoehe") AS "BeitrHoehe"
FROM (SELECT COUNT( "tbl_Boot"."ID" ) AS "Anzahl","tbl_BootArt"."beiID", COALESCE ( "tbl_Mitglied"."Gruppe_mitID","tbl_Mitglied"."ID" ) AS "gruID" FROM "tbl_Boot", "tbl_BootArt","tbl_Mitglied" WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID" AND"tbl_Boot"."mitID" = "tbl_Mitglied"."ID" GROUP BY"tbl_BootArt"."beiID", "gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID"
WHERE "tbl_Beitrag_Staffel". "Anzahl" = "a"."Anzahl" OR"tbl_Beitrag_Staffel". "Anzahl" IS NULL) AS "b",Vereinsverwaltung355

"tbl_Beitrag"
WHERE "b"."beiID" = "tbl_Beitrag"."ID"023 UNION
024 -- Abteilungsbeitrag
SELECT SUM("b"."BeitrHoehe") AS "Beitrag","b"."gruID"
FROM (SELECT "a"."Anzahl", "a"."beiID", "a"."gruID",
COALESCE( "tbl_Beitrag"."BeitrHoehe","tbl_Beitrag_Staffel"."BeitrHoehe") AS "BeitrHoehe"
FROM (SELECT COUNT( "tbl_Mitglied_Abteilung"."mitID" ) AS "Anzahl","tbl_Abteilung"."beiID", COALESCE( "tbl_Mitglied"."Gruppe_mitID", "tbl_Mitglied"."ID" ) AS "gruID"
FROM "tbl_Mitglied_Abteilung", "tbl_Abteilung", "tbl_Mitglied"
WHERE "tbl_Mitglied_Abteilung"."abtID" = "tbl_Abteilung"."ID"AND "tbl_Mitglied_Abteilung"."mitID" = "tbl_Mitglied"."ID"AND NOT "tbl_Abteilung"."beiID" IS NULLGROUP BY "tbl_Abteilung"."beiID", "gruID") AS "a"LEFT JOIN "tbl_Beitrag" ON "a"."beiID" = "tbl_Beitrag"."ID"
LEFT JOIN "tbl_Beitrag_Staffel" ON "tbl_Beitrag"."ID" ="tbl_Beitrag_Staffel"."beiID"
WHERE "tbl_Beitrag_Staffel". "Anzahl" = "a"."Anzahl" OR"tbl_Beitrag_Staffel". "Anzahl" IS NULL) AS "b","tbl_Beitrag"
WHERE "b"."beiID" = "tbl_Beitrag"."ID" GROUP BY "gruID") GROUP BY "gruID"

viw_Beitrag_Gruppe_kalkuliert

Datenquelle
Tabelle:tbl_Beitrag

Ansicht:

viw_Gruppe

Datenziel
Makro:

SubscriptionSave

001 SELECT "Gruppe", "Anzahl", "beiID", "Anzahl"*(SELECT "BeitrHoehe" FROM
"tbl_Beitrag" WHERE "ID" = "a"."beiID") AS "Beitrag" FROM
002
(SELECT "Gruppe", CASE WHEN "Anzahl_korr" = 0 THEN 1 ELSE "Anzahl_korr"
END AS "Anzahl", "beiID_korr" AS "beiID" FROM
003
(SELECT "Gruppe", "beiID", "Anzahl", "MinAlter", "Beitrag","Anzahl_kor1", CASE WHEN "Anzahl" > "Anzahl_kor1" AND "MinAlter" <18 THEN 0 WHEN "Anzahl_kor1" < 0 THEN 0 ELSE "Anzahl_kor1" END AS"Anzahl_korr", CASE WHEN "Anzahl" > "Anzahl_kor1" AND
Vereinsverwaltung

356

"Anzahl_kor1" < 1 THEN (SELECT "ID" FROM "tbl_Beitrag" WHERE"BeitrBezug" = 'MitG') ELSE "beiID" END AS "beiID_korr" FROM
004
(SELECT "b"."Gruppe", "b"."beiID", "b"."Anzahl", "b"."MinAlter","b"."Beitrag", CASE WHEN (SELECT SUM(( SELECT "BeitrHoehe" FROM
"tbl_Beitrag" WHERE "BeitrBezug" = 'Mit' AND "a"."Alter_Beitrag"
BETWEEN "MinAlter" AND "MaxAlter" )) FROM "viw_Gruppe" AS "a"
WHERE "Gruppe" = "b"."Gruppe") > (SELECT "BeitrHoehe" FROM
"tbl_Beitrag" WHERE "BeitrBezug" = 'MitG') THEN "Anzahl" - 2 ELSE
"Anzahl" END AS "Anzahl_kor1" FROM
005
(SELECT "a"."Gruppe", "a"."beiID", "a"."Anzahl","a"."MinAlter", "Anzahl" * (SELECT "BeitrHoehe" FROM
"tbl_Beitrag" WHERE "ID" = "a"."beiID") AS "Beitrag" FROM
006
(SELECT "Gruppe", "beiID", COUNT("beiID") AS "Anzahl","MinAlter" FROM
007
(SELECT "Gruppe", ( SELECT "ID" FROM "tbl_Beitrag" WHERE"BeitrBezug" = 'Mit' AND "a"."Alter_Beitrag" BETWEEN"MinAlter" AND "MaxAlter" ) AS "beiID",( SELECT "MinAlter" FROM "tbl_Beitrag" WHERE"BeitrBezug" = 'Mit' AND "a"."Alter_Beitrag" BETWEEN"MinAlter" AND "MaxAlter" ) AS "MinAlter" FROM
"viw_Gruppe" AS "a"008)
GROUP BY "Gruppe", "beiID", "MinAlter"
009) AS "a"010) AS "b"
011
)
012
)
013
) AS "a"014 GROUP BY "Gruppe", "beiID", "Anzahl"
Mit dieser Ansicht soll der Beitrag genau kalkuliert werden. Werden nur Einzelbeiträge erhoben,
so ist das Ganze nur eine einfache Addition (Beiträge, die zu dem Alter passen, gruppenweise
summieren). Die Beitragstabelle sieht hier aber einen Gruppentarif für Paare und Familien vor,
der günstiger ist als z. B. Zwei Erwachsenenbeiträge oder auch ein Erwachsenenbeitrag und ein
Kinderbeitrag.
Der Code ist so aufgebaut, dass die äußeren Abfragen immer wieder auf innere Abfragenzugreifen müssen. Die Ursprungsabfrage ist die, die Bei Zeile 7 beginnt und bei Zeile 8 endet.
Hier wird aus der "viw_Gruppe" die Gruppenbezeichnung ausgelesen. Außerdem wird über zwei
korrelierende Unterabfragen zuerst die "beiID" für alle Beiträge, die direkt Mitglieder betreffen("BeitrBezug = 'Mit'), danach dann das dazugehörige Mindestalter "MinAlter" ermittelt.
Um diese Abfrage herum liegt von Zeile 6 bis Zeile 9 die nächste Abfrage. Sie dient lediglichdazu, die Anzahl der gleichen Beiträge zu ermitteln. Deswegen wird die Abfrage nach der"Gruppe", "beiID" und "MinAlter" gruppiert. Zu der innersten Abfrage kommt also lediglich das
Feld mit der Anzahl hinzu. So sind in der Gruppe '3' an dieser Stelle insgesamt 3 Personen mit
der "beiID" = '3', also 18 Jahre und älter.
In der Abfrage, die von Zeile 5 bis Zeile 10 läuft, wird jetzt anhand der vorher ermittelten
Anzahl der gleichen Beiträge durch eine Multiplikation mit der passenden "BeitrHoehe" der Beitrag ermittelt, der in der Gruppe jeweils für eine Altersgruppe zu entrichten wäre.
Die jetzt weiter außen liegende Abfrage von Zeile 4 bis Zeile 11 greift auf die innenliegende
Abfrage mit dem Alias "b" zu. In dieser Abfrage wird über eine CASE WHEN … THEN … ELSE …
END Bedingung ermittelt, wie hoch der Beitrag für eine komplette Gruppe bisher wäre. Ist derBeitrag höher als der Beitrag mit dem "BeitrBezug" = 'MitG', so wird von jeder vorher ermittelten Anzahl 2 abgezogen. In dem Beitrag für Gruppen sind 2 Erwachsene und beliebig viele Kinder und Jugendliche enthalten.
Von Zeile 3 bis Zeile 12 wird in der Abfrage auf diese neu ermittelte "Anzahl_kor1" Bezug
genommen. Alle Mitglieder, bei der sich vorher eine Gruppenmitgliedschaft ergab, stimmt"Anzahl_kor1" nicht mehr mit "Anzahl" überein. Für unter 18-Jährige wird jetzt die Anzahl
Vereinsverwaltung

357

grundsätzlich auf '0' gesetzt, da sie bereits im Gruppenbeitrag enthalten sind. Für sie wird jetzt
der Gruppenbeitrag als "beiID_korr" statt der vorherigen "beiID" notiert. Bei Mitgliedern ab 18
wird die korrigierte Anzahl auf 0 gesetzt, sofern sie nicht größer als 0 ist. Dadurch werden maximal 2 erwachsene Mitglieder mit in die Gruppe aufgenommen.
In der vorletzten Abfrage von Zeile 2 bis Zeile 13 wird dann lediglich die vorherige 0-Setzung insoweit korrigiert, als daraus eine '1' für den Gruppenbeitrag gemacht wird.
In der äußersten Abfrage von Zeile 1 bis Zeile 14 wird jetzt mit Hilfe dieser korrigierten Werte
der Beitrag endgültig berechnet. Würde die Abfrage nicht zum Schluss gruppiert, so würde bei
Familien mit 2 Erwachsenen ein Gruppenbeitrag für die unter 18-jährigen und einer für die
Erwachsenen ausgegeben. Durch die Gruppierung sind hier die Datensätze aber gleich, so dasseiner der Datensätze unterdrückt wird.

viw_Boote_Mitglied

Datenquelle
Tabelle:tbl_Boot, tbl_BootArt, tbl_BootStaender, tbl_BootHalle

Datenziel
Ansicht:

viw_Filter_Mitglieder_komplett_AusEin, viw_Mitglieder_komplett

001 SELECT DISTINCT "c"."mitID",
002( SELECT "tbl_BootArt"."Bootsart" || ': ' || "b"."Bootsname" || ' → '|| "tbl_BootHalle"."Halle_lang" || ': ' ||"tbl_BootStaender"."BootStaend" AS "Boote"
003
FROM ( SELECT "a"."ID", "a"."Bootsname", "a"."bo_aID", "a"."bo_sID","a"."mitID",
004( SELECT COUNT( "mitID" ) FROM "tbl_Boot" WHERE "mitID" ="a"."mitID" AND "ID" <= "a"."ID" ) AS "Nr"005
FROM "tbl_Boot" AS "a" ) AS "b", "tbl_BootArt", "tbl_BootStaender","tbl_BootHalle"
006
WHERE "b"."bo_aID" = "tbl_BootArt"."ID" AND "b"."bo_sID" ="tbl_BootStaender"."ID" AND "tbl_BootStaender"."bo_hID" ="tbl_BootHalle"."ID" AND "mitID" = "c"."mitID" AND "b"."Nr" = 1 ) ||007
COALESCE ( ', ' || ( SELECT "tbl_BootArt"."Bootsart" || ': ' ||"b"."Bootsname" || ' → ' || "tbl_BootHalle"."Halle_lang" || ': ' ||"tbl_BootStaender"."BootStaend" AS "Boote"
008
FROM ( SELECT "a"."ID", "a"."Bootsname", "a"."bo_aID", "a"."bo_sID","a"."mitID",
009( SELECT COUNT( "mitID" ) FROM "tbl_Boot" WHERE "mitID" ="a"."mitID" AND "ID" <= "a"."ID" ) AS "Nr"010
FROM "tbl_Boot" AS "a" ) AS "b", "tbl_BootArt", "tbl_BootStaender","tbl_BootHalle"011
WHERE "b"."bo_aID" = "tbl_BootArt"."ID" AND "b"."bo_sID" ="tbl_BootStaender"."ID" AND "tbl_BootStaender"."bo_hID" ="tbl_BootHalle"."ID" AND "mitID" = "c"."mitID" AND "b"."Nr" = 2 ),'' ) ||
Vereinsverwaltung

358

012

COALESCE ( ', ' || ( SELECT "tbl_BootArt"."Bootsart" || ': ' ||"b"."Bootsname" || ' → ' || "tbl_BootHalle"."Halle_lang" || ': ' ||"tbl_BootStaender"."BootStaend" AS "Boote"
013
FROM ( SELECT "a"."ID", "a"."Bootsname", "a"."bo_aID", "a"."bo_sID","a"."mitID",
014( SELECT COUNT( "mitID" ) FROM "tbl_Boot" WHERE "mitID" ="a"."mitID" AND "ID" <= "a"."ID" ) AS "Nr"015
FROM "tbl_Boot" AS "a" ) AS "b", "tbl_BootArt", "tbl_BootStaender","tbl_BootHalle"
016
WHERE "b"."bo_aID" = "tbl_BootArt"."ID" AND "b"."bo_sID" ="tbl_BootStaender"."ID" AND "tbl_BootStaender"."bo_hID" ="tbl_BootHalle"."ID" AND "mitID" = "c"."mitID" AND "b"."Nr" = 3 ),'' )017 AS "Boote"
018 FROM "tbl_Boot" AS "c"
Mit dieser Ansicht werden dem Mitglied in einem Feld alle Boote zugewiesen, die es im Bootshaus gelagert hat. Eine derartig aufwendige Abfrage ist nur bei der internen HSQLDB notwendig.
Die interne FIREBIRD Datenbank hat dafür die Funktion LIST().
Die Unterabfrage von Zeile 2 bis Zeile 6 wiederholt sich hier nahezu identisch drei mal. Die einzigen Unterschiede sind die im Code mit rot hervorgehobenen Zahlen sowie ab der ersten Wiederholung die Verknüpfung über COALESCE( ', '|| … , '' ).
In der am weitesten innen liegenden Abfrage von Zeile 4 wird dem Boot, das einem einzelnen
Mitglied gehört, eine laufende "Nr" zugewiesen. Dadurch kann aus der Abfrage anschließend
das Boot mit der "Nr" = 1 herausgefiltert werden.
Zeile 3 und 5 umschließen diese Abfrage und geben zuerst einmal eine "ID" des Bootes, den"Bootsname", eine "bo_aID" (für die Bootsart, hier 'K1', 'K2' usw.), eine "bo_sID" (für den Bootsständer, zusammengesetzt aus der Halle und der Ständernummer), eine "mitID" und eine "Nr"aus.
Zeile 2 und 6 dienen dann dazu, diese Unterabfrage mit den Tabellen "tbl_BootArt","tbl_BootHalle", "tbl_BootStaneder" und "tbl_Boot" zu kombinieren und das Boot mit Lagerort
auszulesen, das in der Unterabfrage die "Nr" = 1 zugewiesen bekommen hat.
Über die folgenden gleichlautenden Abfragen werden die Boote mit den Nummern 2 und 3
zugeordnet. Drei Boote sind die Maximalzahl, die jede Gruppe in den bootshallen lagern darf.

viw_Filter_Mitglieder_komplett_AusEin

Vereinsverwaltung

359

Datenquelle
Tabelle:tbl_Mitglied, tbl_Adresse, tbl_Ort, tbl_Konto, tbl_Bank, tbl_Filter

Ansicht:

Ansicht_Datum, viw_Boote_Mitglied, viw_ServVar

Datenziel
Bericht:

rpt_Mitglieder_AusEin, rpt_Mitglieder_AusEin_Konto

Diese Ansicht dient dazu, Austritte und Eintritte in den Verein über einen Bericht dokumentieren zu können. In dem obigen Screenshot sind alle Eintritte aus dem Jahr 2022 dokumentiert. In
der Tabelle "tbl_Filter"."StatDat" ist der 1.1.22 als Startdatum eingestellt. 11
Besondere Probleme können bei so einer Ansicht auftauchen, wenn, wie beim 4. Datensatz(Schulze, Jolantha), eine Person zwar zu einer Gruppe (über das Konto) gehört, aber eine
andere Adresse hat (Auszug zur Ausbildung/zum Studium …). Ist dann auch noch das Festnetztelefon bei der einen Adresse vorhanden und bei der anderen Adresse nicht, dann lassen sicheinige einfache Lösungsmöglichkeiten nicht mehr nutzen.
Die Abfrage gestaltet sich sehr umfangreich, weil neben Konto und Adresse auch die Abteilungen und Boote aufgelistet werden. Für eine solche Auflistung steht für die interne HSQLDB keine
Funktion zur Verfügung. Mit der internen FIREBIRD Datenbank wäre der Code über die Funktion
LIST() entsprechend kürzer.
001 SELECT COALESCE ( "a"."Gruppe_mitID", "a"."ID" ) AS "gruID",
002 COALESCE ( "tbl_Konto"."Nachname" || COALESCE ( ', ' ||"tbl_Konto"."Vorname", '' ), "a"."Nachname" || COALESCE ( ', ' ||"a"."Vorname", '' ) ) AS "Kontoinhaber",
Immer wieder tauchen Abfragen mit COALESCE auf. Wenn der erste Begriff leer ist, dann nimm
den zweiten. Ist der auch leer, dann den dritten usw. Ist in der "tbl_Konto" ein "Nachname"angegeben, dann wird dieser genommen. Der Name aus "tbl_Mitglied" (hier mit dem Alias "a")spielt dann keine Rolle mehr. Gibt es einen Vornamen, so wird der mit einem führenden Komma
an den Nachnamen angehängt. Diese Konstruktion ist notwendig, da bei einem leeren Feld
"Vorname" sonst auch die Verbindung von "Nachname" und "Vorname" über || leer wäre.
003
004
005
006

"tbl_Konto"."IBAN","tbl_Bank"."BIC","tbl_Bank"."Bank",
COALESCE("b"."Straße_Nr", (SELECT "Straße_Nr" FROM "tbl_Adresse" WHERE"mitID" = COALESCE ( "a"."Gruppe_mitID", "a"."ID" ))) AS "Straße_Nr",
007 COALESCE("tbl_Ort"."Postleitzahl"||' '||"tbl_Ort"."Ort", (SELECT"tbl_Ort"."Postleitzahl"||' '||"tbl_Ort"."Ort" FROM "tbl_Ort","tbl_Adresse" WHERE "tbl_Adresse"."ortID" = "tbl_Ort"."ID" AND"tbl_Adresse"."mitID" = COALESCE ( "a"."Gruppe_mitID", "a"."ID" ))) AS"PLZ_Ort",
008 COALESCE("b"."Telefon_fest", (SELECT "Telefon_fest" FROM "tbl_Adresse"
WHERE "mitID" = CASE WHEN "b"."mitID" = "a"."ID" THEN "a"."ID" ELSE
"a"."Gruppe_mitID" END)) AS "Telefon_fest",
Für die Zeilen 6 und 8 ist wichtig, ob in der Abfrage mit dem Alias "b" jeweils ein Wert steht. Ist
dies der Fall, dann wird dieser Wert auch genommen. Ansonsten wird die Adressbindung derGruppe benutzt. Die Abfrage mit dem Alias "b" (weiter unten) ermittelt also eine Adresse, die
für ein Gruppenmitglied gilt, das nicht (mehr) mit den anderen zusammen wohnt.
009
010
011
012
013

"a"."Nachname","a"."Vorname","a"."GebDat",
CASE WHEN "a"."Schwimmer" = TRUE THEN '✅' ELSE '⛔' END AS "Schwimmer","viw_Abteilungen_Mitglied"."Abteilungen",11 Im Feld "Schwimmer" wurde statt des Markierfeldes die Darstellung mit UTF8-Symbolen genutzt. Die
sind dann auch im Bericht zu sehen.

Vereinsverwaltung

360

014
015
016
017

"viw_Boote_Mitglied"."Boote","a"."AusDat","a"."AufDat",
CASE WHEN (SELECT "Austritte" FROM "tbl_Filter" WHERE "ID" = COALESCE( ( SELECT "ID" FROM "viw_ServVar" ), '1' )) = TRUE THEN "a"."AusDat" ELSE
"a"."AufDat" END AS "FilterDat",
018 (SELECT COALESCE("StartDat", CAST(EXTRACT(YEAR FROM CURRENT_DATE)||'-0101' AS DATE)) FROM "tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM
"viw_ServVar" ), '1' )) AS "StartDat",
019 (SELECT COALESCE("EndDat", CAST(EXTRACT(YEAR FROM CURRENT_DATE)||'-12-31'
AS DATE)) FROM "tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM
"viw_ServVar" ), '1' )) AS "EndDat",
020 (SELECT "Austritte" FROM "tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT"ID" FROM "viw_ServVar" ), '1' )) AS "Austritte"
Die Einbindung der Werte aus "tbl_Filter" in den Zeilen 17 bis 20 ist notwendig, damit der
Bericht bei der Ausführung den Zeitraum der Entsprechenden Daten anzeigen kann, auch inder Überschrift erwähnt, ob es sich um Beitritte oder Austritte handelt usw.
021
022
023
024

FROM "tbl_Mitglied" AS "a"LEFT JOIN "tbl_Adresse" AS "b" ON "b"."mitID" = "a"."ID"
LEFT JOIN "tbl_Ort" ON "b"."ortID" = "tbl_Ort"."ID"
LEFT JOIN "tbl_Konto" ON ( "tbl_Konto"."mitID" = "a"."Gruppe_mitID" OR"tbl_Konto"."mitID" = "a"."ID" )025 LEFT JOIN "tbl_Bank" ON "tbl_Konto"."banID" = "tbl_Bank"."ID"026 LEFT JOIN "viw_Abteilungen_Mitglied" ON"viw_Abteilungen_Mitglied"."mitID" = "a"."ID"027 LEFT JOIN "viw_Boote_Mitglied" ON "viw_Boote_Mitglied"."mitID" = "a"."ID"028 WHERE "FilterDat" BETWEEN "StartDat" AND "EndDat"
Zum Schluss wird in Zeile 28 auf die Zeilen 17, 18 und 19 Bezug genommen. In "FilterDat" ist je
nach Art der Filterung entweder das Aufnahmedatum oder das Austrittsdatum des Mitglieds
gefragt. Und dieses Datum soll in dem Bereich liegen, der ebenfalls durch die Filterung, oder
bei leeren Feldern durch Bezug auf den Start des aktuellen Jahres ("StartDat") bzw. das Endedes aktuellen Jahres ("EndDat") begrenzt ist.

viw_Gruppe

Vereinsverwaltung

361

Datenquelle
Tabelle:tbl_Mitglied, tbl_Filter

Ansicht:

viw_ServVar

Datenziel
Ansicht:

viw_Beitrag_Gruppe_kalkuliert

Abfrage:qry_Filter_Gruppe

Formular: Einzelmessung_System
Makro:

Preferences, SubscriptionSave

001
002
003
004
005
006
007
008
009
010
011
012
013

SELECT "ID","Nachname","Vorname","GebDat",
COALESCE("Gruppe_mitID","ID") AS "Gruppe","Geschlecht","AufDat","Schwimmer","Telefon_mobil","Photo","AusDat",
DATEDIFF('yy',"GebDat",CURRENT_DATE) AS "Alter_Jahr",
CASE WHEN (MONTH("GebDat") > MONTH(CURRENT_DATE)) OR ((MONTH("GebDat") =MONTH(CURRENT_DATE)) AND (DAY("GebDat") > DAY(CURRENT_DATE)))
THEN DATEDIFF('yy',"GebDat",CURRENT_DATE)-1
ELSE DATEDIFF('yy',"GebDat",CURRENT_DATE)END
AS "Alter_momentan",
014 CASE WHEN (MONTH("GebDat") >MONTH(COALESCE("Beitrag_Stichtag",CURRENT_DATE))) OR ((MONTH("GebDat")= MONTH(COALESCE("Beitrag_Stichtag",CURRENT_DATE))) AND (DAY("GebDat")> DAY(COALESCE("Beitrag_Stichtag",CURRENT_DATE))))THEN DATEDIFF('yy',"GebDat",COALESCE("Beitrag_Stichtag",CURRENT_DATE))-1
ELSE DATEDIFF('yy',"GebDat",COALESCE("Beitrag_Stichtag",CURRENT_DATE))
END
AS "Alter_Beitrag"
015 FROM "tbl_Mitglied", "tbl_Filter"
016 WHERE "tbl_Filter"."ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),'1' )Die meisten Felder werden direkt aus der Tabelle "tbl_Mitglied" ausgelesen. In Zeile 5 kommt
dann die Gruppenzuweisung hinzu: Steht in "Gruppe_mitID" ein Wert, so wird der als Zuweisung zur Gruppe genommen. Sonst wird die "ID" des Mitgliedes selbst dort angezeigt. Das ist
notwendig, da die "Gruppe_mitID" beim ersten Mitglied einer Gruppe nicht automatischVereinsverwaltung362

geschrieben wird. Erst die anderen Mitglieder werden darüber mit dem ersten Mitglied verbunden.
Das Alter wird auf verschiedene Art berechnet. In Zeile 12 ist die einfachste Berechnung aufgeführt, die so z. B. auch im Sport gilt: Es wird einfach die Jahresdifferenz zwischen dem aktuellen
Datum und dem Geburtsdatum errechnet. Mitglieder, die später im Jahr geboren wurden, werden also erst einmal mit einem zu hohen Alter versehen. In Zeile 13 wird das Alter schließlich
mit Hilfe von Monat und Tag genauer berechnet. Ist der Geburtsmonat größer als der Monat desaktuellen Datums oder der Geburtsmonat gleich und der Tag größer, dann ist das Alter um 1
Jahr kleiner als der Jahresunterschied. In Zeile 14 wird schließlich das Alter zu einem ganz
besonderen Datum, z. B. einem Stichtag zur Weitergabe der Statistik, berechnet. Dieses Feld
"Beitrag_Stichtag" befindet sich in der Tabelle "tbl_Filter". Da kein gleichlautendes Feld in"tbl_Mitglied" vorhanden ist kann die Angabe des Tabellennamens hier entfallen.
In Zeile 16 wird lediglich deutlich gemacht, aus welcher Zeile der "tbl_Filter" gelesen werdensoll. Diese Tabelle wird grundsätzlich für einen Nutzer nur mit einer Zeile beschreiben. Weil das
bei der Tabelle so ist muss auch keine Beziehung zur Tabelle "tbl_Mitglied" erstellt werden. Die
Zeile in "tbl_Filter" existiert auf jeden Fall. Und eine Kombination von z. B. 13 Datensätzen aus"tbl_Mitglied" mit einem Datensatz aus "tbl_Filter" ergibt wieder 13*1=13 Datensätze.

viw_Mitglieder_komplett

Datenquelle
Tabelle:tbl_Mitglied, tbl_Adresse, tbl_Ort, tbl_Konto, tbl_Bank, tbl_Filter

Ansicht:

Ansicht_Datum, viw_Boote_Mitglied, viw_Schluessel_Mitglied, viw_ServVar

Datenziel
Abfrage:qry_Filter_Austritt, qry_Vorstand_aktuell, qry_Vorstand_aktuell_lt_Satzung

Bericht:

rpt_Mitglieder_komplett, rpt_Mitglieder_komplett_Konto

001 SELECT "a"."ID" AS "mitID",
002 COALESCE ( "a"."Gruppe_mitID", "a"."ID" ) AS "gruID",
Vereinsverwaltung

363

003 CASE WHEN "b"."mitID" = "a"."ID" THEN "a"."ID" ELSE "a"."Gruppe_mitID" END
AS "adrID",
004 COALESCE ( "tbl_Konto"."Nachname" || COALESCE ( ', ' ||"tbl_Konto"."Vorname", '' ), "a"."Nachname" || COALESCE ( ', ' ||"a"."Vorname", '' ) ) AS "Kontoinhaber",
005 "tbl_Konto"."IBAN",
006 "tbl_Bank"."BIC",
007 "tbl_Bank"."Bank",
008 COALESCE ( "b"."Straße_Nr", ( SELECT "Straße_Nr" FROM "tbl_Adresse" WHERE"mitID" = COALESCE ( "a"."Gruppe_mitID", "a"."ID" ) ) ) AS "Straße_Nr",
009 COALESCE ( "tbl_Ort"."Postleitzahl" || ' ' || "tbl_Ort"."Ort", ( SELECT"tbl_Ort"."Postleitzahl" || ' ' || "tbl_Ort"."Ort" FROM "tbl_Ort","tbl_Adresse" WHERE "tbl_Adresse"."ortID" = "tbl_Ort"."ID" AND"tbl_Adresse"."mitID" = COALESCE ( "a"."Gruppe_mitID", "a"."ID" ) ) ) AS
"PLZ_Ort",
010 COALESCE ( "b"."Telefon_fest", ( SELECT "Telefon_fest" FROM "tbl_Adresse"
WHERE "mitID" = CASE WHEN "b"."mitID" = "a"."ID" THEN "a"."ID" ELSE
"a"."Gruppe_mitID" END ) ) AS "Telefon_fest",
011 "a"."Nachname",
012 "a"."Vorname",
013 "a"."Geschlecht",
014 "a"."GebDat",
015 "a"."E-Mail",
016 "a"."Telefon_mobil",
017 CASE WHEN "a"."Schwimmer" = TRUE THEN '✅' ELSE '⛔' END AS "Schwimmer",018 "viw_Abteilungen_Mitglied"."Abteilungen",019 "viw_Boote_Mitglied"."Boote",
020 "viw_Schluessel_Mitglied"."Schlüssel",021 "a"."AufDat",
022 "a"."AusDat"
023 FROM "tbl_Mitglied" AS "a"024 LEFT JOIN "tbl_Adresse" AS "b" ON "b"."mitID" = "a"."ID"025 LEFT JOIN "tbl_Ort" ON "b"."ortID" = "tbl_Ort"."ID"026 LEFT JOIN "tbl_Konto" ON ( "tbl_Konto"."mitID" = "a"."Gruppe_mitID" OR"tbl_Konto"."mitID" = "a"."ID" )027 LEFT JOIN "tbl_Bank" ON "tbl_Konto"."banID" = "tbl_Bank"."ID"028 LEFT JOIN "viw_Abteilungen_Mitglied" ON "viw_Abteilungen_Mitglied"."mitID"= "a"."ID"029 LEFT JOIN "viw_Boote_Mitglied" ON "viw_Boote_Mitglied"."mitID" = "a"."ID"030 LEFT JOIN "viw_Schluessel_Mitglied" ON "viw_Schluessel_Mitglied"."mitID" ="a"."ID"
031 WHERE ( "a"."AusDat" IS NULL OR "a"."AusDat" > CURRENT_DATE )032 OR "a"."AusDat" BETWEEN ( SELECT "StartDat" FROM "tbl_Filter" WHERE "ID" =COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ) AND ( SELECT"EndDat" FROM "tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM
"viw_ServVar" ), '1' ) )Diese Ansicht hat sehr viel Ähnlichkeit mit viw_Filter_Mitglieder_komplett_AusEin. Hier werden
lediglich noch einige Felder hinzugefügt. In Zeile 31 wird definiert, dass nur die Mitglieder angezeigt werden, bei denen das Austrittsdatum "AusDat" leer ist oder vom momentanen Datum
aus gesehen größer ist, also in der Zukunft liegt. Da Berichte auch auf einen bestimmten Zeitraum bezogen werden können, werden bei einer Angabe des "StartDat" und des "EndDat" auchdie Mitglieder angezeigt, deren "AusDat" innerhalb dieser Zeit erfolgt ist.

Vereinsverwaltung

364

viw_Reports

Datenquelle
Tabelle:tbl_EinstellungenDatenziel
Formular: frm_Berichte

001 SELECT '😟 📨 Austrittsbestätigung' AS "Title", 'rpt_Austritt' AS"Report", 'Austrittsbestätigung' AS "Sort" FROM "tbl_Einstellungen" WHERE"ID" = TRUE
002 UNION
003 SELECT '📃 📯 Anschriftsliste (Tabellendatei)' AS "Title",'rpt_Anschriften_Tabelle' AS "Report", 'Anschriftsliste (Tabellendatei)'
AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE
004 UNION
005 SELECT '☰☰ Adressetiketten (2-spaltig)' AS "Title",'rpt_Etiketten_2_Spalten' AS "Report", 'Adressetiketten (2-spaltig)' AS"Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE
006 UNION
007 SELECT '☰☰☰ Adressetiketten (3-spaltig)' AS "Title",'rpt_Etiketten_3_Spalten' AS "Report", 'Adressetiketten (3-spaltig)' AS"Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE
008 UNION
009 SELECT '🏅 Urkunde Jubiläum' AS "Title", 'rpt_Jubilaeum_Ehrenurkunde' AS"Report", 'Urkunde Jubiläum' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID"= TRUE
010 UNION
011 SELECT '📆 Dauer Mitgliedschaft' AS "Title", 'rpt_Mitgliedsdauer' AS"Report", 'Dauer Mitgliedschaft' AS "Sort" FROM "tbl_Einstellungen" WHERE"ID" = TRUE
012 UNION
013 SELECT '👪 👥 Vorstand aktuell komplett' AS "Title",'rpt_Vorstand_aktuell' AS "Report", 'Vorstand aktuell komplett' AS "Sort"
FROM "tbl_Einstellungen" WHERE "ID" = TRUE

Vereinsverwaltung

365

014 UNION
015 SELECT '👪 👥 Vorstand aktuell lt. Satung' AS "Title",'rpt_Vorstand_aktuell_lt_Satzung' AS "Report", 'Vorstand aktuell lt.
Satzung' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE
016 UNION
017 SELECT '📊 Statistik - Altergruppen am Stichtag' AS "Title",'rpt_Statistik' AS "Report", 'Statistik - Altergruppen am Stichtag' AS"Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE
018 UNION
019 SELECT '🚣 🛶 Bootsständer' AS "Title", 'rpt_Bootstaender_Belegung' AS"Report", 'Bootsständer' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" =TRUE AND "Bootstaenderverwaltung" = TRUE
020 UNION
021 SELECT '💰 💶 Beitragsübersicht' AS "Title", 'rpt_Beitraege' AS "Report",'Beitragsübersicht' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE
AND "Beitragsverwaltung" = TRUE AND "Kontoverwaltung" = FALSE
022 UNION
023 SELECT '💰 💶 Beitragsübersicht' AS "Title", 'rpt_Beitraege_Konto' AS"Report", 'Beitragsübersicht' AS "Sort" FROM "tbl_Einstellungen" WHERE"ID" = TRUE AND "Beitragsverwaltung" = TRUE AND "Kontoverwaltung" = TRUE
024 UNION
025 SELECT '👪 🗂 Mitgliederliste (komplett, gruppiert)' AS "Title",'rpt_Mitglieder_komplett' AS "Report", 'Mitgliederliste (komplett,gruppiert)' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE AND"Kontoverwaltung" = FALSE
026 UNION
027 SELECT '👪 🗂 Mitgliederliste (komplett, gruppiert)' AS "Title",'rpt_Mitglieder_komplett_Konto' AS "Report", 'Mitgliederliste (komplett,gruppiert)' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE AND"Kontoverwaltung" = TRUE
028 UNION
029 SELECT '📤📥 Mitgliederbewegung (Zeitraum, Austritte?)' AS "Title",'rpt_Mitglieder_AusEin' AS "Report", 'Mitgliederbewegung (Zeitraum,
Austritte?)' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE AND"Kontoverwaltung" = FALSE
030 UNION
031 SELECT '📤📥 Mitgliederbewegung (Zeitraum, Austritte?)' AS "Title",'rpt_Mitglieder_AusEin_Konto' AS "Report", 'Mitgliederbewegung (Zeitraum,
Austritte?)' AS "Sort" FROM "tbl_Einstellungen" WHERE "ID" = TRUE AND"Kontoverwaltung" = TRUE
032 UNION
033 SELECT '🔑 Schlüssel augeliehen' AS "Title", 'rpt_Schluesseluebersicht' AS"Report", 'Schlüssel augeliehen' AS "Sort" FROM "tbl_Einstellungen" WHERE"ID" = TRUE AND "Schluesselverwaltung" = TRUE
034 ORDER BY 3
Diese Ansicht dient dazu, in dem Formular "frm_Berichte" über ein Listenfeld sämtliche Bericht
zugänglich zu machen. Die Anzeige wird durch die "tbl_Einstellungen" beeinflusst, in der ja
bestimmte Module der Datenbank abgewählt werden können. Natürlich macht die Auswahl eine
Berichtes zu den ausgeliehenen Schlüsseln (Zeile 33) nur dann Sinn, wenn in"tbl_Einstellungen" die "Schluesselverwaltung" auf TRUE steht.
Der Ansicht wurden zur besseren Übersicht UTF8-Symbole12 hinzugefügt, die genauso wie
Schrift dann in dem Listenfeld erscheinen. Da diese Symbole allerdings die für uns ersichtliche
Sortierung durcheinander bringen ist in der Ansicht noch eine 3. Spalte eingefügt worden, dieaus der ersten Spalte ohne die Symbole besteht. Wenn Abfragen über UNION zusammengeführt
werden, dann kann für die Sortierung die Spalte angegeben werden. Das erfolgt in Zeile 34.
12 Die komplette Übersicht zu UTF-8-Zeichen gibt es auf https://www.utf8-zeichentabelle.de/Symbole werden kompakt auch hier präsentiert: https://www.utf8icons.com/

Vereinsverwaltung

366

viw_Schluessel_Mitglied

Datenquelle
Tabelle:tbl_MitgliedSchluessel

Datenziel
Ansicht:

viw_Mitglieder_komplett

001 SELECT DISTINCT "c"."mitID",
002( SELECT "SNr"003
FROM ( SELECT "a"."mitID", "a"."ID",004( SELECT COUNT( "mitID" ) FROM "tbl_Mitglied_Schluessel"
WHERE "mitID" = "a"."mitID" AND "ID" <= "a"."ID" AND "RueckDat"
IS NULL AND NOT "Verlust" = TRUE ) AS "Nr"005
FROM "tbl_Mitglied_Schluessel" AS "a" ) AS "b","tbl_Mitglied_Schluessel"
006
WHERE "b"."ID" = "tbl_Mitglied_Schluessel"."ID" AND "b"."Nr" = 1 AND"mitID" = "c"."mitID" ) ||007
COALESCE ( ', ' || ( SELECT "SNr"008
FROM ( SELECT "a"."mitID", "a"."ID",009( SELECT COUNT( "mitID" ) FROM "tbl_Mitglied_Schluessel"
WHERE "mitID" = "a"."mitID" AND "ID" <= "a"."ID" AND "RueckDat"
IS NULL AND NOT "Verlust" = TRUE ) AS "Nr"010
FROM "tbl_Mitglied_Schluessel" AS "a" ) AS "b","tbl_Mitglied_Schluessel"
011
WHERE "b"."ID" = "tbl_Mitglied_Schluessel"."ID" AND "b"."Nr" = 2 AND"mitID" = "c"."mitID" ), '' ) ||
...
022
023
024

COALESCE ( ', ' || ( SELECT "SNr"
FROM ( SELECT "a"."mitID", "a"."ID",( SELECT COUNT( "mitID" ) FROM "tbl_Mitglied_Schluessel"
WHERE "mitID" = "a"."mitID" AND "ID" <= "a"."ID" AND "RueckDat"
IS NULL AND NOT "Verlust" = TRUE ) AS "Nr"025
FROM "tbl_Mitglied_Schluessel" AS "a" ) AS "b","tbl_Mitglied_Schluessel"
026
WHERE "b"."ID" = "tbl_Mitglied_Schluessel"."ID" AND "b"."Nr" = 5 AND"mitID" = "c"."mitID" ), '' )027 AS "Schlüssel"
028 FROM "tbl_Mitglied_Schluessel" AS "c"
Mit dieser Ansicht werden dem Mitglied in einem Feld alle Schlüssel zugewiesen, die es ausgeleihen hat. Eine derartig aufwendige Abfrage ist nur bei der internen HSQLDB notwendig. Die
interne FIREBIRD Datenbank hat dafür die Funktion LIST().
Die Unterabfrage von Zeile 2 bis Zeile 6 wiederholt sich hier nahezu identisch fünf mal. Die einzigen Unterschiede sind die im Code mit rot hervorgehobenen Zahlen sowie ab der ersten Wiederholung die Verknüpfung über COALESCE( ', '|| … , '' ).
In der am weitesten innen liegenden Abfrage von Zeile 4 wird dem Schlüssel, der einem einzelnen Mitglied zugeordnet wurde, eine laufende "Nr" zugewiesen. Dadurch kann aus der Abfrageanschließend der Schlüssel mit der "Nr" = 1 herausgefiltert werden.
Vereinsverwaltung

367

Zeile 3 und 5 umschließen diese Abfrage und geben zuerst einmal eine "mitID", eine "ID" (aus"tbl_Mitglied_Schluessel" und eine "Nr" aus.
Zeile 2 und 6 dienen dann dazu, diese Unterabfrage mit der "tbl_Mitglied_Schluessel" zu kombinieren und die Schlüssel auszulesen, die in der Unterabfrage die "Nr" = 1 zugewiesen bekommen hat.
Über die folgenden gleichlautenden Abfragen werden die Schlüssel mit den Nummern 2 bis 5
zugeordnet. Es können hier also höchstens 5 Schlüssel hintereinander, durch ein Komma
getrennt, in das Feld "Schlüssel" geschrieben werden. In der Praxis dürfte das aber völlig genügen. Tatsächlich gehen wohl viele Vereine inzwischen zu Transpondern und elektronischer Steuerung über, so dass einem Transponder nur noch ein entsprechender Schließbereich zugeordnet wird.

viw_ServVar

Datenquelle
Tabelle:Systemtabellen der jeweiligen Datenbank

Datenziel
Ansicht:

viw_Filter_Mitglieder_komplett_AusEin, viw_Gruppe, viw_Mitglieder_komplett, viw_Statistik_Alter_DetailAbfrage:qry_Filter, qry_Filter_Austritt, qry_Filter_Gruppe, qry_Filter_Mitglied, qry_Filter_Vorstand

Makro:

FilterIDSet, FormStart

001 SELECT "USER_NAME", "SESSION_ID", "SESSION_ID" AS "ID"002 FROM "INFORMATION_SCHEMA"."SYSTEM_SESSIONS"
Diese Ansicht dient lediglich dazu, die aktuelle "SESSION_ID" zu ermitteln und als "ID" für die"tbl_Filter" zu nutzen. Bei der internen HSQLDB ist diese "SESSION_ID" '1', sofern sie überhaupt
zu ermitteln ist. Die Arbeit mit der Session ist bei der Nutzung von Filtertabellen zusammen mit
Serverdatenbanken sinnvoll. So können Filterungen in Abhängigkeit von der aktuellen Datenbankverbindung erfolgen und mehrere Nutzer unabhängig voneinander eine Filterung derDaten vornehmen.

Vereinsverwaltung

368

viw_Statistik_Alter_Detail

Datenquelle
Tabelle:tbl_Mitglied, tbl_Altergruppen, tbl_Filter

Ansicht:

viw_ServVar

Datenziel
Bericht:

rpt_Statistik

001 SELECT "MinAlter" || ' bis ' || "MaxAlter" AS "Alter",
002 'Frauen' AS "Geschlecht",
003 (SELECT COUNT("ID") FROM "tbl_Mitglied" WHERE CASE WHEN
DAYOFYEAR( "GebDat" ) > DAYOFYEAR( ( SELECT "Statistik_Stichtag" FROM
"tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),'1' ) ) )THEN DATEDIFF( 'yy', "GebDat", ( SELECT "Statistik_Stichtag" FROM
"tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),'1' ) ) ) - 1
ELSE DATEDIFF( 'yy', "GebDat", ( SELECT "Statistik_Stichtag" FROM
"tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),'1' ) ) ) END
BETWEEN "a"."MinAlter" AND "a"."MaxAlter" AND "Geschlecht" = 'w' AND( "AusDat" > ( SELECT "Statistik_Stichtag" FROM "tbl_Filter" WHERE "ID" =COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ) OR "AusDat" IS NULL)) AS "Anzahl",
004 ( SELECT "Statistik_Stichtag" FROM "tbl_Filter" WHERE "ID" = COALESCE( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ) AS "Stichtag",
005 "MinAlter"
006 FROM "tbl_Altersgruppen" AS "a"007 UNION
008 SELECT "MinAlter" || ' bis ' || "MaxAlter" AS "Alter",
009 'Männer' AS "Geschlecht",
010 (SELECT COUNT("ID") FROM "tbl_Mitglied" WHERE CASE WHEN
DAYOFYEAR( "GebDat" ) > DAYOFYEAR( ( SELECT "Statistik_Stichtag" FROM
"tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),'1' ) ) )THEN DATEDIFF( 'yy', "GebDat", ( SELECT "Statistik_Stichtag" FROM
"tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),'1' ) ) ) - 1
ELSE DATEDIFF( 'yy', "GebDat", ( SELECT "Statistik_Stichtag" FROM
"tbl_Filter" WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ),Vereinsverwaltung369

'1' ) ) ) END
BETWEEN "a"."MinAlter" AND "a"."MaxAlter" AND "Geschlecht" = 'm' AND( "AusDat" > ( SELECT "Statistik_Stichtag" FROM "tbl_Filter" WHERE "ID" =COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ) OR "AusDat" IS NULL)) AS "Anzahl",
011 ( SELECT "Statistik_Stichtag" FROM "tbl_Filter" WHERE "ID" = COALESCE( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ) AS "Stichtag",
012 "MinAlter"
013 FROM "tbl_Altersgruppen" AS "a"014 ORDER BY "Geschlecht", "MinAlter"
In der "tbl_Altersgruppen" sind für das Anlegen einer Statistik wichtige Altersgruppen zusammengefasst. Diese Ansicht verknüpft diese Altersgruppen mit der Anzahl der tatsächlich vorhandenen Mitglieder im Verein bezogen auf einen bestimmten Stichtag. Anhand dieser Ansichtkann dann ein entsprechendes Diagramm innerhalb eines Berichtes angezeigt werden.
Die Altersgruppen werden in Zeile 3 bzw. 10 mit dem aktuellen Alter bezogen auf den eingetragenen Stichtag verglichen. Entsprechend erfolgt die Einordnung der Mitglieder in die Altersgruppen. Das Mitglied darf allerdings zum Stichtag nicht aus dem Verein ausgetreten sein.
Die Ansicht umfasst dabei alle Altersgruppen, so dass gegebenenfalls auch 0-Werte erscheinen.

AbfragenAbfragen können in Formularen zur weiteren Bearbeitung eingesetzt werden, wenn die Eingabe
von Daten ermöglicht wird. Dies geschieht dadurch, dass auf jeden Fall alle betreffenden Tabellen mit Primärschlüsseln in der Abfrage vertreten sind.
Bei nicht editierbaren Abfragen gibt es Abfragen, die nur über Bearbeiten → SQL-Befehl direkt
ausführen überhaupt ablaufen. Das hängt damit zusammen, dass die GUI den Code, der dort
steht, nicht richtig deuten kann und einfach so direkt an die Datenbank weiterleitet. Solche
Abfragen haben den Vorteil, dass sich der Code auch formatieren lässt. Absätze können eingefügt werden, Kommentare sind auch möglich. Leider haben diese Abfragen aber den Nachteil,dass auch der Report Designer damit nicht so viel anfangen kann. Die gesamte Gruppierung
und Sortierung ist damit im Report Designer unmöglich. Für den Report Designer sind in diesem
Fall Ansichten (Views) deutlich besser geeignet. Den Code bekommt der Report Designer nicht
zu Gesicht. Für ihn ist das Ganze nur eine Tabelle, die nicht beschreibbar ist.
Nicht editierbare Abfragen, die ohne das Umstellen in die direkte Ausführung funktionieren,sind für den Report Designer auch zur Gruppierung und Sortierung brauchbar.

HinweisIn der Liste der Abfragen tauchen auch die Abfragen auf, die anschließend zu Ansichten
gemacht wurden. Die normalen Abfragen beginnen mit "qry_…", die Abfragen, die zu
Ansichten der Datenbank wurden, beginnen mit "v_qry_…".
Diese Maßnahme ist sinnvoll, wenn an der Datenbank noch weiter gearbeitet wird.
Ansichten werden in der Datenbank gespeichert und werden dort bei jeder Änderungauch überprüft. So kann z. B. eine Ansicht nicht mehr geändert werden, wenn eine
andere Ansicht Daten aus dieser Ansicht bezieht. Auch kann eine Tabelle nicht mehr
geändert werden, wenn eine Ansicht Felder dieser Tabelle nutzt. Da hilft es dann nur,
die Ansichten entsprechend zu löschen und anschließend aus den als Abfragen gesicherten Ansichten wieder her zu stellen.
Um dieses Vorgehen zu vereinfachen existiert dafür die Prozedur ViewsErstellen, die allevorhandenen Ansichten löscht und aus den existierenden Abfragen "v_qry_…" anschließend in der korrekten Reihenfolge neue Ansichten erstellt. Deshalb sollten die entsprechenden Abfragen nicht gelöscht werden.

Vereinsverwaltung

370

qry_Adresse_Gruppe

Datenquelle
Tabelle:tbl_Adresse, tbl_Mitglied

Datenziel
Formular: frm_Mitglied

001 SELECT "tbl_Adresse".*,002 "tbl_Mitglied"."ID",003 "tbl_Mitglied"."Gruppe_mitID"
004 FROM "tbl_Adresse", "tbl_Mitglied"
005 WHERE "tbl_Adresse"."mitID" = "tbl_Mitglied"."ID"
Diese Abfrage gibt neben den Feldern der "tbl_Adresse" die Felder "tbl_Mitglied"."ID" und"tbl_Mitglied"."Gruppe_mitID" wieder. Damit zeigt sie die Gruppenzugehörigkeit an, auch wenneine Person wie die mit der "mitID" = '7' eine eigene Adresse hat.
Die Abfrage ist beschreibbar, da die Primärschlüssel beider betroffenen Tabellen in der Abfrage
enthalten sind.

qry_Anschriften_2spaltig

Datenquelle
Ansicht:

viw_Anschrift

Datenziel
Bericht:

001
002
003
004
005
006

Urkunden

SELECT "b"."lfdNr" AS "lfdNr_1","b"."Anschrift" AS "Anschrift_1","c"."lfdNr" + 1 AS "lfdNr_2","c"."Anschrift" AS "Anschrift_2"
FROM ( SELECTCOALESCE ( NULLIF ( "Anrede" || CHAR( 13 ) || CHAR( 10 ), CHAR( 13 ) ||CHAR( 10 ) ), '' ) || COALESCE ( "Vornamen" || ' ', '' ) || "Nachnamen"|| CHAR( 13 ) || CHAR( 10 ) || "Straße_Nr" || CHAR( 13 ) || CHAR( 10 )|| "Postleitzahl" || ' ' || "Ort" AS "Anschrift",

Vereinsverwaltung

371

007

( SELECT COUNT( "adrID" ) FROM "viw_Anschrift" WHERE "Nachnamen" ||"Straße_Nr" || "Postleitzahl" || "Ort" <= "a"."Nachnamen" ||"a"."Straße_Nr" || "a"."Postleitzahl" || "a"."Ort" ) AS "lfdNr"
008 FROM "viw_Anschrift" AS "a" )009 AS "b"
010 LEFT JOIN ( SELECT
011
COALESCE ( NULLIF ( "Anrede" || CHAR( 13 ) || CHAR( 10 ), CHAR( 13 ) ||CHAR( 10 ) ), '' ) || COALESCE ( "Vornamen" || ' ', '' ) || "Nachnamen"|| CHAR( 13 ) || CHAR( 10 ) || "Straße_Nr" || CHAR( 13 ) || CHAR( 10 )|| "Postleitzahl" || ' ' || "Ort" AS "Anschrift",
012( SELECT COUNT( "adrID" ) FROM "viw_Anschrift" WHERE "Nachnamen" ||"Straße_Nr" || "Postleitzahl" || "Ort" <= "a"."Nachnamen" ||"a"."Straße_Nr" || "a"."Postleitzahl" || "a"."Ort" ) -1 AS "lfdNr"
013 FROM "viw_Anschrift" AS "a" )014 AS "c"015 ON "c"."lfdNr" = "b"."lfdNr"
016 WHERE MOD( "b"."lfdNr", 2 ) = 1
Diese Abfrage wird für Etiketten benötigt, die zweispaltig als Bericht gedruckt werden können.
Die beiden Spalten werden über die Unterabfragen von Zeile 5 bis 9 und Zeile 10 bis 14 zusammengestellt.
Die Abfrage mit dem Alias "b" (Zeile 9) gibt dabei alle Datensätze wieder, deren laufende Nummer ungerade ist (Zeile 16 – der Rest der Teilung der "lfdNr" durch '2' ist '1').
Die Abfrage mit dem Alias "c" (Zeile 14), erhält eine "lfdNr", die um 1 vermindert wird. Dadurch
kann "c"."lfdNr" und "b"."lfdNr" gleichgesetzt werden. Sie zeigen so nicht die gleichen Datensätze an. Der Datensatz in "c" zeigt den Inhalt der "lfdNr" = 2 , während in der gleichen Zeile
der Datensatz in "b" den Inhalt der "lfdNr" = 1 anzeigt.
In Zeile 3 wird die Anzeige der "lfdNr_2" wieder auf den ursprünglichen Wert über eine Addition
mit 1 zurückgesetzt.
Da auf den Etiketten bei vielen Namen in einer Gruppe wenig Platz ist soll der Platz für die"Anrede" nicht durch einen Zeilenumbruch CHAR( 13 ) || CHAR( 10 ) belegt werden, wenn
die Anrede leer ist. Der Zeilenumbruch selbst stellt aber bereits einen Inhalt dar, so dass hierein einfaches Vorgehen mit COALSECE() nicht greift. Deshalb muss mit NULLIF dann NULL zugewiesen werden, wenn nur noch der Code für den Zeilenumbruch enthalten ist (Zeile 6 und Zeile
11).
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Sortierung geeignet ist. Dadurch ist sie auch im Bericht für die Sortierung und
Gruppierung nutzbar.

qry_Anschriften_3spaltig

Datenquelle
Tabelle:Ansicht:

viw_Anschrift

Datenziel
Bericht:

rpt_Etiketten_3_Spalten

001 SELECT "b"."lfdNr" AS "lfdNr_1",
Vereinsverwaltung

372

002
003
004
005
006
007
008

"b"."Anschrift" AS "Anschrift_1","c"."lfdNr" + 1 AS "lfdNr_2","c"."Anschrift" AS "Anschrift_2","d"."lfdNr" + 2 AS "lfdNr_3","d"."Anschrift" AS "Anschrift_3"
FROM ( SELECTCOALESCE ( NULLIF ( "Anrede" || CHAR( 13 ) || CHAR( 10 ), CHAR( 13 ) ||CHAR( 10 ) ), '' ) || COALESCE ( "Vornamen" || ' ', '' ) || "Nachnamen"|| CHAR( 13 ) || CHAR( 10 ) || "Straße_Nr" || CHAR( 13 ) || CHAR( 10 )|| "Postleitzahl" || ' ' || "Ort" AS "Anschrift",
009( SELECT COUNT( "adrID" ) FROM "viw_Anschrift" WHERE "Nachnamen" ||"Straße_Nr" || "Postleitzahl" || "Ort" <= "a"."Nachnamen" ||"a"."Straße_Nr" || "a"."Postleitzahl" || "a"."Ort" ) AS "lfdNr"
010 FROM "viw_Anschrift" AS "a" )011 AS "b"
012 LEFT JOIN ( SELECT
013
COALESCE ( NULLIF ( "Anrede" || CHAR( 13 ) || CHAR( 10 ), CHAR( 13 ) ||CHAR( 10 ) ), '' ) || COALESCE ( "Vornamen" || ' ', '' ) || "Nachnamen"|| CHAR( 13 ) || CHAR( 10 ) || "Straße_Nr" || CHAR( 13 ) || CHAR( 10 )|| "Postleitzahl" || ' ' || "Ort" AS "Anschrift",
014( SELECT COUNT( "adrID" ) FROM "viw_Anschrift" WHERE "Nachnamen" ||"Straße_Nr" || "Postleitzahl" || "Ort" <= "a"."Nachnamen" ||"a"."Straße_Nr" || "a"."Postleitzahl" || "a"."Ort" ) -1 AS "lfdNr"
015 FROM "viw_Anschrift" AS "a" )016 AS "c"017 ON "c"."lfdNr" = "b"."lfdNr"
018 LEFT JOIN ( SELECT
019
COALESCE ( NULLIF ( "Anrede" || CHAR( 13 ) || CHAR( 10 ), CHAR( 13 ) ||CHAR( 10 ) ), '' ) || COALESCE ( "Vornamen" || ' ', '' ) || "Nachnamen"|| CHAR( 13 ) || CHAR( 10 ) || "Straße_Nr" || CHAR( 13 ) || CHAR( 10 )|| "Postleitzahl" || ' ' || "Ort" AS "Anschrift",
020( SELECT COUNT( "adrID" ) FROM "viw_Anschrift" WHERE "Nachnamen" ||"Straße_Nr" || "Postleitzahl" || "Ort" <= "a"."Nachnamen" ||"a"."Straße_Nr" || "a"."Postleitzahl" || "a"."Ort" ) -2 AS "lfdNr"
021 FROM "viw_Anschrift" AS "a" )022 AS "d"023 ON "c"."lfdNr" = "d"."lfdNr"
024 WHERE MOD( "b"."lfdNr", 3 ) = 1
Diese Abfrage wird für Etiketten benötigt, die dreispaltig als Bericht gedruckt werden können.
Die drei Spalten werden über die Unterabfragen von Zeile 7 bis 11, Zeile 12 bis 16 und Zeile 18
bis 22 zusammengestellt.
Die Abfrage mit dem Alias "b" (Zeile 11) gibt dabei alle Datensätze wieder, deren laufende
Nummer bei einer Teilung durch 3 einen Rest von '1' ergibt (1, 4, 7 usw., MOD() in Zeile 24)
Die Abfrage mit dem Alias "c" (Zeile 16), erhält eine "lfdNr", die um 1 vermindert wird. Dadurch
kann "c"."lfdNr" und "b"."lfdNr" gleichgesetzt werden. Sie zeigen so nicht die gleichen Datensätze an. Der Datensatz in "c" zeigt den Inhalt der "lfdNr" = 2 , während in der gleichen Zeile
der Datensatz in "b" den Inhalt der "lfdNr" = 1 anzeigt.
In Zeile 3 wird die Anzeige der "lfdNr_2" wieder auf den ursprünglichen Wert über eine Addition
mit 1 zurückgesetzt.
Entsprechend erfolgt eine Korrektur für die 3. Spalte , indem zuerst die "lfdNr" um 2 vermindert
wird (Zeile 20) und dann in der Anzeige wieder um 2 erhöht wird (Zeile 5).
Da auf den Etiketten bei vielen Namen in einer Gruppe wenig Platz ist soll der Platz für die"Anrede" nicht durch einen Zeilenumbruch CHAR( 13 ) || CHAR( 10 ) belegt werden, wenn
die Anrede leer ist. Der Zeilenumbruch selbst stellt aber bereits einen Inhalt dar, so dass hierein einfaches Vorgehen mit COALSECE() nicht greift. Deshalb muss mit NULLIF dann NULL zuge-

Vereinsverwaltung373

wiesen werden, wenn nur noch der Code für den Zeilenumbruch enthalten ist (Zeile 6 und Zeile
11).
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Sortierung geeignet ist. Dadurch ist sie auch im Bericht für die Sortierung und
Gruppierung nutzbar.

qry_Beitraege

Datenquelle
Tabelle:tbl_Mitglied, tbl_Konto, tbl_Bank

Ansicht:

viw_Beitrag

Datenziel
Bericht:

rpt_Beitraege, rpt_Beitraege_Konto

001 SELECT "tbl_Mitglied"."ID" AS "gruID",
002 COALESCE ( "tbl_Konto"."Nachname" || COALESCE ( ', ' ||"tbl_Konto"."Vorname", '' ), "tbl_Mitglied"."Nachname" || COALESCE ( ', '|| "tbl_Mitglied"."Vorname", '' ) ) AS "Kontoinhaber",003 "tbl_Konto"."IBAN",
004 "tbl_Bank"."BIC",
005 "tbl_Bank"."Bank",
006 "a"."Sort",
007 "a"."Beitragsart",008 "a"."Beitrag",009 ( SELECT "Beitrag" FROM "viw_Beitrag" WHERE "gruID" = "a"."gruID" AND"Sort" = 4 ) AS "Quartal",
010 ( SELECT "Beitrag" FROM "viw_Beitrag" WHERE "gruID" = "a"."gruID" AND"Sort" = 5 ) AS "Quartal1",
011 ( SELECT "Beitrag" FROM "viw_Beitrag" WHERE "gruID" = "a"."gruID" AND"Sort" = 6 ) AS "Jahr"
012 FROM "tbl_Mitglied"
013 LEFT JOIN "tbl_Konto" ON "tbl_Konto"."mitID" = "tbl_Mitglied"."ID"014 LEFT JOIN "tbl_Bank" ON "tbl_Konto"."banID" = "tbl_Bank"."ID"
015 LEFT JOIN "viw_Beitrag" AS "a" ON "tbl_Mitglied"."ID" = "a"."gruID"
016 WHERE "a"."Sort" < 4
Hier werden sämtliche Beiträge, die in "viw_Beitrag" ermittelt wurden, zusammen mit dem Kontoinhaber für einen entsprechenden Bericht zusammengeführt. Die Kontoinformationen und die
Beträge in "Quartal", "Quartal1" und "Jahr" lauten in allen Zeilen für das entsprechende Konto
gleich. Nach diesen Informationen wird der Bericht gruppiert, so dass sie im Gruppenkopf bzw.
Gruppenfuß jeweils nur einmal auftauchen.

Vereinsverwaltung

374

Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung und Sortierung geeignet ist. Dadurch ist sie auch im Bericht für die Sortierung und Gruppierung nutzbar.

qry_Bootsstaender

Datenquelle
Tabelle:tbl_Mitglied, tbl_Boot, tbl_BootArt, tbl_BootStaender, tbl_BootHalle

Datenziel
Bericht:

001
002
003
004
005
006

007
008

rpt_Bootstaender_Belegung

SELECT "tbl_BootHalle"."Halle_lang","tbl_BootHalle"."Halle_kurz","a"."bo_hID",( SELECT COUNT( "ID" ) FROM "tbl_BootStaender" WHERE "bo_hID" ="a"."bo_hID" AND "BootStaend" <= "a"."BootStaend" ) AS "lfdNr","a"."BootStaend" AS "BSt1",( SELECT "tbl_BootArt"."Bootsart" || ': ' || "tbl_Boot"."Bootsname" ||CHAR( 13 ) || CHAR( 10 ) || 'gehört: ' || COALESCE( "tbl_Mitglied"."Vorname" || ' ', '' ) || "tbl_Mitglied"."Nachname" AS"Boot"FROM "tbl_Boot", "tbl_BootArt", "tbl_Mitglied", "tbl_BootStaender"
WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID"AND "tbl_Boot"."mitID" = "tbl_Mitglied"."ID"AND "tbl_BootStaender"."ID" = "tbl_Boot"."bo_sID"
AND "tbl_BootStaender"."bo_hID" = "a"."bo_hID"
AND "tbl_BootStaender"."BootStaend" = "a"."BootStaend" ) AS "Boot1",( SELECT "BootStaend" FROM "tbl_BootStaender"
WHERE RIGHT( "BootStaend", 1 ) = '2' AND "bo_hID" = "a"."bo_hID"
AND LEFT( "BootStaend", 1 ) = LEFT( "a"."BootStaend", 1 ) ) AS "BSt2",( SELECT "tbl_BootArt"."Bootsart" || ': ' || "tbl_Boot"."Bootsname" ||CHAR( 13 ) || CHAR( 10 ) || 'gehört: ' || COALESCE( "tbl_Mitglied"."Vorname" || ' ', '' ) || "tbl_Mitglied"."Nachname" AS"Boot"FROM "tbl_Boot", "tbl_BootArt", "tbl_Mitglied", "tbl_BootStaender"
WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID"AND "tbl_Boot"."mitID" = "tbl_Mitglied"."ID"AND "tbl_BootStaender"."ID" = "tbl_Boot"."bo_sID"
AND "tbl_BootStaender"."bo_hID" = "a"."bo_hID"
AND RIGHT( "tbl_BootStaender"."BootStaend", 1 ) = '2'
AND LEFT( "tbl_BootStaender"."BootStaend", 1 ) = LEFT( "a"."BootStaend", 1) ) AS "Boot2",

Vereinsverwaltung

375

...
017 ( SELECT "BootStaend" FROM "tbl_BootStaender"
WHERE RIGHT( "BootStaend", 1 ) = '7' AND "bo_hID" = "a"."bo_hID"
AND LEFT( "BootStaend", 1 ) = LEFT( "a"."BootStaend", 1 ) ) AS "BSt7",018 ( SELECT "tbl_BootArt"."Bootsart" || ': ' || "tbl_Boot"."Bootsname" ||CHAR( 13 ) || CHAR( 10 ) || 'gehört: ' || COALESCE( "tbl_Mitglied"."Vorname" || ' ', '' ) || "tbl_Mitglied"."Nachname" AS"Boot"FROM "tbl_Boot", "tbl_BootArt", "tbl_Mitglied", "tbl_BootStaender"
WHERE "tbl_Boot"."bo_aID" = "tbl_BootArt"."ID"AND "tbl_Boot"."mitID" = "tbl_Mitglied"."ID"AND "tbl_BootStaender"."ID" = "tbl_Boot"."bo_sID"
AND "tbl_BootStaender"."bo_hID" = "a"."bo_hID"
AND RIGHT( "tbl_BootStaender"."BootStaend", 1 ) = '7'
AND LEFT( "tbl_BootStaender"."BootStaend", 1 ) = LEFT( "a"."BootStaend", 1) ) AS "Boot7"
019 FROM "tbl_BootStaender" AS "a", "tbl_BootHalle"
020 WHERE "a"."bo_hID" = "tbl_BootHalle"."ID"021 AND RIGHT( "a"."BootStaend", 1 ) = '1'
Bei dem Beispielverein können in einer Halle maximal 7 Boote nebeneinander liegen. Entsprechend werden hier die Informationen für maximal sieben Bootslagerplätze je Halle nebeneinander erfasst, um anschließend in einem Bericht ausdruckbar zu sein.
In Zeile 4 wird eine laufende Nummer erzeugt, die die Einsortierung der Bootsständer nacheinander ermöglicht. Diese laufende Nummer beginnt für jede Bootshalle neu. Der "BSt1" wird
durch die Bedingung in Zeile 21 festgelegt. Alle anderen Bootsständer ("BSt2" bis "BSt7") werden durch Unterabfragen (Zeile 7 und Zeile 17) zugeordnet.
Das Boot in dem jeweiligen Bootsständer wird mit allen Informationen, nicht nur dem Bootsnamen, angezeigt. Dazu sind die Informationen aus der "tbl_Mitglied", der "tbl_Boot", der"tbl_BootArt" und der "tbl_BootHalle" erforderlich. Der entsprechende Code ist in Zeile 6, 8 und
18 zu sehen.
Der Code für die Bootsständer 3 bis 6 ist entsprechend dem Code für die Bootsständer 2 und 7
aufgebaut.
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung und Sortierung geeignet ist. Dadurch ist sie auch im Bericht für die Sortierung und Gruppierung nutzbar.

qry_Filter

Datenquelle
Tabelle:tbl_Filter

Ansicht:

viw_ServVar

Vereinsverwaltung

376

Datenziel
Formular: Einzelmessung_System, frm_Berichte, frm_Boot, frm_Einstellungen, frm_Mitglied,
frm_Schluessel, frm_Vorstand

001 SELECT *002 FROM "tbl_Filter"
003 WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' )Mit dieser Abfrage wird aus der "tbl_Filter" auch im Mehrbenutzerbetrieb nur der Datensatz
angezeigt, der für die jeweilige Verbindung gilt. Das Ganze geschieht abhängig von der Variablen "ID" in "viw_ServVar", die allerdings bei der internen HSQLDB '1' ist oder manchmal auchgar nicht belegt ist. Daher die feste Zuschreibung: Kommt die Variable nicht vor, so soll sie '1'sein.
Die Abfrage ist beschreibbar, da die Primärschlüssel beider betroffenen Tabellen in der Abfrage
enthalten sind.

qry_Filter_Austritt

Datenquelle
Tabelle:tbl_Einstellungen, qry_Filter

Ansicht:

viw_Mitglieder_komplett, viw_ServVar

Datenziel
Bericht:

Ergebnisliste_Zeit

001 SELECT "tbl_Einstellungen"."Vereinsname",002 "tbl_Einstellungen"."Vereinsname" || ', ' ||"tbl_Einstellungen"."Anschrift" AS "Anschrift_hintereinander",003 "tbl_Einstellungen"."Vereinsname" || CHAR( 13 ) || CHAR( 10 ) ||REPLACE( "tbl_Einstellungen"."Anschrift", ', ', CHAR( 13 ) || CHAR( 10 ) )AS "Anschrift_untereinander",004 "tbl_Einstellungen"."Logo",
005 'IBAN: ' || "tbl_Einstellungen"."IBAN" || ' BIC: ' ||"tbl_Einstellungen"."BIC" || ' Bank: ' || "tbl_Einstellungen"."Bank" AS"Bankverbindung",
006 CASE WHEN "a"."Geschlecht" = 'm' THEN 'Herrn' WHEN "a"."Geschlecht" = 'w'
THEN 'Frau' ELSE '' END || CHAR( 13 ) || CHAR( 10 ) || "a"."Vorname" || '' || "a"."Nachname" || CHAR( 13 ) || CHAR( 10 ) || "a"."Straße_Nr" ||CHAR( 13 ) || CHAR( 10 ) || "a"."PLZ_Ort" AS "Anschrift_Mitglied",
007 CASE WHEN "a"."Geschlecht" = 'm' THEN 'Sehr geehrter Herr' WHEN
"a"."Geschlecht" = 'w' THEN 'Sehr geehrte Frau ' ELSE 'Hallo ' END ||"a"."Vorname" || ' ' || "a"."Nachname" || ',' || CHAR( 13 ) || CHAR( 10 )|| CHAR( 13 ) || CHAR( 10 ) || CASE WHEN "a"."Geschlecht" = 'm' THEN 'Siehaben am ' WHEN "a"."Geschlecht" = 'w' THEN 'Sie haben am ' ELSE 'Du hastam ' END || EXTRACT( DAY FROM "a"."AusDat" ) || '.' || EXTRACT( MONTH FROM
"a"."AusDat" ) || '.' || EXTRACT( YEAR FROM "a"."AusDat" ) || ' den
Austritt aus dem Verein erklärt.' AS "Anrede",

Vereinsverwaltung

377

008 COALESCE ( 'Boote müssen zum Ende der Mitgliedschaft aus den Räumen desVereins entfernt werden. Andernfalls muss weiterhin der Mitgliedsbeitragzusammen mit der Bootsständermiete entrichtet werden.' || CHAR( 13 ) ||CHAR( 10 ) || 'Verzeichnete Boote: ' || "a"."Boote" || CHAR( 13 ) ||CHAR( 10 ), '' ) || COALESCE ( 'Schlüssel müssen zum Ende derMitgliedschaft zurückgegeben werden. Andernfalls werden sie als Verlust
gekennzeichnet. Der Pfand wird dann einbehalten.' || CHAR( 13 ) ||CHAR( 10 ) || 'Verzeichnete Schlüssel: ' || "a"."Schlüssel", '' ) AS
"Zusatz_Boote_Schluessel",009 (SELECT "tbl_Mitglied"."Vorname" || ' ' || "tbl_Mitglied"."Nachname" ||CHAR( 13 ) || CHAR( 10 ) || '(' ||CASE WHEN "tbl_Mitglied"."Geschlecht" = 'm' THEN "a"."Posten_m"
WHEN "tbl_Mitglied"."Geschlecht" = 'w' THEN "a"."Posten_w"
ELSE "a"."Posten" END || ')'FROM "tbl_Mitglied_VorstandPosten", "tbl_Mitglied", "tbl_VorstandPosten"
AS "a"
WHERE "tbl_Mitglied_VorstandPosten"."mitID" = "tbl_Mitglied"."ID"AND "tbl_Mitglied_VorstandPosten"."vorID" = "a"."ID"AND "tbl_Mitglied_VorstandPosten"."EndDat" IS NULL
AND "a"."ID" = COALESCE ( ( SELECT "Unterschrift1ID" FROM "tbl_Filter"
WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ),1 ) )AS "Unterschrift"
010 FROM "viw_Mitglieder_komplett" AS "a", "tbl_Einstellungen"
011 WHERE
012 "a"."mitID" = COALESCE ( ( SELECT "mitID" FROM "tbl_Filter" WHERE "ID" =COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ), "a"."mitID" )013 AND NOT "a"."AusDat" IS NULL
Mit dieser Abfrage wird ein Bestätigungsbrief für einen Austritt bereit gestellt. Für den Brief
wird im Adressfeld die Anschrift in einer Zeile benötigt (Zeile 2), im Briefkopf aber statt mit
Kommatrennung mit einem Zeilenumbruch getrennt untereinander (Zeile 3, Funktion REPLACE).
Danach werden das Logo und die Bankverbindung aus der "tbl_Einstellungen" gelesen.
Für die Anschrift muss die Anrede entsprechend dem Geschlecht ermittelt werden. Auch hiererfolgt anschließend die Zusammenführung zu einem Block über CHAR( 13 ) || CHAR( 10 )mit einem Zeilenumbruch.
Auch die Anrede (Zeile 7) im Brief selbst ist abhängig vom Geschlecht, das aus"viw_Mitglieder_komplett" ausgelesen wird. Hinzu kommt in dieser Anrede, dass das Datum derAustrittserklärung in die deutschsprachige Schreibweise umgewandelt werden muss. Sonst
würde dort ein Datum wie '2022-08-20' erscheinen. Es wird also jeweils Tag, Monat und Jahr
ausgelesen und dann mit einem Punkt verbunden. Dabei wird Tag und Monat nicht unbedingt
zweistellig erfasst.
Gibt es noch Boote in der Bootshalle oder entliehene Schlüssel, so wird darauf in Zeile 8 hingewiesen. Der Text für Boote wird nur angezeigt, wenn "viw_Mitglieder_komplett"."Boote" für den
aktuellen Datensatz nicht leer ist. Entsprechend wird auch beim Schlüssel verfahren. Die Texte
für die verzeichneten Boote bzw. die verzeichneten Schlüssel werden durch eine Einrückung
mit festem Leerzeichen im Bericht übersichtlicher angezeigt.
Die Unterschrift wird anschließend in einem Feld geleistet, das bereits mit Namen und Funktion
der unterschreibenden Person versehen ist.
Die Abfrage ist nicht beschreibbar, da sie auf eine Ansicht Bezug nimmt, die grundsätzlich nichtbeschreibbar ist.

Vereinsverwaltung

378

qry_Filter_Gruppe

Datenquelle
Tabelle:tbl_Filter

Ansicht:

viw_Gruppe, viw_ServVar

Datenziel
Formular: Einzelmessung_System

001 SELECT *002 FROM "viw_Gruppe"
003 WHERE "ID" = COALESCE ( ( SELECT "mitID" FROM "tbl_Filter" WHERE "ID" =COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ), "ID" )Hier wird lediglich die "viw_Gruppe" nach dem Eintrag "mitID" in "tbl_Filter" gefiltert. Ist dort
ein Eintag vorhanden, so wird nur der eine Datensatz der Mitglieds aus ""viw_Gruppe" angezeigt. Ist der Eintrag leer, so werden alle Mitglieder angezeigt.
Die Abfrage ist nicht beschreibbar, da eine Ansicht grundsätzlich nicht beschreibbar ist.

qry_Filter_Mitglied

Vereinsverwaltung

379

Datenquelle
Tabelle:tbl_Mitglied, tbl_Filter

Ansicht:

viw_ServVar

Datenziel
Formular: frm_Boot, frm_Mitglied, frm_Schluessel

001 SELECT *002 FROM "tbl_Mitglied"
003 WHERE "ID" = COALESCE ( ( SELECT "mitID" FROM "tbl_Filter" WHERE "ID" =COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ), "ID" )Hier wird lediglich die "tbl_Mitglied" nach dem Eintrag "mitID" in "tbl_Filter" gefiltert. Ist dort ein
Eintag vorhanden, so wird nur der eine Datensatz angezeigt. Ist der Eintrag leer, so werden alle
Mitglieder angezeigt.
Die Abfrage ist beschreibbar, da die Primärschlüssel beider betroffenen Tabellen in der Abfrage
enthalten sind.

qry_Filter_VorstandDatenquelle
Tabelle:tbl_Mitglied_VorstandPosten, tbl_Filter

Ansicht:

viw_ServVar

Datenziel
Formular: frm_Vorstand

001 SELECT *002 FROM "tbl_Mitglied_VorstandPosten"
003 WHERE "WahlDat" = COALESCE ( ( SELECT "WahlDat" FROM "tbl_Filter" WHERE"ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ), "WahlDat" )Diese Abfrage geht es lediglich darum, aus der entsprechenden Tabelle "tbl_Mitglied_VorstandPosten" die an dem Wahldatum (neu) gewählten Vorstandsmitglieder anzuzeigen. Der Screenshot zeigt hier nur die Tabelle mit den Fremdschlüssel. Das deutet an, dass diese Abfrage als
Grundlage in einem Formular benötigt wird, bei dem die Fremdschlüssel dann durch Listenfelder mit Inhalten dargestellt werden können.
Die Abfrage ist beschreibbar, da die Primärschlüssel beider betroffenen Tabellen in der Abfrage
enthalten sind.

Vereinsverwaltung

380qry_Forms

Datenquelle
Tabelle:tbl_EinstellungenDatenziel
Formular: Einzelmessung_System, frm_Berichte, frm_Boot, frm_Einstellungen, frm_Mitglied,
frm_Schluessel, frm_Vorstand

001 SELECT 'Mitglieder' AS "Title", 'frm_Mitglied' AS "Form" FROM
"tbl_Einstellungen" WHERE "ID" = TRUE
002 UNION
003 SELECT 'Vorstand' AS "Title", 'frm_Vorstand' AS "Form" FROM
"tbl_Einstellungen" WHERE "ID" = TRUE
004 UNION
005 SELECT 'Einstellungen' AS "Title", 'frm_Einstellungen' AS "Form" FROM
"tbl_Einstellungen" WHERE "ID" = TRUE
006 UNION
007 SELECT 'Berichte' AS "Title", 'frm_Berichte' AS "Form" FROM
"tbl_Einstellungen" WHERE "ID" = TRUE
008 UNION
009 SELECT 'Boote' AS "Title", 'frm_Boot' AS "Form" FROM "tbl_Einstellungen"
WHERE "ID" = TRUE AND "Bootstaenderverwaltung" = TRUE
010 UNION
011 SELECT 'Schlüssel' AS "Title", 'frm_Schluessel' AS "Form" FROM
"tbl_Einstellungen" WHERE "ID" = TRUE AND "Schluesselverwaltung" = TRUE
012 UNION
013 SELECT 'Beitrag' AS "Title", 'frm_Beitrag' AS "Form" FROM
"tbl_Einstellungen" WHERE "ID" = TRUE AND "Beitragsverwaltung" = TRUE
Mit dieser Abfrage wird von einem Formular zum anderen navigiert. Die anzuzeigenden Formulare werden danach ausgewählt, ob sie in den Einstellungen ("tbl_Einstellungen") auch angewählt wurden.
In der ersten Spalte stehen die anzuzeigenden Namen, in der zweiten Spalte der Name des Formulars, der so an das Makro zum Öffnen des Formular weiter gegeben wird. Die Sortierung
erfolgt bei einer UNION-Verknüpfung nach der ersten Spalte, sofern nichts anderes festgelegt
wurde.

qry_Jubilaeum

Vereinsverwaltung

381

Datenquelle
Tabelle:tbl_Mitglied, tbl_Einstellungen, tbl_Mitglied_VorstandPosten, tbl_VorstandPosten

Datenziel
Bericht:

rpt_Jubilaeum_Ehrenurkunde

001 SELECT "tbl_Mitglied"."ID",002 "tbl_Mitglied"."Vorname" || ' ' || "tbl_Mitglied"."Nachname" AS "Name",
003 'für ' || DATEDIFF( 'YY', "tbl_Mitglied"."AufDat", CURRENT_DATE ) || 'jährige Mitgliedschaft' AS "Mitgliedsjahre",
004 "tbl_Einstellungen"."Logo",
005 "tbl_Einstellungen"."Vereinsname",
006 "tbl_Einstellungen"."Ort" || ', den' AS "Ort",
007 "b"."Posten" AS "Posten_1",
008 "c"."Posten" AS "Posten_2",
009 DATEDIFF( 'YY', "tbl_Mitglied"."AufDat", CURRENT_DATE ) AS "Jahre"
010 FROM "tbl_Mitglied",
011 "tbl_Einstellungen",012 ( SELECT "tbl_Mitglied"."Vorname" || ' ' || "tbl_Mitglied"."Nachname" ||CHAR( 13 ) || CHAR( 10 ) || '(' ||CASE WHEN "tbl_Mitglied"."Geschlecht" = 'm' THEN "a"."Posten_m"
WHEN "tbl_Mitglied"."Geschlecht" = 'w' THEN "a"."Posten_w"
ELSE "a"."Posten" END || ')' AS "Posten"
FROM "tbl_Mitglied_VorstandPosten", "tbl_Mitglied", "tbl_VorstandPosten"
AS "a"
WHERE "tbl_Mitglied_VorstandPosten"."mitID" = "tbl_Mitglied"."ID"AND "tbl_Mitglied_VorstandPosten"."vorID" = "a"."ID"AND "tbl_Mitglied_VorstandPosten"."EndDat" IS NULL
AND "a"."ID" = COALESCE ( ( SELECT "Unterschrift1ID" FROM "tbl_Filter"
WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ),1 ) )AS "b",
013 ( SELECT "tbl_Mitglied"."Vorname" || ' ' || "tbl_Mitglied"."Nachname" ||CHAR( 13 ) || CHAR( 10 ) || '(' ||CASE WHEN "tbl_Mitglied"."Geschlecht" = 'm' THEN "a"."Posten_m"
WHEN "tbl_Mitglied"."Geschlecht" = 'w' THEN "a"."Posten_w"
ELSE "a"."Posten" END || ')' AS "Posten"
FROM "tbl_Mitglied_VorstandPosten", "tbl_Mitglied", "tbl_VorstandPosten"
AS "a"
WHERE "tbl_Mitglied_VorstandPosten"."mitID" = "tbl_Mitglied"."ID"AND "tbl_Mitglied_VorstandPosten"."vorID" = "a"."ID"AND "tbl_Mitglied_VorstandPosten"."EndDat" IS NULL
AND "a"."ID" = COALESCE ( ( SELECT "Unterschrift2ID" FROM "tbl_Filter"
WHERE "ID" = COALESCE ( ( SELECT "ID" FROM "viw_ServVar" ), '1' ) ),2 ) )AS "c"
014 WHERE "AusDat" IS NULL
015 AND "Jahre" = 70016 OR "Jahre" = 60
017 OR "Jahre" = 50
018 OR "Jahre" = 40
Vereinsverwaltung

382

019 OR "Jahre" = 25
020 ORDER BY "Mitgliedsjahre" DESC
Mit Hilfe dieser Abfrage soll ein Bericht gestartet werden, der zum Ausdruck von Urkunden bei
langjähriger Mitgliedschaft dient. Die erste Spalte enthält die "ID" aus der "tbl_Mitglied".
Danach folgt der Name, wobei hier fest davon ausgegangen wird, dass Vor- und Nachname bei
langjährigen Mitgliedern bekannt ist. In Zeile 3 wird dann der Grundtext festgelegt, in den die
Dauer der Mitgliedschaft eingebunden ist. Diese Dauer der Mitgliedschaft wird über Zeile 9(Definition der Jahre mit dem Alias "Jahre", nicht im Screenshot gezeigt) und die Zeilen 15 bis
19 (Jubiläumsjahre) auf bestimmte Jahre eingeschränkt.
In den Zeilen 4, 5 und 6 werden Informationen aus der Tabelle "tbl_Einstellungen" geholt, die
zur grafischen Gestaltung ("Logo"), als Vereinsbezeichnung oder zur Gestaltung des Datumseintrags sinnvoll sind.
Am Aufwendigsten ist es, in den Unterschriftszeilen die beiden Vorstandsmitglieder mit Posten
zu ermitteln und als "Posten_1" und "Posten_2" für die Unterschrift nutzen zu können. Deren
Ermittlung in Zeile 12 und 13 ist nahezu identisch. In Zeile 12 wird die erste Beschriftungzusammengesetzt, die auf dem Eintag in "tbl_Filter" oder der "ID" = '1' aus den Vorstandsposten beruht. Entsprechend wird in Zeile 13 das gleiche für die zweite Beschriftung vollzogen.
Über die CASE WHEN … THEN … ELSE – Formulierung wird ermittelt, welche Vorstandspostenbezeichnung aus der Liste der Mitglieder genommen werden soll. Ist weder 'w' noch 'm' in der Mitgliedstabelle verzeichnet, so wird einfach die Standardbezeichnung "tbl_VorstandsPosten"."Posten" gewählt.
Eine Verknüpfung der Tabellen ist nicht erforderlich, da die "tbl_Einstellungen" nur einen Datensatz enthält. Dadurch enthält auch die Verbindung mit "tbl_Mitglied" nur genau so viele Datensätze, wie "tbl_Mitglied" liefern kann.
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung und Sortierung geeignet ist.

qry_Mitglied_Konto

Vereinsverwaltung

383

Datenquelle
Tabelle:tbl_Konto, tbl_Bank, tbl_Mitglied

Datenziel
Formular: frm_Mitglied

001 SELECT "a"."ID",002 COALESCE ( 'IBAN: ' || "tbl_Konto"."IBAN" || CHAR( 13 ) || CHAR( 10 ) ||'bei: ' || "tbl_Bank"."Bank" || ', BIC: ' || "tbl_Bank"."BIC" ||CHAR( 13 ) || CHAR( 10 ) || 'Kontoinhaber: ' ||COALESCE ( "tbl_Konto"."Nachname" ||COALESCE ( ', ' || "tbl_Konto"."Vorname", '' ),"a"."Nachname" || COALESCE ( ', ' || "a"."Vorname", '' )003 ), 'keine eigene Kontoverbindung vorhanden.' || CHAR( 13 ) || CHAR( 10 )|| 'Kontoverbindung von: ' ||( SELECT "Nachname" || COALESCE ( ', ' || "Vorname", '' )FROM "tbl_Mitglied" WHERE "ID" = "a"."Gruppe_mitID" ) )AS "Konto"
004 FROM "tbl_Mitglied" AS "a"005 LEFT JOIN "tbl_Konto" ON "a"."ID" = "tbl_Konto"."mitID"
006 LEFT JOIN "tbl_Bank" ON "tbl_Konto"."banID" = "tbl_Bank"."ID"
Hier werden sämtliche Kontoverbindungen aufgelistet. Bei Gruppenmitgliedern, deren Beitragvon der Kontoverbindung eines anderen Mitglieds erhoben wird, wird aufgelistet, welche Kontoverbindung genutzt werden kann (Zeile 3). Für den unteren Datensatze im Screenshot ist nochkeine Kontoverbindung registriert. Bei Datensätzen, die eine Kontoverbindung enthalten, wird
die komplette Verbindung mit Kontoinhaber aufgelistet.
Bei der Zusammenführung der Daten mittels || taucht hier immer wieder die Kombination
CHAR( 13 ) || CHAR( 10 ) auf. Sie dient dazu, einen Zeilenumbruch zu erzeugen.
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung und Sortierung geeignet ist.

qry_Mitgliedsdauer

Datenquelle
Tabelle:tbl_Mitglied

Datenziel
Ansicht:

001
002
003
004
005
006
007

rpt_Mitgliedsdauer

SELECT "ID","Nachname","Vorname","AufDat",
DATEDIFF( 'YY', "AufDat", CURRENT_DATE ) AS "Mitgliedsjahre"
FROM "tbl_Mitglied"
WHERE "AufDat" IS NOT NULL

Vereinsverwaltung

384

008 AND ( "AusDat" IS NULL OR "AusDat" > CURRENT_DATE )009 AND DATEDIFF( 'YY', "AufDat", CURRENT_DATE ) > 9
010 ORDER BY "AufDat" ASC
Hier wird eine Übersicht nach der Dauer der Mitgliedschaft erzeugt. Die Mitgliedsjahre werden
einfach als Jahresunterschied zum Aufnahmedatum berechnet. Dabei dürfen die Mitglieder
nicht ausgetreten sein (Zeile 8). Auch sollen nur Mitglieder aufgelistet werden, die wenigstens
9 Jahre im Verein sind. Die Übersicht wird also auf langjährige Mitglieder eingegrenzt.
Die Abfrage ist beschreibbar, da der Primärschlüssel "ID" der betroffenen Tabelle in der Abfrage
enthalten ist.

qry_Schluessel_Uebersicht

Datenquelle
Tabelle:tbl_MitgliedSchluessel, tbl_Mitglied

Datenziel
Bericht:

rpt_Schluesseluebersicht

001 SELECT "tbl_Mitglied_Schluessel"."SNr",002 "tbl_Mitglied"."Nachname" || ', ' || "tbl_Mitglied"."Vorname" AS "Name"
003 FROM "tbl_Mitglied_Schluessel", "tbl_Mitglied"
004 WHERE "tbl_Mitglied_Schluessel"."mitID" = "tbl_Mitglied"."ID"005 AND "tbl_Mitglied_Schluessel"."RueckDat" IS NULL
006 AND NOT "tbl_Mitglied_Schluessel"."Verlust" = TRUE
007 ORDER BY "tbl_Mitglied_Schluessel"."SNr" ASC, "Name" ASC
Mit der Abfrage werden alle ausgegebenen Schlüssel und die Nahmen der entsprechenden Entleiher aufgelistet. Die Schlüssel dürfen nach der Bedingung von Zeile 5 nicht mit einem Rückgabedatum "RueckDat" versehen sein. Außerdem dürfen sie nicht als verloren gekennzeichnet
sein (Zeile 6). Die Liste wird sortiert nach den Schlüsselnummern "SNr" und dann nach den
Namen.
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung geeignet ist. Eine Änderung der Sortierung ist nur bedingt möglich, da die
bereits in der Abfrage enthalten ist. Die Sortierung für die "SNr" kann lediglich durch die GUI
umgekehrt werden. Die Sortierung nach dem Feld "Name" greift nur dann, wenn die "SNr" beiDatensätzen gleich ist, im Screenshot also bei den beiden Schlüsseln 'GS 2'.

Vereinsverwaltung

385qry_Vorstand_aktuell

Datenquelle
Tabelle:tbl_VorstandPosten, tbl_Mitglied_VorstandPosten

Ansicht:

viw_Mitglieder_komplett

Datenziel
Bericht:

rpt_Vorstand_aktuell

001
002
003
004
005
006
007
008

SELECT "tbl_VorstandPosten"."ID","tbl_VorstandPosten"."Posten","a"."Nachname","a"."Vorname","a"."Telefon_fest" AS "Telefon","tbl_Mitglied_VorstandPosten"."WahlDat"
FROM "tbl_VorstandPosten"
LEFT JOIN "tbl_Mitglied_VorstandPosten" ON"tbl_Mitglied_VorstandPosten"."vorID" = "tbl_VorstandPosten"."ID"009 LEFT JOIN "viw_Mitglieder_komplett" AS "a" ON"tbl_Mitglied_VorstandPosten"."mitID" = "a"."mitID"
010 WHERE "tbl_Mitglied_VorstandPosten"."EndDat" IS NULL
011 ORDER BY "tbl_VorstandPosten"."ID" ASC
Mit dieser Abfrage werden alle aktuellen Vorstandsmitglieder aufgelistet. Als Zusatzinformationen zu den Namen ist hier die Telefonnummer aus dem Festnetz und das Wahldatum enthalten.
Die Datensätze aus "tbl_VorstandPosten" werden komplett angezeigt, auch wenn nicht alle Posten besetzt sind. Alle anderen Datenquellen sind mit dieser Tabelle über einen LEFT JOIN verbunden. Das Auslesen der Mitgliedsdaten erfolgt anhand der ausführlichen Auflistung in"viw_Mitglieder_komplett".
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung geeignet ist. Eine Änderung der Sortierung ist nicht möglich, da die
bereits in der Abfrage enthalten ist. Die Sortierung für die "ID" kann lediglich durch die GUI
umgekehrt werden.

Vereinsverwaltung

386

qry_Vorstand_aktuell_lt_Satzung

Datenquelle
Tabelle:tbl_VorstandPosten, tbl_Mitglied_VorstandPosten

Ansicht:

viw_Mitglieder_komplett

Datenziel
Bericht:

rpt_Vorstand_aktuell_lt_Satzung

001
002
003
004

SELECT "tbl_VorstandPosten"."ID","tbl_VorstandPosten"."Posten","a"."Nachname" || COALESCE ( ', ' || "a"."Vorname", '' ) AS "Name",
COALESCE ( 'fest: ' || "a"."Telefon_fest" || ' ', '' ) || COALESCE( 'mobil: ' || "a"."Telefon_mobil", '' ) AS "Telefon",005 "a"."E-Mail",
006 "a"."Straße_Nr" || ', ' || "a"."PLZ_Ort" AS "Anschrift"
007 FROM "tbl_VorstandPosten"
008 LEFT JOIN "tbl_Mitglied_VorstandPosten" ON"tbl_Mitglied_VorstandPosten"."vorID" = "tbl_VorstandPosten"."ID"009 LEFT JOIN "viw_Mitglieder_komplett" AS "a" ON"tbl_Mitglied_VorstandPosten"."mitID" = "a"."mitID"
010 WHERE "tbl_Mitglied_VorstandPosten"."EndDat" IS NULL
011 AND "tbl_VorstandPosten"."Posten_laut_Satzung" = TRUE
012 ORDER BY "tbl_VorstandPosten"."ID" ASC
Der Vorstand lt. Satzung besteht aus den Mitgliedern, die auch regelmäßig zu Vorstandssitzungen eingeladen werden. Deshalb sind hier nicht nur Namen sondern auch die Kontaktdaten wie
Telefon, E-Mail und Anschrift aufgelistet.
Die Datensätze aus "tbl_VorstandPosten" werden komplett angezeigt, auch wenn nicht alle Posten besetzt sind. Alle anderen Datenquellen sind mit dieser Tabelle über einen LEFT JOIN verbunden. Das Auslesen der Mitgliedsdaten erfolgt anhand der ausführlichen Auflistung in"viw_Mitglieder_komplett".
Es werden nach den Bedingungen aus Zeile 10 und Zeile 11 nur die Vorstandsmitglieder angezeigt, bei denen in "tbl_Mitglied_VorstandPosten" kein "EndDat" steht und in "tbl_VorstandPosten" der Eintrag für "Posten_laut_Satzung" ausgewählt wurde, also auf TRUE steht.
Die Abfrage ist nicht beschreibbar, wird aber auch nicht im direkten SQL-Modus ausgeführt, sodass sie zur Filterung geeignet ist. Eine Änderung der Sortierung ist nicht möglich, da die
bereits in der Abfrage enthalten ist. Die Sortierung für die "ID" kann lediglich durch die GUI
umgekehrt werden.

Vereinsverwaltung

387

Formulare
frm_Beitrag

1 frmFilter (Abfrage: qry_Filter)2

frmMitglied (Abfrage:
frmGruppe (Ansicht:
2.1
qry_Filter_Gruppe)
viw_Gruppe)

2.1.1

FrmGruppeBeitrag (Tabelle:
tbl_Gruppe_Beitrag)

2.1.2

FrmBeitragsuebersicht (Ansicht:
viw_Beitrag)

1 Listenfeld Formularauswahl (Abfrage: qry_Forms)
Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

2.1.1

Nach der Datensatzaktion

Speichern.SubscriptionChanged

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

1

Listenfeld Mitglied (Status geändert)

Filter.Filtern

1

Schaltfläche Beitrag zum Stichtag berechnen(Aktion ausführen)

Speichern.SubscriptionSave

Vereinsverwaltung

388

SQL-Code in Listenfeldern
1

Mitglied
tbl_Mitglied

001 SELECT "Nachname"||COALESCE(', '||"Vorname",'') AS"Name", "ID"002 FROM "tbl_Mitglied"
003 ORDER BY "Name"

2.1.1

Beitragsart
tbl_Beitrag

001 SELECT "BeitrArt" || ' → monatl. ' ||REPLACE( "BeitrHoehe", '.', ',' ) || ' €' AS"Beitrag", "ID"002 FROM "tbl_Beitrag"
003 WHERE ( "BeitrBezug" = 'Mit' OR "BeitrBezug" ='MitG' )Formularfilter
2.1

001 ("AusDat" > EXTRACT(YEAR FROM CURRENT_DATE)||'-12-31'
002 OR "AusDat" IS NULL)

Mit diesem Formular wird die Beitragsermittlung durchgeführt. Der Beitrag kann berechnet werden. Falls dabei aber Fehler auftauchen oder eine Korrektur gewünscht ist (Beitragsermäßigung), dann kann das im Tabellenkontrollfeld links unten (2.1.1) erledigt werden.

frm_Berichte

1 frmFilter (Abfrage: qry_Filter)
1 Listenfeld Formularauswahl (Abfrage: qry_Forms)

Vereinsverwaltung389

Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

1

Listenfeld Mitglied (Status geändert)

Filter.Filtern

1

Listenfeld Bericht (Status geändert)

Filter.Filtern

1

Schaltfläche Bericht starten (Aktion ausführen)

Druck.ReportStart

SQL-Code in Listenfeldern
1

Bericht
viw_Reports

001 SELECT "Title", "Report"
002 FROM "viw_Reports"

1

Austritt Mitglied
tbl_Mitglied

001 SELECT "Nachname" ||COALESCE ( ', ' || "Vorname", '' ) AS "Name", "ID"002 FROM "tbl_Mitglied"
003 WHERE NOT "AusDat" IS NULL ORDER BY "Name" ASC1

1. Unterschrift von(2. Unterschrift entsprechend)
tbl_Mitglied_
VorstandPosten
tbl_VorstandPosten
tbl_Mitglied

001
002
003
004
005
006
007
008
009
010
011

SELECT CASE WHEN "tbl_Mitglied"."Geschlecht" = 'm'
THEN "tbl_VorstandPosten"."Posten_m"
WHEN "tbl_Mitglied"."Geschlecht" = 'w'
THEN "tbl_VorstandPosten"."Posten_w"
ELSE "tbl_VorstandPosten"."Posten" END ||'('||"tbl_Mitglied"."Nachname"||COALESCE(','||"tbl_Mitglied"."Vorname",'')||')' AS "Posten","tbl_VorstandPosten"."ID"FROM "tbl_Mitglied_VorstandPosten", "tbl_Mitglied","tbl_VorstandPosten"
WHERE "tbl_Mitglied_VorstandPosten"."mitID" ="tbl_Mitglied"."ID"AND "tbl_Mitglied_VorstandPosten"."vorID" ="tbl_VorstandPosten"."ID"AND "EndDat" IS NULLORDER BY "tbl_VorstandPosten"."ID"

Dieses Formular dient zum Start sämtlicher Berichte: Für manche Berichte sind noch Voreinstellungen erforderlich. Soll z. B. die vorausgewählte «Mitgliederbewegung» ausgegeben werden,
so sind Einstellungen in der Gruppe «Mitgliederbewegung» notwendig. Dabei handelt es sichum das «Startdatum» und das «Enddatum» für den Bericht. Sollen statt der Beitritte die Austritte angezeigt werden, so kann das durch einen Klick auf das entsprechende Markierfeld erfolgen.

Vereinsverwaltung390frm_Boot

1 frmFilter (Abfrage: qry_Filter)2 frmMitglied (Abfrage: qry_Filter_Mitglied)

2.1 frmGruppe (Tabelle: tbl_Boot)

1 Listenfeld Formularauswahl (Abfrage: qry_Forms)
Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

2

Nach dem Datensatzwechsel

Eingabekontrolle.NewBoatAllowed

2.1

Nach der Datensatzaktion

Eingabekontrolle.NewBoatAllowed

2.1

Nach dem Datensatzwechsel

Eingabekontrolle.SelectBoat

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

1

Listenfeld Mitglied (Status geändert)

Filter.Filtern

SQL-Code in Listenfeldern
1

Mitglied
tbl_Mitglied

001 SELECT "Nachname"||COALESCE(', '||"Vorname",'') AS"Name", "ID"002 FROM "tbl_Mitglied"
003 ORDER BY "Name"

2.1

Bootsart (2*)
tbl_BootArt

001 SELECT "Bootsart", "ID"002 FROM "tbl_BootArt"
003 ORDER BY "Bootsart" ASC

Vereinsverwaltung

391

2.1

Bootsständer
(2*)
tbl_BootStaender
tbl_BootHalle

001 SELECT "tbl_BootHalle"."Halle_lang"||' →'||"tbl_BootStaender"."BootStaend" AS "Bootstaender","tbl_BootStaender"."ID"002 FROM "tbl_BootStaender", "tbl_BootHalle"
003 WHERE "tbl_BootStaender"."bo_hID" ="tbl_BootHalle"."ID"

Hier geht es darum, Booten in den Hallen entsprechende Lagerplätze zuzuweisen. Dabei sollen
nur die Lagerplätze anwählbar sein, die wirklich frei sind. Deswegen ist das Listenfeld für die
Auswahl der Bootsständer entsprechend konfiguriert und wird bei jedem Datensatzwechsel
aktualisiert.
Auch eine Beschränkung der Bootsplätze ist möglich. So wird bei der Datenbank davon ausgegangen, dass pro Beitragszahler maximal ein Canadier (größeres Boot mit Stechpaddeln) oderdrei Kajaks (kleinere Boote mit Doppelpaddeln) gelagert werden können. In dem obigen Formular erscheint deswegen die Meldung, dass die maximale Anzahl an Bootsständern ausgeschöpft
ist. Zwei Boote sind dem ausgewählten Mitglied zugewiesen. Es wird also noch ein drittes Boot
bei einem anderen Mitglied der Gruppe registriert sein.

Vereinsverwaltung

392

frm_Einstellungen

Vereinsverwaltung393

1 frmFilter (Abfrage: qry_Filter)2 frmEinstellungen (Tabelle: tbl_Einstellungen)
3.1 frmBeitragStaffel (Tabelle: tbl_Beitrag_Staffel)
3 frmBeitrag (Tabelle: tbl_Beitrag)

3.2 FrmAbteilung (Tabelle: tbl_Abteilung)
3.3 FrmBootArt (Tabelle: tbl_BootArt)

1 Listenfeld Formularauswahl (Abfrage: qry_Forms)
Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

2

Nach der Datensatzaktion

Backup.Preferences3

Beim Laden

Design.PreferencesHide

3.1

Beim Laden

Design.PreferencesHide

3.2

Beim Laden

Design.PreferencesHide

3.3

Beim Laden

Design.PreferencesHide

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

2

Maskiertes Feld IBAN (Text modifiziert)

Eingabekontrolle.IBAN_Length_Change

2

Maskiertes Feld IBAN (Bei Fokusverlust)

Eingabekontrolle.IBAN_Test

Formularfilter
2001 ( "tbl_Einstellungen"."ID" = TRUE )Dieses Formular ist vom Screenshot her zu breit, so dass es in 2 Abschnitten unterteilt untereinander dargestellt wurde.
Hier geht es um Standardeinstellungen, die die Datenbank auch so gestalten können, dasbestimmte Module ausgeblendet werden und nicht weiter zur Verfügung stehen. Die Einstellungen (siehe oberer Screenshot) werden in einer einzeiligen Tabelle gespeichert.
Daneben ist eine Eingabemöglichkeit für die Beitragsstruktur. Es gibt hier die Möglichkeit, nicht
nur Standardbeiträge in eine Tabelle ein zu geben, sondern auch eine Staffelung von Beiträgenin einer separaten Tabelle ab zu speichern.

Vereinsverwaltung

394

frm_Mitglied

Abbildung 6: Dieses Formular ist von der Breite her im Screenshot nicht komplett darstellbar. Hier die
Standardansicht für eine Person, die sowohl eine eigene Adresse als auch ein Konto angegeben hat.

Vereinsverwaltung

395

Abbildung 7: Rechts von dem vorhergehenden Screenshot ist noch das Bild des Mitglieds mit eingeblendet. Daneben als Unterformular die Übersicht über die Abteilungen. Die Formulare für die Orte und die
Banken werden über Buttons im Formular separat angezeigt und wieder ausgeblendet.

Vereinsverwaltung

396

Abbildung 8: Ist unter Gruppe (Familie) eine Adresse einer anderen Person ausgewählt worden, so
erscheint statt der Adresseingabe und der Kontoeingabe lediglich ein Verweis, welches Konto dieser Person über die Gruppe zugeordnet wurde.

Abbildung 9: Hat die Person eine eigene Adresse, wird aber über das Konto einer Gruppe zugeordnet, so
wird zuerst die Adresse angegeben und dort dann die Zuordnung zum Konto und damit zur Gruppe vorgenommen.

Vereinsverwaltung397

1 frmFilter (Abfrage: qry_Filter)
2.1 frmAdresse (Abfrage: Abfrage_Start)
2

2.2 frmKonto (Tabelle: tbl_Konto)

frmMitglied (Abfrage:
qry_Filter_Mitglied)

2.3 frmAbteilung (Tabelle: tbl_Mitglied_Abteilung)2.4 frmGruppe (Abfrage: qry_Mitglied_Konto)

3 frmOrt (Tabelle: tbl_Ort)
4 frmBank (Tabelle: tbl_Bank)
1 Listenfeld Formularauswahl (Abfrage: qry_Forms)
Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

2

Nach dem Datensatzwechsel

Design.AfterRecordChange

2.1

Nach dem Datensatzwechsel

Design.BoolStart

3

Nach der Datensatzaktion

Speichern.ListboxContentChange

4Nach der Datensatzaktion

Speichern.ListboxContentChange

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

1

Listenfeld Mitglied (Status geändert)

Filter.Filtern2

Schaltfläche Schwimmer (Taste gedrückt)Design.BoolChangeKey

2

Schaltfläche Schwimmer (Maustaste gedrückt)Design.BoolChange

2

Schaltfläche Nichtdruck (Taste gedrückt)Design.BoolChangeKey

2

Schaltfläche Nichtdruck (Maustaste gedrückt)Design.BoolChange

2

Listenfeld Gruppe (Status geändert)

Speichern.NewGroup

2.1

Schaltfläche Nichtdruck (Taste gedrückt)Design.BoolChangeKey

2.1

Schaltfläche Nichtdruck (Maustaste gedrückt)Design.BoolChange

2.1

Grafische Schaltfläche (Maustaste gedrückt

Speichern.ShowTableControl

2.1

Listenfeld Gruppe (Status geändert)

Speichern.NewGroup

2.2

Maskiertes Feld IBAN (Text modifiziert)

Eingabekontrolle.IBAN_Length_Change

2.2

Maskiertes Feld IBAN (Bei Fokusverlust)

Eingabekontrolle.IBAN_Test

2.2

Grafische Schaltfläche (Maustaste gedrückt

Speichern.ShowTableControl

3

Schaltfläche (Aktion ausführen)

Speichern.ShowTableControl

4

Schaltfläche (Aktion ausführen)

Speichern.ShowTableControl

SQL-Code in Listenfeldern
1

Mitglied
tbl_Mitglied

Vereinsverwaltung

001 SELECT "Nachname"||COALESCE(', '||"Vorname",'') AS"Name", "ID"002 FROM "tbl_Mitglied"
003 ORDER BY "Name"

398

2

Gruppe(Familie)
tbl_Einstellungentbl_Adresse
tbl_Orttbl_Mitglied

001 SELECT DISTINCT '' AS "Gruppe", NULL AS "ID", -1 AS"Sort"FROM "tbl_Einstellungen"
002 UNION
003 SELECT DISTINCT 'Neu: Personengruppe existiert nochnicht.' AS "Gruppe", NULL AS "ID", 0 AS "Sort"
FROM "tbl_Einstellungen"
004 UNION
005 SELECT COALESCE ( "tbl_Adresse"."Straße_Nr" || ' ' ||"tbl_Ort"."Postleitzahl" || ' ' || "tbl_Ort"."Ort",'keine Adresse' ) || COALESCE ( ', Festnetz: ' ||"tbl_Adresse"."Telefon_fest", ', kein Festnetztelefon') AS "Gruppe", "tbl_Mitglied"."ID", 1 AS SORTFROM "tbl_Mitglied", "tbl_Adresse", "tbl_Ort"
WHERE "tbl_Mitglied"."ID" = "tbl_Adresse"."mitID"
AND "tbl_Adresse"."ortID" = "tbl_Ort"."ID"
AND ("tbl_Mitglied"."Gruppe_mitID" IS NULL
OR "tbl_Mitglied"."ID" ="tbl_Mitglied"."Gruppe_mitID")
006 ORDER BY "Sort", "Gruppe"

2.1

PLZ Orttbl_Ort001 SELECT COALESCE("Land"||' ','')||"Postleitzahl" || ' '|| "Ort" AS "Ort", "ID"002 FROM "tbl_Ort"
003 ORDER BY "Ort" ASC

2.1

Gruppe(Familie) –nur Konto
tbl_Einstellungentbl_Konto
tbl_Bank
tbl_Mitglied

001 SELECT DISTINCT '' AS "Gruppe", NULL AS "ID", -1 AS"Sort"FROM "tbl_Einstellungen"
002 UNION
003 SELECT DISTINCT 'Neu: Konto existiert noch nicht.' AS"Gruppe", NULL AS "ID", 0 AS "Sort"
FROM "tbl_Einstellungen"
004 UNION
005 SELECT 'IBAN: '||"tbl_Konto"."IBAN"||', BIC:'||"tbl_Bank"."BIC"||', Bank: '||"tbl_Bank"."Bank" AS"Gruppe", "tbl_Mitglied"."ID", 1 AS SORTFROM "tbl_Mitglied", "tbl_Konto", "tbl_Bank"
WHERE "tbl_Mitglied"."ID" = "tbl_Konto"."mitID"
AND "tbl_Konto"."banID" = "tbl_Bank"."ID"
AND ("tbl_Mitglied"."Gruppe_mitID" IS NULL
OR "tbl_Mitglied"."ID" ="tbl_Mitglied"."Gruppe_mitID")
006 ORDER BY "Sort", "Gruppe"

2.2

Bank
tbl_Bank

001 SELECT "Bank"||', BIC: '||"BIC" AS "Bankverbindung","ID"
002 FROM "tbl_Bank"
003 ORDER BY "Bankverbindung"

2.3

Abteilungtbl_Abteilung

001 SELECT "Abteilung", "ID"002 FROM "tbl_Abteilung"
003 ORDER BY "Abteilung" ASC

Vereinsverwaltung

399

frm_Schluessel

1 frmFilter (Abfrage: qry_Filter)2

frmMitglied (Abfrage: qry_Filter_Mitglied)

2.1 frmSchluessel (Tabelle: tbl_MitgliedSchluessel)

1 Listenfeld Formularauswahl (Abfrage: qry_Forms)
Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

2.1

Nach dem Datensatzwechsel

Eingabekontrolle.SelectKey

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

1

Listenfeld Mitglied (Status geändert)

Filter.Filtern

2.1

Schaltfläche Verlust (Taste gedrück)

Design.BoolChangeKey

2.1

Schaltfläche Verlust (Maustaste gedrück)

Design.BoolChange

SQL-Code in Listenfeldern
1

Mitglied
tbl_Mitglied

001 SELECT "Nachname"||COALESCE(', '||"Vorname",'') AS"Name", "ID"002 FROM "tbl_Mitglied"
003 ORDER BY "Name"

2.1

Schlüssel(2*)
tbl_Schluessel

001 SELECT "Bereich"||' → '||"SNr" AS "Schluessel", "SNr"002 FROM "tbl_Schluessel"
003 ORDER BY "Schluessel" ASC

Vereinsverwaltung

400frm_Vorstand1 frmFilter (Abfrage: qry_Filter)2 frmVorstand (Abfrage: qry_Filter_Vorstand)
1 Listenfeld Formularauswahl (Abfrage: qry_Forms)
Makros in Formulareigenschaften
1

Beim Laden

Navigation.FormStart

2

Nach dem Datensatzwechsel

Eingabekontrolle.SelectBoard

Makros in Feldeigenschaften
1

Listenfeld Formularauswahl (Status geändert)

Navigation.Navigation

1

Listenfeld Mitglied (Status geändert)

Filter.Filtern

2

Listenfeld Vorstandsposten (Status geändert)

Eingabekontrolle.BoardFilterSelectSQL-Code in Listenfeldern
1, 2

Mitglied (3*)
tbl_Mitglied

001 SELECT "Nachname"||COALESCE(', '||"Vorname",'') AS"Name", "ID"002 FROM "tbl_Mitglied"
003 ORDER BY "Name"

2

Vorstandsposten (2*)
tbl_VorstandPosten

001 SELECT "Posten" || ' → ' ||CASE WHEN "Zyklus" = 'g' THEN 'gerade Jahreszahlen'
WHEN "Zyklus" = 'u' THEN 'ungerade Jahreszahlen'
ELSE 'kein Zyklus' END AS "VPosten", "ID",CASE WHEN "Zyklus" = '-' THEN 'h' ELSE "Zyklus" END AS"Zyklus"002 FROM "tbl_VorstandPosten"
003 ORDER BY "Zyklus" ASC, "ID" ASC

Vereinsverwaltung

401Formularfilter
2001 ( "tbl_Mitglied_VorstandPosten"."EndDat" IS NULL )Berichte
rpt_Anschriften_Tabelle

Datenquelle
Ansicht:

viw_Anschrift

Damit eine derartige Übersicht ohne Probleme in eine Tabelle übertragen werden kann, müssen
die Felder für die Titelleiste und für den Inhalt lediglich die gleiche Breite haben. Eine voreingestellte Breite wird nach Calc hin leider nicht übertragen, so dass die Breite der einzelnen Spalten nach der Erstellung des Berichts gegebenenfalls über eine Spaltenmarkierung und die Wahlder optimalen Breite eingestellt werden muss.

Damit die Titelleiste fest über dem gesamten Inhalt erscheint ist über Sortierung und Gruppierung die Gruppe «Nichtdruck» erstellt worden. Alle Anschriften dieses Berichtes sind ja in diesem Wert gleich. Deshalb ändert sich die Gruppe nicht und der Inhalt der Gruppe wird entsprechend nicht wiederholt.
Die Sortierung wird nach der Gruppe «Nachname» vorgenommen. Diese Gruppe ist hier für die
bessere Übersicht im Entwurf angezeigt, aber über Eigenschaften Gruppenkopf → Allgemein →Sichtbar: Nein von der Anzeige im ausgeführten Bericht ausgeschlossen.

Vereinsverwaltung

402

rpt_Austritt

Datenquelle
Abfrage:qry_Filter_Austritt

Datenziel
Makro:

ReportStart

Vereinsverwaltung

403

Hier wird ein Schreiben zum Austritts eines ausgewählten Mitglieds mit Hilfe des Berichtsmoduls erstellt.

Der Bericht besteht aus einem großen Bereich «Detail». Eine Gruppierung wird nicht benötigt,
da nur genau ein Datensatz über den Bericht ausgedruckt wird. Das Feld «Zusatz_Boote_Schluessel» ist als automatisch anwachsendes Feld ausgelegt. Ist kein Inhalt vorgegeben, so belegt
es lediglich 0,5 cm des Briefes und fällt nicht weiter auf. Kommt aber, wie in dem angezeigten
Beispiel ein entsprechender Text hinzu, so nimmt das Feld den dafür vorgesehenen Platz ein.
Mehrere Felder werden mit komplett zusammengestelltem Text aus der Abfrage beschickt, sodass z. B. «Anschrift_Mitglied» nicht die ganzen einzelnen Felder wie Vorname, Nachname usw.
nacheinander anzeigt sondern komplett als Block mit Zeilenumbrüchen in der Abfrage enthalten ist. Das erleichtert die Formatierung besonders bei Text, der aus verschiedenen Feldern
innerhalb einer Zeile stehen muss. Würde «Vorname» und «Nachname» in einzelnen Feldern
erscheinen, so hätten die Inhalte im Bericht einen fest vorgegebenen Abstand, der nichts mit

Vereinsverwaltung404

einer einfachen Leertaste zu tun hat. Reicht der Platz nicht, so muss später der Bericht noch
angepasst werden.

rpt_Beitraege

Datenquelle
Abfrage:qry_Beitraege

Mit diesem Bericht werden die Beiträge angegeben, die für das entsprechende Mitglied fällig
sind. Dabei richtet sich die Beitragshöhe nach der entsprechenden Gruppenzuweisung.
Vereinsverwaltung

405

Seitenkopf und Seitenfuß erscheinen auf jeder Seite des Berichtes. Es gibt keine Möglichkeit
verschiedene Seitenvorlagen zu erstellen, so dass der Bericht z. B. mit dem Inhalt des Berichtskopfes statt dem des Seitenkopfes auf der ersten Seite beginnen würde. Der Berichtskopf
erscheint in einem Bericht nur einmal zum Beginn. Der Berichtsfuß würde nur einmal zum
Schluss erscheinen – vor dem Seitenfuß der letzten Seite. In diesem Falle ist der Berichtsfußallerdings leer und in den Eigenschaften auf Sichtbar: → Nein eingestellt.
Damit die Beitragsarten immer in der gleichen Reihenfolge erscheinen ist eine Gruppe «Sort»
erstellt worden. Das Feld "Sort" ist in der Abfrage für den Bericht als Sortierungsfeld enthalten.
Der Kopf-Bereich für «Sort» wird nicht angezeigt.

Vereinsverwaltung

406

rpt_Beitraege_Konto

Datenquelle
Abfrage:qry_Beitraege

Dieser Bericht entspricht dem vorhergehenden Bericht. Hier wurde lediglich in dem Bereich«Kontoinhaber Kopf» neben dem Kontoinhaber auch das Konto mit aufgenommen. Dieser
Bericht ist für die Nutzung der Datenbank mit Kontoverwaltung gedacht.

Vereinsverwaltung

407

rpt_Bootstaender_Belegung

Datenquelle
Abfrage:qry_Bootsstaender

Um alle Spalten dieser Abfrage aufnehmen zu können ist dieser Bericht im Querformat gehalten.

Die Bereiche «Seitenkopf» und «Detail» sind unsichtbar geschaltet. Die Gruppierung «lfdNr» ist
notwendig, damit die Reihenfolge innerhalb der jeweiligen Hallen gewahrt wird. Der gesamte
Inhalt in «lfdNr Kopf» hätte auch in «Detail» stehen können. Leider hat aber der Bereich«Detail» den Fehler, dass dort auch bei Zusammenhalten → Ja der Bereich beim Seitenumbruchmanchmal in zwei Teile zerfällt. Ein Gruppenkopf oder Gruppenfuß lässt unter den Umständen
einen Umbruch mitten im Bereich nicht zu.

Vereinsverwaltung

408

rpt_Etiketten_2_Spalten

Datenquelle
Abfrage:qry_Anschriften_2spaltig

Der gesamte Inhalt für den Druck auf ein Etikett ist durch die Abfrage in einem Feld zusammengefasst. Deshalb reicht für den Bericht auch der Bereich «Detail» aus. Die Positionierung
der Etiketten ist durch die Seitenrandeinstellungen (Format → Seite) und die Einstellungen für
die aufgezogenen Rahmen beeinflussbar.

rpt_Etiketten_3_Spalten

Datenquelle
Abfrage:qry_Anschriften_3spaltig

Hier werden lediglich drei statt zwei Etiketten nebeneinander abgebildet. Auch hier muss natürlich die Anordnung abhängig von dem benutzen Etikettenmaterial angeglichen werden.

Vereinsverwaltung

409

rpt_Jubilaeum_Ehrenurkunde

Datenquelle
Abfrage:qry_Jubilaeum

Die Größe der Bereiche für den Ausdruck dieser Urkunde ist so gewählt, dass die komplette
Seite gefüllt wird. Trotzdem ist im Bereich «Detail» noch einmal eingestellt, dass vor dem
Bereich ein Seitenumbruch eingefügt werden muss. So ein Umbruch gilt nicht für den Seitenkopf und den Seitenfuß, so dass der komplette Inhalt für die erste Urkunde auch auf der ersten
Seite zu sehen ist.
Vereinsverwaltung

410

Im Seitenkopf ist ein Bereich für das Logo vorgesehen, dass in "tbl_Einstellungen" festgelegt
wurde. In den dafür vorgesehenen grafischen Kontrollfeldern erscheint leider nicht so ein in
Hellgrau gehaltener Verweis wie bei den Textfeldern. Das Feld für das Logo muss hier natürlichbei anderen Dimensionen entsprechend angepasst werden. Hier ist das Design auch zusammen
mit der Schriftgröße auf ein Logo im Querformat abgestimmt.

Vereinsverwaltung

411

rpt_Mitglieder_AusEin

Vereinsverwaltung

412

Datenquelle
Ansicht:

viw_Filter_Mitglieder_komplett_AusEin

Abhängig von der Einstellung in "tbl_Filter" werden Austritte oder Beitritte in einem entsprechenden Zeitraum angezeigt. Im Seitenkopf befindet sich deshalb ein Feld für die Überschrift,
das mit einer Funktion versehen ist:
001 IF([Austritte];"Mitgliederbewegung - Austritte";"Mitgliederbewegung Beitritte")

Zu einem Kontoinhaber können Personen mit verschiedenen Adressen zugeordnet werden. Deshalb sind in dem Bericht zwei Gruppierungen enthalten, bei denen zuerst nach dem Kontoinhaber gefragt wird. Allerdings wird diese Gruppierung nur beim folgenden Bericht mit einem
Konto beschrieben. Hier steht lediglich die Person, der zuerst die jeweilige Adresse zugewiesen
wurde.
Rechts unten im Bereich Detail sind zwei Felder, die abhängig davon, ob Austritte oder Beitritte
gezeigt werden, ihren Inhalt entsprechend zeigen:001 IF([Austritte];"Austrittsdatum:";"Aufnahmedatum:")002 IF([Austritte];[AusDat];[AufDat])
In dem oberen Feld wird angezeigt, ob das folgende Feld ein Austrittsdatum oder ein Aufnahmedatum anzeigt. In dem unteren Feld erfolgt dann die Anzeige des "AusDat" oder "AufDat"
aus der Ansicht für diesen Bericht.

Vereinsverwaltung

413

rpt_Mitglieder_AusEin_Konto

Vereinsverwaltung

414

Datenquelle
Ansicht:

viw_Filter_Mitglieder_komplett_AusEin

Dieser Bericht unterscheidet sich von dem vorhergehenden nur dadurch, dass jetzt auch eine
Gruppierung nach den Kontoeinträgen erfolgt. Der Bericht wird dann als Standard genutzt,
wenn in dem Einstellungen die Kontoführung mit angewählt wurde.

Vereinsverwaltung

415rpt_Mitglieder_komplett

Datenquelle
Ansicht:

viw_Mitglieder_komplett

Vereinsverwaltung

416

Hier werden die Mitglieder zu den Adressen gruppiert. Die Reihenfolge der Mitglieder in derAdressengruppe ist egal. Da der Bereich «Detail» über mehrere Zeilen läuft kann es passieren,dass der Inhalt aufgesplittet wird, auch wenn hier Zusammenhalten → Ja gesetzt ist.

Vereinsverwaltung

417

rpt_Mitglieder_komplett_Konto

Datenquelle
Ansicht:

viw_Mitglieder_komplett

Vereinsverwaltung

418

Bei diesem Bericht wird gegenüber dem vorhergehenden das Konto mit angezeigt. Außerdem
ist der Bereich «Detail» leer gelassen worden und wird nicht angezeigt. Das hat damit zu tun,dass die Elemente im Bereich «Detail» trotz gegenteiliger Einstellung nicht zusammengehalten
werden, wenn ein Seitenumbruch erfolgt. Stattdessen ist gegenüber dem vorherigen Bericht
der entsprechende Inhalt in einen Gruppenkopf ausgelagert worden. Der Vorteil dieser Gruppe
ist neben dem Zusammenhalten der Informationen auch, dass so eine einheitliche Sortierung
der in einem Haus wohnenden Personen nach dem Vornamen erfolgt.
Einem Kontoinhaber können mehrere Adressen zugeordnet werden. Einer Adresse können dannwieder mehrere Personen zugeordnet werden. Das entspricht hier dem Aufbau der Gruppierung.

rpt_Mitgliedsdauer

Datenquelle
Abfrage:qry_Mitgliedsdauer

Dieser Bericht dient lediglich dazu, die Dauer der Mitgliedschaft für Jubiläen zu überprüfen. In
Vereinen kommt es immer wieder vor, dass eine Person sich übergangen fühlt und meint "Ichbin schon lange dran …"

Vereinsverwaltung

419

rpt_Schluesseluebersicht

Datenquelle
Abfrage:qry_Schluessel_Uebersicht

Hier werden alle Schlüsselausgaben angezeigt. Zu der jeweiligen Schlüsselart werden die Mitglieder benannt, die sich so einen Schlüssel entliehen haben. Vor dem Namen ist durch das Einfügen einer Zählerfunktion (Eigenschaften → Daten → Datenfeld-Typ → Zähler) die Nummerierung der Mitglieder dargestellt.

Vereinsverwaltung

420rpt_Statistik

Datenquelle
Ansicht:

viw_Statistik_Alter_Detail

Für die Erstellung der Diagramme wird über die Steuerelement-Eigenschaften unter Daten →Inhalt eine Abfrage innerhalb des Berichtes durchgeführt:Vereinsverwaltung

421

001 SELECT "Alter",
002 "Anzahl",
003 "Geschlecht"
004 FROM "viw_Statistik_Alter_Detail"Das ist notwendig, weil sich in der Ansicht auch ein Datumsfeld befindet. Das Diagrammmodul
hat leider seit längerem Probleme mit Datumsfeldern, so dass mit Datumsfeldern zusammen
Inhalte von Diagrammen grundsätzlich nicht dargestellt werden. Hier deswegen nur die notwendigen Felder "Alter" und "Anzahl" zur Anzeige der Daten und "Geschlecht" zur Verbindung
der Diagramme mit der vorhergehenden tabellarischen Übersicht. Bei der Erstellung des Diagramms muss dann auch noch für die Datengrundlage das Feld "Geschlecht" entfernt werden,
da der Assistent annimmt, dass über die Abfrage eben zwei Säulen dargestellt werden sollen:
Die Säule "Anzahl" und die Säule "Geschlecht". Da im Feld "Geschlecht" kein numerischer Wert
liegt fällt nur auf, dass die Säulen für die "Anzahl" nicht mittig über den Bereichen auf der xAchse liegen.

rpt_Vorstand_aktuell

Datenquelle
Abfrage:qry_Vorstand_aktuell

Eine einfache Übersicht über alle Vorstandsmitglieder. Jeder Posten wird zusammen mit dem
Wahldatum und dem Namen aufgeführt. Der Bereich «Detail» ist zwar auf Zusammenhalten →Ja eingestellt, nur funktioniert dies leider nicht unbedingt. Gegebenenfalls müsste also nacheinem entsprechenden Kriterium sortiert werden, wobei die Reihenfolge der Posten laut Satzung maßgebend sein sollte.

Vereinsverwaltung422rpt_Vorstand_aktuell_lt_Satzung

Datenquelle
Abfrage:qry_Vorstand_aktuell_lt_Satzung

Hier eine ausführlichere Übersicht, in der dann nur die Vorstandsmitglieder verzeichnet sind,die lt. Satzung zum Vorstand dazu gehören. Damit der Inhalt pro Posten sicher zusammengehalten wird, wird der Detailbereich nicht genutzt. Die Sortierung der Gruppe erfolgt nach der"ID" aus der "tbl_VorstandPosten". Damit ist die Sortierung immer gleich der Sortierung lt. Satzung.

Vereinsverwaltung

423

Makros
BackupDatabaseBackup
Aufruf aus
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038

DatabaseStart

SUB DatabaseBackup(inMax AS INTEGER)
DIM oPath AS OBJECTDIM oDoc AS OBJECT
DIM sTitel AS STRING
DIM sUrl_Ziel AS STRING
DIM sUrl_Start AS STRING
DIM i AS INTEGERDIM k AS INTEGER
oDoc = ThisComponentsTitel = oDoc.Title
sUrl_Start = oDoc.URL
DO WHILE sUrl_Start = ""oDoc = oDoc.ParentsTitel = oDoc.Title
sUrl_Start = oDoc.URL
LOOP
oPath = createUnoService("com.sun.star.util.PathSettings")
FOR i = 1 TO inMax + 1
IF NOT FileExists(oPath.Backup & "/" & i & "_" & sTitel) THEN
IF i > inMax THEN
FOR k = inMax - 1 TO 1 STEP -1
IF FileDateTime(oPath.Backup & "/" & k & "_" & sTitel) <=FileDateTime(oPath.Backup & "/" & k+1 & "_" & sTitel) THEN
IF k = 1 THENi = k
EXIT FOR
END IF
ELSE
i = k+1
EXIT FOR
END IFNEXT
END IF
EXIT FOR
END IFNEXT
sUrl_Ziel = oPath.Backup & "/" & i & "_" & sTitel
FileCopy(sUrl_Start,sUrl_Ziel)
END SUB

Von der Datenbankdatei *.odb wird eine Kopie in das Backup-Verzeichnis erstellt. Die Maximalzahl ist auf den im Aufruf angegebenen Wert beschränkt. Danach wird das älteste Backup überschrieben. Solch ein Backup ist für die internen Datenbanken empfehlenswert. Bei externen
Datenbanken und Serverdatenbanken macht es hingegen keinen Sinn, da dort ja die Daten an
einem anderen Ort liegen.
In Zeile 10 wird zuerst der Name der Datenbankdatei ausgelesen. Anschließend dann die URL,
die zu dieser Datenbankdatei führt. Würde die Prozedur von einem internen Formular und nicht
beim Start der Base-Datei ausgeführt, so müsste der Name der Datei und die URL erst über die
Schleife von Zeile 12 bis 16 ermittelt werden.
Im Backup-Verzeichnis des Datenbanknutzers von LO soll die Speicherung erfolgen. Das Verzeichnis ist über Extras → Optionen → LibreOffice → Pfade → Sicherungskopien einstellbar. Die
Vereinsverwaltung424

Speicherung erfolgt beginnend mit der Nummer, gefolgt durch einen Unterstrich und weiter mit
dem ursprünglichen Titel (Zeile 19).
Wenn i größer als die maximale Zahl wird muss nachgesehen werden, welche Kopie die älteste
ist, um diese zu ersetzen. Dazu wird ab Zeile 21 rückwärts durch die erstellten Backup-Dateien
gesucht. Ist die Datei mit der größeren Nummer älter als die mit der kleineren Nummer, dannwird diese ältere Datei ersetzt. Die Suche beginnt also bei einem Vergleich der Backup-Datei
mit der zweithöchsten Nummer mit der Datei mit der höchsten Nummer und wird abwärts
fortgesetzt, immer im direkten Vergleich mit der nächst höheren Ziffer. Ist 1 erreicht und immer
noch vor 2 erstellt worden, so wird 1 ersetzt.
In Zeile 36 wird der Pfad und Name für die Zieldatei zusammengestellt und schließlich in Zeile
37 eine Kopie der Ausgangsdatei in das Backup-Verzeichnis kopiert.

DatabaseStart
Aufruf aus
Base-Datei über Extras → Anpassen → Ansicht wurde erzeugt

Benötigt
Makro:
001
002
003
004
005
006

DatabaseBackup, FilterIDSet, Preferences

SUB DatabaseStart
DatabaseBackup(10)FilterIDSet
Preferences
ThisDatabaseDocument.FormDocuments.getByName("frm_Mitglied").open
END SUB

Beim Öffnen der Datenbankdatei wird zuerst ein Backup geschrieben. Hier ist die Anzahl der
Backups auf 10 begrenzt worden. Anschließend werden die folgenden beiden Prozeduren ausgeführt. Danach erfolgt dann der Start des Formulars zum Eintragen neuer Mitglieder, da diesdas Formular ist, das wohl am häufigsten benötigt wird.

FilterIDSet
Aufruf aus
Makro:

DatabaseStart

Benötigt
Tabelle:tbl_Filter

Ansicht:

viw_ServVar

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016

SUB FilterIDSet
DIM oDatasource AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECT
DIM stSql AS STRING
DIM oResult AS OBJECT
DIM stID AS STRING
oDatasource = thisDatabaseDocument.CurrentController
IF NOT (oDatasource.isConnected()) THEN oDatasource.connect()oConnection = oDatasource.ActiveConnection()oSQL_Statement = oConnection.createStatement()
stSql = "SELECT ""ID"" FROM ""viw_ServVar"" "oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
stID = oResult.getString(1)WEND

Vereinsverwaltung

425

017
018
019
020
021
022
023
024
025
026

IF stID = "" THEN
stID = "1"
END IF
stSql = "SELECT ""ID"" FROM ""tbl_Filter"" WHERE ""ID"" = '"+stID+"' "oResult = oSQL_Statement.executeQuery(stSql)
IF NOT oResult.next THEN
stSql = "INSERT INTO ""tbl_Filter"" (""ID"") VALUES ('"+stID+"')"oSQL_Statement.executeUpdate(stSql)
END IF
END SUB

Für die Filtertabelle soll ein Primärschlüssel gesetzt werden, der der Verbindungs-ID der Datenbank entspricht. Dieses Verfahren ermöglicht es, eine Filter-Tabelle auch bei Serverdatenbanken
zu nutzen, bei denen mehrere Nutzer unabhängig voneinander Inhalte über die Tabelle filternkönnen.
Bei der internen HSQLDB funktioniert leider die Abfrage aus Zeile 12 nicht beständig. Selbst im
laufenden Betrieb gibt die Ansicht nicht immer einen entsprechenden Wert wieder sondern
kann auch leer bleiben. Aus dem Grunde wird bei einer leeren "ID" stattdessen '1' genutzt(Zeile 18).
Ist ein Datensatz mit entsprechender "ID" bereits in der Tabelle "tbl_Filter" vorhanden, dannwird dieser Datensatz genutzt (Zeile 20 bis 22). Ansonsten wird ein neuer Datensatz mit diesem Primärschlüssel erstellt (Zeile 23 und 24).
Ein vorher erstellter Datensatz mit gleicher Verbindungs-ID bleibt so bestehen. Die Filterungenvom letzten Programmstart bleiben erhalten. Sollte dies nicht gewünscht sein, so kann aucheinfach zu Beginn aus der "tbl_Filter" der entsprechende Datensatz gelöscht und anschließend
neu erstellt werden.

Preferences
Aufruf aus
Formular: frm_Einstellungen
Makro:

DatabaseStart

Benötigt
Tabelle:tbl_EinstellungenAnsicht:

viw_Gruppe

001
002
003
004
005
006

GLOBAL boAccount AS BOOLEAN
GLOBAL boSubscription AS BOOLEAN
GLOBAL boKey AS BOOLEAN
GLOBAL boBoatRack AS BOOLEAN
GLOBAL boSection AS BOOLEAN
GLOBAL arSubscription() AS STRING

001
002
003
004
005
006
007
008
009
010
011
012

SUB Preferences
DIM oDatasource AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECT
DIM stSql AS STRING
DIM oResult AS OBJECT
DIM i AS INTEGER
oDatasource = thisDatabaseDocument.CurrentController
IF NOT (oDatasource.isConnected()) THEN oDatasource.connect()oConnection = oDatasource.ActiveConnection()oSQL_Statement = oConnection.createStatement()
stSql = "SELECT ""Kontoverwaltung"", ""Beitragsverwaltung"",""Schluesselverwaltung"", ""Bootstaenderverwaltung"",""Abteilungsverwaltung"" FROM ""tbl_Einstellungen"" WHERE ""ID"" = True "oResult = oSQL_Statement.executeQuery(stSql)

013

Vereinsverwaltung

426

014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030

WHILE oResult.next
boAccount = oResult.getBoolean(1)boSubscription = oResult.getBoolean(2)boKey = oResult.getBoolean(3)boBoatRack = oResult.getBoolean(4)boSection = oResult.getBoolean(5)WENDIF boSubscription THEN
stSql = "SELECT ""ID"" FROM ""viw_Gruppe"" WHERE ""Alter_Beitrag"" = 17"
oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
ReDim Preserve arSubscription(i)arSubscription(i) = oResult.getString(1)i = i + 1
WEND
END IF
END SUB

Für die Voreinstellungen der Module muss klar sein, welche Module denn angewählt wurden.
Nur so lässt sich regeln, ob bestimmte Elemente bei Formularen nicht von vornherein ausgeblendet werden sollen.
Für die Beitragsverwaltung ist hier gleich ein kleines Makro mit integriert, dass die Gruppen
herausfiltert, bei denen im Moment Mitglieder 17 Jahre alt sind. Bei dem Übergang von 17 Jahren zu 18 Jahren fallen diese Mitglieder gegebenenfalls aus dem Gruppenbeitrag heraus.
Zuerst werden die globalen Variablen außerhalb der Prozedur deklariert. So stehen sie die komplette Zeit ab dem Öffnen der Datenbankdatei allen anderen Prozeduren zur Verfügung. In Zeile
12 wird der Abfragecode vorformuliert, in der Schleife von Zeile 14 bis Zeile 20 werden danndie Werte aus der Tabelle "tbl_Einstellungen" in die entsprechenden Variablen geschrieben.
Eine Schleife wäre hier eigentlich gar nicht nötig, da ja genau nur ein Datensatz in der "tbl_Einstellungen" existiert.
Ist «boSubcription» (also "Beitragsverwaltung" aus der "tbl_Einstellungen") wahr, dann erfolgt
von Zeile 22 bis Zeile 27 die Übertragung von allen Gruppen mit 17-jährigen Mitgliedern in ein
Array. Dies wird später benötigt, wenn eventuell Beiträge für die nächste Abbuchung neu
bestimmt werden sollen.

DesignPreferencesHide
Aufruf aus
Formular: frm_Einstellungen
001
002
003
004
005
006
007
008
009
010
011

SUB PreferencesHide(oEvent AS OBJECT)
oForm = oEvent.Source
IF boSubscription <> True THEN
FOR k = 0 TO oForm.Count - 1
oField = oForm.getByIndex(k)IF oField.ServiceName <> "stardiv.one.form.component.Form" ANDoField.ServiceName <> "stardiv.one.form.component.Hidden" THENoField.EnableVisible = False
END IFNEXT
END IF
END SUB

Das Formular frm_Einstellungen enthält einen großen Bereich zur Beitragsverwaltung. Ist die
Variable boSubscripion nicht True, dann ist die Beitragsverwaltung abgewählt. Unter diesen
Umständen wird im Formular frm_Einstellungen alles, was mit der Beitragsverwaltung zu tun
hat, unsichtbar geschaltet (Zeile 7). Lediglich Formulare und bereits versteckte Kontrollfelder

Vereinsverwaltung427

lassen sich nicht unsichtbar schalten, da sie das bereits sind. Deswegen sind in Zeile 6 diesebeiden Elemente ausgenommen.

BoolChange
Aufruf aus
Formular: frm_Schluessel, frm_Mitglied
Makro:

BoolChangeKey

Benötigt
Makro:
012
013
014
015
016
017
018
019
020
021
022
023
024

BoolStart

SUB BoolChange(oEvent AS OBJECT)
DIM oField AS OBJECTDIM oForm AS OBJECT
oField = oEvent.Source.ModeloForm = oField.Parent
IF oField.Label = stChecked THENoField.Label = stUnCheckedoForm.updateBoolean(oForm.findColumn(oField.Tag),false)
ELSE
oField.Label = stCheckedoForm.updateBoolean(oForm.findColumn(oField.Tag),true)
END IF
END SUBDie Variablen «stUnChecked» und «stChecked» sind globale Variablen, Sie werden durch die
Prozedur «BoolStart» mit Inhalt gefüllt. Wenn die Beschriftung eines Feldes dem Text «stChecked» entspricht (Zeile 7), dann wird diese Beschriftung geändert und dann das Feld derTabelle, das in den Zusatzinformationen der Schaltfläche benannt ist, als nicht ausgewählt(false) markiert.

BoolChangeKey
Aufruf aus
Formular: frm_Schluessel, frm_Mitglied

Benötigt
Makro:
001
002
003
004
005

BoolChange

SUB BoolChangeKey(oEvent AS OBJECT)
IF oEvent.KeyCode <> 1282 THEN
BoolChange(oEvent)END IF
END SUB

Das Symbol auf dem Button soll nicht ausgetauscht werden, wenn der Button nur mit dem
Tabulator angesprungen wird, wohl aber, wenn z. B. die Leertaste gedrückt wird. Hier wird mit
dem KeyCode13 1282 auf den Tabulator gezielt. Nur wenn nicht der Tabulator auf der Schaltfläche ausgelöst wird wird die Prozedur «BoolChange» aufgerufen.

13 KeyCodes siehe:

http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1Key.html

Vereinsverwaltung428

BoolStart
Aufruf aus
Formular: frm_Mitglied
001
002

GLOBAL stChecked AS STRING
GLOBAL stUnChecked AS STRING

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029

SUB BoolStart(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oField AS OBJECT
DIM stCheckbox AS STRING
DIM stCheck AS STRING
DIM stValue AS STRING
DIM arCheck()oForm = oEvent.Source
IF hasUnoInterfaces( oForm, "com.sun.star.form.XForm" ) THEN
stChecked = "✅"stUnChecked = "☐"stCheckbox = oForm.getByName("hidCheckbox").HiddenValuearCheck = split(stCheckbox,";")FOR n = LBound(arCheck()) TO UBound(arCheck())
oField = oForm.getByName(Trim(arCheck(n)))
stCheck = oField.Tag
IF oForm.IsRowCountFinal AND oForm.RowCount = 0 THEN
stValue = "false"
ELSE
stValue = oForm.getString(oForm.findColumn(stCheck))
END IFIF stValue = "true" THENoField.Label = stChecked
ELSE
oField.Label = stUnChecked
END IFNEXT
END IF
END SUB

Für die Darstellung von Markierfeldern werden hier Schaltflächen benutzt. Markierfelder können
nicht vom Design her angepasst werden. Dies stört vor allem bei der Größe der zu markierenden Fläche. Die Symbole "✅" und "☐" stammen aus dem UTF-8-Zeichensatz. Das Symbol für
die 'Ja' ist dabei eindeutig. Ein vorher ausgewähltes Stop-Symbol für 'Nein' (siehe z. B. viw_Mitglieder_komplett) ist für eine allgemeine Verwendung zu wertend. 'Nein' wäre, wie bei den fehlenden Schwimmkenntnissen in der genannten Ansicht, negativ belegt. Bei einem Verlust eines
Schlüssels wird aber gerade mit 'Ja' bestätigt, dass der Schlüssel weg ist. Daher also einfach
ein leere Kasten für "nicht angekreuzt".
In den Formularen wird ein verstecktes Feld positioniert, das mit dem Namen "hidCheckbox"
angesprochen werden kann. In diesem Feld sind alle Felder, durch ein Semikolon getrennt, aufgeführt, die als «grafische Markierfelder» dienen sollen (Zeilen 12 und 13).
Die «grafischen Markierfelder» werden in Zeile 18 auf false voreingestellt, wenn der Datensatz
noch nicht existiert. Ansonsten wird aus dem zugehörigen Tabellenfeld ermittelt, ob es ausgewählt oder nicht ausgewählt ist. Danach wird dann auf true oder false eingestellt. Welche
Tabellenfeldern mit welchen als «grafische Markierfelder» dienenden Schaltflächen zusammen
hängen steht in den Zusatzinformationen (Tag) der jeweiligen Schaltfläche.

AfterRecordChange
Aufruf aus
Formular: frm_Mitglied

Vereinsverwaltung

429

Benötigt
Makro:
001
002
003
004
005

BoolStart, Group, SelectGroupSUB AfterRecordChange(oEvent AS OBJECT)
BoolStart(oEvent)Group(oEvent)SelectGroup(oEvent)END SUB

Diese Prozedur dient lediglich dazu, mit einem Prozedurstart nacheinander drei verschiedene
Prozeduren ablaufen zu lassen.

Group
Aufruf aus
Makro:

AfterRecordChange

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040

tbl_Adresse, tbl_Mitglied

SUB Group(oEvent AS OBJECT)
DIM arForms()
DIM arFormNames()
DIM i AS INTEGERDIM k AS INTEGERDIM oFormTarget AS OBJECT
DIM boLeader AS BOOLEAN
DIM stID AS STRING
DIM stIDAccount AS STRING
DIM stIDAddress AS STRING
DIM stadrID AS STRING
DIM stgruID AS STRING
DIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECT
oForm = oEvent.Source
stID = oForm.getString(oForm.findColumn("ID"))
oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
IF stID <> "" THEN
stSql = "SELECT ""mitID"" FROM ""tbl_Adresse"" WHERE ""mitID"" = '"+stID+"'"oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
stIDAddress = oResult.getString(1)WEND
IF boAccount THEN
stSql = "SELECT ""mitID"" FROM ""tbl_Konto"" WHERE ""mitID"" = '"+stID+"'"oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
stIDAccount = oResult.getString(1)WEND
END IF
stSql = "SELECT ""tbl_Adresse"".""mitID"", ""tbl_Mitglied"".""Gruppe_mitID""
FROM ""tbl_Adresse"", ""tbl_Mitglied"" WHERE "stSql = stSql + """tbl_Adresse"".""mitID"" = ""tbl_Mitglied"".""ID"" AND""tbl_Mitglied"".""ID"" = '"+stID+"' AND NOT""tbl_Mitglied"".""Gruppe_mitID"" IS NULL "oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
stadrID = oResult.getString(1)stgruID = oResult.getString(2)WEND
END IFIF stIDAddress <> "" THEN

Vereinsverwaltung

430

041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070

071
072
073
074
075
076
077
078

boLeader = True
ELSE
boLeader = False
END IFarForms = array("frmAdresse", "frmKonto")
FOR i = 0 TO UBound(arForms)
oFormTarget = oForm.getByName(arForms(i))
FOR k = 0 TO oFormTarget.Count - 1
oField = oFormTarget.getByIndex(k)IF oField.ServiceName <> "stardiv.one.form.component.Form" ANDoField.ServiceName <> "stardiv.one.form.component.Hidden" THEN
IF boLeader = True THEN
IF (arForms(i) = "frmKonto" AND boAccount AND stadrID = stgruID) OR
arForms(i) <> "frmKonto" THENoField.EnableVisible = True
ELSE
oField.EnableVisible = False
END IF
ELSEoField.EnableVisible = False
END IF
END IF
NEXT
NEXT
REDIM arForms()
arForms = array("frmGruppe", "frmAbteilung")
FOR i = 0 TO UBound(arForms)
oFormTarget = oForm.getByName(arForms(i))
FOR k = 0 TO oFormTarget.Count - 1
oField = oFormTarget.getByIndex(k)IF oField.ServiceName <> "stardiv.one.form.component.Form" ANDoField.ServiceName <> "stardiv.one.form.component.Hidden" THEN
IF (arForms(i) = "frmGruppe" AND(boLeader = True OR boAccount = False))
OR (arForms(i) = "frmAbteilung" AND boSection = False)
OR (stID = "") THENoField.EnableVisible = False
ELSE
oField.EnableVisible = True
END IF
END IFNEXT
NEXT
END SUB

Mit dieser Prozedur soll die Zuweisung von Mitliedern zu einer Gruppe ermöglicht werden.
In Zeile 14 wird zuerst die ID des angezeigten Mitglieds ausgelesen. Dann wird nachgeschaut,ob dieses Mitglied mit einer Adresse direkt verbunden ist (Zeile 20 – Zeile 24). Falls in den Einstellungen die Kontoverwaltung eingestellt wurde (boAccount = True), dann wird auch nochnachgeschaut, ob genau dieses Mitglied mit einem Konto direkt verbunden ist (Zeile 26 – Zeile30). "mitID" ist sowohl in der Tabelle "tbl_Adresse" als auch in der Tabelle "tbl_Konto" Primärschlüssel.
Jetzt wird ermittelt, welcher Gruppe das entsprechende Mitglied angehört, sofern es nicht
selbst als erste Person mit einer Adresse oder einem Konto verbunden ist. Mitglieder, die direkt
mit Adresse (und Konto) verbunden sind weisen in "tbl_Mitglied"."Gruppe_mitID" keinen Datensatz auf. Die "tbl_Adresse"."mitID" wird in der Variablen stadrID (36), die"tbl_Mitglied"."Gruppe_mitID" in der Variablen stgruID (37) gespeichert. An diesen Variablen
ist zu erkennen, ob die Person eine eigene Adresse, aber mit einer anderen Person gruppiertein entsprechendes Konto hat.
Ist der Person eine eigene Adresse zugewiesen, so handelt es sich erst einmal um eine Person,
die als Gruppenleitung angenommen wird. boLeader wird auf True gesetzt (Zeile 41).

Vereinsverwaltung

431

Von Zeile 45 bis Zeile 62 werden die Inhalte der Unterformulare «frmAdresse» und «frmKonto»
von verschiedenen Bedingungen abhängig auf sichtbar oder unsichtbar geschaltet. Beide werden grundsätzlich nur sichtbar geschaltet, wenn es sich um die erste Person handelt, die einer
Adresse oder einem Konto zugeordnet wird (Zeile 51). «frmAdresse» erscheint dann auf jeden
Fall, «frmKonto» nur dann, wenn die Kontoverwaltung aktiviert wurde (boAccount = True) und
die in Zeile 36 und 37 ermittelten Werte gleich sind.
Von Zeile 65 bis Zeile 77 werden die Inhalte der Unterformulare «frmGruppe» und «frmAbteilung» von verschiedenen Bedingungen abhängig auf sichtbar oder unsichtbar geschaltet. Bei«frmGruppe» handelt es sich lediglich um ein Feld, in dem die Information zur Gruppenzugehörigkeit steht. Das Geld kann dann nicht angezeigt werden, wenn es sich um einen Gruppenleiter handelt oder eine Kontoverwaltung nicht angewählt ist. Außerdem wird dieses Feld nicht
angezeigt, wenn das Hauptformular noch gar keine Daten für ein Mitglied enthält.
Auch für «frmAbteilung» gilt, dass es erst angezeigt wird, wenn ein Mitglied abgespeichert
wurde. Zusätzlich muss natürlich die Abteilungsverwaltung eingeschaltet worden sein
(boSection = True).
Formulare selbst und versteckte Kontrollfelder können nicht unsichtbar geschaltet werden, daes sich hierbei nicht um sichtbare Elemente handelt. Deswegen werden diese beiden Elemente
in den entsprechenden Schleifen jeweils von der Unsichtbarschaltung ausgenommen (Zeilen 50
und 69).

Druck
ReportStart
Aufruf aus
Formular: frm_Berichte

Benötigt
Makro:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018

Ergebnisliste_Zeit

SUB ReportStart(oEvent AS OBJECT)
DIM stReport AS STRING
DIM stmitID AS STRINGoForm = oEvent.Source.Model.Parent
stReport = oForm.getString(oForm.findColumn("Bericht"))
IF stReport <> "" THEN
IF stReport = "rpt_Austritt" THEN
stmitID = oForm.getString(oForm.findColumn("mitID"))
IF stmitID = "" THEN
msgbox "Für den Bericht zum Austritt muss eine Person ausgewählt
werden."EXIT SUBEND IF
END IF
ThisDatabaseDocument.ReportDocuments.getByName(stReport).open
ELSE
msgbox "Bitte einen Bericht auswählen."
END IF
END SUB

Hiermit wird die Öffnung eines Berichtes veranlasst. Zuerst wird in der Tabelle "tbl_Einstellungen" im Feld "Bericht" (Zeile 5) nachgesehen, welcher Berichtsname gerade über das Listenfeld
abgespeichert wurde. Lediglich für das Anschreiben zum Austritt wird nachgeschaut, ob aucheine entsprechende Person ausgewählt wurde, an die der Bericht gehen soll. Ansonsten wird in
Zeile 14 der Bericht geöffnet. Ist kein Bericht ausgewählt, so erscheint eine Information, dassdie Auswahl vor dem Öffnen erfolgen muss.

Vereinsverwaltung

432

Eingabekontrolle
NewBoatAllowed
Aufruf aus
Formular: frm_Boot

Benötigt
Makro:

SelectSQL

Tabelle:tbl_Mitglied, tbl_BootArt, tbl_Boot

019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060

SUB NewBoatAllowed
DIM stList AS STRING
DIM stID AS STRING
DIM oForm AS OBJECTDIM oField AS OBJECT
DIM inAnzahl AS INTEGER
DIM stGruppeID AS STRING
oMainForm = ThisComponent.Drawpage.forms.getByName("frmMitglied")oForm = oMainForm.getByName("frmBoot")
oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()stID = SelectSQL(oMainForm.getString(oMainForm.findColumn("ID")))
stList = "SELECT COALESCE(""Gruppe_mitID"", ""ID"") AS ""gruID"" FROM""tbl_Mitglied"" WHERE ""ID"" "+stID+""
oResult = oSQL_Statement.executeQuery(stList)
WHILE oResult.next
stGruppeID = SelectSQL(oResult.getString(1))
WEND
stList = "SELECT LEFT(""tbl_BootArt"".""Bootsart"", 1) AS ""Art"",
COUNT(""tbl_BootArt"".""ID"") AS ""Anzahl"", "stList = stList + "COALESCE(""tbl_Mitglied"".""Gruppe_mitID"",""tbl_Mitglied"".""ID"") AS ""gruID"" FROM ""tbl_Boot"", ""tbl_Mitglied"",""tbl_BootArt"" "stList = stList + "WHERE ""tbl_Mitglied"".""ID"" = ""tbl_Boot"".""mitID"" AND""tbl_Boot"".""bo_aID"" = ""tbl_BootArt"".""ID"" AND ""gruID"" "+stGruppeID+"
GROUP BY ""gruID"", ""Art"" "oResult = oSQL_Statement.executeQuery(stList)
WHILE oResult.next
stArt = oResult.getString(1)inAnzahl = oResult.getInt(2)IF stArt = "C" OR inAnzahl = 3 THEN
oForm.AllowInserts = False
oForm.getByName("lblKeineNeueingabe").EnableVisible = True
ELSEoForm.AllowInserts = TrueoForm.getByName("lblKeineNeueingabe").EnableVisible = False
END IFIF stArt = "K" AND inAnzahl < 3 THEN
boBoatAddableK = True
ELSE
boBoatAddableK = False
END IFWEND
DIM loRow AS LONG
loRow = oForm.getRow()oForm.reload()
oForm.absolute(loRow)
END SUB

Mit dieser Prozedur wird entschieden, ob für die Gruppe noch weitere Boote eingelagert werden
können. Dazu wird zuerst einmal die Gruppenmitgliedschaft für das aktuell ausgewählte MitVereinsverwaltung433

glied ermittelt (Zeile 31 bis Zeile 35). Anschließend wird die Bootsart und die Anzahl der bootefür diese Gruppe bestimmt (Zeile 36 bis Zeile 42).
Enthält die Bootsart ein 'C' als ersten Buchstaben, so handelt es sich um einen Canadier. Ein
weiteres Boot kann nicht eingelagert werden. Auch wenn bereits 3 Boote eingelagert wurden,
kann kein weiteres Boot hinzugefügt werden. Das Formular wird so eingestellt, dass keine Neueingaben erfolgen können (Zeile 44). Außerdem wird das Feld sichtbar gemacht, dass auf die
nicht erlaubte Neueingabe hinweist (Zeile 45). Eine Änderung der vorherigen Eingabe ist allerdings möglich.
Sind noch nicht 3 Boote eingelagert und ist auch kein Canadier eingelagert, so wird in den Formulareinstellungen die Neueingabe erlaubt (47) und der Hinweis auf die nicht erlaubte Eingabe
unsichtbar geschaltet (48).
Handelt es sich bei den bisherigen Booten um Kajaks und ist die Anzahl von 3 Booten nochnicht erreicht, dann können weitere Boote hinzugefügt werden. Die globale Variable
boBoatAddableK wird auf True gesetzt (51).
Zum Schluss der Prozedur wird die aktuelle Zeilennummer ausgelesen, das Formular neu
geladen und wieder auf die entsprechende Zeilennummer eingestellt, damit die Formulareinstellungen wirksam werden.

SelectBoat
Aufruf aus
Formular: frm_Boot

Benötigt
Tabelle:tbl_BootArt

001

GLOBAL boBoatAddableK AS BOOLEAN

001
002
003
004
005
006
007
008
009
010

SUB SelectBoat(oEvent AS OBJECT)
DIM stSql(0) AS STRING
DIM stList AS STRING
DIM stID AS STRING
DIM oForm AS OBJECTDIM oField AS OBJECT
oForm = oEvent.Source
IF hasUnoInterfaces(oForm, "com.sun.star.form.XForm" ) THEN
IF boBoatAddableK THEN
stList = "SELECT ""Bootsart"", ""ID"" FROM ""tbl_BootArt"" WHERE
LEFT(""Bootsart"", 1) = 'K' ORDER BY ""Bootsart"" ASC "ELSE
stList = "SELECT ""Bootsart"", ""ID"" FROM ""tbl_BootArt"" ORDER BY""Bootsart"" ASC "END IFoField = oForm.getByName("lboBootsart")
stSql(0) = stList
oField.ListSource = stSql
oField.refresh
SelectBoatRack(oEvent)END IF
END SUB

011
012
013
014
015
016
017
018
019
020

Es kann nur ein Canadier oder maximal drei Kajaks im Bootshaus unter gebracht werden.
Belegt ein Kajak der Gruppe bereits einen Bootsständer, so werden in dieser Liste nur noch weitere Kajaks zur Auswahl angeboten. Ob überhaupt noch ein Boot unter gebracht werden darf
entscheidet dabei die Prozedur NewBoatAllowed.

Vereinsverwaltung

434

SelectBoatRack
Aufruf aus
Formular: frm_Boot

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021

tbl_BootStaender, tbl_BootHalle, tbl_Boot

SUB SelectBoatRack(oEvent AS OBJECT)
DIM stSql(0) AS STRING
DIM stList AS STRING
DIM stID AS STRING
DIM oForm AS OBJECTDIM oField AS OBJECT
oForm = oEvent.Source
IF hasUnoInterfaces(oForm, "com.sun.star.form.XForm" ) THEN
IF NOT oForm.MaxRows = 0 THEN
stID = SelectSQL(oForm.getString(oForm.findColumn("bo_sID")))oField = oForm.getByName("lboBootsstaender")stList = "SELECT ""tbl_BootHalle"".""Halle_lang""||' →'||""tbl_BootStaender"".""BootStaend"" AS ""Bootstaender"",""tbl_BootStaender"".""ID"" "stList = stList + "FROM ""tbl_BootStaender"", ""tbl_BootHalle"" WHERE""tbl_BootStaender"".""bo_hID"" = ""tbl_BootHalle"".""ID"" "stList = stList + "AND (""tbl_BootStaender"".""ID"" NOT IN (SELECT ""bo_sID""
FROM ""tbl_Boot"") OR ""tbl_BootStaender"".""ID"" "+stID+") "stList = stList + "ORDER BY ""Bootstaender"" ASC"
stSql(0) = stList
oField.ListSource = stSql
oField.refresh
END IF
END IF
END SUBBootsständer können nur für ein Boot vergeben werden. Bei einer Auswahlliste dürfen alsokeine Bootsständer angeboten werden, die bereits vergeben wurden. Dies würde aber dazu
führen, dass bei bestehenden Datensätzen auch keine Bootsständer angezeigt werden können.
Deswegen muss per Makro neben den freien Bootsständern auch der Bootsständer in die Liste
aufgenommen werden, der mit dem aktuellen Boot (Zeile 10) zusammenhängt.

SelectSQL
Aufruf aus
Makro:
001
002
003
004
005
006
007
008
009
010
011
012

NewBoatAllowed

FUNCTION SelectSQL(stVar AS STRING) AS STRING
stVar = Trim(stVar)
IF InStr(stVar,"'") THEN
stVar = Join(Split(stVar,"'"),"''")END IFIF stVar = "" THEN
stVar = "IS NULL"
ELSE
stVar = "='"+stVar+"'"END IF
SelectSQL = stVar
END FUNCTION

Hier geht es darum, dass Variablen so vorbereitet werden, dass sie problemlos in die Datenbank per SQL übertragen werden.

Vereinsverwaltung

435

In Zeile 4 werden einfache Anführungszeichen mit einfachen Anführungszeichen maskiert.
Sonst würde SQL das einfache Anführungszeichen als Ende eines Textes nehmen und könnte
die anschließenden Daten nicht mehr einlesen.
Ist die Variable leer, so soll IS NULL zurückgegeben werden (Zeile 6 bis ^). Ansonsten wird ein
Gleichheitszeichen vor die Variable gesetzt und außerdem der anschließende Inhalt in einfacheAnführungszeichen geschrieben.
Im SQL-Code selbst entfällt so das Gleichheitszeichen und die einfachen Anführungszeichen.
Dadurch ist es auch möglich, ein Feld daraufhin abzufragen, ob es leer ist.

SelectGroup
Aufruf aus
Makro:

AfterRecordChange

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028

tbl_Adresse, tbl_Ort, tbl_Einstellungen, tbl_Mitglied, tbl_Konto, tbl_BankSUB SelectGroup(oEvent AS OBJECT)
DIM stSql(0) AS STRING
DIM stList AS STRING
DIM stID AS STRING
DIM oForm AS OBJECTDIM oField AS OBJECT
DIM stadrID AS STRING
DIM stkonID AS STRINGoForm = oEvent.Source
stID = SelectSQL(oForm.getString(oForm.findColumn("ID")))oField = oForm.getByName("lboGruppe_mitID")oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()stList = "SELECT ""mitID"" FROM ""tbl_Adresse"" WHERE ""mitID"" "+stID+""
oResult = oSQL_Statement.executeQuery(stList)
WHILE oResult.next
stadrID = oResult.getString(1)WEND
IF stadrID <> "" AND boAccount THEN ' Konto anzeigen zur Auswahl
stList = "SELECT DISTINCT '' AS ""Gruppe"", NULL AS ""ID"", -1 AS ""Sort""
FROM ""tbl_Einstellungen"" UNION "stList = stList + "SELECT DISTINCT 'Neu: Konto existiert noch nicht.'
AS ""Gruppe"", NULL AS ""ID"", 0 AS ""Sort"" FROM ""tbl_Einstellungen""
UNION "
stList = stList + "SELECT 'IBAN: '||""tbl_Konto"".""IBAN""||', BIC:'||""tbl_Bank"".""BIC""||', Bank: '||""tbl_Bank"".""Bank"" AS ""Gruppe"",""tbl_Mitglied"".""ID"", 1 AS SORT "stList = stList + "FROM ""tbl_Mitglied"", ""tbl_Konto"", ""tbl_Bank"" WHERE""tbl_Mitglied"".""ID"" = ""tbl_Konto"".""mitID"" AND""tbl_Konto"".""banID"" = ""tbl_Bank"".""ID"" "stList = stList + "AND (""tbl_Mitglied"".""Gruppe_mitID"" IS NULL OR""tbl_Mitglied"".""ID"" = ""tbl_Mitglied"".""Gruppe_mitID"") ORDER BY""Sort"", ""Gruppe"" "ELSE ' Adresse anzeigen zur Auswahl
stList = "SELECT DISTINCT '' AS ""Gruppe"", NULL AS ""ID"", -1 AS ""Sort""
FROM ""tbl_Einstellungen"" UNION "stList = stList + "SELECT DISTINCT 'Neu: Personengruppe existiert nochnicht.' AS ""Gruppe"", NULL AS ""ID"", 0 AS ""Sort"" FROM""tbl_Einstellungen"" UNION "stList = stList + "SELECT COALESCE ( ""tbl_Adresse"".""Straße_Nr"" || ' ' ||""tbl_Ort"".""Postleitzahl"" || ' ' || ""tbl_Ort"".""Ort"", 'keine
Adresse' ) || "Vereinsverwaltung

436

029
030
031
032
033
034
035
036

stList = stList + "COALESCE ( ', Festnetz: ' ||""tbl_Adresse"".""Telefon_fest"", ', kein Festnetztelefon' ) AS""Gruppe"", ""tbl_Mitglied"".""ID"", 1 AS SORT "stList = stList + "FROM ""tbl_Mitglied"", ""tbl_Adresse"", ""tbl_Ort"" WHERE""tbl_Mitglied"".""ID"" = ""tbl_Adresse"".""mitID"" AND""tbl_Adresse"".""ortID"" = ""tbl_Ort"".""ID"" "stList = stList + "AND (""tbl_Mitglied"".""Gruppe_mitID"" IS NULL OR""tbl_Mitglied"".""ID"" = ""tbl_Mitglied"".""Gruppe_mitID"") ORDER BY""Sort"", ""Gruppe"" "END IF
stSql(0) = stList
oField.ListSource = stSql
oField.refresh
END SUB

Hiermit wird ein Listenfeld abhängig von den Voreinstellungen der Datenbank entweder mit
Kontodaten oder mit Adressdaten gefüllt.
Existiert bereits eine Adresse und gibt es eine Kontoverwaltung, so wird das Konto angezeigt(Zeile 20). Soll die Datenbank keine Kontoverwaltung enthalten, so wird grundsätzlich die
Adresse zur Auswahl für eine Gruppenzuordnung angezeigt (Zeile 25).
Jedes der Listenfelder enthält dabei eine Zeile zum Zurücksetzen des vorherigen Wertes undeine Zeile, mit der ggf. neue Datensätze eingefügt werden können, die eine Kontozuordnung
oder eine Adresszuordnung ermöglichen. Diese Zeilen (21 und 27) ermöglichen, dass im Formular entsprechend die Formularfelder für die Neueingabe eingeblendet werden.

BoardFilterSelectAufruf aus
Formular: frm_Vorstand
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022

SUB BoardFilterSelect(oEvent AS OBJECT)
DIM stmitID AS STRING
DIM stDate AS STRING
oField = oEvent.Source.ModeloForm = oField.Parent
oListField = oForm.getByName("lboMitglied")oDateField = oForm.getByName("datWahlDat")
oFilterForm = oForm.Parent.GetByName("frmFilter")stmitID = oFilterForm.getString(oFilterForm.findColumn("mitID"))
IF stmitID <> "" THEN
IF oForm.IsNew THEN
oListField.BoundField.updateString(stmitID)
END IF
END IF
stDate = oFilterForm.getString(oFilterForm.findColumn("WahlDat"))IF stDate <> "" THEN
IF oForm.IsNew THEN
oDateField.BoundField.updateString(stDate)
END IF
END IFoField.commit
END SUB

Bei einer Auswahl des Vorstandspostens soll eine Anzeige in dem unteren Tabellenkontrollfeldübertragen werden. Hierzu werden die Einzelfelder für die Anzeige des Mitglieds (Zeile 6) unddas Wahldatum (Zeile 7) unter den Umständen mit einem Update versehen, wenn der Datensatz komplett neu ist. Vorherige Einträge sollen nicht überschrieben werden.
Zum Schluss wird der Inhalt des Listenfeldes für den Vorstandsposten übertragen (Zeile 21).

Vereinsverwaltung

437

SelectBoard
Aufruf aus
Formular: frm_Vorstand

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027

028
029
030
031
032

tbl_VorstandPosten, tbl_Mitglied_VorstandPosten

SUB SelectBoard(oEvent AS OBJECT)
DIM stSql(0) AS STRING
DIM stList AS STRING
DIM stID AS STRING
DIM oForm AS OBJECTDIM oField AS OBJECT
DIM i AS INTEGER
oForm = oEvent.Source
stID = SelectSQL(oForm.getString(oForm.findColumn("vorID")))oFilterForm = oForm.Parent.GetByName("frmFilter")stDate = oFilterForm.getString(oFilterForm.findColumn("WahlDat"))IF stDate <> "" THEN
inYear = CInt(Left(stDate,4))i = inYear/2
IF i * 2 = inYear THEN
stOrder = "ASC"ELSE
stOrder = "DESC"
END IF
ELSE
stOrder = "ASC"END IFoField = oForm.getByName("lboVorstandPosten")stList = "SELECT ""Posten"" || ' → ' || CASE WHEN ""Zyklus"" = 'g' THEN
'gerade Jahreszahlen' WHEN ""Zyklus"" = 'u' THEN 'ungerade Jahreszahlen' ELSE
'kein Zyklus' END AS ""VPosten"", "stList = stList + """ID"", CASE WHEN ""Zyklus"" = '-' THEN 'h' ELSE ""Zyklus""
END AS ""Zyklus"" FROM ""tbl_VorstandPosten"" "stList = stList + "WHERE ""ID"" NOT IN (SELECT""tbl_Mitglied_VorstandPosten"".""vorID"" FROM ""tbl_VorstandPosten"",""tbl_Mitglied_VorstandPosten"" "stList = stList + "WHERE ""tbl_VorstandPosten"".""ID"" =""tbl_Mitglied_VorstandPosten"".""vorID"" AND""tbl_VorstandPosten"".""Zyklus"" <> '-' AND""tbl_Mitglied_VorstandPosten"".""EndDat"" IS NULL) "stList = stList + "OR ""ID"" "+stID+" ORDER BY ""Zyklus"" "+stOrder+", ""ID""ASC "stSql(0) = stList
oField.ListSource = stSql
oField.refresh
END SUB

Der Inhalt für das Listenfeld, mit dem die Vorstandsposten zugewiesen werden, wird abhängig
von dem Jahr erstellt. Da die Posten nach geraden und ungeraden Jahren vergeben werden
wird von Zeile 14 bis Zeile 19 die Sortierung nach dem Wahldatum vorgenommen. Entweder
erscheinen die Posten mit den ungeraden oder mit den geraden Jahreszahlen in der Liste oben.
Es werden nur Posten angezeigt, die zur Zeit frei verfügbar sind. Ausnahme ist der Posten, dermit dem aktuellen Datensatz bereits abgespeichert ist. Wäre das nicht der Fall, dann würden
alle Zuweisungen über ein Formular nicht mehr sichtbar sein.
Dieses Makro ist bei dem Formular mit dem Listenfeld verbunden, das als einzelnes Kontrollfeld
eingebaut ist (Zeile 23). Das Listenfeld im Tabellenkontrollfeld ist davon nicht berührt.

Vereinsverwaltung

438

SelectKey
Aufruf aus
Formular: frm_Schluessel

Benötigt
Tabelle:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020

tbl_Schluessel, tbl_MitgliedSchluessel, tbl_SchluesselAnzahl

SUB SelectKey(oEvent AS OBJECT)
BoolStart(oEvent)
DIM stSql(0) AS STRING
DIM stList AS STRING
DIM stID AS STRING
DIM oForm AS OBJECTDIM oField AS OBJECT
oForm = oEvent.Source
stID = SelectSQL(oForm.getString(oForm.findColumn("SNr")))oField = oForm.getByName("lboSchluessel")stList = "SELECT ""tbl_Schluessel"".""Bereich"" || ' → ' ||""tbl_Schluessel"".""SNr"" AS ""Schluessel"", ""tbl_Schluessel"".""SNr"" "stList = stList + "FROM ""tbl_Schluessel"" LEFT JOIN ( SELECT COUNT( ""ID"" )AS ""Anzahl"", ""SNr"" FROM ""tbl_Mitglied_Schluessel"" "stList = stList + "WHERE ""RueckDat"" IS NULL GROUP BY ""SNr"" ) AS ""a"" ON""tbl_Schluessel"".""SNr"" = ""a"".""SNr"" "stList = stList + "LEFT JOIN ( SELECT SUM( ""Anzahl"" ) AS ""Anzahl"", ""SNr""
FROM ""tbl_SchluesselAnzahl"" GROUP BY ""SNr"" ) AS ""b"" "stList = stList + "ON ""tbl_Schluessel"".""SNr"" = ""b"".""SNr"" WHERE COALESCE( ""b"".""Anzahl"", 0 ) - COALESCE ( ""a"".""Anzahl"", 0 ) > 0 "stList = stList + "OR ""SNr"" "+stID+" ORDER BY ""Schluessel"" ASC"
stSql(0) = stList
oField.ListSource = stSql
oField.refresh
END SUB

Mit dieser Prozedur wird ein Listenfeld für die Schlüsselauswahl mit neuem Inhalt gefüllt. Dabei
werden nur die Schlüssel angezeigt, bei denen die Anzahl größer als 0 ist. Einzige Ausnahme
bildet der Schlüssel, der im aktuellen Datensatz bereits vorher ausgewählt wurde. Wäre dies
nicht der Fall, so würden auch bereits vergebene Schlüssel nicht mehr zuverlässig angezeigt,
sofern eben kein Schlüssel mehr zur Ausgabe vorhanden ist.
Solch ein Makro funktioniert für Listenfelder nur zuverlässig in Formularen mit einzelnen Kontrollfeldern. Innerhalb eines Tabellenkontrollfeldes wird die Aktualisierung des Listenfeldes nicht
zuverlässig durchgeführt, so dass eben manchmal der anzuzeigende Inhalt fehlt.

IBAN_Test
Aufruf aus
Formular: frm_Einstellungen, frm_Mitglied
001
002
003
004
005
006
007
008
009
010
011
012

SUB IBAN_Test(oEvent AS OBJECT)
DIM arChar()
DIM arCountry()
DIM stIBAN AS STRING
DIM stStart AS STRING
arChar = array("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M","N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z")oFieldIBAN = oEvent.Source.Model
stIBAN = Join(Split(oFieldIBAN.Text),"")
stIBAN = Join(Split(stIBAN,"_")
stChar = Left(stIBAN,1)
FOR k = LBound(arChar) TO UBound(arChar)IF arChar(k) = stChar THEN

Vereinsverwaltung

439

013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031

stCharNum1 = k + 10
EXIT FOR
END IFNEXT
stChar = Right(Left(stIBAN,2),1)
FOR k = LBound(arChar) TO UBound(arChar)IF arChar(k) = stChar THEN
stCharNum2 = k + 10
EXIT FOR
END IFNEXT
stIBAN_PZ = Mid(stIBAN,3,2)
FOR x = 5 TO Len(stIBAN) STEP 7
stStart = Trim(Str(CDbl(stStart & Mid(stIBAN,x,7)) Mod 97))
NEXT
IF CDbl(stStart & stCharNum1 & stCharNum2 & stIBAN_PZ) Mod 97 <> 1 THEN
msgbox "Die IBAN ist falsch."
END IF
END SUBDie IBAN soll hier überprüft werden. Dazu wird in Zeile 8 von dem eingegebenen Text die Leerzeichen entfernt. Sind noch irgendwelche Unterstriche der Eingabemaske vorhanden, so stören
die bei der Berechnung. Das sollte natürlich nicht vorkommen, da die Länge der IBAN direkt
vorgegeben ist, aber es könnte ja passieren, dass jemand einfach das Feld nach den ersten Eingaben verlässt. Das Entfernen der Unterstriche geschieht in Zeile 9.
Für die Berechnung der IBAN müssen die Landeskennungen in Zahlen umgewandelt werden.
Dafür ist in Zeile 6 ein Array mit allen Buchstaben nach der Reihenfolge im Alphabet zusammengestellt worden. Der erste Buchstabe wird in Zeile 10 ausgelesen. Anschließend wird untersucht, an welcher Position im Array dieser Buchstabe liegt (Zeilen 11 bis 16). Zu der ermittelten
Position (0 für "A", 1 für "B" usw.) wird nach dem Standard für die Prüfberechnung '10' addiert.
Von Zeile 17 bis Zeile 23 erfolgt das gleiche Verfahren für den zweiten Buchstaben. Dadurch
wird aus 'DE' die Ziffernkombination '1314'.
In Zeile 24 wird die Prüfziffer von der IBAN abgeschnitten. Von dem Rest der IBAN wird danach
ermittelt, wie groß der Rest ist, wenn die gesamte Zahl durch 97 dividiert wird (MOD 97). Dies
geht wegen der Größe der Zahl nicht in einem Schritt. Stattdessen werden die ersten 7 Ziffern
genommen und der Rest hieraus ermittelt, anschließend die folgenden 7 Ziffern an den Rest
angehängt usw. Dies Verfahren ist ähnlich der schriftlichen Division, wie es in der Schule praktiziert wird.
Im letzten Schritt (Zeile 28)wird der Rest dieser Division mit der Zahl für den ersten Landesbuchstaben, der Zahl für den zweiten Landesbuchstaben und der IBAN-Prüfziffer nach einmal
durch 97 dividiert. Der Rest dieser Division muss '1' sein – nur dann ist die eingegebene IBAN
korrekt.

IBAN_Length_Change
Aufruf aus
Formular: frm_Einstellungen, frm_Mitglied
001
002
003
004
005
006
007
008
009
010
011

SUB IBAN_Length_Change(oEvent AS OBJECT)
DIM i AS INTEGERDIM oSel AS OBJECT
oSel = oEvent.Source.Selection
oFieldIBAN = oEvent.Source.Model
stIBAN = Left(oFieldIBAN.Text, oSel.Min)
stLiteralMask = Left(oFieldIBAN.LiteralMask, 2)
stEMask = "AANNLNNNNLNNNNLNNNNLNNNNLNNNNLNNNNLNNNNLNN"
stLMask = stIBAN & "__ ____ ____ ____ ____ ____ ____ ____ __"IF stLiteralMask <> stIBAN THEN
SELECT CASE stIBAN

Vereinsverwaltung

440

012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039

CASE "DE"i = 27
CASE "NL"i = 22
CASE "BE"i = 19CASE "LU"i = 24
CASE "FR"i = 33
CASE "CH"i = 26CASE "AT"i = 24
CASE "CZ"i = 29CASE "PL"i = 34
CASE "DK"i = 22END SELECT
IF i >= 18 THENoFieldIBAN.EditMask = Left(stEMask,i)oFieldIBAN.LiteralMask = Left(stLMask,i)
END IFoEvent.Source.setSelection(oSel)
END IF
END SUB

Mit dieser Prozedur wird das maskierte Feld für die IBAN auf eine neue Eingabemaske eingestellt. Die Grundsätzliche Maske ist so aufgebaut, dass in den ersten beiden Positionen Großbuchstaben erlaubt sind, in den folgenden Positionen nur Zahlen. Nach jeweils 4 Zeichen ist
keine Eingabe erlaubt. An diesen Stellen wird ein Leerzeichen eingefügt (Zeile 5). Die maximale
IBAN-Länge beträgt 34 Zeichen. Werden die Leerzeichen nach jeweils 4 Zeichen hinzugefügt,
dann sind das mit den 8 Leerzeichen maximal 42 Zeichen für das Feld.
Die IBAN ist abhängig von der Landeskennung unterschiedlich lang 14. So hat die IBAN mit derKennung 'DE' eine Gesamtlänge von 22 Zeichen. Da über das maskierte Feld eine IBAN mit
Leerzeichen nach jeweils 4 Zeichen versehen wird, ergibt sich daraus eine Länge von 22 Zeichen und 22/4 = 5 Rest 2, also 5 Leerzeichen. Deshalb in Zeile 13 die Zuweisung von 27 Zeichen.
In Zeile 34 und 35 wird die Eingabemaske in das Formularfeld übertragen. Die Länge i muss
dafür mindestens 18 betragen, da dies die kürzeste IBAN ist, die möglich ist.
Am Anfang der Prozedur wurde in Zeile 4 die aktuelle Cursorposition über die SELECTION ermittelt. Diese Position wird zum Schluss genutzt um den Cursor direkt hinter der Landeskennung
zu positionieren. So kann bei der Eingabe ohne Probleme eine Änderung der Landeskennung
erfolgen und der Rest des maskierten Feldes wird während der Eingabe angepasst.

FilterFiltern
Aufruf aus
Formular: Einzelmessung_System, frm_Berichte, frm_Boot
001
002
003
004

SUB Filtern(oEvent AS OBJECT)
DIM oForm AS OBJECTDIM oField AS OBJECT
DIM stFormTarget AS STRING

14 https://de.wikipedia.org/wiki/Internationale_Bankkontonummer

Vereinsverwaltung

441

005
006
007
008
009
010
011
012
013
014
015
016

DIM n AS INTEGER
DIM arForm()oField = oEvent.Source.ModeloField.commit()
stFormTarget = oField.Tag
oForm = oField.Parent
oForm.updateRow' Das Formular wird gespeichert
arForm = Split(stFormTarget,",")FOR n = LBound(arForm()) TO UBound(arForm())
oForm.Parent.getByName(arForm(n)).reload()NEXT
END SUB

Von einem Listenfeld aus wird die Filterung von Formularen veranlasst. In Zeile 8 wird der Inhalt
des Feldes übertragen. In den Zusatzinformationen (Zeile 9) des Feldes stehen die Formulare,
die auf den Filter eingestellt werden sollen. Die Namen der Formulare sind durch Kommas voneinander getrennt. Nachdem das Formular abgespeichert ist werden diese Namen in ein Array
eingelesen (Zeile 12). Das Array wird in einer Schleife ausgelesen und die entsprechenden Formulare neu geladen.

Navigation
FormStart
Aufruf aus
Formular: Einzelmessung_System, frm_Berichte, frm_Boot, frm_Einstellungen, frm_Mitglied,
frm_Schluessel, frm_Vorstand

Benötigt
Tabelle:tbl_Filter

Ansicht:

viw_ServVar

001
002
003
004
005
006
007
008
009
010
011

SUB FormStart(oEvent AS OBJECT)
DIM arTitle()
oForm = oEvent.Source
arTitle = split(ThisComponent.Title, ThisComponent.UntitledPrefix)
stTitle = arTitle(1)oConnection = oForm.activeConnection()oSQL_Statement = oConnection.createStatement()
stSql = "UPDATE ""tbl_Filter"" SET ""Formular"" = '"+stTitle+"' WHERE ""ID"" =COALESCE ( ( SELECT ""ID"" FROM ""viw_ServVar"" ), '1' )"oSQL_Statement.executeUpdate(stSql)
oForm.reload()
END SUB

Wird irgendein Formular über Mausklick als erstes Formular gestartet, so muss das Listenfeld
für die Formularanzeige das korrekte Formular auch anzeigen. Hier wird aus dem Titel des Formularfensters der Name des Formulars geschlossen (Zeile 5). Anschließend wird dieser Text in
die Tabelle "tbl_Filter" übertragen und das Formular, das ja beständig nur den einen Datensatz
aus "tbl_Filter" anzeigt, neu geladen. Danach zeigt das Listenfeld den Namen des aktuellen Formulars an.

Navigation
Aufruf aus
Formular: Einzelmessung_System, frm_Berichte, frm_Boot, frm_Einstellungen, frm_Mitglied,
frm_Schluessel, frm_Vorstand001

SUB Navigation(oEvent AS OBJECT)

Vereinsverwaltung

442

002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036

DIM oListField AS OBJECT
DIM stZiel AS STRING
DIM stStart AS STRING
DIM arStart()
oListField = oEvent.Source.ModeloListField.commit()oForm = oListField.Parent
IF oForm.Parent.hasByName("frmMitglied") THEN
oForm1 = oForm.Parent.getByName("frmMitglied")stID = oForm1.getString(oForm1.findColumn("ID"))oForm.updateString(oForm.findColumn("mitID"), stID)ELSEIF oForm.Parent.hasByName("frmVorstand") THEN
oForm1 = oForm.Parent.getByName("frmVorstand")stID = oForm1.getString(oForm1.findColumn("mitID"))oForm.updateString(oForm.findColumn("mitID"), stID)ELSE
END IFoForm.updateRowarStart = split(ThisComponent.Title, ThisComponent.UntitledPrefix)
stStart = arStart(1)stZiel = oListField.CurrentValueoFormStart = ThisDatabaseDocument.FormDocuments.getByName(stStart)oFormStart.Component.CurrentController.Frame.ContainerWindow.Visible = False
oFormZiel = ThisDatabaseDocument.FormDocuments.getByName(stZiel)
IF NOT IsNull(oFormZiel.Component) THEN
IF NOT IsNull(oFormZiel.Component.CurrentController) THEN
oFormZiel.Component.CurrentController.Frame.ContainerWindow.Visible = True
ELSEoFormZiel.open
END IF
ELSEoFormZiel.open
END IFoFormZiel.Component.Drawpage.Forms.getByName("frmFilter").reload()
END SUB

Mit dieser Prozedur wird von einem Formular zum anderen über ein Listenfeld gewechselt.
Dabei wird ein einmal geöffnetes Formular nicht wieder geschlossen sondern nur unsichtbar
geschaltet, um einen Wechsel von Formular zu Formular reibungsloser ablaufen zu lassen.
In Zeile 7 wird der Inhalt des Listenfeldes zur Formularauswahl auf die Tabelle "tbl_Filter" übertragen. Das dazugehörige Formular hat die Bezeichnung "frmFilter". Von diesem Formular aus
wird jetzt zuerst gesucht, ob ein Parallelformular "frmMitglied" existiert (Zeile 9). Unter diesen
Umständen wird die Mitgliedsnummer aus dem Formular in die "tbl_Filter" übertragen. Bei
einem Formular mit der Bezeichnung "frmVorstand" wird entsprechend verfahren. So wird die
Mitgliedsnummer von einem Formular zum nächsten mitgenommen.
Danach wird das Formular, das auf der "tblFilter" als Datenbasis beruht, abgespeichert (Zeile19).
Der Name des Formulars, aus dem die Prozedur aufgerufen wird, wird aus dem Titel des Fensters ausgelesen und in der globalen Variablen oFormStart gespeichert. Der Name des Zielformulars wird aus dem Listenfeld ausgelesen, das die Prozedur ausgelöst hat. Wenn dieses Formular bereits geöffnet ist, dann ist oFormZiel.Component.CurrentController nicht leer (Zeile27). In dem Fall muss dieses Formular nur sichtbar gemacht werden. Andernfalls wird das Formular geöffnet (Zeile 30, 33). Zum Schluss wird das Formular noch einmal neu geladen, damit
eventuell in anderen Formularen gemachte Änderungen auch übernommen werden.

Vereinsverwaltung

443

Speichern
ListboxContentChange
Aufruf aus
Formular: frm_Mitglied

Benötigt
Makro:
001
002
003
004
005
006

ShowTableControl

SUB ListboxContentChange(oEvent AS OBJECT)
oForm = oEvent.Source
stListbox = oForm.getByName("hidListbox").HiddenValueoListbox = oFormStart.getByName(stListbox)
oListbox.refresh()
END SUB

Wurde in den einzublendenden Tabellenkontrollfeldern ein Eintrag geändert, so sollte der Eintrag auch in den entsprechenden Listenfeldern anschließend zur Verfügung stehen. Innerhalb
des Formulars, in dem die Tabellenkontrollfelder liegen, liegt deswegen auch ein verstecktes
Feld «hidListbox», aus dem die Bezeichnung der Listbox gelesen wird (Zeile 3). Das entsprechende Formular, in dem diese Listbox liegt, wurde durch die Prozedur ShowTableControl ineiner globalen Variablen «oFormStart» hinterlegt.

NewGroup
Aufruf aus
Formular: frm_Mitglied
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017

SUB NewGroup(oEvent AS OBJECT)
DIM loRow AS LONG
oField = oEvent.Source.ModeloField.Commit()oForm = oField.Parent
IF oForm.IsNew THEN
oForm.insertRow()
ELSEoForm.updateRow()
END IF
IF oField.Tag = "parent" THEN
oForm = oForm.parent
END IF
loRow = oForm.getRow()oForm.reload()
oForm.absolute(loRow)
END SUBIn dem «frm_Mitglied» sind zwei Listenfelder enthalten, mit denen eine Gruppenzuweisung verbunden ist. In dem einen wird eine Adresse ausgewählt und davon abhängig soll dann ggf. eine
Kontoverbindung erscheinen. Im zweiten Feld wird bei unterschiedlichen Adressen ggf. eine
Kontoverbindung gewählt.
Beide Listenfelder sollen den entsprechenden Datensatz zuerst sichern und dann das Formular
neu laden. Da ein Listenfeld in einem Unterformular liegt ist in diesem in den Zusatzinformationen "parent" angegeben (Zeile 11). Beim Auslösen über dieses Feld wird das Laden im darüberliegenden Formular veranlasst, so dass das bis dahin noch sichtbare weitere Formular für die
Kontoverbindung unsichtbar geschaltet wird.

Vereinsverwaltung

444

ShowTableControl
Aufruf aus
Formular: frm_Mitglied
Makros:Navigation, ListboxContentChange (beide: globale Variable oFormStart)

001

GLOBAL oFormStart AS OBJECT

001
002
003
004
005
006
007
008
009
010
011
012
013

SUB ShowTableControl(oEvent AS OBJECT)
DIM stForm AS STRING
oField = oEvent.Source.Model
stForm = oField.Tag
IF stForm <> "" THEN
oForm = ThisComponent.Drawpage.Forms.getByName(stForm)oFormStart = oField.Parent
ELSE
oForm = oField.Parent
END IF
FOR k = 0 TO oForm.Count - 1
oField = oForm.getByIndex(k)IF oField.ServiceName <> "stardiv.one.form.component.Form" ANDoField.ServiceName <> "stardiv.one.form.component.Hidden" THEN
IF stForm <> "" THENoField.EnableVisible = True
ELSE
oField.EnableVisible = False
END IF
END IFNEXT
END SUB

014
015
016
017
018
019
020
021

Im Formulardokument «frm_Mitglied» befinden sich zwei Tabellenkontrollfelder (für Banken undfür Orte), die beim Öffnen des Formulars nicht sichtbar sind. Sie sollen nur bei Bedarf eingeblendet werden und eine Datenänderung ermöglichen. Die Prozedur schaltet dabei abhängig
von dem auslösenden Element entweder das jeweiligen Tabellenkontrollfeld mit der Schaltfläche «Schließen» sichtbar (stForm <> "") oder unsichtbar. Die Schaltfläche zum Schließen hat
dabei keinen Eintrag in den Zusatzinformationen oField.Tag (Zeile 4), so dass sie eben lediglich das Tabellenkontrollfeld und sich selbst unsichtbar macht.
Um immer zu wissen, welches Formular denn nun sichtbar gemacht wurde wird der entsprechende Eintrag in der globalen Variable oFormStart zwischengespeichert. Diese Variable wird
auch in den Prozeduren Navigation und ListboxContentChange genutzt.

SubscriptionChanged
Aufruf aus
Formular: Einzelmessung_System
001
002
003
004

SUB SubscriptionChanged(oEvent AS OBJECT)
oForm = oEvent.Source.Parent.getByName("frmBeitragsuebersicht")oForm.reload()
END SUB

Diese Prozedur dient nur dazu, nach einer Datenänderung im Formulardokument «frm_Beitrag»
das Formular «frmBeitragsuebersicht» neu zu laden.

SubscriptionSave
Aufruf aus
Formular: Einzelmessung_System

Vereinsverwaltung

445

Benötigt
Tabelle:tbl_Gruppe_Beitrag

Ansicht:

viw_Gruppe, viw_Beitrag_Gruppe_kalkuliert

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023

SUB SubscriptionSave(oEvent AS OBJECT)
oForm = oEvent.Source.Model.Parent
oForm.updateRow()oConnection = oForm.ActiveConnection()oSQL_Statement = oConnection.createStatement()oSQL_Statement1 = oConnection.createStatement()
stSql = "SELECT ""ID"", ""Gruppe"" FROM ""viw_Gruppe"" WHERE""Alter_Beitrag"" = 18"
oResult = oSQL_Statement.executeQuery(stSql)
WHILE oResult.next
stID = oResult.getString(1)stgruID = SelectSQL(oResult.getString(2))
FOR i = LBound(arSubscription) TO UBound(arSubscription)
IF arSubscription(i) = stID THEN
stSql = "DELETE FROM ""tbl_Gruppe_Beitrag"" WHERE""gruID"" "+stgruID+" "oSQL_Statement1.executeUpdate(stSql)
stSql = "INSERT INTO ""tbl_Gruppe_Beitrag"" (""mitID"", ""beiID"",""Anzahl"") (SELECT ""Gruppe"", ""beiID"", ""Anzahl"" FROM""viw_Beitrag_Gruppe_kalkuliert"" WHERE ""Gruppe"" "+stgruID+")"oSQL_Statement1.executeUpdate(stSql)
EXIT FOR
END IFNEXT
WENDoForm.Parent.getByName("frmMitglied").getByName("frmGruppe").reload()
END SUB

Vor dem Update eines Datensatzes für die Beitragsberechnung wird zuerst nachgeschaut, ob
eine vorher 17-jährige Person jetzt für den nächsten Beitrag 18 Jahre alt geworden ist. Diese
Information ist durch die Prozedur Preferences beim Start der Datenbankdatei in das Array
arSubscription geschrieben worden.
In Zeile 7 wird die Abfrage formuliert, mit der alle Gruppen ausgelesen werden, bei denen eine
Person aktuell 18 Jahre alt ist. In der Schleife beginnend ab Zeile 13 wird dann nachgesehen, ob
diese Gruppen-ID gleich einem Wert aus dem Array arSubscription ist, also vorher eine Person hatte, die eben gerade noch 17 Jahre alt war. Nur bei diesen Gruppen wird die Neuberechnung des Beitrages angestoßen. Dies erfasst dann auf jeden Fall alle Gruppen, bei denen eventuell ein vorher 17-jähriges Mitglied jetzt 18 Jahre alt geworden ist.

Wartung
Im Modul «Wartung» befinden sich nur Prozeduren, die keine weitere Verbindung zu Formularen
haben. Sie werden direkt über Extras → Makros → Makro ausführen gestartet.

Tabellenindex_runter
001
002
003
004
005
006
007
008
009
010
011
012

Sub Tabellenindex_runter(tablename AS STRING)DIM inAnzahl AS INTEGER
DIM inSequence_Value AS INTEGER
oDatenquelle = ThisDatabaseDocument.CurrentController
IF NOT (oDatenquelle.isConnected()) THEN oDatenquelle.connect()oVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()
stSql = "SELECT MAX(""ID"") FROM """+tablename+""""
oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
IF NOT ISNULL(oAbfrageergebnis) THEN
WHILE oAbfrageergebnis.next
inAnzahl = oAbfrageergebnis.getInt(1)Vereinsverwaltung

446

013
014
015
016
017
018
019
020
021

WEND
IF inAnzahl = "" THEN
inAnzahl = -1
END IF
inSequence_Value = inAnzahl + 1
oSQL_Anweisung1 = oVerbindung.createStatement()oSQL_Anweisung1.executeQuery("ALTER TABLE """+tablename+""" ALTER COLUMN""ID"" RESTART WITH "+inSequence_Value+"")END IFEnd Sub

Mit dieser Prozedur wird der automatisch hoch geschriebene Primärschlüssel mit der vorgegebenen Bezeichnung "ID" auf den niedrigst möglichen Wert eingestellt.
Der Code für den höchsten in "ID" eingetragene Wert wird in Zeile 8 erstellt. Die Abfrage wirdanschließend gestartet und der Rückgabewert in einer Variablen oAbfrageergebnis gespeichert.
In Zeile 12 wird das Ergebnis der Abfrage ausgelesen. Ein weiterer Datensatz existiert nicht, sodass die WHILE-Schleife auch entfallen könnte.
Falls der höchste Wert gar kein Wert ist, also die Tabelle leer ist wird der höchste Wert als -1
angenommen. Hier könnte in Zeile 15 auch gegebenenfalls ein anderer Wert eingetragen werden. Bei der internen HSQLDB beginnt die Zählung aber standardmäßig bei 0. Das wurde hierso übernommen.
Der höchste Wert wird um 1 erhöht und anschließend direkt in die Tabellendefinition übertragen.

Tabellenstart
Benötigt
Tabelle:
001
002
003
004
005

tbl_Mitglied, tbl_Ort, tbl_Bank

Sub Tabellenstart
Tabellenindex_runter("tbl_Mitglied")
Tabellenindex_runter("tbl_Ort")
Tabellenindex_runter("tbl_Bank")
End Sub

In diese Prozedur werden nacheinander alle Tabellen eingetragen, bei denen der Wert für den
automatisch erstellten Primärschlüssel so weit wie möglich herunter gesetzt werden soll. Diese
Prozedur kann beliebig um entsprechende Tabellen wie z. B. tbl_SchluesselAnzahl erweitert werden.

Tabellenbereinigung
001
002
003
004
005
006
007

SUB Tabellenbereinigung(NameBezugsTabelle AS STRING, NameBezugsID AS STRING,NameLoeschTabelle AS STRING)
oDatenquelle = ThisDatabaseDocument.CurrentController
If NOT (oDatenquelle.isConnected()) Then oDatenquelle.connect()oVerbindung = oDatenquelle.ActiveConnection()oSQL_Anweisung = oVerbindung.createStatement()oSQL_Anweisung.executeQuery("DELETE FROM """+NameLoeschTabelle+""" WHERE(""ID"" NOT IN (SELECT """+NameBezugsID+""" FROM"""+NameBezugsTabelle+"""))")
END SUB

Hiermit werden alle Datensätze entfernt, die in der Bezugstabelle keine Verwendung mehr finden. Dies geht nicht mittels ON DELETE CASCADE, was in dem Relationenentwurf eingestellt
werden kann, wenn sich die Bezüge durch eine n:1-Beziehung ergeben. Es ließe sich so z.B.
beim Löschen eines Ortes die Adresse löschen, nicht aber der Ort, falls keine entsprechende
Adresse existiert.
In Zeile 6 werden die Datensätze der Zieltabelle gelöscht, für die in der Bezugstabelle keine
Einträge mehr existieren.
Vereinsverwaltung

447

Tabellenbereinigung_Ort
Benötigt
Tabelle:
001
002
003
004

tbl_Adresse, tbl_Ort

SUB Tabellenbereinigung_Ort
Tabellenbereinigung("tbl_Adresse","ortID","tbl_Ort")
Tabellenindex_runter("tbl_Ort")
END SUB

Zuerst wird in der Tabelle tbl_Adresse überprüft, ob noch ein Eintag im Feld "ort_ID" existiert.
Existiert kein Eintag mehr, sehr wohl aber ein entsprechender "Ort" in der Tabelle tbl_Ort, so
wird dieser Ort gelöscht.
Anschließend wird der automatisch hoch zählende Primärschlüssel für die Tabelle tbl_Ort so
weit wie möglich zurück gesetzt.

Tabellenbereinigung_Bank
Benötigt
Tabelle:
001
002
003
004

tbl_Konto, tbl_BankSUB Tabellenbereinigung_Bank
Tabellenbereinigung("tbl_Konto","banID","tbl_Bank")
Tabellenindex_runter("tbl_Bank")
END SUB

Zuerst wird in der Tabelle tbl_Konto überprüft, ob noch ein Eintag im Feld "ban_ID" existiert.
Existiert kein Eintag mehr, sehr wohl aber eine entsprechende "Bank" in der Tabelle tbl_Bank,so wird diese Bank gelöscht.
Anschließend wird der automatisch hoch zählende Primärschlüssel für die Tabelle tbl_Bank soweit wie möglich zurück gesetzt.

ViewsErstellen
Benötigt
Abfrage:
001
002
003
004
005
006
007
008
009
010
011
012
013
014

015
016
017
018
019

tbl_Gruppe_Beitrag

SUB ViewsErstellen
DIM oDatasource AS OBJECTDIM oConnection AS OBJECTDIM oSQL_Statement AS OBJECTDIM oQuery AS OBJECT
DIM stSql AS STRING
DIM stView AS STRING
DIM stQuery AS STRING
DIM arQueries()
DIM arViews()
oDatasource = thisDatabaseDocument.CurrentController
IF NOT (oDatasource.isConnected()) THEN oDatasource.connect()oConnection = oDatasource.ActiveConnection()arViews = array("viw_Reports", "viw_Beitrag_Gruppe_kalkuliert", "viw_Gruppe","viw_Beitrag", "viw_Mitglieder_komplett","viw_Filter_Mitglieder_komplett_AusEin", "viw_Statistik_Alter_Detail","viw_Anschrift", "viw_Adresse_Num_Namen", "viw_Schluessel_Mitglied","viw_Boote_Mitglied", "viw_Abteilungen_Mitglied", "viw_ServVar" )FOR i = LBound(arViews()) TO UBound(arViews())
stSql = "DROP VIEW IF EXISTS """+arViews(i)+""""oSQL_Statement = oConnection.createStatement()oSQL_Statement.executeUpdate(stSql)
NEXT

Vereinsverwaltung

448

020
021

022
023
024
025
026
027
028
029

oQuery = oDatasource.DataSource.getQueryDefinitions()arQueries = array("v_qry_ServVar", "v_qry_Abteilungen_Mitglied","v_qry_Boote_Mitglied", "v_qry_Schluessel_Mitglied","v_qry_Adresse_Num_Namen", "v_qry_Anschrift","v_qry_Statistik_Alter_Detail", "v_qry_Filter_Mitglieder_komplett_AusEin","v_qry_Mitglieder_komplett", "v_qry_Beitrag", "v_qry_Gruppe","v_qry_Beitrag_Gruppe_kalkuliert", "v_qry_Reports")
FOR i = LBound(arQueries()) TO UBound(arQueries())
stQuery = oQuery.getByName(arQueries(i)).Command
stView = arViews(UBound(arViews()) - i)stSql = "CREATE VIEW """+stView+""" AS "+stQuery+""oSQL_Statement = oConnection.createStatement()oSQL_Statement.executeUpdate(stSql)
NEXT
END SUBVon Zeile 11 bis Zeile 13 wird die Verbindung zur Datenbank eingelesen, gegebenenfalls neu
erstellt.
Anschließend werden alle Ansichten in einem Array zusammengefasst. Die Reihenfolge der
Ansichten ist so gewählt, dass sie nacheinander gelöscht werden können, ohne dass sich eine
nachfolgende Ansicht noch auf die vorhergehende Ansicht bezieht. Die Löschung der Ansichten
erfolgt in einer Schleife (Zeile 15 bis Zeile 19).
Zeile 20 ermöglicht den Zugriff auf den SQL-Code für die Abfragen der Datenbank. Die Abfragen für die Ansichten werden wie die Ansichten selbst in einem Array zusammengefasst. Nur
ist die Reihenfolge genau anders herum. Zuerst sollen die Ansichten erstellt werden, auf die die
anderen Ansichten aufbauen.
Auch die Erstellung der neuen Ansichten erfolgt in einer Schleife. Dabei wird das Array für die
Ansichten in Zeile 24 in umgekehrter Reihenfolge (von hinten nach vorne) eingelesen. Die
Ansichten werden durch den Code der entsprechenden Abfragen (Zeile 25) erstellt.

Vereinsverwaltung449