diff --git a/smash/web/migrations/0179_visitimportdata.py b/smash/web/migrations/0179_visitimportdata.py
index 43beb8cc01cc67d43c77dee80cfd28a3d97c0d22..98be3741686f3b768e3e541319a43803beff7a4b 100644
--- a/smash/web/migrations/0179_visitimportdata.py
+++ b/smash/web/migrations/0179_visitimportdata.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.0.13 on 2020-11-17 07:22
+# Generated by Django 2.0.13 on 2020-11-18 10:51
 
 from django.db import migrations, models
 import django.db.models.deletion
@@ -12,18 +12,41 @@ class Migration(migrations.Migration):
 
     operations = [
         migrations.CreateModel(
-            name='VisitImportData',
+            name='EtlData',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('filename', models.CharField(blank=True, default='', max_length=128, verbose_name='File used for automatic import')),
+                ('run_at_times', models.CharField(blank=True, default='', max_length=1024, verbose_name='At what time automatic import should run')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='VisitImportData',
+            fields=[
+                ('etldata_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='web.EtlData')),
+                ('subject_id_column_name', models.CharField(default='donor_id', max_length=128, verbose_name='Subject id column name')),
+                ('visit_date_column_name', models.CharField(default='dateofvisit', max_length=128, verbose_name='Visit date column name')),
+                ('location_column_name', models.CharField(default='adressofvisit', max_length=128, verbose_name='Location column name')),
+                ('visit_number_column_name', models.CharField(default='visit_id', max_length=128, verbose_name='Visit number column name')),
                 ('appointment_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.AppointmentType', verbose_name='Default appointment type')),
-                ('import_worker', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name='Worker used by importer')),
-                ('study', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.Study', verbose_name='Study')),
-                ('subject_id_column_name', models.CharField(blank=False, default='donor_id', max_length=128, null=False,verbose_name='Subject id column name')),
-                ('visit_date_column_name', models.CharField(blank=False, default='dateofvisit', max_length=128, null=False,verbose_name='Visit date column name')),
-                ('location_column_name', models.CharField(blank=False, default='adressofvisit', max_length=128, null=False,verbose_name='Location column name')),
-                ('visit_number_column_name', models.CharField(blank=False, default='visit_id', max_length=128, null=False,verbose_name='Visit number column name')),
-                ('filename', models.CharField(blank=True, default='', max_length=128, null=False, verbose_name='File used for automatic import')),
-                ('run_at_times', models.CharField(blank=True, default='', max_length=1024, null=False, verbose_name='At what time automatic import should run')),
             ],
+            bases=('web.etldata',),
+        ),
+        migrations.AddField(
+            model_name='etldata',
+            name='import_worker',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name='Worker used by importer'),
         ),
+        migrations.AddField(
+            model_name='etldata',
+            name='study',
+            field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.Study', verbose_name='Study'),
+        ),
+        migrations.CreateModel(
+            name='SubjectImportData',
+            fields=[
+                ('etldata_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='web.EtlData')),
+            ],
+            bases=('web.etldata',),
+        ),
+
     ]
diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py
index 376f0f96d5e9f820e6ed3862418b08280fd2b624..07cf110eacd4187c33590a8ca3d64f06ea4f5f54 100644
--- a/smash/web/models/__init__.py
+++ b/smash/web/models/__init__.py
@@ -39,7 +39,7 @@ from .mail_template import MailTemplate
 from .missing_subject import MissingSubject
 from .inconsistent_subject import InconsistentSubject, InconsistentField
 
-from .etl import VisitImportData
+from .etl import VisitImportData, SubjectImportData
 
 __all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room,
            Subject, StudySubject, StudySubjectList, SubjectColumns, StudyNotificationParameters,
diff --git a/smash/web/models/etl/__init__.py b/smash/web/models/etl/__init__.py
index 98fb9988d5cf755972c854b35ae5d39684f97dfa..7b6f5699f77ab6e49c3576cd96e8872ae3c5d1c1 100644
--- a/smash/web/models/etl/__init__.py
+++ b/smash/web/models/etl/__init__.py
@@ -1,3 +1,4 @@
 from .visit_import import VisitImportData
+from .subject_import import SubjectImportData
 
-__all__ = [VisitImportData]
+__all__ = [VisitImportData, SubjectImportData]
diff --git a/smash/web/models/etl/etl.py b/smash/web/models/etl/etl.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2a430f197248a37b2fbe862ac3ed827ed3a1cda
--- /dev/null
+++ b/smash/web/models/etl/etl.py
@@ -0,0 +1,49 @@
+# coding=utf-8
+import logging
+import os
+
+from django.conf import settings
+from django.db import models
+
+logger = logging.getLogger(__name__)
+
+
+class EtlData(models.Model):
+    study = models.ForeignKey("web.Study",
+                              verbose_name='Study',
+                              editable=False,
+                              null=False,
+                              on_delete=models.CASCADE
+                              )
+
+    import_worker = models.ForeignKey("web.Worker",
+                                      verbose_name='Worker used by importer',
+                                      blank=True,
+                                      null=True,
+                                      on_delete=models.CASCADE
+                                      )
+
+    filename = models.CharField(max_length=128,
+                                verbose_name='File used for automatic import',
+                                default='',
+                                null=False,
+                                blank=True
+                                )
+    run_at_times = models.CharField(max_length=1024,
+                                    verbose_name='At what time automatic import should run',
+                                    default='',
+                                    null=False,
+                                    blank=True
+                                    )
+
+    def file_available(self) -> bool:
+        if self.filename is None or self.filename == '':
+            return False
+        absolute_path = self.get_absolute_file_path()
+        if os.path.basename(absolute_path) != self.filename:
+            logger.warning('File "{}" outside defined ETL_ROOT: {}'.format(self.filename, settings.ETL_ROOT))
+            return False
+        return os.path.isfile(absolute_path)
+
+    def get_absolute_file_path(self) -> str:
+        return os.path.join(settings.ETL_ROOT, self.filename)
diff --git a/smash/web/models/etl/subject_import.py b/smash/web/models/etl/subject_import.py
new file mode 100644
index 0000000000000000000000000000000000000000..5884586995e0b39eeb05c7b07caaa483ea6394ed
--- /dev/null
+++ b/smash/web/models/etl/subject_import.py
@@ -0,0 +1,10 @@
+# coding=utf-8
+import logging
+
+from web.models.etl.etl import EtlData
+
+logger = logging.getLogger(__name__)
+
+
+class SubjectImportData(EtlData):
+    pass
diff --git a/smash/web/models/etl/visit_import.py b/smash/web/models/etl/visit_import.py
index ac83ecf7f7ace8a88326483535afb875c1bcdba9..b7b4abf57bf0f3e94f1eda241028094ad0e2c045 100644
--- a/smash/web/models/etl/visit_import.py
+++ b/smash/web/models/etl/visit_import.py
@@ -1,21 +1,14 @@
 # coding=utf-8
 import logging
-import os
 
-from django.conf import settings
 from django.db import models
 
-logger = logging.getLogger(__name__)
+from web.models.etl.etl import EtlData
 
+logger = logging.getLogger(__name__)
 
-class VisitImportData(models.Model):
-    study = models.ForeignKey("web.Study",
-                              verbose_name='Study',
-                              editable=False,
-                              null=False,
-                              on_delete=models.CASCADE
-                              )
 
+class VisitImportData(EtlData):
     appointment_type = models.ForeignKey("web.AppointmentType",
                                          verbose_name='Default appointment type',
                                          blank=True,
@@ -23,13 +16,6 @@ class VisitImportData(models.Model):
                                          on_delete=models.CASCADE
                                          )
 
-    import_worker = models.ForeignKey("web.Worker",
-                                      verbose_name='Worker used by importer',
-                                      blank=True,
-                                      null=True,
-                                      on_delete=models.CASCADE
-                                      )
-
     subject_id_column_name = models.CharField(max_length=128,
                                               verbose_name='Subject id column name',
                                               default='donor_id',
@@ -43,39 +29,17 @@ class VisitImportData(models.Model):
                                               null=False,
                                               blank=False
                                               )
+
     location_column_name = models.CharField(max_length=128,
                                             verbose_name='Location column name',
                                             default='adressofvisit',
                                             null=False,
                                             blank=False
                                             )
+
     visit_number_column_name = models.CharField(max_length=128,
                                                 verbose_name='Visit number column name',
                                                 default='visit_id',
                                                 null=False,
                                                 blank=False
                                                 )
-    filename = models.CharField(max_length=128,
-                                verbose_name='File used for automatic import',
-                                default='',
-                                null=False,
-                                blank=True
-                                )
-    run_at_times = models.CharField(max_length=1024,
-                                    verbose_name='At what time automatic import should run',
-                                    default='',
-                                    null=False,
-                                    blank=True
-                                    )
-
-    def file_available(self):
-        if self.filename is None or self.filename == '':
-            return False
-        absolute_path = self.get_absolute_file_path()
-        if os.path.basename(absolute_path) != self.filename:
-            logger.warning('File "{}" outside defined ETL_ROOT: {}'.format(self.filename, settings.ETL_ROOT))
-            return False
-        return os.path.isfile(absolute_path)
-
-    def get_absolute_file_path(self) -> str:
-        return os.path.join(settings.ETL_ROOT, self.filename)