Index: docs/ref/contrib/localflavor.txt
===================================================================
--- docs/ref/contrib/localflavor.txt	(revision 17015)
+++ docs/ref/contrib/localflavor.txt	(working copy)
@@ -551,6 +551,17 @@
 
 .. _Personalausweis: http://de.wikipedia.org/wiki/Personalausweis
 
+.. class:: de.forms.DEPhoneNumberField
+
+    A form field that validates input as a German telephone
+    number. `Several formats`_ are accepted. The field value is
+    normalized to format 0301234567, which always includes the area
+    code with a leading zero and does not include the (optional)
+    country code. Extra spaces, hyphens, and parentheses in the phone
+    number are removed.
+
+.. _Several formats: http://de.wikipedia.org/wiki/Rufnummer#Schreibweisen
+
 .. class:: de.forms.DEZipCodeField
 
     A form field that validates input as a German zip code. Valid codes
Index: tests/regressiontests/localflavor/de/tests.py
===================================================================
--- tests/regressiontests/localflavor/de/tests.py	(revision 17015)
+++ tests/regressiontests/localflavor/de/tests.py	(working copy)
@@ -1,5 +1,5 @@
 from django.contrib.localflavor.de.forms import (DEZipCodeField, DEStateSelect,
-    DEIdentityCardNumberField)
+    DEIdentityCardNumberField, DEPhoneNumberField)
 
 from django.test import SimpleTestCase
 
@@ -47,3 +47,49 @@
             '0434657485D-6407276-0508137-9': error_format,
         }
         self.assertFieldOutput(DEIdentityCardNumberField, valid, invalid)
+
+    def test_DEPhoneNumberField(self):
+        error_format = [u'Enter a valid German phone number in (030) 1234567 format.']
+        valid = {
+            '+49 30 12345-67': '0301234567',
+            '+49 30 1234567': '0301234567',
+            '+49 (30) 1234567': '0301234567',
+            '+49-30-1234567': '0301234567',
+            '+49 (0)30 12345-67': '0301234567',
+            '030 12345-67': '0301234567',
+            '(030) 12345 67': '0301234567',
+            '0900 5 123456': '09005123456',
+            '(0)30 12345 67': '0301234567',
+            '(0)301234567': '0301234567',
+            '0049 (30) 1234567': '0301234567',
+            '0049301234567': '0301234567',
+            '+49301234567': '0301234567',
+            '0301234567': '0301234567',
+        }
+        invalid = {
+            # Unexpected country code:
+            '+1 30 12345-67': error_format,
+            # Invalid area code:
+            '(0030) 12345-67': error_format,
+            # Ambiguous area code:
+            '30 12345-67': error_format,
+            # Ambiguous spacing:
+            '(0) 301234567': error_format,
+            # Leading zero in number:
+            '(030) 012345-67': error_format,
+            '030-012345-67': error_format,
+            '(0)30 012345-67': error_format,
+            # Too many spaces/hyphens.
+            '+49 (0)30 12345--67': error_format,
+            ' (0)30 12345-67': error_format,
+            '(0)30 12345-67 ': error_format,
+            '-030 1234567': error_format,
+            '030  1234567': error_format,
+        }
+        self.assertFieldOutput(DEPhoneNumberField, valid, invalid)
+        valid = {}
+        invalid = {
+            '+1 30 12345-67': [u'Just enter something!'],
+        }
+        kwargs = {'error_messages': {'invalid': u'Just enter something!'}}
+        self.assertFieldOutput(DEPhoneNumberField, valid, invalid, field_kwargs=kwargs)
Index: django/contrib/localflavor/de/forms.py
===================================================================
--- django/contrib/localflavor/de/forms.py	(revision 17015)
+++ django/contrib/localflavor/de/forms.py	(working copy)
@@ -66,7 +66,7 @@
         return str(calculated_checksum)[-1] == given_checksum
 
     def clean(self, value):
-        super(DEIdentityCardNumberField, self).clean(value)
+        value = super(DEIdentityCardNumberField, self).clean(value)
         if value in EMPTY_VALUES:
             return u''
         match = re.match(id_re, value)
@@ -86,3 +86,47 @@
                 raise ValidationError(self.error_messages['invalid'])
 
         return u'%s%s-%s-%s-%s' % (residence, origin, birthday, validity, checksum)
+
+class DEPhoneNumberField(Field):
+    """
+    A German phone number.
+
+    The phone number may be in one of the following formats:
+
+      * DIN 5008: +49 30 12345-67, or 030 12345-67
+      * E.123: +49 30 1234567, or (030) 12345 67
+      * Microsoft: +49 (30) 1234567
+      * RFC 3966 (URI): +49-30-1234567
+      * Informal notation: +49 (0)30 12345-67
+
+    As an alternative to the optional +49, a prefix of 0049 is also
+    acceptable. The country code (if given) is stripped. The leading
+    zero in the area code is stated explicitly if omitted. The phone
+    numbers above all resolve to the normalized form 0301234567.
+
+    For more details, see:
+    http://de.wikipedia.org/wiki/Rufnummer#Schreibweisen
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a valid German phone number in (030) 1234567 format.'),
+    }
+    phone_regex = re.compile(r'^(?P<country>(\+|00)49[\s-]?)?(?P<phone>(\(0?[1-9]\d{1,3}\)|0?[1-9]\d{1,3}|\(0\)[1-9]\d{1,3})([\s-]?[1-9]\d*)([\s-]?\d+)*)$')
+    strip_regex = re.compile(r'[()\s-]')
+
+    def clean(self, value):
+        value = super(DEPhoneNumberField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        match = self.phone_regex.match(value)
+        if not match:
+            raise ValidationError(self.error_messages['invalid'])
+        phone = self.strip_regex.sub('', match.group('phone'))
+        # At this point, phone has at least two characters.
+        if phone[0] != u'0':
+            if not match.group('country'):
+                # When no country code was given, we expect a leading
+                # 0 in the area code to avoid any ambiguities between
+                # numbers with and without area code.
+                raise ValidationError(self.error_messages['invalid'])
+            phone = u'0' + phone
+        return phone
