diff --git a/bootstrap.php b/bootstrap.php
index a4e56510c61a44e676040d6e20a54f2b89abb583..8eb90fd17abbe5c76a1d575cad473fda11eaaed6 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -117,6 +117,7 @@ class Bootstrap {
         define('FILTER_RULE_TABLE', $prefix.'filter_rule');
 
         define('PLUGIN_TABLE', $prefix.'plugin');
+        define('SEQUENCE_TABLE', $prefix.'sequence');
 
         define('API_KEY_TABLE',$prefix.'api_key');
         define('TIMEZONE_TABLE',$prefix.'timezone');
@@ -323,8 +324,6 @@ define('THISPAGE', Misc::currentURL());
 define('DEFAULT_MAX_FILE_UPLOADS',ini_get('max_file_uploads')?ini_get('max_file_uploads'):5);
 define('DEFAULT_PRIORITY_ID',1);
 
-define('EXT_TICKET_ID_LEN',6); //Ticket create. when you start getting collisions. Applies only on random ticket ids.
-
 #Global override
 if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
     // Take the left-most item for X-Forwarded-For
diff --git a/include/ajax.sequence.php b/include/ajax.sequence.php
new file mode 100644
index 0000000000000000000000000000000000000000..37be03269c87483a77e8d99cc7aaf840ffaeae34
--- /dev/null
+++ b/include/ajax.sequence.php
@@ -0,0 +1,106 @@
+<?php
+
+require_once(INCLUDE_DIR . 'class.sequence.php');
+
+class SequenceAjaxAPI extends AjaxController {
+
+    /**
+     * Ajax: GET /sequence/<id>
+     *
+     * Fetches the current value of a sequence
+     *
+     * Get-Arguments:
+     * format - (string) format string used to format the current value of
+     *      the sequence.
+     *
+     * Returns:
+     * (string) Current sequence number, optionally formatted
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 404 - Unknown sequence id
+     * 422 - Invalid sequence id
+     */
+    function current($id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif ($id == 0)
+            $sequence = new RandomSequence();
+        elseif (!$id || !is_numeric($id))
+            Http::response(422, 'Id is required');
+        elseif (!($sequence = Sequence::lookup($id)))
+            Http::response(404, 'No such object');
+
+        return $sequence->current($_GET['format']);
+    }
+
+    /**
+     * Ajax: GET|POST /sequence/manage
+     *
+     * Gets a dialog box content or updates data from the content
+     *
+     * Post-Arguments:
+     * seq[<id>][*] - Updated information for existing sequences
+     * seq[<new-*>[*] - Information for new sequences
+     * seq[<id>][deleted] - If set to true, indicates that the sequence
+     *      should be deleted from the database
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 422 - Information sent for update of unknown sequence
+     */
+    function manage() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+
+        $sequences = Sequence::objects()->all();
+        $info = array(
+            'action' => '#sequence/manage',
+        );
+
+        $valid = true;
+        if ($_POST) {
+            foreach ($_POST['seq'] as $id=>$info) {
+                if (strpos($id, 'new-') === 0) {
+                    unset($info['id']);
+                    $sequences[] = Sequence::create($info);
+                }
+                else {
+                    foreach ($sequences as $s) {
+                        if ($s->id == $id)
+                            break;
+                        $s = false;
+                    }
+                    if (!$s) {
+                        Http::response(422, $id . ': Invalid or unknown sequence');
+                    }
+                    elseif ($info['deleted']) {
+                        $s->delete();
+                        continue;
+                    }
+                    foreach ($info as $f=>$val) {
+                        if (isset($s->{$f}))
+                            $s->set($f, $val);
+                        elseif ($f == 'current')
+                            $s->next = $val;
+                    }
+                    if (($v = $s->isValid()) !== true) {
+                        $msg = sprintf('%s: %s', $s->getName(), $valid);
+                        $valid = false;
+                    }
+                }
+            }
+            if ($valid) {
+                foreach ($sequences as $s)
+                    $s->save();
+                Http::response(205, 'All sequences updated');
+            }
+        }
+
+        include STAFFINC_DIR . 'templates/sequence-manage.tmpl.php';
+    }
+}
diff --git a/include/class.config.php b/include/class.config.php
index 0616bb96b1a6ac569efceed44db7f82880da78d7..79bb05fd4b354dd50e5ce167b69591c4e13a6ae6 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -624,8 +624,20 @@ class OsticketConfig extends Config {
         return true; //No longer an option...hint: big plans for headers coming!!
     }
 
