Index: main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/accesscode.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/accesscode.py	(revision 9263)
+++ main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/accesscode.py	(revision 9265)
@@ -81,5 +81,5 @@
     grok.implements(IAccessCode)
 
-    def __init__(self, batch_serial, random_num):
+    def __init__(self, batch_serial=None, random_num=None):
         super(AccessCode, self).__init__()
         self.batch_serial = batch_serial
@@ -154,5 +154,5 @@
 
     def __init__(self, creation_date=None, creator=None, batch_prefix=None,
-                 cost=None, entry_num=None, num=None):
+                 cost=None, entry_num=0, num=None):
         super(AccessCodeBatch, self).__init__()
         self.creation_date = creation_date
@@ -317,8 +317,16 @@
 
     def addBatchByImport(self, batch, batch_id):
-        """Add an already created `batch` by import
-        with defined id.
-        """
+        """Add an already created `batch` by import with defined id.
+
+        We want to create a batch without access codes. Since num_entry
+        access codes are automatically added by handle_batch_added when
+        the batch is added to the ZODB, we have to temporarily set entry_num
+        to zero when adding the batch persistently.
+        """
+        orig_entry_num = batch.entry_num
+        batch.entry_num = 0
         self[batch_id] = batch
+        self._p_changed = True
+        batch.entry_num = orig_entry_num
         return
 
@@ -480,8 +488,24 @@
 
     def __call__(self, *args, **kw):
-        return AccessCodeBatch(*args, **kw)
+        return AccessCodeBatch()
 
     def getInterfaces(self):
         return implementedBy(AccessCodeBatch)
+
+class AccessCodeFactory(grok.GlobalUtility):
+    """A factory for accesscodes.
+
+    We need this factory for the accesscodeprocessor.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.AccessCode')
+    title = u"Create a new accesscode.",
+    description = u"This factory instantiates new accesscode instances."
+
+    def __call__(self, *args, **kw):
+        return AccessCode(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(AccessCode)
 
 def get_access_code(access_code):
Index: main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/batching.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/batching.py	(revision 9263)
+++ main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/batching.py	(revision 9265)
@@ -20,8 +20,9 @@
 """
 import grok
+from hurry.workflow.interfaces import IWorkflowState
 from zope.interface import Interface
-from waeup.kofa.interfaces import IBatchProcessor, IGNORE_MARKER
+from waeup.kofa.interfaces import IBatchProcessor, IGNORE_MARKER, IObjectHistory
 from waeup.kofa.utils.batching import BatchProcessor
-from waeup.kofa.accesscodes.interfaces import IAccessCodeBatch
+from waeup.kofa.accesscodes.interfaces import IAccessCodeBatch, IAccessCode
 
 class AccessCodeBatchProcessor(BatchProcessor):
@@ -67,25 +68,104 @@
         Returns a string describing the fields changed.
         """
-        changed = []
-        for key, value in row.items():
-            # Skip fields to be ignored.
-            if value == IGNORE_MARKER:
-                continue
-            if not hasattr(obj, key):
-                continue
-            try:
-                evalvalue = eval(value)
-                if isinstance(evalvalue, list):
-                    value = eval(value)
-            except:
-                pass
-            setattr(obj, key, value)
-            log_value = getattr(value, 'code', value)
-            changed.append('%s=%s' % (key, log_value))
-
+        items_changed = super(AccessCodeBatchProcessor, self).updateEntry(
+            obj, row, site)
         # Log actions...
         location_field = self.location_fields[0]
-        items_changed = ', '.join(changed)
         grok.getSite()['accesscodes'].logger.info('%s - %s - Batch updated: %s'
             % (self.name, row[location_field], items_changed))
         return
+
+class AccessCodeProcessor(BatchProcessor):
+    """A batch processor for IAccessCode objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'accesscodeprocessor'
+    grok.name(util_name)
+
+    name = u'AccessCode Processor'
+    iface = IAccessCode
+
+    location_fields = ['representation', 'batch_prefix', 'batch_num']
+    factory_name = 'waeup.AccessCode'
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.location_fields + [
+                        'random_num', 'owner', 'cost',
+                        'state', 'batch_serial',
+                        'history',]
+                    )))
+
+    @property
+    def required_fields(self):
+        return ['random_num', 'state', 'batch_serial', 'history',]
+
+    def parentsExist(self, row, site):
+        return self.getParent(row,site) is not None
+
+    def entryExists(self, row, site):
+        parent = self.getParent(row, site)
+        if parent is None:
+            return False
+        return row['representation'] in parent.keys()
+
+    def getParent(self, row, site):
+        if not 'accesscodes' in site.keys():
+            return None
+        batch_id = '%s-%s' % (row['batch_prefix'], row['batch_num'])
+        return site['accesscodes'].get(batch_id, None)
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['representation'], None)
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        obj.batch_serial = row['batch_serial']
+        obj.random_num = row['random_num']
+        parent[row['representation']] = obj
+        return
+
+    def updateEntry(self, obj, row, site):
+        """Update obj to the values given in row.
+
+        Returns a string describing the fields changed.
+        """
+        items_changed = ''
+        # Update state
+        if row.has_key('state'):
+            state = row.get('state', IGNORE_MARKER)
+            if state not in (IGNORE_MARKER, ''):
+                value = row['state']
+                IWorkflowState(obj).setState(value)
+                items_changed += ('%s=%s, ' % ('state', state))
+            row.pop('state')
+        # Update history
+        if row.has_key('history'):
+            history = row.get('history', IGNORE_MARKER)
+            if history not in (IGNORE_MARKER, ''):
+                values = row['history'].split('||')
+                for value in values:
+                    IObjectHistory(obj).addMessage(value)
+                items_changed += ('%s=%s, ' % ('history', history))
+            row.pop('history')
+
+        # In import files we can use the hash symbol at the end of a
+        # random_num string to avoid annoying automatic number transformation
+        # by Excel or Calc
+        row['random_num'] = row['random_num'].strip('#')
+
+        items_changed += super(AccessCodeProcessor, self).updateEntry(
+            obj, row, site)
+
+        # Log actions...
+        grok.getSite()['accesscodes'].logger.info('%s - %s - AC updated: %s'
+            % (self.name, row['representation'], items_changed))
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/browser.py	(revision 9263)
+++ main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/browser.py	(revision 9265)
@@ -67,4 +67,5 @@
                 continue
             del self.context[name]
