Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/__init__.py	(revision 6334)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/__init__.py	(revision 6359)
@@ -1,8 +1,10 @@
 # Make this a package.
-from waeup.sirp.accesscodes.accesscodes import get_access_code
+from waeup.sirp.accesscodes.accesscodes import (
+    get_access_code, invalidate_accesscode)
 
 # Public API of this submodule
 __all__ = [
-    get_access_code
+    get_access_code,
+    invalidate_accesscode,
     ]
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 6334)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 6359)
@@ -6,4 +6,5 @@
 from BTrees.OIBTree import OIBTree
 from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo
 from random import SystemRandom as random
 from waeup.sirp.interfaces import IWAeUPSIRPPluggable
@@ -11,9 +12,10 @@
     IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
     )
+from waeup.sirp.accesscodes.workflow import DISABLED
 
 class ManageACBatches(grok.Permission):
     grok.name('waeup.manageACBatches')
 
-class AccessCode(grok.Context):
+class AccessCode(grok.Model):
     grok.implements(IAccessCode)
 
@@ -25,4 +27,5 @@
         self.student_id = student_id
         self._disabled = disabled
+        IWorkflowInfo(self).fireTransition('init')
 
     @property
@@ -155,5 +158,5 @@
         self.invalidated_num += 1
 
-    def disable(self, ac_id, user_id):
+    def disable(self, ac_id, user_id=None):
         """Disable the AC with ID ``ac_id``.
 
@@ -389,4 +392,13 @@
         return
 
+    def invalidate(self, ac_id):
+        """Invalidate the AC with ID ``ac_id``.
+        """
+        ac = self.getAccessCode(ac_id)
+        if ac is None:
+            return
+        ac.__parent__.invalidate(ac_id)
+        return
+
 
 class AccessCodePlugin(grok.GlobalUtility):
@@ -430,5 +442,4 @@
     if batch is None:
         return None
-
     try:
         code = batch.getAccessCode(access_code)
@@ -436,2 +447,20 @@
         return None
     return code
+
+def invalidate_accesscode(access_code):
+    ac = get_access_code(access_code)
+    info = IWorkflowInfo(ac)
+    info.fireTransition('use')
+    return
+
+def disable_accesscode(access_code):
+    ac = get_access_code(access_code)
+    info = IWorkflowInfo(ac)
+    info.fireTransitionToward(DISABLED)
+    return
+
+def reenable_accesscode(access_code):
+    ac = get_access_code(access_code)
+    info = IWorkflowInfo(ac)
+    info.fireTransition('reenable')
+    return
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 6359)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 6359)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscodes.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscodes.py	(revision 6359)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscodes.py	(revision 6359)
@@ -0,0 +1,103 @@
+##
+## test_accesscodes.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jun 12 13:07:58 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import shutil
+import tempfile
+import unittest
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component.hooks import setSite
+from waeup.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer
+from waeup.sirp.accesscodes.accesscodes import (
+    AccessCodeBatch, get_access_code, invalidate_accesscode,
+    disable_accesscode)
+
+class AccessCodeHelpersTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeHelpersTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        # Create batch
+        batch = AccessCodeBatch('now', 'manfred', 'APP', 6.6, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        # Fill batch with accesscodes
+        batch.addAccessCode(0, '11111111')
+        batch.addAccessCode(1, '22222222')
+        batch.addAccessCode(2, '33333333')
+        self.ac1 = batch.getAccessCode('APP-1-11111111')
+        self.ac2 = batch.getAccessCode('APP-1-22222222')
+        self.ac3 = batch.getAccessCode('APP-1-33333333')
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeHelpersTests, self).tearDown()
+        return
+
+    def test_get_access_code(self):
+        ac = get_access_code('APP-1-11111111')
+        assert ac is self.ac1
+
+    def test_get_access_code_not_string(self):
+        ac = get_access_code(object())
+        assert ac is None
+
+    def test_get_access_code_no_proper_pin(self):
+        ac = get_access_code('APP-without_pin')
+        assert ac is None
+
+    def test_get_access_code_invalid_batch_num(self):
+        ac = get_access_code('APP-invalid-11111111')
+        assert ac is None
+
+    def test_get_access_code_invalid_pin(self):
+        ac = get_access_code('APP-1-notexistent')
+        assert ac is None
+
+    def test_invalidate_accesscode(self):
+        assert self.ac1._invalidation_date is None
+        invalidate_accesscode('APP-1-11111111')
+        assert self.ac1._invalidation_date is not None
+
+    def test_disable_accesscode_unused(self):
+        assert self.ac1._disabled is False
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1._disabled is True
+
+    def test_disable_accesscode_used(self):
+        assert self.ac1._disabled is False
+        invalidate_accesscode('APP-1-11111111')
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1._disabled is True
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/workflow.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/workflow.py	(revision 6359)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/workflow.py	(revision 6359)
@@ -0,0 +1,123 @@
+##
+## workflow.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat Jun 11 00:35:16 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Workflows for access codes/pins.
+"""
+import grok
+from datetime import datetime
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
+from waeup.sirp.accesscodes.interfaces import IAccessCode
+from waeup.sirp.interfaces import IObjectHistory, IWAeUPWorkflowInfo
+from waeup.sirp.workflow import WAeUPWorkflow, WAeUPWorkflowInfo
+
+INITIALIZED = 'initialized'
+USED = 'used'
+DISABLED = 'disabled'
+
+def invalidate_action(wf, context):
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    pin = context.representation
+    batch.invalidate(pin)
+    return
+
+def disable_action(wf, context):
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    pin = context.representation
+    batch.disable(pin)
+    return
+
+def reenable_action(wf, context):
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    pin = context.representation
+    batch.enable(pin)
+    return
+
+ACCESSCODE_TRANSITIONS = (
+    Transition(
+        transition_id = 'init',
+        title = 'Initialize PIN',
+        source = None,
+        condition = NullCondition,
+        destination = INITIALIZED),
+
+    Transition(
+        transition_id = 'use',
+        title = 'Use PIN',
+        source = INITIALIZED,
+        destination = USED,
+        action = invalidate_action),
+
+    Transition(
+        transition_id = 'disable_unused',
+        title = 'Disable unused PIN',
+        source = INITIALIZED,
+        destination = DISABLED,
+        action = disable_action),
+
+    Transition(
+        transition_id = 'disable_used',
+        title = 'Disable used PIN',
+        source = USED,
+        destination = DISABLED,
+        action = disable_action),
+
+    Transition(
+        transition_id = 'reenable',
+        title = 'Reenable disabled PIN',
+        source = DISABLED,
+        destination = INITIALIZED,
+        action = reenable_action),
+    )
+
+accesscode_workflow = WAeUPWorkflow(ACCESSCODE_TRANSITIONS)
+
+class AccessCodeWorkflowState(WorkflowState, grok.Adapter):
+    grok.context(IAccessCode)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.accesscode.state'
+    state_id = 'wf.accesscode.id'
+
+class AccessCodeWorkflowInfo(WAeUPWorkflowInfo, grok.Adapter):
+    grok.context(IAccessCode)
+    grok.provides(IWAeUPWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = accesscode_workflow
+
+@grok.subscribe(IAccessCode, IWorkflowTransitionEvent)
+def handle_accesscode_transition_event(obj, event):
+    # append message to history
+    timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
+    msg = '%s - %s (new state: %s)' % (
+        timestamp, event.destination.title, event.destination)
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    return
