diff --git a/core/admin.py b/core/admin.py
index f7c75db..bc01b52 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -4,8 +4,9 @@ from django.core import serializers
 from django.forms import ModelForm
 from django.http import HttpResponse
 
-from troggle.core.models.caves import (QM, Area, Cave, CaveAndEntrance,
-                                       Entrance, LogbookEntry, PersonTrip)
+from troggle.core.models.caves import (Area, Cave, CaveAndEntrance,
+                                       Entrance)
+from troggle.core.models.logbooks import (QM, LogbookEntry, PersonTrip)
 from troggle.core.models.survex import (DrawingFile, SingleScan, SurvexBlock,
                                         SurvexDirectory, SurvexFile,
                                         SurvexPersonRole, SurvexStation)
diff --git a/core/forms.py b/core/forms.py
index 4b1399d..31f6de5 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -6,8 +6,8 @@ from django.contrib.admin.widgets import AdminDateWidget
 from django.forms import ModelForm
 from django.forms.models import modelformset_factory
 
-from troggle.core.models.caves import (QM, Cave, CaveAndEntrance, Entrance,
-                                       LogbookEntry)
+from troggle.core.models.caves import Cave, CaveAndEntrance, Entrance
+from troggle.core.models.logbooks import QM, LogbookEntry
 from troggle.core.models.troggle import Expedition, Person, PersonExpedition
 from troggle.core.views.editor_helpers import HTMLarea
 
diff --git a/core/models/caves.py b/core/models/caves.py
index 0b3aa1d..bc2974e 100644
--- a/core/models/caves.py
+++ b/core/models/caves.py
@@ -41,8 +41,6 @@ todo='''
   these are not just a single field on the Model. Do we ever need more
   than one slug per cave or entrance? Surely that would break everything??
   
-- Move PersonTrip to be with Person and Expedition elsewhere
-  
 - Restore constraint:   unique_together = (("area", "kataster_number"), ("area", "unofficial_number"))
 '''
 
@@ -428,152 +426,6 @@ class Entrance(TroggleModel):
                 return ""
                 
 
