Index: src/mlx/const.py
===================================================================
--- src/mlx/const.py	(revision 858)
+++ src/mlx/const.py	(revision 859)
@@ -1,4 +1,5 @@
 
 import sys
+import datetime
 
 #-------------------------------------------------------------------------------
@@ -559,2 +560,6 @@
 
 #-------------------------------------------------------------------------------
+
+defaultDate = datetime.date(1900, 1, 1)
+
+#-------------------------------------------------------------------------------
Index: src/mlx/gui/flight.py
===================================================================
--- src/mlx/gui/flight.py	(revision 854)
+++ src/mlx/gui/flight.py	(revision 859)
@@ -528,5 +528,5 @@
         if self._wizard.loggedIn:
             for flight in loginResult.flights:
-                self._addFlight(flight)
+                self.addFlight(flight)
             for flight in loginResult.reportedFlights:
                 self._pendingFlightsWindow.addReportedFlight(flight)
@@ -536,5 +536,5 @@
         self._updatePendingButton()
 
-    def _addFlight(self, flight):
+    def addFlight(self, flight):
         """Add the given file to the list of flights."""
         self._flights.append(flight)
@@ -543,5 +543,5 @@
     def _reflyFlight(self, flight):
         """Refly the given flight."""
-        self._addFlight(flight)
+        self.addFlight(flight)
         self._updatePending()
 
@@ -641,5 +641,5 @@
                 with open(fileName, "rt") as f:
                     bookedFlight.readFromFile(f)
-                self._addFlight(bookedFlight)
+                self.addFlight(bookedFlight)
             except Exception, e:
                 print "Failed to load flight:", util.utf2unicode(str(e))
@@ -5479,4 +5479,8 @@
         """Reload the flights from the MAVA server."""
         self.login(callback, None, None)
+
+    def addFlight(self, bookedFlight):
+        """Add the given booked flight to the flight selection page."""
+        self._flightSelectionPage.addFlight(bookedFlight)
 
     def reflyFlight(self, bookedFlight):
Index: src/mlx/gui/flightlist.py
===================================================================
--- src/mlx/gui/flightlist.py	(revision 858)
+++ src/mlx/gui/flightlist.py	(revision 859)
@@ -28,4 +28,9 @@
         self._defaultDescending = defaultDescending
         self._cellDataFn = cellDataFn
+
+    @property
+    def attribute(self):
+        """Get the attribute the column belongs to."""
+        return self._attribute
 
     @property
Index: src/mlx/gui/gui.py
===================================================================
--- src/mlx/gui/gui.py	(revision 858)
+++ src/mlx/gui/gui.py	(revision 859)
@@ -97,4 +97,7 @@
         self._credentialsPassword = None
 
+        self._bookFlightsUserCallback = None
+        self._bookFlightsBusyCallback = None
+
         self.webHandler = web.Handler(config, self._getCredentialsCallback)
         self.webHandler.start()
@@ -557,4 +560,37 @@
         self._flightInfo.enable(aircraftType)
 
+    def bookFlights(self, callback, flightIDs, date, tailNumber,
+                    busyCallback = None):
+        """Initiate the booking of flights with the given timetable IDs and
+        other data"""
+        self._bookFlightsUserCallback = callback
+        self._bookFlightsBusyCallback = busyCallback
+
+        self.beginBusy(xstr("bookflights_busy"))
+        if busyCallback is not None:
+            busyCallback(True)
+
+        self.webHandler.bookFlights(self._bookFlightsCallback,
+                                    flightIDs, date, tailNumber)
+
+    def _bookFlightsCallback(self, returned, result):
+        """Called when the booking of flights has finished."""
+        gobject.idle_add(self._handleBookFlightsResult, returned, result)
+
+    def _handleBookFlightsResult(self, returned, result):
+        """Called when the booking of flights is done.
+
+        If it was successful, the booked flights are added to the list of the
+        flight selector."""
+        if self._bookFlightsBusyCallback is not None:
+            self._bookFlightsBusyCallback(False)
+        self.endBusy()
+
+        if returned:
+            for bookedFlight in result.bookedFlights:
+                self._wizard.addFlight(bookedFlight)
+
+        self._bookFlightsUserCallback(returned, result)
+
     def cancelFlight(self):
         """Cancel the current file, if the user confirms it."""
