diff --git a/composer.json b/composer.json
index a87056dda7900da79e668c650d73bbf149b1fba8..8cbc008b5fa68441de63a39a6c94e803046b0730 100644
--- a/composer.json
+++ b/composer.json
@@ -1,28 +1,23 @@
 {
   "name": "quiqqer/coupons",
-  "type": "quiqqer-plugin",
+  "type": "quiqqer-module",
   "description": "Coupons for QUIQQER",
-  "version": "dev-master",
-  "license": "",
+  "version": "dev-dev",
+  "license": "GPL-3.0+",
   "authors": [
     {
-      "name": "Henning Leutz",
-      "email": "leutz@pcsg.de",
+      "name": "Patrick Müller",
+      "email": "support@pcsg.de",
       "homepage": "http://www.pcsg.de",
       "role": "Developer"
     }
   ],
   "support": {
-    "email": "support@pcsg.de",
-    "url": "http://www.pcsg.de"
-  },
-  "require": {
-    "php": ">=5.3",
-    "quiqqer/quiqqer": "*@dev"
+    "email": "support@pcsg.de"
   },
   "autoload": {
-    "psr-0": {
-      "Hen": "src/"
+    "psr-4": {
+      "QUI\\ERP\\Coupons\\": "src/QUI/ERP/Coupons"
     }
   }
 }
\ No newline at end of file
diff --git a/locale.xml b/locale.xml
new file mode 100644
index 0000000000000000000000000000000000000000..297030e27a5efe21fba7c97c9d9c527e535b2c2b
--- /dev/null
+++ b/locale.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<locales>
+    <groups name="quiqqer/coupons" datatype="php,js">
+        <locale name="package.title">
+            <de><![CDATA[QUIQQER ERP - Coupons]]></de>
+            <en><![CDATA[QUIQQER ERP - Coupons]]></en>
+        </locale>
+        <locale name="package.description">
+            <de><![CDATA[Verwaltung von Coupon-Codes für die Verwendung im QUIQQER ERP Shop]]></de>
+            <en><![CDATA[Management of coupon codes for usage in the QUIQQER ERP Shop]]></en>
+        </locale>
+    </groups>
+</locales>
diff --git a/package.xml b/package.xml
new file mode 100644
index 0000000000000000000000000000000000000000..25c2f4915d85e28b49fc1703aa622d911d93b2b1
--- /dev/null
+++ b/package.xml
@@ -0,0 +1,25 @@
+<quiqqer>
+    <package>
+        <title>
+            <locale group="quiqqer/coupons" var="package.title"/>
+        </title>
+
+        <description>
+           <locale group="quiqqer/coupons" var="package.description"/>
+        </description>
+
+        <support>
+            <email><![CDATA[support@pcsg.de]]></email>
+            <forum><![CDATA[https://community.quiqqer.com]]></forum>
+            <source><![CDATA[https://dev.quiqqer.com/quiqqer/coupons]]></source>
+            <issues><![CDATA[https://dev.quiqqer.com/quiqqer/coupons/issues]]></issues>
+            <wiki><![CDATA[https://dev.quiqqer.com/quiqqer/coupons/wikis/home]]></wiki>
+        </support>
+
+        <copyright>
+            <name><![CDATA[PCSG - Computer & Internet Service OHG]]></name>
+            <license><![CDATA[GPL-3.0+]]></license>
+        </copyright>
+
+    </package>
+</quiqqer>
\ No newline at end of file
diff --git a/src/QUI/ERP/Coupons/CouponCode.php b/src/QUI/ERP/Coupons/CouponCode.php
new file mode 100644
index 0000000000000000000000000000000000000000..4296cb05d59e916f345b5f641db36f3da5e5b581
--- /dev/null
+++ b/src/QUI/ERP/Coupons/CouponCode.php
@@ -0,0 +1,437 @@
+<?php
+
+namespace QUI\ERP\Coupons;
+
+use QUI;
+use QUI\InviteCode\Exception\InviteCodeException;
+use QUI\InviteCode\Exception\InviteCodeMailException;
+use QUI\Permissions\Permission;
+
+/**
+ * Class InviteCode
+ */
+class CouponCode
+{
+    /**
+     * InviteCode ID
+     *
+     * @var int
+     */
+    protected $id;
+
+    /**
+     * Actual code
+     *
+     * @var string
+     */
+    protected $code;
+
+    /**
+     * User that is assigned to this Code
+     *
+     * @var QUI\Users\User
+     */
+    protected $User = null;
+
+    /**
+     * Email address that is asasigned to this Code
+     *
+     * @var string
+     */
+    protected $email = null;
+
+    /**
+     * Creation Date
+     *
+     * @var \DateTime
+     */
+    protected $CreateDate;
+
+    /**
+     * Use Date
+     *
+     * @var \DateTime
+     */
+    protected $UseDate = null;
+
+    /**
+     * Date until the Invite code is valid
+     *
+     * @var \DateTime
+     */
+    protected $ValidUntilDate = null;
+
+    /**
+     * InviteCode title
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * Flag if mail has been sent
+     *
+     * @var bool
+     */
+    protected $mailSent;
+
+    /**
+     * @var bool
+     */
+    protected $valid = true;
+
+    /**
+     * InviteCode constructor.
+     *
+     * @param int $id - Invite Code ID
+     * @throws InviteCodeException
+     */
+    public function __construct($id)
+    {
+        $result = QUI::getDataBase()->fetch(array(
+            'from'  => Handler::getTable(),
+            'where' => array(
+                'id' => $id
+            )
+        ));
+
+        if (empty($result)) {
+            throw new InviteCodeException(array(
+                'quiqqer/invitecode',
+                'exception.invitecode.not_found',
+                array(
+                    'id' => $id
+                )
+            ), 404);
+        }
+
+        $data = current($result);
+
+        $this->id       = (int)$data['id'];
+        $this->code     = $data['code'];
+        $this->title    = $data['title'];
+        $this->mailSent = boolval($data['mailSent']);
+
+        if (!empty($data['userId'])) {
+            try {
+                $this->User = QUI::getUsers()->get($data['userId']);
+            } catch (\Exception $Exception) {
+//                QUI\System\Log::addWarning(
+//                    'Could not find User #' . $data['userId'] . ' for Invite Code #' . $this->id . '.'
+//                );
+//
+//                QUI\System\Log::writeException($Exception);
+            }
+        }
+
+        if (!empty($data['email'])) {
+            $this->email = $data['email'];
+        }
+
+        $this->CreateDate = new \DateTime($data['createDate']);
+
+        if (!empty($data['useDate'])) {
+            $this->UseDate = new \DateTime($data['useDate']);
+        }
+
+        if (!empty($data['validUntilDate'])) {
+            $this->ValidUntilDate = new \DateTime($data['validUntilDate']);
+
+            $Now = new \DateTime();
+
+            if (!$this->isRedeemed() && $Now > $this->ValidUntilDate) {
+                $this->valid = false;
+            }
+        }
+    }
+
+    /**
+     * @return int
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * @return string
+     */
+    public function getCode()
+    {
+        return $this->code;
+    }
+
+    /**
+     * @return QUI\Users\User|null
+     */
+    public function getUser()
+    {
+        return $this->User;
+    }
+
+    /**
+     * @param QUI\Users\User $User
+     */
+    public function setUser(QUI\Users\User $User)
+    {
+        $this->User = $User;
+    }
+
+    /**
+     * @return string
+     */
+    public function getEmail()
+    {
+        return $this->email;
+    }
+
+    /**
+     * @param string $email
+     */
+    public function setEmail($email)
+    {
+        $this->email = $email;
+    }
+
+    /**
+     * @return \DateTime
+     */
+    public function getCreateDate()
+    {
+        return $this->CreateDate;
+    }
+
+    /**
+     * @return \DateTime|null
+     */
+    public function getUseDate()
+    {
+        return $this->UseDate;
+    }
+
+    /**
+     * @return \DateTime|null
+     */
+    public function getValidUntilDate()
+    {
+        return $this->ValidUntilDate;
+    }
+
+    /**
+     * @return string
+     */
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isMailSent()
+    {
+        return $this->mailSent;
+    }
+
+    /**
+     * Redeems this InviteCode
+     *
+     * Hint: This invalidates the code for future use
+     *
+     * @param QUI\Users\User $User
+     * @return void
+     * @throws InviteCodeException
+     */
+    public function redeem($User)
+    {
+        if ($this->isRedeemed()) {
+            throw new InviteCodeException(array(
+                'quiqqer/invitecode',
+                'exception.invitecode.already_used'
+            ));
+        }
+
+        if (!$this->isValid()) {
+            throw new InviteCodeException(array(
+                'quiqqer/invitecode',
+                'exception.invitecode.no_longer_valid'
+            ));
+        }
+
+        $Now = new \DateTime();
+
+        QUI::getDataBase()->update(
+            Handler::getTable(),
+            array(
+                'useDate' => $Now->format('Y-m-d H:i:s'),
+                'userId'  => $User->getUniqueId()
+            ),
+            array(
+                'id' => $this->id
+            )
+        );
+
+        $this->UseDate = $Now;
+    }
+
+    /**
+     * Check if this InviteCode is still valid
+     *
+     * @return bool
+     */
+    public function isValid()
+    {
+        return $this->valid;
+    }
+
+    /**
+     * Checks if an InviteCode is redeemed
+     *
+     * @return bool
+     */
+    public function isRedeemed()
+    {
+        return !is_null($this->getUseDate());
+    }
+
+    /**
+     * Send this Invite Code via E-Mail
+     *
+     * @param bool $resend (optional) - Send again if already send [default: false]
+     * @return void
+     * @throws InviteCodeMailException
+     */
+    public function sendViaMail($resend = false)
+    {
+        if (!$resend && $this->isMailSent()) {
+            return;
+        }
+
+        $email = $this->getEmail();
+
+        if (empty($email)) {
+            throw new InviteCodeMailException(array(
+                'quiqqer/invitecode',
+                'exception.invitecode.no_email'
+            ));
+        }
+
+        $Mailer = new QUI\Mail\Mailer();
+
+        $Mailer->addRecipient($email);
+
+        $Engine         = QUI::getTemplateManager()->getEngine();
+        $dir            = QUI::getPackage('quiqqer/invitecode')->getDir() . 'templates/';
+        $validUntilDate = $this->getValidUntilDate();
+        $data           = array(
+            'code'           => $this->getCode(),
+            'validUntilDate' => empty($validUntilDate) ? '' : QUI::getLocale()->formatDate($validUntilDate->getTimestamp())
+        );
+
+        $RegistrationSite = Handler::getRegistrationSite();
+
+        if (empty($RegistrationSite)) {
+            throw new InviteCodeMailException(array(
+                'quiqqer/invitecode',
+                'exception.invitecode.no_registration_site'
+            ));
+        }
+
+        $data['registrationUrl'] = $RegistrationSite->getUrlRewrittenWithHost();
+        $data['email']           = $email;
+        $data['inviteCode']      = $this->getCode();
+
+        if (empty($validUntilDate)) {
+            $translationVar = 'mail.invite_code.body';
+        } else {
+            $translationVar = 'mail.invite_code.body_date';
+        }
+
+        $Engine->assign(array(
+            'body' => QUI::getLocale()->get(
+                'quiqqer/invitecode',
+                $translationVar,
+                $data
+            )
+        ));
+
+        $Mailer->setSubject(QUI::getLocale()->get(
+            'quiqqer/invitecode',
+            'mail.invite_code.subject'
+        ));
+
+        $Mailer->setBody($Engine->fetch($dir . 'mail.invite_code.html'));
+        $Mailer->send();
+
+        // update internal flag
+        QUI::getDataBase()->update(
+            Handler::getTable(),
+            array(
+                'mailSent' => 1
+            ),
+            array(
+                'id' => $this->getId()
+            )
+        );
+    }
+
+    /**
+     * Permanently delete this InviteCode
+     *
+     * @return void
+     */
+    public function delete()
+    {
+        Permission::checkPermission(Handler::PERMISSION_DELETE);
+
+        QUI::getDataBase()->delete(
+            Handler::getTable(),
+            array(
+                'id' => $this->id
+            )
+        );
+    }
+
+    /**
+     * Get InviteCode attributes as array
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $data = array(
+            'id'             => $this->getId(),
+            'code'           => $this->getCode(),
+            'userId'         => false,
+            'username'       => false,
+            'email'          => $this->getEmail() ?: false,
+            'createDate'     => $this->getCreateDate()->format('Y-m-d H:i:s'),
+            'useDate'        => false,
+            'validUntilDate' => false,
+            'title'          => $this->getTitle() ?: false,
+            'mailSent'       => $this->isMailSent(),
+            'valid'          => $this->isValid()
+        );
+
+        $User = $this->getUser();
+
+        if ($User) {
+            $data['userId']   = $User->getId();
+            $data['username'] = $User->getName();
+        }
+
+        $UseDate = $this->getUseDate();
+
+        if ($UseDate) {
+            $data['useDate'] = $UseDate->format('Y-m-d H:i:s');
+        }
+
+        $ValidUntilDate = $this->getValidUntilDate();
+
+        if ($ValidUntilDate) {
+            $data['validUntilDate'] = $ValidUntilDate->format('Y-m-d H:i:s');
+        }
+
+        return $data;
+    }
+}
diff --git a/src/QUI/ERP/Coupons/Handler.php b/src/QUI/ERP/Coupons/Handler.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2b97662641239071157e8a99018c4a73717cffa
--- /dev/null
+++ b/src/QUI/ERP/Coupons/Handler.php
@@ -0,0 +1,364 @@
+<?php
+
+namespace QUI\ERP\Coupons;
+
+use QUI;
+use QUI\Utils\Grid;
+use QUI\Utils\Security\Orthos;
+use QUI\InviteCode\Exception\InviteCodeException;
+
+/**
+ * Class Handler
+ *
+ * Main CouponCode Code handler
+ */
+class Handler
+{
+    /**
+     * Permissions
+     */
+    const PERMISSION_VIEW      = 'quiqqer.invitecode.view';
+    const PERMISSION_CREATE    = 'quiqqer.invitecode.create';
+    const PERMISSION_SEND_MAIL = 'quiqqer.invitecode.send_mail';
+    const PERMISSION_DELETE    = 'quiqqer.invitecode.delete';
+
+    /**
+     * InviteCode runtime cache
+     *
+     * @var InviteCode[]
+     */
+    protected static $inviteCodes = array();
+
+    /**
+     * Get InviteCode
+     *
+     * @param int $id
+     * @return InviteCode
+     */
+    public static function getInviteCode($id)
+    {
+        if (isset(self::$inviteCodes[$id])) {
+            return self::$inviteCodes[$id];
+        }
+
+        self::$inviteCodes[$id] = new InviteCode($id);
+
+        return self::$inviteCodes[$id];
+    }
+
+    /**
+     * Get InviteCode by its actual code
+     *
+     * @param string $code
+     * @return InviteCode
+     *
+     * @throws InviteCodeException
+     */
+    public static function getInviteCodeByCode($code)
+    {
+        $result = QUI::getDataBase()->fetch(array(
+            'select' => array(
+                'id'
+            ),
+            'from'   => self::getTable(),
+            'where'  => array(
+                'code' => $code
+            ),
+            'limit'  => 1
+        ));
+
+        if (empty($result)) {
+            throw new InviteCodeException(array(
+                'quiqqer/invitecode',
+                'exception.handler.code_not_found',
+                array(
+                    'code' => $code
+                )
+            ), 404);
+        }
+
+        return self::getInviteCode($result[0]['id']);
+    }
+
+    /**
+     * Create new InviteCode
+     *
+     * @param array $data
+     * @return InviteCode
+     *
+     * @throws \Exception
+     */
+    public static function createInviteCode($data)
+    {
+        $Now = new \DateTime();
+
+        $inviteCode = array(
+            'title'      => empty($data['title']) ? '' : $data['title'],
+            'createDate' => $Now->format('Y-m-d H:i:s'),
+            'code'       => CodeGenerator::generate()
+        );
+
+        if (!empty($data['validUntil'])) {
+            $ValidUntil                   = new \DateTime($data['validUntil']);
+            $inviteCode['validUntilDate'] = $ValidUntil->format('Y-m-d H:i:s');
+        }
+
+        if (!empty($data['email'])) {
+            try {
+                QUI::getUsers()->getUserByMail($data['email']);
+
+                throw new InviteCodeException(array(
+                    'quiqqer/invitecode',
+                    'exception.handler.user_already_exists',
+                    array(
+                        'email' => $data['email']
+                    )
+                ));
+            } catch (QUI\Users\Exception $Exception) {
+                if ($Exception->getCode() !== 404) {
+                    throw $Exception;
+                }
+            }
+
+            $inviteCode['email'] = trim($data['email']);
+        }
+
+        QUI::getDataBase()->insert(
+            self::getTable(),
+            $inviteCode
+        );
+
+        return self::getInviteCode(QUI::getPDO()->lastInsertId());
+    }
+
+    /**
+     * Search InviteCodes
+     *
+     * @param array $searchParams
+     * @param bool $countOnly (optional) - get result count only [default: false]
+     * @return InviteCode[]|int
+     */
+    public static function search($searchParams, $countOnly = false)
+    {
+        $inviteCodes = array();
+        $Grid        = new Grid($searchParams);
+        $gridParams  = $Grid->parseDBParams($searchParams);
+
+        $binds = array();
+        $where = array();
+
+        if ($countOnly) {
+            $sql = "SELECT COUNT(*)";
+        } else {
+            $sql = "SELECT id";
+        }
+
+        $sql .= " FROM `" . self::getTable() . "`";
+
+        if (!empty($searchParams['search'])) {
+            $searchColumns = array(
+                'id',
+                'code',
+                'email'
+            );
+
+            $whereOr = array();
+
+            foreach ($searchColumns as $searchColumn) {
+                $whereOr[] = '`' . $searchColumn . '` LIKE :search';
+            }
+
+            if (!empty($whereOr)) {
+                $where[] = '(' . implode(' OR ', $whereOr) . ')';
+
+                $binds['search'] = array(
+                    'value' => '%' . $searchParams['search'] . '%',
+                    'type'  => \PDO::PARAM_STR
+                );
+            }
+        }
+
+        // build WHERE query string
+        if (!empty($where)) {
+            $sql .= " WHERE " . implode(" AND ", $where);
+        }
+
+        // ORDER
+        if (!empty($searchParams['sortOn'])
+        ) {
+            $sortOn = Orthos::clear($searchParams['sortOn']);
+            $order  = "ORDER BY " . $sortOn;
+
+            if (isset($searchParams['sortBy']) &&
+                !empty($searchParams['sortBy'])
+            ) {
+                $order .= " " . Orthos::clear($searchParams['sortBy']);
+            } else {
+                $order .= " ASC";
+            }
+
+            $sql .= " " . $order;
+        } else {
+            $sql .= " ORDER BY id DESC";
+        }
+
+        // LIMIT
+        if (!empty($gridParams['limit'])
+            && !$countOnly
+        ) {
+            $sql .= " LIMIT " . $gridParams['limit'];
+        } else {
+            if (!$countOnly) {
+                $sql .= " LIMIT " . (int)20;
+            }
+        }
+
+        $Stmt = QUI::getPDO()->prepare($sql);
+
+        // bind search values
+        foreach ($binds as $var => $bind) {
+            $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']);
+        }
+
+        try {
+            $Stmt->execute();
+            $result = $Stmt->fetchAll(\PDO::FETCH_ASSOC);
+        } catch (\Exception $Exception) {
+            QUI\System\Log::addError(
+                self::class . ' :: search() -> ' . $Exception->getMessage()
+            );
+
+            return array();
+        }
+
+        if ($countOnly) {
+            return (int)current(current($result));
+        }
+
+        foreach ($result as $row) {
+            $inviteCodes[] = self::getInviteCode($row['id']);
+        }
+
+        return $inviteCodes;
+    }
+
+    /**
+     * Check if an invite code already eixsts
+     *
+     * @param string $code
+     * @return bool
+     */
+    public static function existsCode($code)
+    {
+        $result = QUI::getDataBase()->fetch(array(
+            'select' => 'id',
+            'from'   => self::getTable(),
+            'where'  => array(
+                'code' => $code
+            ),
+            'limit'  => 1
+        ));
+
+        return !empty($result);
+    }
+
+    /**
+     * Get Registration site
+     *
+     * @return QUI\Projects\Site|false
+     */
+    public static function getRegistrationSite()
+    {
+        $Conf    = QUI::getPackage('quiqqer/invitecode')->getConfig();
+        $regSite = $Conf->get('settings', 'registrationSite');
+
+        if (empty($regSite)) {
+            return false;
+        }
+
+        try {
+            return QUI\Projects\Site\Utils::getSiteByLink($regSite);
+        } catch (\Exception $Exception) {
+            return false;
+        }
+    }
+
+    /**
+     * Deletes all InviteCodes that are expired
+     *
+     * @param int $days (optional) - Delete expired Codes that are older than X days [default: delete all]
+     * @return void
+     *
+     * @throws \Exception
+     */
+    public static function deleteExpiredInviteCodes($days = null)
+    {
+        $Now   = new \DateTime();
+        $where = array(
+            'validUntilDate' => array(
+                'type'  => '<=',
+                'value' => $Now->format('Y-m-d H:i:s')
+            )
+        );
+
+        if (!is_null($days)) {
+            $days    = (int)$days;
+            $OldDate = new \DateTime();
+            $OldDate->sub(new \DateInterval('P' . $days . 'D'));
+
+            $where['validUntilDate'] = array(
+                'type'  => '<=',
+                'value' => $OldDate->format('Y-m-d H:i:s')
+            );
+        }
+
+        QUI::getDataBase()->delete(
+            self::getTable(),
+            $where
+        );
+    }
+
+    /**
+     * Deletes all InviteCodes that have been redeemed
+     *
+     * @param int $days (optional) - Delete redeemed Codes that are older than X days [default: delete all]
+     * @return void
+     *
+     * @throws \Exception
+     */
+    public static function deleteRedeemedInviteCodes($days = null)
+    {
+        $where = array(
+            'useDate' => array(
+                'type'  => 'NOT',
+                'value' => null
+            )
+        );
+
+        if (!is_null($days)) {
+            $days    = (int)$days;
+            $OldDate = new \DateTime();
+            $OldDate->sub(new \DateInterval('P' . $days . 'D'));
+
+            $where['useDate'] = array(
+                'type'  => '<=',
+                'value' => $OldDate->format('Y-m-d H:i:s')
+            );
+        }
+
+        QUI::getDataBase()->delete(
+            self::getTable(),
+            $where
+        );
+    }
+
+    /**
+     * Get InviteCode table
+     *
+     * @return string
+     */
+    public static function getTable()
+    {
+        return 'quiqqer_invitecodes';
+    }
+}