-class LogbookEntry(TroggleModel):
-    """Single parsed entry from Logbook
-    """
-    date    = models.DateField()#MJG wants to turn this into a datetime such that multiple Logbook entries on the same day can be ordered.ld()
-    expeditionday = models.ForeignKey("ExpeditionDay", null=True,on_delete=models.SET_NULL)#MJG wants to KILL THIS (redundant information)
-    expedition = models.ForeignKey(Expedition,blank=True, null=True,on_delete=models.SET_NULL)  # yes this is double-
-    title      = models.CharField(max_length=200)
-    cave_slug  = models.SlugField(max_length=50, blank=True, null=True)
-    place      = models.CharField(max_length=100,blank=True, null=True,help_text="Only use this if you haven't chosen a cave")
-    text       = models.TextField()
-    slug       = models.SlugField(max_length=50)
-    time_underground = models.FloatField(null=True,help_text="In decimal hours")
-
-    class Meta:
-        verbose_name_plural = "Logbook Entries"
-        # several PersonTrips point in to this object
-        ordering = ('-date',)
-
-    def cave(self): # Why didn't he just make this a foreign key to Cave ? 
-        c = CaveSlug.objects.get(slug=self.cave_slug, primary=True).cave
-        return c
-
-    def isLogbookEntry(self): # Function used in templates
-        return True
-
-    def get_absolute_url(self):
-        return urljoin(settings.URL_ROOT, reverse('logbookentry',kwargs={'date':self.date,'slug':self.slug}))
-
-    def __str__(self):
-        return f'{self.date}: {self.title}'
-
-    def get_next_by_id(self):
-        LogbookEntry.objects.get(id=self.id+1)
-
-    def get_previous_by_id(self):
-        LogbookEntry.objects.get(id=self.id-1)
-
-    def DayIndex(self):
-        return list(self.expeditionday.logbookentry_set.all()).index(self)
-
-class QM(TroggleModel):
-    """This is based on qm.csv in trunk/expoweb/1623/204 which has the fields:
-    "Number","Grade","Area","Description","Page reference","Nearest station","Completion description","Comment"
-    """
-    cave = models.ForeignKey(Cave, related_name='QMs',blank=True, null=True,on_delete=models.SET_NULL )
-    block       = models.ForeignKey('SurvexBlock', null=True,on_delete=models.SET_NULL) # only for QMs from survex files
-    blockname=models.TextField(blank=True,null=True) # NB truncated copy of survexblock name with last char added
-    expoyear    = models.CharField(max_length=4,blank=True, null=True) # could change to datetime if logbooks similarly chnaged
-    found_by = models.ForeignKey(LogbookEntry, related_name='QMs_found',blank=True, null=True,on_delete=models.SET_NULL )
-    ticked = models.BooleanField(default=False) # for ticked QMs not attached to a logbook entry, should imply completion_description has text
-    ticked_off_by = models.ForeignKey(LogbookEntry, related_name='QMs_ticked_off',blank=True, null=True,on_delete=models.SET_NULL) # unused, ever?!
-    number = models.IntegerField(help_text="this is the sequential number in the year, only unique for CSV imports", )
-    GRADE_CHOICES=(
-    ('A', 'A: Large obvious lead'),
-    ('B', 'B: Average lead'),
-    ('C', 'C: Tight unpromising lead'),
-    ('D', 'D: Dig'),
-    ('X', 'X: Unclimbable aven')
-    ) # also seen "?" and "V" in imported data - see urls.py
-    grade = models.CharField(max_length=1, choices=GRADE_CHOICES)
-    location_description = models.TextField(blank=True)
-    nearest_station_description = models.CharField(max_length=400,blank=True, null=True)
-    nearest_station_name = models.CharField(max_length=200,blank=True, null=True)
-    nearest_station = models.ForeignKey(SurvexStation,blank=True, null=True,on_delete=models.SET_NULL)
-    area = models.CharField(max_length=100,blank=True, null=True)
-    completion_description = models.TextField(blank=True,null=True)
-    comment=models.TextField(blank=True,null=True)
-
-    def __str__(self):
-        return f'{self.code()}'
-
-    def code(self):
-        if self.cave:
-            cavestr = str(self.cave.slug())[5:]
-        else:
-            cavestr = ""
-        if self.expoyear:
-            expoyearstr = str(self.expoyear)
-        else:
-            expoyearstr = str(self.cave.slug())[5:9]
-        if self.blockname:
-            blocknamestr = "-" + str(self.blockname)
-        else:
-            blocknamestr = ""
-        return f'{cavestr}-{expoyearstr}-{self.number}{self.grade}{blocknamestr}' 
-
-    def get_completion_url(self):
-        '''assumes html file named is in same folder as cave description file
-        '''
-        cd = None
-        if self.completion_description:
-            try:
-                dir = Path(self.cave.url).parent
-                cd = dir / self.completion_description
-            except:
-                cd = None
-        return cd
-            
-    def newslug(self): 
-        qmslug = f'{str(self.cave)}-{self.expoyear}-{self.blockname}{self.number}{self.grade}' 
-        return qmslug
-        
-    def get_absolute_url(self):
-        # This reverse resolution stuff is pure magic. Just change the regex in urls.py and everything changes to suit. Whacky.
-        return urljoin(settings.URL_ROOT, reverse('qm',kwargs={'cave_id':self.cave.slug(),'year':self.expoyear, 'blockname':self.blockname,'qm_id':self.number,'grade':self.grade}))
-            
-    def get_next_by_id(self):
-        return QM.objects.get(id=self.id+1)
-
-    def get_previous_by_id(self):
-        return QM.objects.get(id=self.id-1)
-        
-class PersonTrip(TroggleModel):
-    """Single Person going on a trip, which may or may not be written up.
-    It could account for different T/U for people in same logbook entry.
-    """
-    personexpedition = models.ForeignKey("PersonExpedition",null=True,on_delete=models.CASCADE)
-    time_underground = models.FloatField(help_text="In decimal hours")
-    logbook_entry    = models.ForeignKey(LogbookEntry,on_delete=models.CASCADE)
-    is_logbook_entry_author = models.BooleanField(default=False)
-
-    class Meta:
-        ordering = ('-personexpedition',)
-        #order_with_respect_to = 'personexpedition'
-        
-    def persontrip_next(self):
-        futurePTs = PersonTrip.objects.filter(personexpedition = self.personexpedition, logbook_entry__date__gt = self.logbook_entry.date).order_by('logbook_entry__date').all()
-        if len(futurePTs) > 0:
-            return futurePTs[0]
-        else:
-            return None
-
-    def persontrip_prev(self):
-        pastPTs = PersonTrip.objects.filter(personexpedition = self.personexpedition, logbook_entry__date__lt = self.logbook_entry.date).order_by('-logbook_entry__date').all()
-        if len(pastPTs) > 0:
-            return pastPTs[0]
-        else:
-            return None
-
-    def place(self):
-        return self.logbook_entry.cave and self.logbook_entry.cave or self.logbook_entry.place
-
-    def __str__(self):
-        return f'{self.personexpedition} ({self.logbook_entry.date})'
-
-
 def GetCaveLookup():
     """A very relaxed way of finding probably the right cave given almost any string which might serve to identify it
     
diff --git a/core/models/logbooks.py b/core/models/logbooks.py
new file mode 100644
index 0000000..da8d970
--- /dev/null
+++ b/core/models/logbooks.py
@@ -0,0 +1,181 @@
+import datetime
+import json
+import operator
+import os
+import re
+import string
+import subprocess
+from collections import defaultdict
+from datetime import datetime, timezone
+from pathlib import Path
+from urllib.parse import urljoin
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.core.files.storage import FileSystemStorage
+from django.db import models
+from django.db.models import Max, Min
+from django.shortcuts import render
+from django.template import Context, loader
+from django.urls import reverse
+
+import settings
+from troggle.core.models.caves import (Area, Cave, CaveAndEntrance,
+                                       Entrance)
+from troggle.core.models.survex import SurvexStation
+from troggle.core.models.troggle import (DataIssue, Expedition, Person,
+                                         TroggleModel)
+
+'''The model declarations  LogBookEntry, PersonTrip, QM
+'''
+
+todo='''
+- Rename PersonTrip as PersonLogEntry or similar
+'''
+
+class LogbookEntry(TroggleModel):
+    """Single parsed entry from Logbook
+    """
+    date    = models.DateField()#MJG wants to turn this into a datetime such that multiple Logbook entries on the same day can be ordered.ld()
+    expeditionday = models.ForeignKey("ExpeditionDay", null=True,on_delete=models.SET_NULL)#MJG wants to KILL THIS (redundant information)
+    expedition = models.ForeignKey(Expedition,blank=True, null=True,on_delete=models.SET_NULL)  # yes this is double-
+    title      = models.CharField(max_length=200)
+    cave_slug  = models.SlugField(max_length=50, blank=True, null=True)
+    place      = models.CharField(max_length=100,blank=True, null=True,help_text="Only use this if you haven't chosen a cave")
+    text       = models.TextField()
+    slug       = models.SlugField(max_length=50)
+    time_underground = models.FloatField(null=True,help_text="In decimal hours")
+
+    class Meta:
+        verbose_name_plural = "Logbook Entries"
+        # several PersonTrips point in to this object
+        ordering = ('-date',)
+
+    def cave(self): # Why didn't he just make this a foreign key to Cave ? 
+        c = CaveSlug.objects.get(slug=self.cave_slug, primary=True).cave
+        return c
+
+    def isLogbookEntry(self): # Function used in templates
+        return True
+
+    def get_absolute_url(self):
+        return urljoin(settings.URL_ROOT, reverse('logbookentry',kwargs={'date':self.date,'slug':self.slug}))
+
+    def __str__(self):
+        return f'{self.date}: {self.title}'
+
+    def get_next_by_id(self):
+        LogbookEntry.objects.get(id=self.id+1)
+
+    def get_previous_by_id(self):
+        LogbookEntry.objects.get(id=self.id-1)
+
+    def DayIndex(self):
+        return list(self.expeditionday.logbookentry_set.all()).index(self)
+
+class PersonTrip(TroggleModel):
+    """Single Person going on a trip, which may or may not be written up.
+    It could account for different T/U for people in same logbook entry.
+    """
+    personexpedition = models.ForeignKey("PersonExpedition",null=True,on_delete=models.CASCADE)
+    time_underground = models.FloatField(help_text="In decimal hours")
+    logbook_entry    = models.ForeignKey(LogbookEntry,on_delete=models.CASCADE)
+    is_logbook_entry_author = models.BooleanField(default=False)
+
+    class Meta:
+        ordering = ('-personexpedition',)
+        #order_with_respect_to = 'personexpedition'
+        
+    def persontrip_next(self):
+        futurePTs = PersonTrip.objects.filter(personexpedition = self.personexpedition, logbook_entry__date__gt = self.logbook_entry.date).order_by('logbook_entry__date').all()
+        if len(futurePTs) > 0:
+            return futurePTs[0]
+        else:
+            return None
+
+    def persontrip_prev(self):
+        pastPTs = PersonTrip.objects.filter(personexpedition = self.personexpedition, logbook_entry__date__lt = self.logbook_entry.date).order_by('-logbook_entry__date').all()
+        if len(pastPTs) > 0:
+            return pastPTs[0]
+        else:
+            return None
+
+    def place(self):
+        return self.logbook_entry.cave and self.logbook_entry.cave or self.logbook_entry.place
+
+    def __str__(self):
+        return f'{self.personexpedition} ({self.logbook_entry.date})'
+
+class QM(TroggleModel):
+    """This is based on qm.csv in trunk/expoweb/1623/204 which has the fields:
+    "Number","Grade","Area","Description","Page reference","Nearest station","Completion description","Comment"
+    """
+    cave = models.ForeignKey(Cave, related_name='QMs',blank=True, null=True,on_delete=models.SET_NULL )
+    block       = models.ForeignKey('SurvexBlock', null=True,on_delete=models.SET_NULL) # only for QMs from survex files
+    blockname=models.TextField(blank=True,null=True) # NB truncated copy of survexblock name with last char added
+    expoyear    = models.CharField(max_length=4,blank=True, null=True) # could change to datetime if logbooks similarly chnaged
+    found_by = models.ForeignKey(LogbookEntry, related_name='QMs_found',blank=True, null=True,on_delete=models.SET_NULL )
+    ticked = models.BooleanField(default=False) # for ticked QMs not attached to a logbook entry, should imply completion_description has text
+    ticked_off_by = models.ForeignKey(LogbookEntry, related_name='QMs_ticked_off',blank=True, null=True,on_delete=models.SET_NULL) # unused, ever?!
+    number = models.IntegerField(help_text="this is the sequential number in the year, only unique for CSV imports", )
+    GRADE_CHOICES=(
+    ('A', 'A: Large obvious lead'),
+    ('B', 'B: Average lead'),
+    ('C', 'C: Tight unpromising lead'),
+    ('D', 'D: Dig'),
+    ('X', 'X: Unclimbable aven')
+    ) # also seen "?" and "V" in imported data - see urls.py
+    grade = models.CharField(max_length=1, choices=GRADE_CHOICES)
+    location_description = models.TextField(blank=True)
+    nearest_station_description = models.CharField(max_length=400,blank=True, null=True)
+    nearest_station_name = models.CharField(max_length=200,blank=True, null=True)
+    nearest_station = models.ForeignKey(SurvexStation,blank=True, null=True,on_delete=models.SET_NULL)
+    area = models.CharField(max_length=100,blank=True, null=True)
+    completion_description = models.TextField(blank=True,null=True)
+    comment=models.TextField(blank=True,null=True)
+
+    def __str__(self):
+        return f'{self.code()}'
+
+    def code(self):
+        if self.cave:
+            cavestr = str(self.cave.slug())[5:]
+        else:
+            cavestr = ""
+        if self.expoyear:
+            expoyearstr = str(self.expoyear)
+        else:
+            expoyearstr = str(self.cave.slug())[5:9]
+        if self.blockname:
+            blocknamestr = "-" + str(self.blockname)
+        else:
+            blocknamestr = ""
+        return f'{cavestr}-{expoyearstr}-{self.number}{self.grade}{blocknamestr}' 
+
+    def get_completion_url(self):
+        '''assumes html file named is in same folder as cave description file
+        '''
+        cd = None
+        if self.completion_description:
+            try:
+                dir = Path(self.cave.url).parent
+                cd = dir / self.completion_description
+            except:
+                cd = None
+        return cd
+            
+    def newslug(self): 
+        qmslug = f'{str(self.cave)}-{self.expoyear}-{self.blockname}{self.number}{self.grade}' 
+        return qmslug
+        
+    def get_absolute_url(self):
+        # This reverse resolution stuff is pure magic. Just change the regex in urls.py and everything changes to suit. Whacky.
+        return urljoin(settings.URL_ROOT, reverse('qm',kwargs={'cave_id':self.cave.slug(),'year':self.expoyear, 'blockname':self.blockname,'qm_id':self.number,'grade':self.grade}))
+            
+    def get_next_by_id(self):
+        return QM.objects.get(id=self.id+1)
+
+    def get_previous_by_id(self):
+        return QM.objects.get(id=self.id-1)
+        
diff --git a/core/views/caves.py b/core/views/caves.py
index 0b291f3..b7b800a 100644
--- a/core/views/caves.py
+++ b/core/views/caves.py
@@ -17,9 +17,10 @@ import settings
 import troggle.settings as settings
 from troggle.core.forms import (CaveAndEntranceFormSet, CaveForm, EntranceForm,
                                 EntranceLetterForm)
-from troggle.core.models.caves import (QM, Area, Cave, CaveAndEntrance,
+from troggle.core.models.caves import (Area, Cave, CaveAndEntrance,
                                        CaveSlug, Entrance, EntranceSlug,
                                        GetCaveLookup, SurvexStation)
+from troggle.core.models.logbooks import QM
 from troggle.core.models.troggle import DataIssue, Expedition
 from troggle.core.utils import write_and_commit, writetrogglefile
 from troggle.core.views import expo
diff --git a/core/views/logbooks.py b/core/views/logbooks.py
index ece3f98..6450753 100644
--- a/core/views/logbooks.py
+++ b/core/views/logbooks.py
@@ -14,7 +14,7 @@ from django.utils import timezone
 from django.views.generic.list import ListView
 
 import troggle.settings as settings
-from troggle.core.models.caves import LogbookEntry, PersonTrip
+from troggle.core.models.logbooks import LogbookEntry, PersonTrip
 from troggle.core.models.survex import SurvexBlock, Wallet
 from troggle.core.models.troggle import Expedition, Person, PersonExpedition
 from troggle.core.utils import TROG
diff --git a/core/views/other.py b/core/views/other.py
index 0e02180..70bdfa4 100644
--- a/core/views/other.py
+++ b/core/views/other.py
@@ -12,7 +12,8 @@ from django.shortcuts import render
 from django.template import Context, loader
 from django.urls import reverse
 
-from troggle.core.models.caves import QM, Cave, LogbookEntry, PersonTrip
+from troggle.core.models.caves import Cave
+from troggle.core.models.logbooks import QM, LogbookEntry, PersonTrip
 from troggle.core.models.survex import DrawingFile
 # from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time*
 from troggle.core.models.troggle import Expedition, Person, PersonExpedition
diff --git a/core/views/statistics.py b/core/views/statistics.py
index 34512d8..79bb01e 100644
--- a/core/views/statistics.py
+++ b/core/views/statistics.py
@@ -13,7 +13,8 @@ from django.template.defaultfilters import slugify
 from django.utils import timezone
 
 import troggle.settings as settings
-from troggle.core.models.caves import Cave, Entrance, LogbookEntry
+from troggle.core.models.caves import Cave, Entrance
+from troggle.core.models.logbooks import LogbookEntry
 from troggle.core.models.survex import SurvexBlock, SurvexStation
 from troggle.core.models.troggle import (DataIssue, Expedition, Person,
                                          PersonExpedition)
diff --git a/core/views/survex.py b/core/views/survex.py
index 0a4c7f1..2307298 100644
--- a/core/views/survex.py
+++ b/core/views/survex.py
@@ -14,7 +14,8 @@ from django.views.decorators.csrf import ensure_csrf_cookie
 
 import parsers.survex
 import troggle.settings as settings
-from troggle.core.models.caves import Cave, LogbookEntry, PersonTrip
+from troggle.core.models.caves import Cave
+from troggle.core.models.logbooks import LogbookEntry, PersonTrip
 from troggle.core.models.survex import (SurvexBlock, SurvexDirectory,
                                         SurvexFile, SurvexPersonRole)
 from troggle.core.models.troggle import Expedition, Person, PersonExpedition
diff --git a/core/views/uploads.py b/core/views/uploads.py
index 5f40b30..cceafc9 100644
--- a/core/views/uploads.py
+++ b/core/views/uploads.py
@@ -21,7 +21,8 @@ from django.template import Context, loader
 from django.urls import reverse
 
 import settings
-from troggle.core.models.caves import QM, Cave, LogbookEntry, PersonTrip
+from troggle.core.models.caves import Cave
+from troggle.core.models.logbooks import QM, LogbookEntry, PersonTrip
 from troggle.core.models.survex import (DrawingFile, SurvexBlock, SurvexFile,
                                         SurvexPersonRole, Wallet)
 # from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time*
diff --git a/parsers/QMs.py b/parsers/QMs.py
index 7356efa..7a553be 100644
--- a/parsers/QMs.py
+++ b/parsers/QMs.py
@@ -5,7 +5,8 @@ from pathlib import Path
 
 from django.conf import settings
 
-from troggle.core.models.caves import QM, Cave
+from troggle.core.models.caves import Cave
+from troggle.core.models.logbooks import QM
 from troggle.core.models.troggle import DataIssue
 
 """Reads the CSV files containg QMs for a select few caves
