diff --git a/src/.env.example b/src/.env.example
index 0a00d8c8..6d9bccc4 100644
--- a/src/.env.example
+++ b/src/.env.example
@@ -1,103 +1,109 @@
APP_NAME=Kolab
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://127.0.0.1:8000
APP_PUBLIC_URL=
APP_DOMAIN=kolabnow.com
SUPPORT_URL=
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_DATABASE=kolabdev
DB_HOST=127.0.0.1
DB_PASSWORD=kolab
DB_PORT=3306
DB_USERNAME=kolabdev
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=file
SESSION_LIFETIME=120
2FA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube
2FA_TOTP_DIGITS=6
2FA_TOTP_INTERVAL=30
2FA_TOTP_DIGEST=sha1
IMAP_URI=ssl://127.0.0.1:993
IMAP_ADMIN_LOGIN=cyrus-admin
IMAP_ADMIN_PASSWORD=Welcome2KolabSystems
IMAP_VERIFY_HOST=false
IMAP_VERIFY_PEER=false
LDAP_BASE_DN="dc=mgmt,dc=com"
LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com"
LDAP_HOSTS=127.0.0.1
LDAP_PORT=389
LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com"
LDAP_SERVICE_BIND_PW="Welcome2KolabSystems"
LDAP_USE_SSL=false
LDAP_USE_TLS=false
# Administrative
LDAP_ADMIN_BIND_DN="cn=Directory Manager"
LDAP_ADMIN_BIND_PW="Welcome2KolabSystems"
LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com"
# Hosted (public registration)
LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com"
LDAP_HOSTED_BIND_PW="Welcome2KolabSystems"
LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com"
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
SWOOLE_HTTP_HOST=127.0.0.1
SWOOLE_HTTP_PORT=8000
PAYMENT_PROVIDER=
MOLLIE_KEY=
STRIPE_KEY=
STRIPE_PUBLIC_KEY=
STRIPE_WEBHOOK_SECRET=
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@example.com"
MAIL_FROM_NAME="Example.com"
MAIL_REPLYTO_ADDRESS=null
MAIL_REPLYTO_NAME=null
DNS_TTL=3600
DNS_SPF="v=spf1 mx -all"
DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com."
DNS_COPY_FROM=null
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_SECRET=
+COMPANY_NAME=
+COMPANY_ADDRESS=
+COMPANY_DETAILS=
+COMPANY_EMAIL=
+COMPANY_LOGO=
+
KB_ACCOUNT_DELETE=
KB_ACCOUNT_SUSPENDED=
diff --git a/src/app/Console/Development/TemplateRender.php b/src/app/Console/Development/TemplateRender.php
index 50893431..2e9f5856 100644
--- a/src/app/Console/Development/TemplateRender.php
+++ b/src/app/Console/Development/TemplateRender.php
@@ -1,37 +1,59 @@
argument('template');
- $template = str_replace('/', '\\', $template);
+ $template = str_replace("/", "\\", $template);
- $class = '\\App\\' . $template;
+ $class = "\\App\\{$template}";
- echo $class::fakeRender();
+ // Invalid template, list all templates
+ if (!class_exists($class)) {
+ $this->info("Invalid template name. Available templates:");
+
+ foreach (glob(app_path() . '/Documents/*.php') as $file) {
+ $file = basename($file, '.php');
+ $this->info("Documents/$file");
+ }
+
+ foreach (glob(app_path() . '/Mail/*.php') as $file) {
+ $file = basename($file, '.php');
+ $this->info("Mail/$file");
+ }
+
+ return 1;
+ }
+
+ $mode = 'html';
+ if (!empty($this->option('pdf'))) {
+ $mode = 'pdf';
+ }
+
+ echo $class::fakeRender($mode);
}
}
diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php
new file mode 100644
index 00000000..4fc11e4d
--- /dev/null
+++ b/src/app/Documents/Receipt.php
@@ -0,0 +1,241 @@
+wallet = $wallet;
+ $this->year = $year;
+ $this->month = $month;
+ }
+
+ /**
+ * Render the mail template with fake data
+ *
+ * @return string HTML or PDF output
+ */
+ public static function fakeRender(string $type = 'html'): string
+ {
+ $wallet = new Wallet();
+ $wallet->id = \App\Utils::uuidStr();
+ $wallet->owner = new User(['id' => 123456789]); // @phpstan-ignore-line
+
+ $receipt = new self($wallet, date('Y'), date('n'));
+
+ self::$fakeMode = true;
+
+ if ($type == 'pdf') {
+ return $receipt->pdfOutput();
+ }
+
+ return $receipt->htmlOutput();
+ }
+
+ /**
+ * Render the receipt in HTML format.
+ *
+ * @return string HTML content
+ */
+ public function htmlOutput(): string
+ {
+ return $this->build()->render();
+ }
+
+ /**
+ * Render the receipt in PDF format.
+ *
+ * @return string PDF content
+ */
+ public function pdfOutput(): string
+ {
+ // Parse ther HTML template
+ $html = $this->build()->render();
+
+ // Link fonts from public/fonts to storage/fonts so DomPdf can find them
+ if (!is_link(storage_path('fonts/Roboto-Regular.ttf'))) {
+ symlink(
+ public_path('fonts/Roboto-Regular.ttf'),
+ storage_path('fonts/Roboto-Regular.ttf')
+ );
+ symlink(
+ public_path('fonts/Roboto-Bold.ttf'),
+ storage_path('fonts/Roboto-Bold.ttf')
+ );
+ }
+
+ // Fix font and image paths
+ $html = str_replace('url(/fonts/', 'url(fonts/', $html);
+ $html = str_replace('src="/images/', 'src="images/', $html);
+
+ // TODO: The output file is about ~200KB, we could probably slim it down
+ // by using separate font files with small subset of languages when
+ // there are no Unicode characters used, e.g. only ASCII or Latin.
+
+ // Load PDF generator
+ $pdf = \PDF::loadHTML($html)->setPaper('a4', 'portrait');
+
+ return $pdf->output();
+ }
+
+ /**
+ * Build the document
+ *
+ * @return \Illuminate\View\View The template object
+ */
+ protected function build()
+ {
+ $appName = \config('app.name');
+ $start = Carbon::create($this->year, $this->month, 1, 0, 0, 0);
+ $end = $start->copy()->endOfMonth();
+
+ $month = \trans('documents.month' . intval($this->month));
+ $title = \trans('documents.receipt-title', ['year' => $this->year, 'month' => $month]);
+ $company = $this->companyData();
+
+ if (self::$fakeMode) {
+ $customer = [
+ 'id' => $this->wallet->owner->id,
+ 'wallet_id' => $this->wallet->id,
+ 'customer' => 'Freddie Krüger 7252 Westminster Lane Forest Hills, NY 11375',
+ ];
+
+ $items = collect([
+ (object) [
+ 'amount' => 1234,
+ 'updated_at' => $start->copy()->next(Carbon::MONDAY),
+ ],
+ (object) [
+ 'amount' => 10000,
+ // @phpstan-ignore-next-line
+ 'updated_at' => $start->copy()->next()->next(),
+ ],
+ (object) [
+ 'amount' => 1234,
+ // @phpstan-ignore-next-line
+ 'updated_at' => $start->copy()->next()->next()->next(Carbon::MONDAY),
+ ],
+ (object) [
+ 'amount' => 99,
+ // @phpstan-ignore-next-line
+ 'updated_at' => $start->copy()->next()->next()->next(),
+ ],
+ ]);
+ } else {
+ $customer = $this->customerData();
+
+ $items = $this->wallet->payments()
+ ->where('status', PaymentProvider::STATUS_PAID)
+ ->where('updated_at', '>=', $start)
+ ->where('updated_at', '<', $end)
+ ->where('amount', '>', 0)
+ ->orderBy('updated_at')
+ ->get();
+ }
+
+ $total = 0;
+ $items = $items->map(function ($item) use (&$total, $appName) {
+ $total += $item->amount;
+ return [
+ 'amount' => sprintf('%.2f %s', $item->amount / 100, $this->wallet->currency),
+ 'description' => \trans('documents.receipt-item-desc', ['site' => $appName]),
+ 'date' => $item->updated_at->toDateString(),
+ ];
+ });
+
+ $total = sprintf('%.2f %s', $total / 100, $this->wallet->currency);
+
+ // Load the template
+ $view = view('documents.receipt')
+ ->with([
+ 'site' => $appName,
+ 'title' => $title,
+ 'company' => $company,
+ 'customer' => $customer,
+ 'items' => $items,
+ 'total' => $total,
+ ]);
+
+ return $view;
+ }
+
+ /**
+ * Prepare customer data for the template
+ *
+ * @return array Customer data for the template
+ */
+ protected function customerData(): array
+ {
+ $user = $this->wallet->owner;
+ $name = $user->name();
+ $organization = $user->getSetting('organization');
+ $address = $user->getSetting('billing_address');
+
+ $customer = trim(($organization ?: $name) . "\n$address");
+ $customer = str_replace("\n", ' ', htmlentities($customer));
+
+ return [
+ 'id' => $this->wallet->owner->id,
+ 'wallet_id' => $this->wallet->id,
+ 'customer' => $customer,
+ ];
+ }
+
+ /**
+ * Prepare company data for the template
+ *
+ * @return array Company data for the template
+ */
+ protected function companyData(): array
+ {
+ $header = \config('app.company.name') . "\n" . \config('app.company.address');
+ $header = str_replace("\n", ' ', htmlentities($header));
+
+ $footerLineLength = 110;
+ $footer = \config('app.company.details');
+ $contact = \config('app.company.email');
+ $logo = \config('app.company.logo');
+
+ if ($contact) {
+ $length = strlen($footer) + strlen($contact) + 3;
+ $contact = htmlentities($contact);
+ $footer .= ($length > $footerLineLength ? "\n" : ' | ')
+ . sprintf('%s', $contact, $contact);
+ }
+
+ return [
+ 'logo' => $logo ? "" : '',
+ 'header' => $header,
+ 'footer' => $footer,
+ ];
+ }
+}
diff --git a/src/app/Payment.php b/src/app/Payment.php
index fc17fa12..f501decc 100644
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -1,38 +1,48 @@
'integer'
];
+ protected $fillable = [
+ 'id',
+ 'wallet_id',
+ 'amount',
+ 'description',
+ 'provider',
+ 'status',
+ 'type',
+ ];
+
/**
* The wallet to which this payment belongs.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function wallet()
{
return $this->belongsTo(
'\App\Wallet',
'wallet_id', /* local */
'id' /* remote */
);
}
}
diff --git a/src/composer.json b/src/composer.json
index 84a46ae6..6a50c33e 100644
--- a/src/composer.json
+++ b/src/composer.json
@@ -1,86 +1,87 @@
{
"name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.",
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"repositories": [
{
"type": "vcs",
"url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git"
}
],
"require": {
"php": "^7.1.3",
+ "barryvdh/laravel-dompdf": "^0.8.6",
"doctrine/dbal": "^2.9",
"fideloper/proxy": "^4.0",
"geoip2/geoip2": "^2.9",
"iatstuti/laravel-nullable-fields": "*",
"kolab/net_ldap3": "dev-master",
"laravel/framework": "6.*",
"laravel/tinker": "^1.0",
"mollie/laravel-mollie": "^2.9",
"morrislaptop/laravel-queue-clear": "^1.2",
"silviolleite/laravelpwa": "^1.0",
"spatie/laravel-translatable": "^4.2",
"spomky-labs/otphp": "~4.0.0",
"stripe/stripe-php": "^7.29",
"swooletw/laravel-swoole": "^2.6",
"torann/currency": "^1.0",
"torann/geoip": "^1.0",
"tymon/jwt-auth": "^1.0"
},
"require-dev": {
"beyondcode/laravel-dump-server": "^1.0",
"beyondcode/laravel-er-diagram-generator": "^1.3",
"filp/whoops": "^2.0",
"fzaninotto/faker": "^1.4",
"laravel/dusk": "~5.11.0",
"mockery/mockery": "^1.0",
"nunomaduro/larastan": "^0.5",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^8"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"database/seeds",
"database/factories",
"include"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
}
diff --git a/src/config/app.php b/src/config/app.php
index b7d1baf7..ac9c24cb 100644
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -1,246 +1,256 @@
env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
*/
'url' => env('APP_URL', 'http://localhost'),
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
'asset_url' => env('ASSET_URL', null),
'support_url' => env('SUPPORT_URL', null),
/*
|--------------------------------------------------------------------------
| Application Domain
|--------------------------------------------------------------------------
|
| System domain used for user signup (kolab identity)
*/
'domain' => env('APP_DOMAIN', 'domain.tld'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
+ Barryvdh\DomPDF\ServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
+ 'PDF' => Barryvdh\DomPDF\Facade::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
// Locations of knowledge base articles
'kb' => [
// An article about suspended accounts
'account_suspended' => env('KB_ACCOUNT_SUSPENDED'),
// An article about a way to delete an owned account
'account_delete' => env('KB_ACCOUNT_DELETE'),
],
+
+ 'company' => [
+ 'name' => env('COMPANY_NAME'),
+ 'address' => env('COMPANY_ADDRESS'),
+ 'details' => env('COMPANY_DETAILS'),
+ 'email' => env('COMPANY_EMAIL'),
+ 'logo' => env('COMPANY_LOGO'),
+ ],
];
diff --git a/src/config/dompdf.php b/src/config/dompdf.php
new file mode 100644
index 00000000..1fc929d0
--- /dev/null
+++ b/src/config/dompdf.php
@@ -0,0 +1,243 @@
+ false, // Throw an Exception on warnings from dompdf
+ 'orientation' => 'portrait',
+ 'defines' => [
+ /**
+ * The location of the DOMPDF font directory
+ *
+ * The location of the directory where DOMPDF will store fonts and font metrics
+ * Note: This directory must exist and be writable by the webserver process.
+ * *Please note the trailing slash.*
+ *
+ * Notes regarding fonts:
+ * Additional .afm font metrics can be added by executing load_font.php from command line.
+ *
+ * Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
+ * be embedded in the pdf file or the PDF may not display correctly. This can significantly
+ * increase file size unless font subsetting is enabled. Before embedding a font please
+ * review your rights under the font license.
+ *
+ * Any font specification in the source HTML is translated to the closest font available
+ * in the font directory.
+ *
+ * The pdf standard "Base 14 fonts" are:
+ * Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
+ * Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
+ * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
+ * Symbol, ZapfDingbats.
+ */
+ "font_dir" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
+
+ /**
+ * The location of the DOMPDF font cache directory
+ *
+ * This directory contains the cached font metrics for the fonts used by DOMPDF.
+ * This directory can be the same as DOMPDF_FONT_DIR
+ *
+ * Note: This directory must exist and be writable by the webserver process.
+ */
+ "font_cache" => storage_path('fonts/'),
+
+ /**
+ * The location of a temporary directory.
+ *
+ * The directory specified must be writeable by the webserver process.
+ * The temporary directory is required to download remote images and when
+ * using the PFDLib back end.
+ */
+ "temp_dir" => sys_get_temp_dir(),
+
+ /**
+ * ==== IMPORTANT ====
+ *
+ * dompdf's "chroot": Prevents dompdf from accessing system files or other
+ * files on the webserver. All local files opened by dompdf must be in a
+ * subdirectory of this directory. DO NOT set it to '/' since this could
+ * allow an attacker to use dompdf to read any files on the server. This
+ * should be an absolute path.
+ * This is only checked on command line call by dompdf.php, but not by
+ * direct class use like:
+ * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
+ */
+ "chroot" => realpath(base_path()),
+
+ /**
+ * Whether to enable font subsetting or not.
+ */
+ "enable_font_subsetting" => false,
+
+ /**
+ * The PDF rendering backend to use
+ *
+ * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
+ * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
+ * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
+ * Canvas_Factory} ultimately determines which rendering class to instantiate
+ * based on this setting.
+ *
+ * Both PDFLib & CPDF rendering backends provide sufficient rendering
+ * capabilities for dompdf, however additional features (e.g. object,
+ * image and font support, etc.) differ between backends. Please see
+ * {@link PDFLib_Adapter} for more information on the PDFLib backend
+ * and {@link CPDF_Adapter} and lib/class.pdf.php for more information
+ * on CPDF. Also see the documentation for each backend at the links
+ * below.
+ *
+ * The GD rendering backend is a little different than PDFLib and
+ * CPDF. Several features of CPDF and PDFLib are not supported or do
+ * not make any sense when creating image files. For example,
+ * multiple pages are not supported, nor are PDF 'objects'. Have a
+ * look at {@link GD_Adapter} for more information. GD support is
+ * experimental, so use it at your own risk.
+ *
+ * @link http://www.pdflib.com
+ * @link http://www.ros.co.nz/pdf
+ * @link http://www.php.net/image
+ */
+ "pdf_backend" => "CPDF",
+
+ /**
+ * PDFlib license key
+ *
+ * If you are using a licensed, commercial version of PDFlib, specify
+ * your license key here. If you are using PDFlib-Lite or are evaluating
+ * the commercial version of PDFlib, comment out this setting.
+ *
+ * @link http://www.pdflib.com
+ *
+ * If pdflib present in web server and auto or selected explicitely above,
+ * a real license code must exist!
+ */
+ //"DOMPDF_PDFLIB_LICENSE" => "your license key here",
+
+ /**
+ * html target media view which should be rendered into pdf.
+ * List of types and parsing rules for future extensions:
+ * http://www.w3.org/TR/REC-html40/types.html
+ * screen, tty, tv, projection, handheld, print, braille, aural, all
+ * Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
+ * Note, even though the generated pdf file is intended for print output,
+ * the desired content might be different (e.g. screen or projection view of html file).
+ * Therefore allow specification of content here.
+ */
+ "default_media_type" => "print",
+
+ /**
+ * The default paper size.
+ *
+ * North America standard is "letter"; other countries generally "a4"
+ *
+ * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
+ */
+ "default_paper_size" => "a4",
+
+ /**
+ * The default font family
+ *
+ * Used if no suitable fonts can be found. This must exist in the font folder.
+ * @var string
+ */
+ "default_font" => "serif",
+
+ /**
+ * Image DPI setting
+ *
+ * This setting determines the default DPI setting for images and fonts. The
+ * DPI may be overridden for inline images by explictly setting the
+ * image's width & height style attributes (i.e. if the image's native
+ * width is 600 pixels and you specify the image's width as 72 points,
+ * the image will have a DPI of 600 in the rendered PDF. The DPI of
+ * background images can not be overridden and is controlled entirely
+ * via this parameter.
+ *
+ * For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
+ * If a size in html is given as px (or without unit as image size),
+ * this tells the corresponding size in pt.
+ * This adjusts the relative sizes to be similar to the rendering of the
+ * html page in a reference browser.
+ *
+ * In pdf, always 1 pt = 1/72 inch
+ *
+ * Rendering resolution of various browsers in px per inch:
+ * Windows Firefox and Internet Explorer:
+ * SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
+ * Linux Firefox:
+ * about:config *resolution: Default:96
+ * (xorg screen dimension in mm and Desktop font dpi settings are ignored)
+ *
+ * Take care about extra font/image zoom factor of browser.
+ *
+ * In images, size in pixel attribute, img css style, are overriding
+ * the real image dimension in px for rendering.
+ *
+ * @var int
+ */
+ "dpi" => 96,
+
+ /**
+ * Enable inline PHP
+ *
+ * If this setting is set to true then DOMPDF will automatically evaluate
+ * inline PHP contained within tags.
+ *
+ * Enabling this for documents you do not trust (e.g. arbitrary remote html
+ * pages) is a security risk. Set this option to false if you wish to process
+ * untrusted documents.
+ *
+ * @var bool
+ */
+ "enable_php" => false,
+
+ /**
+ * Enable inline Javascript
+ *
+ * If this setting is set to true then DOMPDF will automatically insert
+ * JavaScript code contained within tags.
+ *
+ * @var bool
+ */
+ "enable_javascript" => false,
+
+ /**
+ * Enable remote file access
+ *
+ * If this setting is set to true, DOMPDF will access remote sites for
+ * images and CSS files as required.
+ * This is required for part of test case www/test/image_variants.html through www/examples.php
+ *
+ * Attention!
+ * This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
+ * allowing remote access to dompdf.php or on allowing remote html code to be passed to
+ * $dompdf = new DOMPDF(, $dompdf->load_html(...,
+ * This allows anonymous users to download legally doubtful internet content which on
+ * tracing back appears to being downloaded by your server, or allows malicious php code
+ * in remote html pages to be executed by your server with your account privileges.
+ *
+ * @var bool
+ */
+ "enable_remote" => false,
+
+ /**
+ * A ratio applied to the fonts height to be more like browsers' line height
+ */
+ "font_height_ratio" => 1.1,
+
+ /**
+ * Use the more-than-experimental HTML5 Lib parser
+ */
+ "enable_html5_parser" => false,
+ ],
+
+];
diff --git a/src/public/fonts/Roboto-Bold.ttf b/src/public/fonts/Roboto-Bold.ttf
new file mode 100644
index 00000000..d998cf5b
Binary files /dev/null and b/src/public/fonts/Roboto-Bold.ttf differ
diff --git a/src/public/fonts/Roboto-Regular.ttf b/src/public/fonts/Roboto-Regular.ttf
new file mode 100644
index 00000000..2b6392ff
Binary files /dev/null and b/src/public/fonts/Roboto-Regular.ttf differ
diff --git a/src/public/images/logo_print.png b/src/public/images/logo_print.png
deleted file mode 100644
index bd0ff6d8..00000000
Binary files a/src/public/images/logo_print.png and /dev/null differ
diff --git a/src/public/images/logo_print.svg b/src/public/images/logo_print.svg
new file mode 100644
index 00000000..8acf94a7
--- /dev/null
+++ b/src/public/images/logo_print.svg
@@ -0,0 +1,32 @@
+
+
diff --git a/src/public/mix-manifest.json b/src/public/mix-manifest.json
index df1588cc..f452e347 100644
--- a/src/public/mix-manifest.json
+++ b/src/public/mix-manifest.json
@@ -1,5 +1,6 @@
{
"/js/admin.js": "/js/admin.js",
"/js/user.js": "/js/user.js",
- "/css/app.css": "/css/app.css"
+ "/css/app.css": "/css/app.css",
+ "/css/document.css": "/css/document.css"
}
diff --git a/src/resources/lang/en/documents.php b/src/resources/lang/en/documents.php
new file mode 100644
index 00000000..d7a444d5
--- /dev/null
+++ b/src/resources/lang/en/documents.php
@@ -0,0 +1,35 @@
+ "Account ID",
+ 'amount' => "Amount",
+ 'customer-no' => "Customer No.",
+ 'date' => "Date",
+ 'description' => "Description",
+ 'period' => "Period",
+ 'total' => "Total",
+
+ 'month1' => "January",
+ 'month2' => "February",
+ 'month3' => "March",
+ 'month4' => "April",
+ 'month5' => "May",
+ 'month6' => "June",
+ 'month7' => "July",
+ 'month8' => "August",
+ 'month9' => "September",
+ 'month10' => "October",
+ 'month11' => "November",
+ 'month12' => "December",
+
+ 'receipt-title' => "Receipt for :month :year",
+ 'receipt-item-desc' => ":site Services",
+
+];
diff --git a/src/resources/sass/document.scss b/src/resources/sass/document.scss
new file mode 100644
index 00000000..dbd2c5ac
--- /dev/null
+++ b/src/resources/sass/document.scss
@@ -0,0 +1,112 @@
+// Variables
+@import 'variables';
+
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: normal;
+ src: url('../fonts/Roboto-Regular.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 700;
+ src: url('../fonts/Roboto-Bold.ttf') format('truetype');
+}
+
+body {
+ font-family: Roboto, sans-serif;
+ margin: 20pt;
+}
+
+a {
+ text-decoration: none;
+ color: $main-color;
+}
+
+h1 {
+ text-align: center;
+ margin: 30pt;
+ font-size: 20pt;
+}
+
+table {
+ width: 100%;
+
+ &.content {
+ border-spacing: initial;
+
+ th {
+ padding: 5px;
+ background-color: #f4f4f4;
+ border-top: 1px solid #eee;
+ }
+
+ td {
+ padding: 5px;
+ border-bottom: 1px solid #eee;
+ }
+ }
+
+ &.head {
+ margin-bottom: 1em;
+ }
+
+
+ td.idents {
+ white-space: nowrap;
+ width: 1%;
+ text-align: right;
+ vertical-align: top;
+ font-size: 10pt;
+ }
+
+ td.logo {
+ width: 1%;
+ }
+
+ td.description {
+ width: 98%;
+ }
+
+ tr.total {
+ background-color: #f4f4f4;
+ }
+}
+
+#footer {
+ font-size: 10pt;
+ color: gray;
+ text-align: center;
+ padding-top: 5pt;
+ position: absolute;
+ margin: 20pt;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ @media print {
+ margin-bottom: 0;
+ }
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.gray {
+ color: gray;
+}
+
+.align-right {
+ text-align: right;
+}
+
+.align-left {
+ text-align: left;
+}
+
+.amount {
+ white-space: nowrap;
+}
diff --git a/src/resources/views/documents/receipt.blade.php b/src/resources/views/documents/receipt.blade.php
new file mode 100644
index 00000000..f920df64
--- /dev/null
+++ b/src/resources/views/documents/receipt.blade.php
@@ -0,0 +1,56 @@
+
+
+