Index: main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py	(revision 7660)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py	(revision 7661)
@@ -378,4 +378,11 @@
         )
 
+    carry_over = schema.Bool(
+        title = u'Carry-over Course',
+        default = False,
+        required = False,
+        readonly = False,
+        )
+
     def getLevel():
         """Returns the id of the level the ticket has been added to.
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/studycourse.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/studycourse.py	(revision 7660)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/studycourse.py	(revision 7661)
@@ -59,6 +59,4 @@
                 ticket.core_or_elective = val.core_or_elective
                 ticket.title = val.course.title
-                ticket.faculty = val.course.__parent__.__parent__.__parent__.title
-                ticket.department = val.course.__parent__.__parent__.title
                 ticket.fcode = val.course.__parent__.__parent__.__parent__.code
                 ticket.dcode = val.course.__parent__.__parent__.code
@@ -66,5 +64,24 @@
                 ticket.passmark = val.course.passmark
                 ticket.semester = val.course.semester
-                self[str(studylevel.level)][val.getCourseCode()] = ticket
+                self[str(studylevel.level)][ticket.code] = ticket
+        # Collect carry-over courses in base levels (not in repeating levels)
+        if studylevel.level % 100 == 0:
+            levels = sorted(self.keys())
+            index = levels.index(str(studylevel.level))
+            if  index > 0:
+                previous_level = self[levels[index-1]]
+                for key, val in previous_level.items():
+                    if val.score < val.passmark:
+                        if key in self[str(studylevel.level)]:
+                            # Carry-over ticket exists
+                            continue
+                        co_ticket = CourseTicket()
+                        for name in ['code', 'title', 'credits', 'passmark',
+                                     'semester', 'core_or_elective',
+                                     'fcode', 'dcode']:
+                            setattr(co_ticket, name, getattr(val, name))
+                        co_ticket.automatic = True
+                        co_ticket.carry_over = True
+                        self[str(studylevel.level)][co_ticket.code] = co_ticket
         return
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py	(revision 7660)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py	(revision 7661)
@@ -640,5 +640,4 @@
         self.browser.getControl(name="addlevel").value = ['100']
         self.browser.getControl("Add study level").click()
-        self.browser.getControl(name="addlevel").value = ['100']
 
         # Managers can view and manage course lists
@@ -669,4 +668,18 @@
         self.browser.getControl("Save").click()
         self.assertTrue('Form has been saved' in self.browser.contents)
+        # Carry-over courses will be collected when next level is created
+        self.browser.open(self.student_path + '/studycourse/manage')
+        # Add next level
+        self.browser.getControl(name="addlevel").value = ['200']
+        self.browser.getControl("Add study level").click()
+        self.browser.getLink("200").click()
+        self.assertMatches(
+            '...: Study Level 200 (Year 2)...', self.browser.contents)
+        # COURSE1 has score 0 and thus will become a carry-over course
+        # in level 200
+        self.assertEqual(
+            sorted(self.student['studycourse']['200'].keys()), [u'COURSE1'])
+        self.assertTrue(
+            self.student['studycourse']['200']['COURSE1'].carry_over)
         return
 
@@ -1174,6 +1187,10 @@
         self.browser.getControl(name="form.course").value = ['COURSE1']
         self.browser.getControl("Add course ticket").click()
-        self.assertMatches('...Successfully added COURSE1...',
-                           self.browser.contents)
+        self.assertMatches('...The ticket exists...',
+                           self.browser.contents)
+        # Indeed the ticket exists as carry-over course from level 100
+        # since its score was 0
+        self.assertTrue(
+            self.student['studycourse']['200']['COURSE1'].carry_over is True)
         # Students can open the pdf course registration slip
         self.browser.open(self.student_path + '/studycourse/200')
@@ -1185,4 +1202,10 @@
         self.browser.getControl("Remove selected", index=0).click()
         self.assertTrue('No ticket selected' in self.browser.contents)
+        # No ticket can be selected since the carry-over course is a core course
+        self.assertRaises(
+            LookupError, self.browser.getControl, name='val_id')
+        self.student['studycourse']['200']['COURSE1'].core_or_elective = False
+        self.browser.open(self.student_path + '/studycourse/200/edit')
+        # Now the student can remove the ticket
         ctrl = self.browser.getControl(name='val_id')
         ctrl.getControl(value='COURSE1').selected = True