+            self.context._p_changed = True
             self.flash(_('Deleted batch ${a}', mapping = {'a':name}))
             message = 'deleted: %s' % name
Index: main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/interfaces.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/interfaces.py	(revision 9263)
+++ main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/interfaces.py	(revision 9265)
@@ -49,4 +49,5 @@
     owner = schema.TextLine(
         title = _(u'Purchaser'),
+        required = False,
         )
     history = schema.Text(
@@ -54,4 +55,5 @@
         default = u'',
         readonly = True,
+        required = False,
        )
 
Index: main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv	(revision 9265)
+++ main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv	(revision 9265)
@@ -0,0 +1,4 @@
+batch_num,batch_prefix,batch_serial,cost,history,owner,random_num,representation,state
+1,CLR,33,100.0,2012-07-11 14:51:56 UTC - initialized by Manager||2012-07-11 14:55:42 UTC - invalidated by Manager,K1000009,1625368961#,CLR-1-1625368961,used
+1,CLR,2,100.0,2012-05-12 16:32:29 UTC - initialized by Manager,,5468453598#,CLR-1-5468453598,initialized
+1,HOS,731,0.0,2012-09-19 08:59:28 UTC - initialized by Manager,,0007044547#,HOS-1-0007044547,initialized
Index: main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/tests/test_batching.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/tests/test_batching.py	(revision 9263)
+++ main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/tests/test_batching.py	(revision 9265)
@@ -36,5 +36,6 @@
 from waeup.kofa.testing import (
     FunctionalLayer, FunctionalTestCase, setUp, tearDown, getRootFolder)
-from waeup.kofa.accesscodes.batching import AccessCodeBatchProcessor
+from waeup.kofa.accesscodes.batching import (
+    AccessCodeBatchProcessor, AccessCodeProcessor)
 
 
@@ -44,4 +45,11 @@
 
 BATCH_HEADER_FIELDS = BATCH_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+AC_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_ac_data.csv'),
+    'rb').read()
+
+AC_HEADER_FIELDS = AC_SAMPLE_DATA.split(
     '\n')[0].split(',')
 
@@ -73,4 +81,5 @@
         self.logfile = os.path.join(
             self.app['datacenter'].storage, 'logs', 'accesscodes.log')
+        self.workdir = tempfile.mkdtemp()
 
     def tearDown(self):
@@ -85,5 +94,4 @@
     def test_import(self):
         self.processor = AccessCodeBatchProcessor()
-        self.workdir = tempfile.mkdtemp()
         self.csv_file = os.path.join(self.workdir, 'sample_batch_data.csv')
         open(self.csv_file, 'wb').write(BATCH_SAMPLE_DATA)
@@ -102,2 +110,35 @@
         shutil.rmtree(os.path.dirname(fin_file))
         return
+
+class ACProcessorTest(ACFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_import(self):
+        self.processor = AccessCodeBatchProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_batch_data.csv')
+        open(self.csv_file, 'wb').write(BATCH_SAMPLE_DATA)
+        self.processor.doImport(self.csv_file, BATCH_HEADER_FIELDS)
+
+        self.processor = AccessCodeProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_ac_data.csv')
+        open(self.csv_file, 'wb').write(AC_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file =  self.processor.doImport(
+            self.csv_file, AC_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].batch_serial, 33)
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].random_num, '1625368961')
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].state, 'used')
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - AccessCode Processor - CLR-1-1625368961 - '
+            'AC updated: state=used, history=2012-07-11 14:51:56 UTC - '
+            'initialized by Manager||2012-07-11 14:55:42 UTC - '
+            'invalidated by Manager, batch_serial=33, '
+            'random_num=1625368961, cost=100.0, owner=K1000009'
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