diff --git a/parsers/logbooks.py b/parsers/logbooks.py
index 22cdf84..f045252 100644
--- a/parsers/logbooks.py
+++ b/parsers/logbooks.py
@@ -11,7 +11,8 @@ from django.conf import settings
 from django.template.defaultfilters import slugify
 
 from parsers.people import GetPersonExpeditionNameLookup, load_people_expos
-from troggle.core.models.caves import GetCaveLookup, LogbookEntry, PersonTrip
+from troggle.core.models.caves import GetCaveLookup
+from troggle.core.models.logbooks import LogbookEntry, PersonTrip
 from troggle.core.models.troggle import DataIssue, Expedition
 from troggle.core.utils import get_process_memory
 
diff --git a/parsers/survex.py b/parsers/survex.py
index bbe61a7..43a2639 100644
--- a/parsers/survex.py
+++ b/parsers/survex.py
@@ -9,7 +9,8 @@ from pathlib import Path
 
 
 import troggle.settings as settings
-from troggle.core.models.caves import QM, Cave, Entrance
+from troggle.core.models.caves import Cave, Entrance
+from troggle.core.models.logbooks import QM
 from troggle.core.models.survex import SurvexBlock, SurvexDirectory, SurvexFile, SurvexPersonRole, SurvexStation, Wallet
 from troggle.core.models.troggle import DataIssue, Expedition
 from troggle.core.utils import chaosmonkey, get_process_memory