@@ -853,5 +889,5 @@
         gobject.idle_add(self.getFleet, callback, force)
 
-    def getFleet(self, callback = None, force = False):
+    def getFleet(self, callback = None, force = False, busyCallback = None):
         """Get the fleet.
 
@@ -860,4 +896,7 @@
         if self._fleet is None or force:
             self._fleetCallback = callback
+            self._fleetBusyCallback = busyCallback
+            if busyCallback is not None:
+                busyCallback(True)
             self.beginBusy(xstr("fleet_busy"))
             self.webHandler.getFleet(self._fleetResultCallback)
@@ -896,4 +935,6 @@
         """Handle the fleet result."""
         self.endBusy()
+        if self._fleetBusyCallback is not None:
+            self._fleetBusyCallback(False)
         if returned:
             self._fleet = result.fleet
@@ -911,4 +952,5 @@
         callback = self._fleetCallback
         self._fleetCallback = None
+        self._fleetBusyCallback = None
         if  callback is not None:
             callback(self._fleet)
Index: src/mlx/gui/timetable.py
===================================================================
--- src/mlx/gui/timetable.py	(revision 858)
+++ src/mlx/gui/timetable.py	(revision 859)
@@ -5,8 +5,10 @@
 from mlx.gui.common import *
 from flightlist import ColumnDescriptor
+from mlx.rpc import ScheduledFlight
 
 import mlx.const as const
 
 import datetime
+import random
 
 #-----------------------------------------------------------------------------
@@ -38,9 +40,14 @@
                          "%02d:%02d" % (duration/3600,
                                         (duration%3600)/60)),
-        ColumnDescriptor("spec", xstr("timetable_vip"), type = bool,
+        ColumnDescriptor("type", xstr("timetable_vip"), type = bool,
                          renderer = _getVIPRenderer(),
                          sortable = True,
-                         convertFn = lambda spec, flight: spec==1)
+                         convertFn = lambda type, flight:
+                             type==ScheduledFlight.TYPE_VIP)
     ]
+
+    columnOrdering = ["callsign", "aircraftType",
+                      "date", "departureTime", "arrivalTime",
+                      "departureICAO", "arrivalICAO", "duration", "type"]
 
     @staticmethod
@@ -48,6 +55,6 @@
         """Determine if the given flight is selected by the given
         filtering conditions."""
-        return ((regularEnabled and flight.spec==0) or \
-                (vipEnabled and flight.spec==1)) and \
+        return ((regularEnabled and flight.type==ScheduledFlight.TYPE_NORMAL) or \
+                (vipEnabled and flight.type==ScheduledFlight.TYPE_VIP)) and \
                flight.aircraftType in aircraftTypes
 
@@ -75,4 +82,8 @@
         self._view = gtk.TreeView(self._model)
 
+        self._tooltips = gtk.Tooltips()
+        self._tooltips.disable()
+        self._view.connect("motion-notify-event", self._updateTooltip)
+
         flightPairIndexColumn = gtk.TreeViewColumn()
         flightPairIndexColumn.set_visible(False)
@@ -83,4 +94,6 @@
             column = columnDescriptor.getViewColumn(index)
             self._view.append_column(column)
+            self._model.set_sort_func(index, self._compareFlights,
+                                      columnDescriptor.attribute)
             index += 1
 
@@ -110,4 +123,16 @@
 
     @property
+    def selectedIndexes(self):
+        """Get the indexes of the selected entries, if any.
+
+        The indexes are sorted."""
+        selection = self._view.get_selection()
+        (model, rows) = selection.get_selected_rows()
+
+        indexes = [self._getIndexForPath(path) for path in rows]
+        indexes.sort()
+        return indexes
+
+    @property
     def hasFlightPairs(self):
         """Determine if the timetable contains any flights."""
@@ -125,4 +150,8 @@
 
         self._flightPairs = flightPairs
+
+    def getFlightPair(self, index):
+        """Get the flight pair with the given index."""
+        return self._flightPairs[index]
 
     def updateList(self, regularEnabled, vipEnabled, types):
@@ -140,15 +169,83 @@
             index += 1
 
+    def _getIndexForPath(self, path):
+        """Get the index for the given path."""
+        iter = self._model.get_iter(path)
+        return self._model.get_value(iter, 0)
+
     def _rowActivated(self, flightList, path, column):
         """Called when a row is selected."""
-        print "_rowActivated"
+        self.emit("row-activated", self._getIndexForPath(path))
 
     def _buttonPressEvent(self, widget, event):
         """Called when a mouse button is pressed or released."""
-        print "_buttonPressEvent", event
+        if event.type!=EVENT_BUTTON_PRESS or event.button!=3 or \
+           self._popupMenuProducer is None:
+            return
+
+        (path, _, _, _) = self._view.get_path_at_pos(int(event.x),
+                                                     int(event.y))
+        selection = self._view.get_selection()
+        selection.unselect_all()
+        selection.select_path(path)
+
+        if self._popupMenu is None:
+            self._popupMenu = self._popupMenuProducer()
+        menu = self._popupMenu
+        if pygobject:
+            menu.popup(None, None, None, None, event.button, event.time)
+        else:
+            menu.popup(None, None, None, event.button, event.time)
 
     def _selectionChanged(self, selection):
         """Called when the selection has changed."""
-        print "_selectionChanged"
+        self.emit("selection-changed", self.selectedIndexes)
+
+    def _compareFlights(self, model, iter1, iter2, mainColumn):
+        """Compare the flights at the given iterators according to the given
+        main column."""
+        index1 = self._model.get_value(iter1, 0)
+        index2 = self._model.get_value(iter2, 0)
+
+        flightPair1 = self._flightPairs[index1]
+        flightPair2 = self._flightPairs[index2]
+
+        result = flightPair1.compareBy(flightPair2, mainColumn)
+        if result==0:
+            for column in Timetable.columnOrdering:
+                if column!=mainColumn:
+                    result = flightPair1.compareBy(flightPair2, column)
+                    if result!=0:
+                        break
+        return result
+
+    def _updateTooltip(self, widget, event):
+        """Update the tooltip for the position of the given event."""
+        try:
+            (path, col, x, y) = widget.get_path_at_pos( int(event.x), int(event.y))
+            index = self._getIndexForPath(path)
+
+            flight = self._flightPairs[index].flight0
+            comment = flight.comment
+            date = flight.date
+
+            if comment or date!=const.defaultDate:
+                text = ""
+                if comment:
+                    text = comment
+                if date!=const.defaultDate:
+                    if text:
+                        text += "; "
+                    text += date.strftime("%Y-%m-%d")
+
+                self._tooltips.set_tip(widget, text)
+                self._tooltips.enable()
+            else:
+                self._tooltips.set_tip(widget, "")
+                self._tooltips.disable()
+        except Exception, e:
+            print e
+            self._tooltips.set_tip(widget, "")
+            self._tooltips.disable()
 
 #-----------------------------------------------------------------------------
@@ -207,4 +304,180 @@
 #-----------------------------------------------------------------------------
 
+class BookDialog(gtk.Dialog):
+    """The dialog box to select additional data for a booking."""
+    def __init__(self, timetableWindow, flightPair, planes):
+        """Construct the dialog box."""
+        super(BookDialog, self).__init__(title = WINDOW_TITLE_BASE +
+                                         " - " +
+                                         xstr("timetable_book_title"),
+                                         parent = timetableWindow)
+        contentArea = self.get_content_area()
+
+        frame = gtk.Frame(xstr("timetable_book_frame_title"))
+        frame.set_size_request(600, -1)
+
+        mainAlignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
+                                      xscale = 0.0, yscale = 0.0)
+        mainAlignment.set_padding(padding_top = 16, padding_bottom = 12,
+                                  padding_left = 8, padding_right = 8)
+
+        table = gtk.Table(6, 2)
+        table.set_row_spacings(8)
+        table.set_col_spacings(16)
+
+        row = 0
+        label = gtk.Label()
+        label.set_markup(xstr("timetable_book_callsign"))
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, row, row + 1)
+
+        text = flightPair.flight0.callsign
+        if flightPair.flight1 is not None:
+            text += " / " + flightPair.flight1.callsign
+        label = gtk.Label(text)
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 1, 2, row, row + 1)
+
+        row += 1
+
+        label = gtk.Label()
+        label.set_markup(xstr("timetable_book_from_to"))
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, row, row + 1)
+
+        text = flightPair.flight0.departureICAO + " - " + \
+               flightPair.flight0.arrivalICAO
+        if flightPair.flight1 is not None:
+            text += " - " + flightPair.flight1.arrivalICAO
+        label = gtk.Label(text)
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 1, 2, row, row + 1)
+
+        row += 1
+
+        if flightPair.flight0.type==ScheduledFlight.TYPE_VIP and \
+           flightPair.flight0.date!=const.defaultDate:
+            label = gtk.Label()
+            label.set_markup(xstr("timetable_book_flightDate"))
+            label.set_use_underline(True)
+            label.set_alignment(0.0, 0.5)
+            table.attach(label, 0, 1, row, row + 1)
+
+            self._flightDate = gtk.Button()
+            self._flightDate.connect("clicked", self._flightDateClicked)
+            self._flightDate.set_tooltip_text(xstr("timetable_book_flightDate_tooltip"))
+            label.set_mnemonic_widget(self._flightDate)
+
+            table.attach(self._flightDate, 1, 2, row, row + 1)
+
+            self._calendarWindow = calendarWindow = CalendarWindow()
+            calendarWindow.set_transient_for(self)
+            calendarWindow.connect("delete-event", self._calendarWindowDeleted)
+            calendarWindow.connect("date-selected", self._calendarWindowDateSelected)
+
+            self._setDate(flightPair.flight0.date)
+
+            row += 1
+        else:
+            self._flightDate = None
+            self._calendarWindow = None
+
+        label = gtk.Label()
+        label.set_markup(xstr("timetable_book_dep_arr"))
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, row, row + 1)
+
+        text = str(flightPair.flight0.departureTime) + " - " + \
+               str(flightPair.flight0.arrivalTime)
+        if flightPair.flight1 is not None:
+            text += " / " + str(flightPair.flight1.departureTime) + " - " + \
+                    str(flightPair.flight1.arrivalTime)
+        label = gtk.Label(text)
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 1, 2, row, row + 1)
+
+        row += 1
+
+        label = gtk.Label()
+        label.set_markup(xstr("timetable_book_duration"))
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, row, row + 1)
+
+
+        duration = flightPair.flight0.duration
+        text = "%02d:%02d" % (duration/3600, (duration%3600)/60)
+        if flightPair.flight1 is not None:
+            duration = flightPair.flight0.duration
+            text += " / %02d:%02d" % (duration/3600, (duration%3600)/60)
+        label = gtk.Label(text)
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 1, 2, row, row + 1)
+
+        row += 2
+
+        label = gtk.Label()
+        label.set_markup(xstr("timetable_book_tailNumber"))
+        label.set_alignment(0.0, 0.5)
+        table.attach(label, 0, 1, row, row + 1)
+
+        self._planes = planes
+        tailNumbersModel = gtk.ListStore(str)
+        for plane in planes:
+            tailNumbersModel.append((plane.tailNumber,))
+
+        self._tailNumber = gtk.ComboBox(model = tailNumbersModel)
+        renderer = gtk.CellRendererText()
+        self._tailNumber.pack_start(renderer, True)
+        self._tailNumber.add_attribute(renderer, "text", 0)
+        self._tailNumber.set_tooltip_text(xstr("timetable_book_tailNumber_tooltip"))
+        self._tailNumber.set_active(random.randint(0, len(planes)-1))
+
+        table.attach(self._tailNumber, 1, 2, row, row + 1)
+
+        mainAlignment.add(table)
+
+        frame.add(mainAlignment)
+        contentArea.pack_start(frame, True, True, 4)
+
+        self.add_button(xstr("button_cancel"), RESPONSETYPE_CANCEL)
+
+        self._okButton = self.add_button(xstr("button_book"), RESPONSETYPE_OK)
+        self._okButton.set_use_underline(True)
+        self._okButton.set_can_default(True)
+
+    @property
+    def plane(self):
+        """Get the currently selected plane."""
+        return self._planes[self._tailNumber.get_active()]
+
+    @property
+    def date(self):
+        """Get the flight date, if selected."""
+        return None if self._calendarWindow is None \
+          else self._calendarWindow.getDate()
+
+    def _setDate(self, date):
+        """Set the date to the given one."""
+        self._flightDate.set_label(date.strftime("%Y-%m-%d"))
+        self._calendarWindow.setDate(date)
+
+    def _flightDateClicked(self, button):
+        """Called when the flight date button is clicked."""
+        self._calendarWindow.set_position(gtk.WIN_POS_MOUSE)
+        self.set_focus(self._calendarWindow)
+        self._calendarWindow.show_all()
+
+    def _calendarWindowDeleted(self, window, event):
+        """Called when the flight date window is deleted."""
+        self._calendarWindow.hide()
+
+    def _calendarWindowDateSelected(self, window):
+        """Called when the flight date window is deleted."""
+        self._calendarWindow.hide()
+        date = window.getDate()
+        self._flightDate.set_label(date.strftime("%Y-%m-%d"))
+
+#-----------------------------------------------------------------------------
+
 class TimetableWindow(gtk.Window):
     """The window to display the timetable."""
@@ -255,4 +528,5 @@
 
         self._flightDate = gtk.Button()
+        self._flightDate.connect("clicked", self._flightDateClicked)
         self._flightDate.connect("clicked", self._flightDateClicked)
         self._flightDate.set_tooltip_text(xstr("timetable_flightdate_tooltip"))
@@ -317,12 +591,27 @@
         vbox.pack_start(filterAlignment, False, False, 2)
 
-        self._timetable = Timetable()
+        self._timetable = Timetable(popupMenuProducer =
+                                        self._createTimetablePopupMenu)
+        self._timetable.connect("row-activated", self._rowActivated)
+        self._timetable.connect("selection-changed", self._selectionChanged)
         vbox.pack_start(self._timetable, True, True, 2)
 
-        alignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
+        alignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
                                   xscale = 0.0, yscale = 0.0)
-        self._closeButton = gtk.Button(xstr("button_ok"))
+        buttonBox = gtk.HBox()
+
+        self._bookButton = gtk.Button(xstr("button_book"))
+        self._bookButton.set_use_underline(True)
+        self._bookButton.set_can_default(True)
+        self._bookButton.connect("clicked", self._bookClicked)
+        self._bookButton.set_sensitive(False)
+        buttonBox.pack_start(self._bookButton, False, False, 4);
+
+        self._closeButton = gtk.Button(xstr("button_close"))
+        self._closeButton.set_use_underline(True)
         self._closeButton.connect("clicked", self._closeClicked)
-        alignment.add(self._closeButton)
+        buttonBox.pack_start(self._closeButton, False, False, 4);
+
+        alignment.add(buttonBox)
         vbox.pack_start(alignment, False, False, 2)
 
@@ -337,4 +626,6 @@
 
         self.setDate(datetime.date.today())
+
+        self._flightPairToBook = None
 
     @property
@@ -423,2 +714,91 @@
                                    self.isVIPEnabled,
                                    aircraftTypes)
+
+    def _bookClicked(self, button):
+        """Called when the book button has been clicked."""
+        self._book(self._timetable.getFlightPair(self._timetable.selectedIndexes[0]))
+
+    def _rowActivated(self, timetable, index):
+        """Called when a row has been activated (e.g. double-clicked) in the
+        timetable."""
+        self._book(self._timetable.getFlightPair(index))
+
+    def _selectionChanged(self, timetable, indexes):
+        """Called when the selection has changed.
+
+        It sets the sensitivity of the book button based on whether a row is
+        selected or not."""
+        self._bookButton.set_sensitive(len(indexes)>0)
+
+    def _book(self, flightPair):
+        """Try to book the given flight pair."""
+        self._flightPairToBook = flightPair
+        self._gui.getFleet(callback = self._continueBook,
+                           busyCallback = self._busyCallback)
+
+    def _busyCallback(self, busy):
+        """Called when the busy state has changed."""
+        self.set_sensitive(not busy)
+
+    def _continueBook(self, fleet):
+        """Continue booking, once the fleet is available."""
+        flightPair = self._flightPairToBook
+        aircraftType = flightPair.flight0.aircraftType
+        planes = [plane for plane in fleet
+                  if plane.aircraftType == aircraftType]
+        planes.sort(cmp = lambda p1, p2: cmp(p1.tailNumber, p2.tailNumber))
+
+        dialog = BookDialog(self, flightPair, planes)
+        dialog.show_all()
+        result = dialog.run()
+        dialog.hide()
+        if result==RESPONSETYPE_OK:
+            flightIDs = [flightPair.flight0.id]
+            if flightPair.flight1 is not None:
+                flightIDs.append(flightPair.flight1.id)
+
+            date = dialog.date
+            if date is None:
+                date = self._calendarWindow.getDate()
+
+            self._gui.bookFlights(self._bookFlightsCallback,
+                                  flightIDs, date, dialog.plane.tailNumber,
+                                  busyCallback = self._busyCallback)
+
+    def _bookFlightsCallback(self, returned, result):
+        """Called when the booking has finished."""
+        if returned:
+            dialog = gtk.MessageDialog(parent = self,
+                                       type = MESSAGETYPE_INFO,
+                                       message_format = xstr("bookflights_successful"))
+            dialog.format_secondary_markup(xstr("bookflights_successful_secondary"))
+        else:
+            dialog = gtk.MessageDialog(parent = self,
+                                       type = MESSAGETYPE_ERROR,
+                                       message_format = xstr("bookflights_failed"))
+            dialog.format_secondary_markup(xstr("bookflights_failed_secondary"))
+
+        dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
+        dialog.set_title(WINDOW_TITLE_BASE)
+
+        dialog.run()
+        dialog.hide()
+
+    def _createTimetablePopupMenu(self):
+        """Get the popuop menu for the timetable."""
+        menu = gtk.Menu()
+
+        menuItem = gtk.MenuItem()
+        menuItem.set_label(xstr("timetable_popup_book"))
+        menuItem.set_use_underline(True)
+        menuItem.connect("activate", self._popupBook)
+        menuItem.show()
+
+        menu.append(menuItem)
+
+        return menu
+
+    def _popupBook(self, menuItem):
+        """Try to book the given flight pair."""
+        index = self._timetable.selectedIndexes[0]
+        self._book(self._timetable.getFlightPair(index))
Index: src/mlx/rpc.py
===================================================================
--- src/mlx/rpc.py	(revision 858)
+++ src/mlx/rpc.py	(revision 859)
@@ -66,6 +66,8 @@
         "arrivalTime": lambda value: ScheduledFlight._decodeTime(value),
         "duration": lambda value: ScheduledFlight._decodeDuration(value),
-        "type": int,
-        "spec": int
+        "spec": int,
+        "validFrom": lambda value: ScheduledFlight._decodeDate(value),
+        "validTo": lambda value: ScheduledFlight._decodeDate(value),
+        "date": lambda value: ScheduledFlight._decodeDate(value)
         }
 
@@ -74,4 +76,12 @@
         """Decode the given value as a time value."""
         return datetime.datetime.strptime(value, "%H:%M:%S").time()
+
+    @staticmethod
+    def _decodeDate(value):
+        """Decode the given value as a date value."""
+        if not value or value=="0000-00-00":
+            return const.defaultDate
+        else:
+            return datetime.datetime.strptime(value, "%Y-%m-%d").date()
 
     @staticmethod
@@ -90,4 +100,21 @@
         del self.typeCode
 
+        self.type = ScheduledFlight.TYPE_VIP if self.spec==1 \
+          else ScheduledFlight.TYPE_NORMAL
+        del self.spec
+
+    def compareBy(self, other, name):
+        """Compare this flight with the other one according to the given
+        attribute name."""
+        if name=="callsign":
+            try:
+                cs1 = int(self.callsign[2:])
+                cs2 = int(other.callsign[2:])
+                return cmp(cs1, cs2)
+            except:
+                return cmp(self.callsign, other.callsign)
+        else:
+            return cmp(getattr(self, name), getattr(other, name))
+
     def __repr__(self):
         return "ScheduledFlight<%d, %d, %s, %s (%s) - %s (%s) -> %d, %d>" % \
@@ -95,5 +122,5 @@
            self.departureICAO, str(self.departureTime),
            self.arrivalICAO, str(self.arrivalTime),
-           self.duration, self.spec)
+           self.duration, self.type)
 
 #---------------------------------------------------------------------------------------
@@ -104,26 +131,41 @@
     Occasionally, one of the flights may be missing."""
     @staticmethod