-    function useRandomIds() {
-        return ($this->get('random_ticket_ids'));
+    function getDefaultSequence() {
+        if ($this->get('sequence_id'))
+            $sequence = Sequence::lookup($this->get('sequence_id'));
+        if (!$sequence)
+            $sequence = new RandomSequence();
+        return $sequence;
+    }
+    function getDefaultNumberFormat() {
+        return $this->get('number_format');
+    }
+    function getNewTicketNumber() {
+        $s = $this->getDefaultSequence();
+        return $s->next($this->getDefaultNumberFormat(),
+            array('Ticket', 'isTicketNumberUnique'));
     }
 
     /* autoresponders  & Alerts */
@@ -976,7 +988,8 @@ class OsticketConfig extends Config {
             $this->update('default_storage_bk', $vars['default_storage_bk']);
 
         return $this->updateAll(array(
-            'random_ticket_ids'=>$vars['random_ticket_ids'],
+            'number_format'=>$vars['number_format'] ?: '######',
+            'sequence_id'=>$vars['sequence_id'] ?: 0,
             'default_priority_id'=>$vars['default_priority_id'],
             'default_help_topic'=>$vars['default_help_topic'],
             'default_sla_id'=>$vars['default_sla_id'],
diff --git a/include/class.orm.php b/include/class.orm.php
index 1d8810919a1bab31a9ca853e88162a86d9de3902..aa05a040d4b03b827958df1e2e107cdba1c3ba2e 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -260,6 +260,12 @@ class SqlFunction {
         $args = (count($this->args)) ? implode(',', db_input($this->args)) : "";
         return sprintf('%s(%s)', $this->func, $args);
     }
+
+    static function __callStatic($func, $args) {
+        $I = new static($func);
+        $I->args = $args;
+        return $I;
+    }
 }
 
 class QuerySet implements IteratorAggregate, ArrayAccess {
@@ -272,6 +278,10 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
     var $offset = 0;
     var $related = array();
     var $values = array();
+    var $lock = false;
+
+    const LOCK_EXCLUSIVE = 1;
+    const LOCK_SHARED = 2;
 
     var $compiler = 'MySqlCompiler';
     var $iterator = 'ModelInstanceIterator';
@@ -299,6 +309,11 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
         return $this;
     }
 
+    function lock($how=false) {
+        $this->lock = $how ?: self::LOCK_EXCLUSIVE;
+        return $this;
+    }
+
     function limit($count) {
         $this->limit = $count;
         return $this;
@@ -932,6 +947,14 @@ class MySqlCompiler extends SqlCompiler {
             $sql .= ' LIMIT '.$queryset->limit;
         if ($queryset->offset)
             $sql .= ' OFFSET '.$queryset->offset;
+        switch ($queryset->lock) {
+        case QuerySet::LOCK_EXCLUSIVE:
+            $sql .= ' FOR UPDATE';
+            break;
+        case QuerySet::LOCK_SHARED:
+            $sql .= ' LOCK IN SHARE MODE';
+            break;
+        }
 
         return new MysqlExecutor($sql, $this->params);
     }
diff --git a/include/class.sequence.php b/include/class.sequence.php
new file mode 100644
index 0000000000000000000000000000000000000000..2569718737b711f9fb86d779da2aefb115b6feda
--- /dev/null
+++ b/include/class.sequence.php
@@ -0,0 +1,214 @@
+<?php
+
+require_once INCLUDE_DIR . 'class.orm.php';
+
+class Sequence extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => SEQUENCE_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('name'),
+    );
+
+    const FLAG_INTERNAL = 0x0001;
+
+    /**
+     * Function: next
+     *
+     * Fetch the next number in the sequence. The next number in the
+     * sequence will be adjusted in the database so that subsequent calls to
+     * this function should never receive the same result.
+     *
+     * Optionally, a format specification can be sent to the function and
+     * the next sequence number will be returned padded. See the `::format`
+     * function for more details.
+     *
+     * Optionally, a check callback can be specified to ensure the next
+     * value of the sequence is valid. This might be useful for a
+     * pseudo-random generator which might repeat existing numbers. The
+     * callback should have the following signature and should return
+     * boolean TRUE to approve the number.
+     *
+     * Parameters:
+     * $format - (string) Format specification for the result
+     * $check - (function($format, $next)) Validation callback function
+     *      where $next will be the next value as an integer, and $formatted
+     *      will be the formatted version of the number, if a $format
+     *      parameter were passed to the `::next` method.
+     *
+     * Returns:
+     * (int|string) - next number in the sequence, optionally formatted and
+     * verified.
+     */
+    function next($format=false, $check=false) {
+        $digits = $format ? $this->getDigitCount($format) : false;
+        do {
+            $next = $this->__next($digits);
+            $formatted = $format ? $this->format($format, $next) : $next;
+        }
+        while ($check && !$check($formatted, $next));
+        return $formatted;
+    }
+
+    /**
+     * Function: current
+     *
+     * Peeks at the next number in the sequence without incrementing the
+     * sequence.
+     *
+     * Parameters:
+     * $format - (string:optional) format string to receive the current
+     *      sequence number
+     *
+     * Returns:
+     * (int|string) - the next number in the sequence without advancing the
+     * sequence, optionally formatted. See the `::format` method for
+     * formatting details.
+     */
+     function current($format=false) {
+        return $format ? $this->format($format, $this->next) : $this->next;
+    }
+
+    /**
+     * Function: format
+     *
+     * Formats a number to the given format. The number will be placed into
+     * the format string according to the locations of hash characters (#)
+     * in the string. If more hash characters are encountered than digits
+     * the digits are left-padded accoring to the sequence padding
+     * character. If fewer are found, the last group will receive all the
+     * remaining digits.
+     *
+     * Hash characters can be escaped with a backslash (\#) and will emit a
+     * single hash character to the output.
+     *
+     * Parameters:
+     * $format - (string) Format string for the number, e.g. "TX-######-US"
+     * $number - (int) Number to appear in the format. If not
+     *      specified the next number in this sequence will be used.
+     */
+    function format($format, $number) {
+        $groups = array();
+        preg_match_all('/(?<!\\\)#+/', $format, $groups, PREG_OFFSET_CAPTURE);
+
+        $total = 0;
+        foreach ($groups[0] as $g)
+            $total += strlen($g[0]);
+
+        $number = str_pad($number, $total, $this->padding, STR_PAD_LEFT);
+        $output = '';
+        $start = $noff = 0;
+        // Interate through the ### groups and replace the number of hash
+        // marks with numbers from the sequence
+        foreach ($groups[0] as $g) {
+            $size = strlen($g[0]);
+            // Add format string from previous marker to current ## group
+            $output .= str_replace('\#', '#',
+                substr($format, $start, $g[1] - $start));
+            // Add digits from the sequence number
+            $output .= substr($number, $noff, $size);
+            // Set offset counts for the next loop
+            $start = $g[1] + $size;
+            $noff += $size;
+        }
+        // If there are more digits of number than # marks, add the number
+        // where the last hash mark was found
+        if (strlen($number) > $noff)
+            $output .= substr($number, $noff);
+        // Add format string from ending ## group
+        $output .= str_replace('\#', '#', substr($format, $start));
+        return $output;
+    }
+
+    function getDigitCount($format) {
+        $total = 0;
+        $groups = array();
+
+        return preg_match_all('/(?<!\\\)#/', $format, $groups);
+    }
+
+    /**
+     * Function: __next
+     *
+     * Internal implementation of the next number generator. This method
+     * will lock the database object backing to protect against concurent
+     * ticket processing. The lock will be released at the conclusion of the
+     * session.
+     *
+     * Parameters:
+     * $digits - (int:optional) number of digits (size) of the number. This
+     *      is useful for random sequences which need a size hint to
+     *      generate a "next" value.
+     *
+     * Returns:
+     * (int) - The current number in the sequence. The sequence is advanced
+     * and assured to be session-wise atomic before the value is returned.
+     */
+    function __next($digits=false) {
+        // Lock the database object -- this is important to handle concurrent
+        // requests for new numbers
+        static::objects()->filter(array('id'=>$this->id))->lock()->one();
+
+        // Increment the counter
+        $next = $this->next;
+        $this->next += $this->increment;
+        $this->updated = SqlFunction::NOW();
+        $this->save();
+
+        return $next;
+    }
+
+    function hasFlag($flag) {
+        return $this->flags & $flag != 0;
+    }
+    function setFlag($flag, $value=true) {
+        if ($value)
+            $this->flags |= $flag;
+        else
+            $this->flags &= ~$flag;
+    }
+
+    function getName() {
+        return $this->name;
+    }
+
+    function isValid() {
+        if (!$this->name)
+            return 'Name is required';
+        if (!$this->increment)
+            return 'Non-zero increment is required';
+        if (!$this->next || $this->next < 0)
+            return 'Positive "next" value is required';
+
+        if (!$this->padding)
+            $this->padding = '0';
+
+        return true;
+    }
+
+    function __get($what) {
+        // Pseudo-property for $sequence->current
+        if ($what == 'current')
+            return $this->current();
+        return parent::__get($what);
+    }
+}
+
+class RandomSequence extends Sequence {
+    var $padding = '0';
+
+    // Override the ORM constructor and do nothing
+    function __construct() {}
+
+    function __next($digits=6) {
+        return Misc::randNumber($digits);
+    }
+
+    function current($format=false) {
+        return $this->next($format);
+    }
+
+    function save() {
+        throw new RuntimeException('RandomSequence is not database-backed');
+    }
+}
diff --git a/include/class.thread.php b/include/class.thread.php
index 3b773bfda402c0633e5aab07a96e5e7ab840e469..8c5920597a9b3bf7d811917d47e89f9cfccb0277 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -896,9 +896,9 @@ Class ThreadEntry {
         $match = array();
         if ($subject
                 && $mailinfo['email']
-                && preg_match("/#(?:[\p{L}-]+)?([0-9]{1,10})/u", $subject, $match)
+                && preg_match("/\b#(\S+)/u", $subject, $match)
                 //Lookup by ticket number
-                && ($ticket = Ticket::lookupByNumber((int)$match[1]))
+                && ($ticket = Ticket::lookupByNumber($match[1]))
                 //Lookup the user using the email address
                 && ($user = User::lookup(array('emails__address' => $mailinfo['email'])))) {
             //We have a valid ticket and user
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 6518d12eae5722401bb1f96a4932ed00e14473c0..1e8be21e29a27b422d6ada677f8af4a6be364cfc 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -2081,16 +2081,9 @@ class Ticket {
         return self::lookup(self:: getIdByNumber($number, $email));
     }
 
-    function genRandTicketNumber($len = EXT_TICKET_ID_LEN) {
-
-        //We can allow collissions...number and email must be unique ...so
-        // same number with diff emails is ok.. But for clarity...we are going to make sure it is unique.
-        $number = Misc::randNumber($len);
-        if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.'
-                        WHERE `number`='.db_input($number))))
-            return Ticket::genRandTicketNumber($len);
-
-        return $number;
+    static function isTicketNumberUnique($number) {
+        return 0 == db_num_rows(db_query(
+            'SELECT ticket_id FROM '.TICKET_TABLE.' WHERE `number`='.db_input($number)));
     }
 
     function getIdByMessageId($mid, $email) {
@@ -2470,7 +2463,7 @@ class Ticket {
         $ipaddress = $vars['ip'] ?: $_SERVER['REMOTE_ADDR'];
 
         //We are ready son...hold on to the rails.
-        $number = Ticket::genRandTicketNumber();
+        $number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber();
         $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
             .' ,lastmessage= NOW()'
             .' ,user_id='.db_input($user->getId())
@@ -2493,13 +2486,6 @@ class Ticket {
 
         /* -------------------- POST CREATE ------------------------ */
 
-        if(!$cfg->useRandomIds()) {
-            //Sequential ticket number support really..really suck arse.
-            //To make things really easy we are going to use autoincrement ticket_id.
-            db_query('UPDATE '.TICKET_TABLE.' SET `number`='.db_input($id).' WHERE ticket_id='.$id.' LIMIT 1');
-            //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable]
-        }
-
         // Save the (common) dynamic form
         $form->setTicketId($id);
         $form->save();
diff --git a/include/class.topic.php b/include/class.topic.php
index ee0f2a47ede9eab61d87bf0955c5aec9fcd8bd30..940d9156b232a210042c1566bebcf771c8f36f51 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -14,6 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
+require_once INCLUDE_DIR . 'class.sequence.php';
+
 class Topic {
     var $id;
 
@@ -27,6 +29,8 @@ class Topic {
 
     const FORM_USE_PARENT = 4294967295;
 
+    const FLAG_CUSTOM_NUMBERS = 0x0001;
+
     function Topic($id) {
         $this->id=0;
         $this->load($id);
@@ -183,7 +187,28 @@ class Topic {
     }
 
     function getInfo() {
-        return $this->getHashtable();
+        $base = $this->getHashtable();
+        $base['custom-numbers'] = $this->hasFlag(self::FLAG_CUSTOM_NUMBERS);
+        return $base;
+    }
+
+    function hasFlag($flag) {
+        return $this->ht['flags'] & $flag != 0;
+    }
+
+    function getNewTicketNumber() {
+        global $cfg;
+
+        if (!$this->hasFlag(self::FLAG_CUSTOM_NUMBERS))
+            return $cfg->getNewTicketNumber();
+
+        if ($this->ht['sequence_id'])
+            $sequence = Sequence::lookup($this->ht['sequence_id']);
+        if (!$sequence)
+            $sequence = new RandomSequence();
+
+        return $sequence->next($this->ht['number_format'] ?: '######',
+            array('Ticket', 'isTicketNumberUnique'));
     }
 
     function setSortOrder($i) {
@@ -332,6 +357,9 @@ class Topic {
             .',page_id='.db_input($vars['page_id'])
             .',isactive='.db_input($vars['isactive'])
             .',ispublic='.db_input($vars['ispublic'])
+            .',sequence_id='.db_input($vars['custom-numbers'] ? $vars['sequence_id'] : 0)
+            .',number_format='.db_input($vars['custom-numbers'] ? $vars['number_format'] : '')
+            .',flags='.db_input($vars['custom-numbers'] ? self::FLAG_CUSTOM_NUMBERS : 0)
             .',noautoresp='.db_input(isset($vars['noautoresp']) && $vars['noautoresp']?1:0)
             .',notes='.db_input(Format::sanitize($vars['notes']));
 
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index 2c7a9f2317743e6ff170236a06ec39ce2355df23..07ed570405dd8d57a565893a78c400254e795dc1 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -125,22 +125,21 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
     <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="70" nowrap>
+            <th nowrap>
                 <a href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Ticket ID">Ticket #</a>
             </th>
-            <th width="100">
+            <th width="120">
                 <a href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Date">Create Date</a>
             </th>
-            <th width="80">
+            <th width="100">
                 <a href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Status">Status</a>
             </th>
-            <th width="300">
+            <th width="320">
                 <a href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Subject">Subject</a>
             </th>
-            <th width="150">
+            <th width="120">
                 <a href="tickets.php?sort=dept&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Department">Department</a>
             </th>
-            <th width="100">Phone Number</th>
         </tr>
     </thead>
     <tbody>
@@ -158,12 +157,9 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
                 $subject="<b>$subject</b>";
                 $ticketNumber="<b>$ticketNumber</b>";
             }
-            $phone=Format::phone($row['phone']);
-            if($row['phone_ext'])
-                $phone.=' '.$row['phone_ext'];
             ?>
             <tr id="<?php echo $row['ticket_id']; ?>">
-                <td class="centered">
+                <td>
                 <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>"
                     href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $ticketNumber; ?></a>
                 </td>
@@ -173,13 +169,12 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
                     <a href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a>
                 </td>
                 <td>&nbsp;<?php echo Format::truncate($dept,30); ?></td>
-                <td><?php echo $phone; ?></td>
             </tr>
         <?php
         }
 
      } else {
-         echo '<tr><td colspan="7">Your query did not match any records</td></tr>';
+         echo '<tr><td colspan="6">Your query did not match any records</td></tr>';
      }
     ?>
     </tbody>
diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml
index 9d1e385c3bd979cf2bd12dcfeb6f6f9aa874085b..a63764ba9c1b3b1aeb8b56488c0cbc6c4aa4409e 100644
--- a/include/i18n/en_US/config.yaml
+++ b/include/i18n/en_US/config.yaml
@@ -78,7 +78,8 @@ core:
     hide_staff_name: 0
     overlimit_notice_active: 0
     email_attachments: 1
-    random_ticket_ids: 1
+    number_format: '######'
+    sequence_id: 0
     log_level: 2
     log_graceperiod: 12
     client_registration: 'public'
diff --git a/include/i18n/en_US/help/tips/manage.helptopic.yaml b/include/i18n/en_US/help/tips/manage.helptopic.yaml
index d2179170cea229fe773d361c85080c4e15c73188..0297f1ceeb93f418b112cdfecbc4213f1ae2cbeb 100644
--- a/include/i18n/en_US/help/tips/manage.helptopic.yaml
+++ b/include/i18n/en_US/help/tips/manage.helptopic.yaml
@@ -112,3 +112,10 @@ ticket_auto_response:
     links:
       - title: Autoresponder Settings
         href: /scp/settings.php?t=autoresp
+
+custom_numbers:
+    title: Custom Ticket Numbers
+    content: >
+        Choose "Custom" here to override the system default ticket numbering
+        format for tickets created in this help topic.  See the help tips on
+        the Settings / Tickets page for more details on the settings.
diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml
index 50693c2a370d1c4b061c96f58e4bab9d271eebe7..e694af2b391a87f3df213a351ee0ec4baa77656b 100644
--- a/include/i18n/en_US/help/tips/settings.ticket.yaml
+++ b/include/i18n/en_US/help/tips/settings.ticket.yaml
@@ -13,6 +13,23 @@
 # must match the HTML #ids put into the page template.
 #
 ---
+number_format:
+    title: Ticket Number Format
+    content: >
+        This setting is used to create numbers for new tickets. Use hash
+        signs (`#`) where numbers are to be replaced. For example, for
+        six-digit numbers, use <code>######</code>. Any other text in the
+        number format will be preserved. Number formats can be overridden by
+        each help topic.
+
+sequence_id:
+    title: Ticket Number Sequence
+    content:
+        Choose a sequence from which to derive new ticket numbers. The
+        system has a incrementing sequence and a random sequence by default.
+        You may create as many sequences as you wish. Use various sequences
+        in the number format configuration for help topics.
+
 default_sla:
     title: Default SLA
     content: >
diff --git a/include/mysqli.php b/include/mysqli.php
index e8bfeb32ebcfa7f6acd5fe26991a6fc07b32c136..899e4ef1807b9e34da57ea6fd0ba023e6045d234 100644
--- a/include/mysqli.php
+++ b/include/mysqli.php
@@ -76,6 +76,19 @@ function db_connect($host, $user, $passwd, $options = array()) {
 
     @db_set_variable('sql_mode', '');
 
+    // Start a new transaction -- for performance. Transactions are always
+    // committed at shutdown (below)
+    $__db->autocommit(false);
+
+    // Auto commit the transaction at shutdown and re-enable statement-level
+    // autocommit
+    register_shutdown_function(function() {
+        global $__db, $err;
+        if (!$__db->commit())
+            $err = 'Unable to save changes to database';
+        $__db->autocommit(true);
+    });
+
     // Use connection timing to seed the random number generator
     Misc::__rand_seed((microtime(true) - $start) * 1000000);
 
diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php
index 00f06d38d0ca604b2adf81daa606132110fd7359..a13daf551d7fe1c323f17cb5e5a5b858e5364ee4 100644
--- a/include/staff/helptopic.inc.php
+++ b/include/staff/helptopic.inc.php
@@ -241,6 +241,61 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                     <i class="help-tip icon-question-sign" href="#ticket_auto_response"></i>
             </td>
         </tr>
+        <tr>
+            <td>
+                Ticket Number Format:
+            </td>
+            <td>
+                <label>
+                <input type="radio" name="custom-numbers" value="0" <?php echo !$info['custom-numbers']?'checked="checked"':''; ?>
+                    onchange="javascript:$('#custom-numbers').hide();"> System Default
+                </label>&nbsp;<label>
+                <input type="radio" name="custom-numbers" value="1" <?php echo $info['custom-numbers']?'checked="checked"':''; ?>
+                    onchange="javascript:$('#custom-numbers').show(200);"> Custom
+                </label>&nbsp; <i class="help-tip icon-question-sign" href="#custom_numbers"></i>
+            </td>
+        </tr>
+    </tbody>
+    <tbody id="custom-numbers" style="<?php if (!$info['custom-numbers']) echo 'display:none'; ?>">
+        <tr>
+            <td style="padding-left:20px">
+                Format:
+            </td>
+            <td>
+                <input type="text" name="number_format" value="<?php echo $info['number_format']; ?>"/>
+                <span class="faded">e.g. <span id="format-example"><?php
+                    if ($info['custom-numbers']) {
+                        if ($info['sequence_id'])
+                            $seq = Sequence::lookup($info['sequence_id']);
+                        if (!isset($seq))
+                            $seq = new RandomSequence();
+                        echo $seq->current($info['number_format']);
+                    } ?></span></span>
+            </td>
+        </tr>
+        <tr>
+<?php $selected = 'selected="selected"'; ?>
+            <td style="padding-left:20px">
+                Sequence:
+            </td>
+            <td>
+                <select name="sequence_id">
+                <option value="0" <?php if ($info['sequence_id'] == 0) echo $selected;
+                    ?>>&mdash; Random &mdash;</option>
+<?php foreach (Sequence::objects() as $s) { ?>
+                <option value="<?php echo $s->id; ?>" <?php
+                    if ($info['sequence_id'] == $s->id) echo $selected;
+                    ?>><?php echo $s->name; ?></option>
+<?php } ?>
+                </select>
+                <button class="action-button" onclick="javascript:
+                $.dialog('ajax.php/sequence/manage', 205);
+                return false;
+                "><i class="icon-gear"></i> Manage</button>
+            </td>
+        </tr>
+    </tbody>
+    <tbody>
         <tr>
             <th colspan="2">
                 <em><strong>Admin Notes</strong>: Internal notes about the help topic.</em>
@@ -260,3 +315,18 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
     <input type="button" name="cancel" value="Cancel" onclick='window.location.href="helptopics.php"'>
 </p>
 </form>
+<script type="text/javascript">
+$(function() {
+    var request = null,
+      update_example = function() {
+      request && request.abort();
+      request = $.get('ajax.php/sequence/'
+        + $('[name=sequence_id] :selected').val(),
+        {'format': $('[name=number_format]').val()},
+        function(data) { $('#format-example').text(data); }
+      );
+    };
+    $('[name=sequence_id]').on('change', update_example);
+    $('[name=number_format]').on('keyup', update_example);
+});
+</script>
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index a7960a02b16552d72bd5377a138ccc049b535d46..793b61b85ad39358768030783c1db4a26b8a926b 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -17,15 +17,41 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
         </tr>
     </thead>
     <tbody>
-        <tr><td width="220" class="required">Ticket IDs:</td>
+        <tr>
+            <td>
+                Default Ticket Number Format:
+            </td>
             <td>
-                <input type="radio" name="random_ticket_ids"  value="0" <?php echo !$config['random_ticket_ids']?'checked="checked"':''; ?> />
-                Sequential
-                <input type="radio" name="random_ticket_ids"  value="1" <?php echo $config['random_ticket_ids']?'checked="checked"':''; ?> />
-                Random
+                <input type="text" name="number_format" value="<?php echo $config['number_format']; ?>"/>
+                <span class="faded">e.g. <span id="format-example"><?php
+                    if ($config['sequence_id'])
+                        $seq = Sequence::lookup($config['sequence_id']);
+                    if (!isset($seq))
+                        $seq = new RandomSequence();
+                    echo $seq->current($config['number_format']);
+                    ?></span></span>
+                <i class="help-tip icon-question-sign" href="#number_format"></i>
+            </td>
+        </tr>
+        <tr><td width="220">Default Ticket Number Sequence:</td>
+<?php $selected = 'selected="selected"'; ?>
+            <td>
+                <select name="sequence_id">
+                <option value="0" <?php if ($config['sequence_id'] == 0) echo $selected;
+                    ?>>&mdash; Random &mdash;</option>
+<?php foreach (Sequence::objects() as $s) { ?>
+                <option value="<?php echo $s->id; ?>" <?php
+                    if ($config['sequence_id'] == $s->id) echo $selected;
+                    ?>><?php echo $s->name; ?></option>
+<?php } ?>
+                </select>
+                <button class="action-button" onclick="javascript:
+                $.dialog('ajax.php/sequence/manage', 205);
+                return false;
+                "><i class="icon-gear"></i> Manage</button>
+                <i class="help-tip icon-question-sign" href="#sequence_id"></i>
             </td>
         </tr>
-
         <tr>
             <td width="180" class="required">
                 Default SLA:
@@ -283,4 +309,18 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
     <input class="button" type="reset" name="reset" value="Reset Changes">
 </p>
 </form>
-
+<script type="text/javascript">
+$(function() {
+    var request = null,
+      update_example = function() {
+      request && request.abort();
+      request = $.get('ajax.php/sequence/'
+        + $('[name=sequence_id] :selected').val(),
+        {'format': $('[name=number_format]').val()},
+        function(data) { $('#format-example').text(data); }
+      );
+    };
+    $('[name=sequence_id]').on('change', update_example);
+    $('[name=number_format]').on('keyup', update_example);
+});
+</script>
diff --git a/include/staff/templates/sequence-manage.tmpl.php b/include/staff/templates/sequence-manage.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c34c9dd99b63af75b800f3d8d6d3613f64d5d10
--- /dev/null
+++ b/include/staff/templates/sequence-manage.tmpl.php
@@ -0,0 +1,147 @@
+<h3><i class="icon-wrench"></i> Manage Sequences</i></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+Sequences are used to generate sequential numbers. Various sequences can be
+used to generate sequences for different purposes.
+<br/>
+<br/>
+<form method="post" action="<?php echo $info['action']; ?>">
+<div id="sequences">
+<?php
+$current_list = array();
+foreach ($sequences as $e) {
+    $field = function($field, $name=false) use ($e) { ?>
+    <input class="f<?php echo $field; ?>" type="hidden" name="seq[<?php echo $e->id;
+        ?>][<?php echo $name ?: $field; ?>]" value="<?php echo $e->{$field}; ?>"/>
+<?php }; ?>
+    <div class="row-item">
+        <?php echo $field('name'); echo $field('current', 'next'); echo $field('increment'); echo $field('padding'); ?>
+        <input type="hidden" class="fdeleted" name="seq[<?php echo $e->get('id'); ?>][deleted]" value="0"/>
+        <i class="icon-sort-by-order"></i>
+        <div style="display:inline-block" class="name"> <?php echo $e->getName(); ?> </div>
+        <div style="display:inline-block;margin-right:60px" class="pull-right">
+            <span class="faded">next</span>
+            <span class="current"><?php echo $e->current(); ?></span>
+        </div>
+        <div class="button-group">
+            <div class="manage"><a href="#"><i class="icon-cog"></i></a></div>
+            <div class="delete"><?php if (!$e->hasFlag(Sequence::FLAG_INTERNAL)) { ?>
+                <a href="#"><i class="icon-trash"></i></a><?php } ?></div>
+        </div>
+        <div class="management hidden" data-id="<?php echo $e->id; ?>">
+            <table width="100%"><tbody>
+                <tr><td><label style="padding:0">Increment:
+                    <input class="-increment" type="text" size="4" value="<?php echo Format::htmlchars($e->increment); ?>"/>
+                    </label></td>
+                    <td><label style="padding:0">Padding Character:
+                    <input class="-padding" maxlength="1" type="text" size="4" value="<?php echo Format::htmlchars($e->padding); ?>"/>
+                    </label></td></tr>
+            </tbody></table>
+        </div>
+    </div>
+<?php } ?>
+</div>
+
+<div class="row-item hidden" id="template">
+    <i class="icon-sort-by-order"></i>
+    <div style="display:inline-block" class="name"> New Sequence </div>
+    <div style="display:inline-block;margin-right:60px" class="pull-right">
+        <span class="faded">next</span>
+        <span class="next">1</span>
+    </div>
+    <div class="button-group">
+        <div class="manage"><a href="#"><i class="icon-cog"></i></a></div>
+        <div class="delete new"><a href="#"><i class="icon-trash"></i></a></div>
+    </div>
+    <div class="management hidden" data-id="<?php echo $e->id; ?>">
+        <table width="100%"><tbody>
+            <tr><td><label style="padding:0">Increment:
+                <input class="-increment" type="text" size="4" value="1"/>
+                </label></td>
+                <td><label style="padding:0">Padding:
+                <input class="-padding" maxlength="1" type="text" size="4" value="0"/>
+                </label></td></tr>
+        </tbody></table>
+    </div>
+</div>
+
+<hr/>
+<button onclick="javascript:
+  var id = ++$.uid, base = 'seq[new-'+id+']';
+  var clone = $('.row-item#template').clone()
+    .appendTo($('#sequences'))
+    .removeClass('hidden')
+    .append($('<input>').attr({type:'hidden',class:'fname',name:base+'[name]',value:'New Sequence'}))
+    .append($('<input>').attr({type:'hidden',class:'fcurrent',name:base+'[current]',value:'1'}))
+    .append($('<input>').attr({type:'hidden',class:'fincrement',name:base+'[increment]',value:'1'}))
+    .append($('<input>').attr({type:'hidden',class:'fpadding',name:base+'[padding]',value:'0'})) ;
+  clone.find('.manage a').trigger('click');
+  return false;
+"><i class="icon-plus"></i> Add New Sequence</button>
+<div id="delete-warning" style="display:none">
+<hr>
+    <div id="msg_warning">
+    Clicking <strong>Save Changes</strong> will permanently remove the
+    deleted sequences.
+    </div>
+</div>
+<hr>
+<div>
+    <span class="buttons pull-right">
+        <input type="submit" value="Save Changes" onclick="javascript:
+$('#sequences .save a').each(function() { $(this).trigger('click'); });
+">
+    </span>
+</div>
+
+<script type="text/javascript">
+$(function() {
+  var remove = function() {
+    if (!$(this).parent().hasClass('new')) {
+      $('#delete-warning').show();
+      $(this).closest('.row-item').hide()
+        .find('input.fdeleted').val('1');
+      }
+    else
+      $(this).closest('.row-item').remove();
+    return false;
+  }, manage = function() {
+    var top = $(this).closest('.row-item');
+    top.find('.management').show(200);
+    top.find('.name').empty().append($('<input class="-name" type="text" size="40">')
+      .val(top.find('input.fname').val())
+    );
+    top.find('.current').empty().append($('<input class="-current" type="text" size="10">')
+      .val(top.find('input.fcurrent').val())
+    );
+    $(this).find('i').attr('class','icon-save');
+    $(this).parent().attr('class','save');
+    return false;
+  }, save = function() {
+    var top = $(this).closest('.row-item');
+    top.find('.management').hide(200);
+     $.each(['name', 'current'], function(i, t) {
+      var val = top.find('input.-'+t).val();
+      top.find('.'+t).empty().text(val);
+      top.find('input.f'+t).val(val);
+    });
+    $.each(['increment', 'padding'], function(i, t) {
+      top.find('input.f'+t).val(top.find('input.-'+t).val());
+    });
+    $(this).find('i').attr('class','icon-cog');
+    $(this).parent().attr('class','manage');
+    return false;
+  };
+  $(document).on('click.seq', '#sequences .manage a', manage);
+  $(document).on('click.seq', '#sequences .save a', save);
+  $(document).on('click.seq', '#sequences .delete a', remove);
+  $('.close, input:submit').click(function() {
+      $(document).die('click.seq');
+      /*
+      $('#sequences .manage a').die();
+      $('#sequences .save a').die();
+      $('#sequences .delete a').die();
+       */
+  });
+});
+</script>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index ce2728dd2adc70580a663146a02de4aa8e476920..797d7f22233c3b37616300c14f5c1e7607941f27 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -408,7 +408,7 @@ if ($results) {
                     <input class="ckb" type="checkbox" name="tids[]" value="<?php echo $row['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
                 </td>
                 <?php } ?>
-                <td align="center" title="<?php echo $row['email']; ?>" nowrap>
+                <td title="<?php echo $row['email']; ?>" nowrap>
                   <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" title="Preview Ticket"
                     href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td>
                 <td align="center" nowrap><?php echo Format::db_datetime($row['effective_date']); ?></td>
diff --git a/scp/ajax.php b/scp/ajax.php
index a192cce8cf92da76bf185a441840093133d03099..d97790120ce8c76b6e87491aabdeb7ba063ff1c9 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -158,6 +158,11 @@ $dispatcher = patterns('',
         url_delete('^(?P<id>\d+)$', 'deleteNote'),
         url_post('^attach/(?P<ext_id>\w\d+)$', 'createNote')
     )),
+    url('^/sequence/', patterns('ajax.sequence.php:SequenceAjaxAPI',
+        url_get('^(?P<id>\d+)$', 'current'),
+        url_get('^manage$', 'manage'),
+        url_post('^manage$', 'manage')
+    )),
     url_post('^/upgrader', array('ajax.upgrader.php:UpgraderAjaxAPI', 'upgrade')),
     url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI',
         url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'),
diff --git a/scp/css/scp.css b/scp/css/scp.css
index e99d9c36bce30f4c7e0b57f83e410dc0efc2ddf8..95d63bbf0efef64e7371bf25b9013dfdab2e43fe 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -1646,43 +1646,58 @@ div.selected-signature .inner {
     cursor: move;
 }
 
-.sortable-row-item {
+.sortable {
+    cursor: move;
+}
+.row-item {
     border: 1px solid rgba(0, 0, 0, 0.7);
     padding: 9px;
-    cursor: move;
     position: relative;
 }
-.sortable-row-item:hover {
+.sortable:hover {
     background: rgba(0, 0, 0, 0.1);
 }
-.sortable-row-item:active {
+.sortable:active {
     background: rgba(0, 0, 0, 0.3);
 }
-.sortable-row-item:first-child {
+.row-item:first-child {
     border-top-right-radius: 5px;
     border-top-left-radius: 5px;
 }
-.sortable-row-item:last-child {
+.row-item:last-child {
     border-bottom-right-radius: 5px;
     border-bottom-left-radius: 5px;
 }
-.sortable-row-item + .sortable-row-item {
+.row-item + .row-item {
     margin-top: -1px;
 }
 
-.sortable-row-item .delete {
+.row-item .delete {
     border-left: 1px solid rgba(0, 0, 0, 0.7);
     border-top-right-radius: inherit;
     border-bottom-right-radius: inherit;
+    width: 35px;
+}
+
+.row-item .button-group {
+    font-size: 105%;
     position: absolute;
     top: 0px;
     right: 0;
+    display: inline-block;
+}
+
+.row-item .button-group div {
     padding: 9px;
     padding-left: 12px;
-    font-size: 105%;
+    display: inline-block;
+}
+.row-item .management {
+    margin-top: 10px;
+    border-top: 1px dashed black;
 }
 
-.sortable-row-item .delete:hover {
+.row-item .delete:hover {
     background: #fc9f41; /* Old browsers */
     color: rgba(255,255,255,0.8) !important;
 }
@@ -1839,3 +1854,7 @@ table.custom-info td {
 .delete-draft:hover {
     background-color: #fc9f41 !important;
 }
+
+.hidden {
+    display: none;
+}
diff --git a/scp/js/scp.js b/scp/js/scp.js
index d79b03955150d2221fe8a3ef920c1a021db5b301..a57503809f98786e275e132923af1cf201ae6abe 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -578,6 +578,8 @@ $.orgLookup = function (url, cb) {
     });
 };
 
+$.uid = 1;
+
 //Tabs
 $(document).on('click.tab', 'ul.tabs li a', function(e) {
     e.preventDefault();