-    def scheduledFlights2Pairs(scheduledFlights):
+    def scheduledFlights2Pairs(scheduledFlights, date):
         """Convert the given list of scheduled flights into a list of flight
         pairs."""
+        weekday = str(date.weekday()+1)
+
         flights = {}
+        weekdayFlights = {}
         for flight in scheduledFlights:
             flights[flight.id] = flight
+            if (flight.type==ScheduledFlight.TYPE_NORMAL and
+                flight.arrivalICAO!="LHBP" and weekday in flight.days and
+                flight.validFrom<=date and flight.validTo>=date) or \
+               flight.type==ScheduledFlight.TYPE_VIP:
+                weekdayFlights[flight.id] = flight
 
         flightPairs = []
 
-        while flights:
-            (id, flight) = flights.popitem()
-            pairID = flight.pairID
-            if pairID in flights:
-                pairFlight = flights[pairID]
-                if flight.departureICAO=="LHBP" or \
-                  (pairFlight.departureICAO!="LHBP" and id<pairID):
-                    flightPairs.append(ScheduledFlightPair(flight, pairFlight))
-                else:
-                    flightPairs.append(ScheduledFlightPair(pairFlight, flight))
-                del flights[pairID]
-            else:
+        while weekdayFlights:
+            (id, flight) = weekdayFlights.popitem()
+            if flight.type==ScheduledFlight.TYPE_NORMAL:
+                pairID = flight.pairID
+                if pairID in flights:
+                    pairFlight = flights[pairID]
+                    if flight.departureICAO=="LHBP" or \
+                      (pairFlight.departureICAO!="LHBP" and
+                       flight.callsign<pairFlight.callsign):
+                        flightPairs.append(ScheduledFlightPair(flight, pairFlight))
+                    else:
+                        flightPairs.append(ScheduledFlightPair(pairFlight, flight))
+                    del flights[pairID]
+                    if pairID in weekdayFlights:
+                        del weekdayFlights[pairID]
+            elif flight.type==ScheduledFlight.TYPE_VIP:
                 flightPairs.append(ScheduledFlightPair(flight))
+
+        flightPairs.sort(cmp = lambda pair1, pair2:
+                         cmp(pair1.flight0.date, pair2.flight0.date))
 
         return flightPairs
@@ -133,4 +175,14 @@
         self.flight0 = flight0
         self.flight1 = flight1
+
+    def compareBy(self, other, name):
+        """Compare this flight pair with the other one according to the given
+        attribute name, considering the first flights."""
+        return self.flight0.compareBy(other.flight0, name)
+
+    def __repr__(self):
+        return "ScheduledFlightPair<%s, %s, %s>" % \
+          (self.flight0.callsign, self.flight0.departureICAO,
+           self.flight0.arrivalICAO)
 
 #---------------------------------------------------------------------------------------
@@ -280,5 +332,6 @@
     _instructions = {
         "status" : lambda value: rpccommon.Plane.str2status(value),
-        "gateNumber" : lambda value: value if value else None
+        "gateNumber" : lambda value: value if value else None,
+        "typeCode": lambda value: BookedFlight._decodeAircraftType(value)
         }
 
@@ -286,4 +339,6 @@
         """Construct the plane."""
         RPCObject.__init__(self, value, instructions = Plane._instructions)
+        self.aircraftType = self.typeCode
+        del self.typeCode
 
 #---------------------------------------------------------------------------------------
@@ -524,8 +579,19 @@
                                    self._server.getTimetable(sessionID,
                                                              date.strftime("%Y-%m-%d"),
-                                                             date.weekday() + 1,
+                                                             date.weekday()+1,
                                                              typeCodes))
         return ScheduledFlightPair.scheduledFlights2Pairs([ScheduledFlight(value)
-                                                          for value in values])
+                                                          for value in values],
+                                                          date)
+
+    def bookFlights(self, flightIDs, date, tailNumber):
+        """Book the flights with the given IDs on the given date to be flown
+        with the plane of the given tail number."""
+        values = self._performCall(lambda sessionID:
+                                   self._server.bookFlights(sessionID,
+                                                            flightIDs,
+                                                            date.strftime("%Y-%m-%d"),
+                                                            tailNumber))
+        return [BookedFlight(value) for value in values]
 
     def _performCall(self, callFn, acceptResults = []):
Index: src/mlx/web.py
===================================================================
--- src/mlx/web.py	(revision 858)
+++ src/mlx/web.py	(revision 859)
@@ -1362,4 +1362,25 @@
 #------------------------------------------------------------------------------
 
+class BookFlights(RPCRequest):
+    """Request to book flights."""
+    def __init__(self, client, callback, flightIDs, date, tailNumber):
+        """Construct the request with the given client and callback function."""
+        super(BookFlights, self).__init__(client, callback)
+        self._flightIDs = flightIDs
+        self._date = date
+        self._tailNumber = tailNumber
+
+    def run(self):
+        """Perform the login request."""
+        result = Result()
+
+        result.bookedFlights = self._client.bookFlights(self._flightIDs,
+                                                        self._date,
+                                                        self._tailNumber)
+
+        return result
+
+#------------------------------------------------------------------------------
+
 class Handler(threading.Thread):
     """The handler for the web services.
@@ -1463,4 +1484,9 @@
         self._addRequest(GetTimetable(self._rpcClient, callback, date, types))
 
+    def bookFlights(self, callback, flightIDs, date, tailNumber):
+        """Enqueue a request to book some flights."""
+        self._addRequest(BookFlights(self._rpcClient, callback,
+                                     flightIDs, date, tailNumber))
+
     def run(self):
         """Process the requests."""
