.
diff --git a/app/Console/Commands/BillReminder.php b/app/Console/Commands/BillReminder.php
new file mode 100755
index 0000000..1df9e6a
--- /dev/null
+++ b/app/Console/Commands/BillReminder.php
@@ -0,0 +1,93 @@
+ $company->id]);
+
+ // Override settings and currencies
+ Overrider::load('settings');
+ Overrider::load('currencies');
+
+ $company->setSettings();
+
+ // Don't send reminders if disabled
+ if (!$company->send_bill_reminder) {
+ continue;
+ }
+
+ $days = explode(',', $company->schedule_bill_days);
+
+ foreach ($days as $day) {
+ $day = (int) trim($day);
+
+ $this->remind($day, $company);
+ }
+ }
+
+ // Unset company_id
+ session()->forget('company_id');
+ }
+
+ protected function remind($day, $company)
+ {
+ // Get due date
+ $date = Date::today()->addDays($day)->toDateString();
+
+ // Get upcoming bills
+ $bills = Bill::with('vendor')->accrued()->notPaid()->due($date)->get();
+
+ foreach ($bills as $bill) {
+ // Notify all users assigned to this company
+ foreach ($company->users as $user) {
+ if (!$user->can('read-notifications')) {
+ continue;
+ }
+
+ $user->notify(new Notification($bill));
+ }
+ }
+ }
+}
diff --git a/app/Console/Commands/CompanySeed.php b/app/Console/Commands/CompanySeed.php
new file mode 100755
index 0000000..200ef4e
--- /dev/null
+++ b/app/Console/Commands/CompanySeed.php
@@ -0,0 +1,47 @@
+laravel->make('CompanySeeder');
+
+ $seeder = $class->setContainer($this->laravel)->setCommand($this);
+
+ $seeder->__invoke();
+ }
+
+}
diff --git a/app/Console/Commands/Install.php b/app/Console/Commands/Install.php
new file mode 100755
index 0000000..9f7de98
--- /dev/null
+++ b/app/Console/Commands/Install.php
@@ -0,0 +1,232 @@
+checkOptions();
+ if (!empty($missingOptions) && $this->option(self::OPT_NO_INTERACTION)) {
+ $this->line('❌ Some options are missing and --no-interaction is present. Please run the following command for more informations :');
+ $this->line('❌ php artisan help install');
+ $this->line('❌ Missing options are : ' . join(', ', $missingOptions));
+
+ return self::CMD_ERROR;
+ }
+
+ $this->line('Setting locale ' . $this->locale);
+ Session::put(self::OPT_LOCALE, $this->locale);
+
+ $this->prompt();
+
+ // Create the .env file
+ Installer::createDefaultEnvFile();
+
+ $this->line('Creating database tables');
+ if (!$this->createDatabaseTables()) {
+ return self::CMD_ERROR;
+ }
+
+ $this->line('Creating company');
+ Installer::createCompany($this->companyName, $this->companyEmail, $this->locale);
+
+ $this->line('Creating admin');
+ Installer::createUser($this->adminEmail, $this->adminPassword, $this->locale);
+
+ $this->line('Applying the final touches');
+ Installer::finalTouches();
+
+ return self::CMD_SUCCESS;
+ }
+
+ /**
+ * Check that all options are presents. otherwise returns an array of the missing options
+ */
+ private function checkOptions()
+ {
+ $missingOptions = array();
+
+ $this->locale = $this->option(self::OPT_LOCALE);
+ if (empty($this->locale)) {
+ $missingOptions[] = self::OPT_LOCALE;
+ }
+
+ $this->dbHost = $this->option(self::OPT_DB_HOST);
+ if (empty($this->dbHost)) {
+ $missingOptions[] = self::OPT_DB_HOST;
+ }
+
+ $this->dbPort = $this->option(self::OPT_DB_PORT);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_DB_PORT;
+ }
+
+ $this->dbName = $this->option(self::OPT_DB_NAME);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_DB_NAME;
+ }
+
+ $this->dbUsername = $this->option(self::OPT_DB_USERNAME);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_DB_USERNAME;
+ }
+
+ $this->dbPassword = $this->option(self::OPT_DB_PASSWORD);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_DB_PASSWORD;
+ }
+
+ $this->companyName = $this->option(self::OPT_COMPANY_NAME);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_COMPANY_NAME;
+ }
+
+ $this->companyEmail = $this->option(self::OPT_COMPANY_EMAIL);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_COMPANY_EMAIL;
+ }
+
+ $this->adminEmail = $this->option(self::OPT_ADMIN_EMAIL);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_ADMIN_EMAIL;
+ }
+
+ $this->adminPassword = $this->option(self::OPT_ADMIN_PASSWORD);
+ if (empty($this->dbPort)) {
+ $missingOptions[] = self::OPT_ADMIN_PASSWORD;
+ }
+
+ return $missingOptions;
+ }
+
+ /**
+ * Ask the user for data if some options are missing.
+ */
+ private function prompt()
+ {
+ if (empty($this->dbHost)) {
+ $this->dbHost = $this->ask('What is the database host?', 'localhost');
+ }
+
+ if (empty($this->dbPort)) {
+ $this->dbPort = $this->ask('What is the database port?', '3606');
+ }
+
+ if (empty($this->dbName)) {
+ $this->dbName = $this->ask('What is the database name?');
+ }
+
+ if (empty($this->dbUsername)) {
+ $this->dbUsername = $this->ask('What is the database username?');
+ }
+
+ if (empty($this->dbPassword)) {
+ $this->dbPassword = $this->secret('What is the database password?');
+ }
+
+ if (empty($this->companyName)) {
+ $this->companyName = $this->ask('What is the company name?');
+ }
+
+ if (empty($this->companyEmail)) {
+ $this->companyEmail = $this->ask('What is the company contact email?');
+ }
+
+ if (empty($this->adminEmail)) {
+ $this->adminEmail = $this->ask('What is the admin email?', $this->companyEmail);
+ }
+
+ if (empty($this->adminPassword)) {
+ $this->adminPassword = $this->secret('What is the admin password?');
+ }
+ }
+
+ private function createDatabaseTables() {
+ $this->dbHost = $this->option(self::OPT_DB_HOST);
+ $this->dbPort = $this->option(self::OPT_DB_PORT);
+ $this->dbName = $this->option(self::OPT_DB_NAME);
+ $this->dbUsername = $this->option(self::OPT_DB_USERNAME);
+ $this->dbPassword = $this->option(self::OPT_DB_PASSWORD);
+
+ $this->line('Connecting to database ' . $this->dbName . '@' . $this->dbHost . ':' . $this->dbPort);
+
+ if (!Installer::createDbTables($this->dbHost, $this->dbPort, $this->dbName, $this->dbUsername, $this->dbPassword)) {
+ $this->error('Error: Could not connect to the database! Please, make sure the details are correct.');
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app/Console/Commands/InvoiceReminder.php b/app/Console/Commands/InvoiceReminder.php
new file mode 100755
index 0000000..a2c0972
--- /dev/null
+++ b/app/Console/Commands/InvoiceReminder.php
@@ -0,0 +1,98 @@
+ $company->id]);
+
+ // Override settings and currencies
+ Overrider::load('settings');
+ Overrider::load('currencies');
+
+ $company->setSettings();
+
+ // Don't send reminders if disabled
+ if (!$company->send_invoice_reminder) {
+ continue;
+ }
+
+ $days = explode(',', $company->schedule_invoice_days);
+
+ foreach ($days as $day) {
+ $day = (int) trim($day);
+
+ $this->remind($day, $company);
+ }
+ }
+
+ // Unset company_id
+ session()->forget('company_id');
+ }
+
+ protected function remind($day, $company)
+ {
+ // Get due date
+ $date = Date::today()->subDays($day)->toDateString();
+
+ // Get upcoming bills
+ $invoices = Invoice::with('customer')->accrued()->notPaid()->due($date)->get();
+
+ foreach ($invoices as $invoice) {
+ // Notify the customer
+ if ($invoice->customer && !empty($invoice->customer_email)) {
+ $invoice->customer->notify(new Notification($invoice));
+ }
+
+ // Notify all users assigned to this company
+ foreach ($company->users as $user) {
+ if (!$user->can('read-notifications')) {
+ continue;
+ }
+
+ $user->notify(new Notification($invoice));
+ }
+ }
+ }
+}
diff --git a/app/Console/Commands/ModuleDisable.php b/app/Console/Commands/ModuleDisable.php
new file mode 100755
index 0000000..79fc618
--- /dev/null
+++ b/app/Console/Commands/ModuleDisable.php
@@ -0,0 +1,78 @@
+argument('alias');
+ $company_id = $this->argument('company_id');
+
+ $model = Module::alias($alias)->companyId($company_id)->first();
+
+ if (!$model) {
+ $this->info("Module [{$alias}] not found.");
+ return;
+ }
+
+ if ($model->status == 1) {
+ $model->status = 0;
+ $model->save();
+
+ $module = $this->laravel['modules']->findByAlias($alias);
+
+ // Add history
+ $data = [
+ 'company_id' => $company_id,
+ 'module_id' => $model->id,
+ 'category' => $module->get('category'),
+ 'version' => $module->get('version'),
+ 'description' => trans('modules.disabled', ['module' => $module->get('name')]),
+ ];
+
+ ModuleHistory::create($data);
+
+ $this->info("Module [{$alias}] disabled.");
+ } else {
+ $this->comment("Module [{$alias}] is already disabled.");
+ }
+ }
+
+ /**
+ * Get the console command arguments.
+ *
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return array(
+ array('alias', InputArgument::REQUIRED, 'Module alias.'),
+ array('company_id', InputArgument::REQUIRED, 'Company ID.'),
+ );
+ }
+}
diff --git a/app/Console/Commands/ModuleEnable.php b/app/Console/Commands/ModuleEnable.php
new file mode 100755
index 0000000..0bb8122
--- /dev/null
+++ b/app/Console/Commands/ModuleEnable.php
@@ -0,0 +1,78 @@
+argument('alias');
+ $company_id = $this->argument('company_id');
+
+ $model = Module::alias($alias)->companyId($company_id)->first();
+
+ if (!$model) {
+ $this->info("Module [{$alias}] not found.");
+ return;
+ }
+
+ if ($model->status == 0) {
+ $model->status = 1;
+ $model->save();
+
+ $module = $this->laravel['modules']->findByAlias($alias);
+
+ // Add history
+ $data = [
+ 'company_id' => $company_id,
+ 'module_id' => $model->id,
+ 'category' => $module->get('category'),
+ 'version' => $module->get('version'),
+ 'description' => trans('modules.enabled', ['module' => $module->get('name')]),
+ ];
+
+ ModuleHistory::create($data);
+
+ $this->info("Module [{$alias}] enabled.");
+ } else {
+ $this->comment("Module [{$alias}] is already enabled.");
+ }
+ }
+
+ /**
+ * Get the console command arguments.
+ *
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return array(
+ array('alias', InputArgument::REQUIRED, 'Module alias.'),
+ array('company_id', InputArgument::REQUIRED, 'Company ID.'),
+ );
+ }
+}
diff --git a/app/Console/Commands/ModuleInstall.php b/app/Console/Commands/ModuleInstall.php
new file mode 100755
index 0000000..fa428a7
--- /dev/null
+++ b/app/Console/Commands/ModuleInstall.php
@@ -0,0 +1,78 @@
+ $this->argument('company_id'),
+ 'alias' => strtolower($this->argument('alias')),
+ 'status' => '1',
+ ];
+
+ $model = Module::create($request);
+
+ $module = $this->laravel['modules']->findByAlias($model->alias);
+
+ $company_id = $this->argument('company_id');
+
+ // Add history
+ $data = [
+ 'company_id' => $company_id,
+ 'module_id' => $model->id,
+ 'category' => $module->get('category'),
+ 'version' => $module->get('version'),
+ 'description' => trans('modules.installed', ['module' => $module->get('name')]),
+ ];
+
+ ModuleHistory::create($data);
+
+ // Update database
+ $this->call('migrate', ['--force' => true]);
+
+ // Trigger event
+ event(new ModuleInstalled($model->alias, $company_id));
+
+ $this->info('Module installed!');
+ }
+
+ /**
+ * Get the console command arguments.
+ *
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return array(
+ array('alias', InputArgument::REQUIRED, 'Module alias.'),
+ array('company_id', InputArgument::REQUIRED, 'Company ID.'),
+ );
+ }
+}
diff --git a/app/Console/Commands/RecurringCheck.php b/app/Console/Commands/RecurringCheck.php
new file mode 100755
index 0000000..8b41c49
--- /dev/null
+++ b/app/Console/Commands/RecurringCheck.php
@@ -0,0 +1,192 @@
+today = Date::today();
+
+ // Get all companies
+ $companies = Company::all();
+
+ foreach ($companies as $company) {
+ // Set company id
+ session(['company_id' => $company->id]);
+
+ // Override settings and currencies
+ Overrider::load('settings');
+ Overrider::load('currencies');
+
+ $company->setSettings();
+
+ foreach ($company->recurring as $recur) {
+ if (!$current = $recur->current()) {
+ continue;
+ }
+
+ $current_date = Date::parse($current->format('Y-m-d'));
+
+ // Check if should recur today
+ if ($this->today->ne($current_date)) {
+ continue;
+ }
+
+ $model = $recur->recurable;
+
+ if (!$model) {
+ continue;
+ }
+
+ switch ($recur->recurable_type) {
+ case 'App\Models\Expense\Bill':
+ $this->recurBill($company, $model);
+ break;
+ case 'App\Models\Income\Invoice':
+ $this->recurInvoice($company, $model);
+ break;
+ case 'App\Models\Expense\Payment':
+ case 'App\Models\Income\Revenue':
+ $model->cloneable_relations = [];
+
+ // Create new record
+ $clone = $model->duplicate();
+
+ $clone->parent_id = $model->id;
+ $clone->paid_at = $this->today->format('Y-m-d');
+ $clone->save();
+
+ break;
+ }
+ }
+ }
+
+ // Unset company_id
+ session()->forget('company_id');
+ }
+
+ protected function recurInvoice($company, $model)
+ {
+ $model->cloneable_relations = ['items', 'totals'];
+
+ // Create new record
+ $clone = $model->duplicate();
+
+ // Set original invoice id
+ $clone->parent_id = $model->id;
+
+ // Days between invoiced and due date
+ $diff_days = Date::parse($clone->due_at)->diffInDays(Date::parse($clone->invoiced_at));
+
+ // Update dates
+ $clone->invoiced_at = $this->today->format('Y-m-d');
+ $clone->due_at = $this->today->addDays($diff_days)->format('Y-m-d');
+ $clone->save();
+
+ // Add invoice history
+ InvoiceHistory::create([
+ 'company_id' => session('company_id'),
+ 'invoice_id' => $clone->id,
+ 'status_code' => 'draft',
+ 'notify' => 0,
+ 'description' => trans('messages.success.added', ['type' => $clone->invoice_number]),
+ ]);
+
+ // Notify the customer
+ if ($clone->customer && !empty($clone->customer_email)) {
+ $clone->customer->notify(new InvoiceNotification($clone));
+ }
+
+ // Notify all users assigned to this company
+ foreach ($company->users as $user) {
+ if (!$user->can('read-notifications')) {
+ continue;
+ }
+
+ $user->notify(new InvoiceNotification($clone));
+ }
+
+ // Update next invoice number
+ $this->increaseNextInvoiceNumber();
+ }
+
+ protected function recurBill($company, $model)
+ {
+ $model->cloneable_relations = ['items', 'totals'];
+
+ // Create new record
+ $clone = $model->duplicate();
+
+ // Set original bill id
+ $clone->parent_id = $model->id;
+
+ // Days between invoiced and due date
+ $diff_days = Date::parse($clone->due_at)->diffInDays(Date::parse($clone->invoiced_at));
+
+ // Update dates
+ $clone->billed_at = $this->today->format('Y-m-d');
+ $clone->due_at = $this->today->addDays($diff_days)->format('Y-m-d');
+ $clone->save();
+
+ // Add bill history
+ BillHistory::create([
+ 'company_id' => session('company_id'),
+ 'bill_id' => $clone->id,
+ 'status_code' => 'draft',
+ 'notify' => 0,
+ 'description' => trans('messages.success.added', ['type' => $clone->bill_number]),
+ ]);
+
+ // Notify all users assigned to this company
+ foreach ($company->users as $user) {
+ if (!$user->can('read-notifications')) {
+ continue;
+ }
+
+ $user->notify(new BillNotification($clone));
+ }
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
new file mode 100755
index 0000000..d8f35a8
--- /dev/null
+++ b/app/Console/Kernel.php
@@ -0,0 +1,53 @@
+command('reminder:invoice')->dailyAt(setting('general.schedule_time', '09:00'));
+ $schedule->command('reminder:bill')->dailyAt(setting('general.schedule_time', '09:00'));
+ $schedule->command('recurring:check')->dailyAt(setting('general.schedule_time', '09:00'));
+ }
+
+ /**
+ * Register the Closure based commands for the application.
+ *
+ * @return void
+ */
+ protected function commands()
+ {
+ require base_path('routes/console.php');
+ }
+}
diff --git a/app/Console/Stubs/Modules/command.stub b/app/Console/Stubs/Modules/command.stub
new file mode 100755
index 0000000..1537601
--- /dev/null
+++ b/app/Console/Stubs/Modules/command.stub
@@ -0,0 +1,68 @@
+view('view.name');
+ }
+}
diff --git a/app/Console/Stubs/Modules/middleware.stub b/app/Console/Stubs/Modules/middleware.stub
new file mode 100755
index 0000000..954583e
--- /dev/null
+++ b/app/Console/Stubs/Modules/middleware.stub
@@ -0,0 +1,21 @@
+increments('id');
+$FIELDS$
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('$TABLE$');
+ }
+}
diff --git a/app/Console/Stubs/Modules/migration/delete.stub b/app/Console/Stubs/Modules/migration/delete.stub
new file mode 100755
index 0000000..53ec1b3
--- /dev/null
+++ b/app/Console/Stubs/Modules/migration/delete.stub
@@ -0,0 +1,32 @@
+increments('id');
+$FIELDS$
+ $table->timestamps();
+ });
+ }
+}
diff --git a/app/Console/Stubs/Modules/migration/plain.stub b/app/Console/Stubs/Modules/migration/plain.stub
new file mode 100755
index 0000000..cc014c6
--- /dev/null
+++ b/app/Console/Stubs/Modules/migration/plain.stub
@@ -0,0 +1,28 @@
+line('The introduction to the notification.')
+ ->action('Notification Action', 'https://laravel.com')
+ ->line('Thank you for using our application!');
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/app/Console/Stubs/Modules/provider.stub b/app/Console/Stubs/Modules/provider.stub
new file mode 100755
index 0000000..4fd17be
--- /dev/null
+++ b/app/Console/Stubs/Modules/provider.stub
@@ -0,0 +1,35 @@
+routesAreCached()) {
+ // require __DIR__ . '/Http/routes.php';
+ // }
+ }
+}
diff --git a/app/Console/Stubs/Modules/routes.stub b/app/Console/Stubs/Modules/routes.stub
new file mode 100755
index 0000000..35865ba
--- /dev/null
+++ b/app/Console/Stubs/Modules/routes.stub
@@ -0,0 +1,6 @@
+ 'web', 'prefix' => '$LOWER_NAME$', 'namespace' => '$MODULE_NAMESPACE$\$STUDLY_NAME$\Http\Controllers'], function()
+{
+ Route::get('/', '$STUDLY_NAME$Controller@index');
+});
diff --git a/app/Console/Stubs/Modules/scaffold/config.stub b/app/Console/Stubs/Modules/scaffold/config.stub
new file mode 100755
index 0000000..0547f55
--- /dev/null
+++ b/app/Console/Stubs/Modules/scaffold/config.stub
@@ -0,0 +1,7 @@
+ '$STUDLY_NAME$',
+
+];
diff --git a/app/Console/Stubs/Modules/scaffold/provider.stub b/app/Console/Stubs/Modules/scaffold/provider.stub
new file mode 100755
index 0000000..1ee8297
--- /dev/null
+++ b/app/Console/Stubs/Modules/scaffold/provider.stub
@@ -0,0 +1,111 @@
+registerTranslations();
+ $this->registerConfig();
+ $this->registerViews();
+ $this->registerFactories();
+ }
+
+ /**
+ * Register the service provider.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ //
+ }
+
+ /**
+ * Register config.
+ *
+ * @return void
+ */
+ protected function registerConfig()
+ {
+ $this->publishes([
+ __DIR__.'/../$PATH_CONFIG$/config.php' => config_path('$LOWER_NAME$.php'),
+ ], 'config');
+ $this->mergeConfigFrom(
+ __DIR__.'/../$PATH_CONFIG$/config.php', '$LOWER_NAME$'
+ );
+ }
+
+ /**
+ * Register views.
+ *
+ * @return void
+ */
+ public function registerViews()
+ {
+ $viewPath = resource_path('views/modules/$LOWER_NAME$');
+
+ $sourcePath = __DIR__.'/../$PATH_VIEWS$';
+
+ $this->publishes([
+ $sourcePath => $viewPath
+ ]);
+
+ $this->loadViewsFrom(array_merge(array_map(function ($path) {
+ return $path . '/modules/$LOWER_NAME$';
+ }, \Config::get('view.paths')), [$sourcePath]), '$LOWER_NAME$');
+ }
+
+ /**
+ * Register translations.
+ *
+ * @return void
+ */
+ public function registerTranslations()
+ {
+ $langPath = resource_path('lang/modules/$LOWER_NAME$');
+
+ if (is_dir($langPath)) {
+ $this->loadTranslationsFrom($langPath, '$LOWER_NAME$');
+ } else {
+ $this->loadTranslationsFrom(__DIR__ .'/../$PATH_LANG$', '$LOWER_NAME$');
+ }
+ }
+
+ /**
+ * Register an additional directory of factories.
+ * @source https://github.com/sebastiaanluca/laravel-resource-flow/blob/develop/src/Modules/ModuleServiceProvider.php#L66
+ */
+ public function registerFactories()
+ {
+ if (! app()->environment('production')) {
+ app(Factory::class)->load(__DIR__ . '/Database/factories');
+ }
+ }
+
+ /**
+ * Get the services provided by the provider.
+ *
+ * @return array
+ */
+ public function provides()
+ {
+ return [];
+ }
+}
diff --git a/app/Console/Stubs/Modules/seeder.stub b/app/Console/Stubs/Modules/seeder.stub
new file mode 100755
index 0000000..dd43490
--- /dev/null
+++ b/app/Console/Stubs/Modules/seeder.stub
@@ -0,0 +1,21 @@
+call("OthersTableSeeder");
+ }
+}
diff --git a/app/Console/Stubs/Modules/start.stub b/app/Console/Stubs/Modules/start.stub
new file mode 100755
index 0000000..140a105
--- /dev/null
+++ b/app/Console/Stubs/Modules/start.stub
@@ -0,0 +1,17 @@
+routesAreCached()) {
+ require __DIR__ . '/Http/routes.php';
+}
diff --git a/app/Console/Stubs/Modules/views/index.stub b/app/Console/Stubs/Modules/views/index.stub
new file mode 100755
index 0000000..4a64852
--- /dev/null
+++ b/app/Console/Stubs/Modules/views/index.stub
@@ -0,0 +1,9 @@
+@extends('$LOWER_NAME$::layouts.master')
+
+@section('content')
+ Hello World
+
+
+ This view is loaded from module: {!! config('$LOWER_NAME$.name') !!}
+
+@stop
diff --git a/app/Console/Stubs/Modules/views/master.stub b/app/Console/Stubs/Modules/views/master.stub
new file mode 100755
index 0000000..14fd322
--- /dev/null
+++ b/app/Console/Stubs/Modules/views/master.stub
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Module $STUDLY_NAME$
+
+
+ @yield('content')
+
+
diff --git a/app/Events/AdminMenuCreated.php b/app/Events/AdminMenuCreated.php
new file mode 100755
index 0000000..9883abb
--- /dev/null
+++ b/app/Events/AdminMenuCreated.php
@@ -0,0 +1,18 @@
+menu = $menu;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/BillCreated.php b/app/Events/BillCreated.php
new file mode 100755
index 0000000..fc4881a
--- /dev/null
+++ b/app/Events/BillCreated.php
@@ -0,0 +1,18 @@
+bill = $bill;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/BillUpdated.php b/app/Events/BillUpdated.php
new file mode 100755
index 0000000..21751e1
--- /dev/null
+++ b/app/Events/BillUpdated.php
@@ -0,0 +1,18 @@
+bill = $bill;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/CompanySwitched.php b/app/Events/CompanySwitched.php
new file mode 100755
index 0000000..3738c8d
--- /dev/null
+++ b/app/Events/CompanySwitched.php
@@ -0,0 +1,18 @@
+company = $company;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/CustomerMenuCreated.php b/app/Events/CustomerMenuCreated.php
new file mode 100755
index 0000000..5766e1b
--- /dev/null
+++ b/app/Events/CustomerMenuCreated.php
@@ -0,0 +1,18 @@
+menu = $menu;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/InvoiceCreated.php b/app/Events/InvoiceCreated.php
new file mode 100755
index 0000000..afa2e3c
--- /dev/null
+++ b/app/Events/InvoiceCreated.php
@@ -0,0 +1,18 @@
+invoice = $invoice;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/InvoicePaid.php b/app/Events/InvoicePaid.php
new file mode 100755
index 0000000..33fab77
--- /dev/null
+++ b/app/Events/InvoicePaid.php
@@ -0,0 +1,21 @@
+invoice = $invoice;
+ $this->request = $request;
+ }
+}
diff --git a/app/Events/InvoicePrinting.php b/app/Events/InvoicePrinting.php
new file mode 100755
index 0000000..f775b69
--- /dev/null
+++ b/app/Events/InvoicePrinting.php
@@ -0,0 +1,18 @@
+invoice = $invoice;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/InvoiceUpdated.php b/app/Events/InvoiceUpdated.php
new file mode 100755
index 0000000..d9d6fc3
--- /dev/null
+++ b/app/Events/InvoiceUpdated.php
@@ -0,0 +1,18 @@
+invoice = $invoice;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/ModuleInstalled.php b/app/Events/ModuleInstalled.php
new file mode 100755
index 0000000..b581eb5
--- /dev/null
+++ b/app/Events/ModuleInstalled.php
@@ -0,0 +1,22 @@
+alias = $alias;
+ $this->company_id = $company_id;
+ }
+}
\ No newline at end of file
diff --git a/app/Events/PaymentGatewayListing.php b/app/Events/PaymentGatewayListing.php
new file mode 100755
index 0000000..65f1ad8
--- /dev/null
+++ b/app/Events/PaymentGatewayListing.php
@@ -0,0 +1,18 @@
+gateways = $gateways;
+ }
+}
diff --git a/app/Events/UpdateFinished.php b/app/Events/UpdateFinished.php
new file mode 100755
index 0000000..d221c60
--- /dev/null
+++ b/app/Events/UpdateFinished.php
@@ -0,0 +1,26 @@
+alias = $alias;
+ $this->old = $old;
+ $this->new = $new;
+ }
+}
\ No newline at end of file
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
new file mode 100755
index 0000000..a747e31
--- /dev/null
+++ b/app/Exceptions/Handler.php
@@ -0,0 +1,65 @@
+expectsJson()) {
+ return response()->json(['error' => 'Unauthenticated.'], 401);
+ }
+
+ return redirect()->guest(route('login'));
+ }
+}
diff --git a/app/Filters/Auth/Permissions.php b/app/Filters/Auth/Permissions.php
new file mode 100755
index 0000000..9ae73cf
--- /dev/null
+++ b/app/Filters/Auth/Permissions.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('display_name', $query);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Auth/Roles.php b/app/Filters/Auth/Roles.php
new file mode 100755
index 0000000..8eb71d6
--- /dev/null
+++ b/app/Filters/Auth/Roles.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('display_name', $query);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Auth/Users.php b/app/Filters/Auth/Users.php
new file mode 100755
index 0000000..950507e
--- /dev/null
+++ b/app/Filters/Auth/Users.php
@@ -0,0 +1,26 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->where('name', 'LIKE', '%' . $query . '%')->orWhere('email', 'LIKE', '%' . $query . '%');
+ }
+
+ public function role($id)
+ {
+ return $this->related('roles', 'role_id', $id);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Banking/Accounts.php b/app/Filters/Banking/Accounts.php
new file mode 100755
index 0000000..df3ae03
--- /dev/null
+++ b/app/Filters/Banking/Accounts.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('name', $query);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Banking/Transactions.php b/app/Filters/Banking/Transactions.php
new file mode 100755
index 0000000..8c64cb9
--- /dev/null
+++ b/app/Filters/Banking/Transactions.php
@@ -0,0 +1,31 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function account($account_id)
+ {
+ return $this->where('account_id', $account_id);
+ }
+
+ public function category($category_id)
+ {
+ // No category for bills/invoices
+ if (in_array($this->getModel()->getTable(), ['bill_payments', 'invoice_payments'])) {
+ return $this;
+ }
+
+ return $this->where('category_id', $category_id);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Banking/Transfers.php b/app/Filters/Banking/Transfers.php
new file mode 100755
index 0000000..594d4ee
--- /dev/null
+++ b/app/Filters/Banking/Transfers.php
@@ -0,0 +1,26 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function fromAccount($account_id)
+ {
+ return $this->where('payments.account_id', $account_id);
+ }
+
+ public function toAccount($account_id)
+ {
+ return $this->related('revenue', 'revenues.account_id', '=', $account_id);
+ }
+}
diff --git a/app/Filters/Common/Companies.php b/app/Filters/Common/Companies.php
new file mode 100755
index 0000000..0e7944f
--- /dev/null
+++ b/app/Filters/Common/Companies.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('domain', $query);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Common/Items.php b/app/Filters/Common/Items.php
new file mode 100755
index 0000000..ec3a218
--- /dev/null
+++ b/app/Filters/Common/Items.php
@@ -0,0 +1,26 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('name', $query);
+ }
+
+ public function category($id)
+ {
+ return $this->where('category_id', $id);
+ }
+}
diff --git a/app/Filters/Customers/Invoices.php b/app/Filters/Customers/Invoices.php
new file mode 100755
index 0000000..3759c82
--- /dev/null
+++ b/app/Filters/Customers/Invoices.php
@@ -0,0 +1,26 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('customer_name', $query);
+ }
+
+ public function status($status)
+ {
+ return $this->where('invoice_status_code', $status);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Customers/Payments.php b/app/Filters/Customers/Payments.php
new file mode 100755
index 0000000..4d0fbcf
--- /dev/null
+++ b/app/Filters/Customers/Payments.php
@@ -0,0 +1,31 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('description', $query);
+ }
+
+ public function category($category)
+ {
+ return $this->where('category_id', $category);
+ }
+
+ public function paymentMethod($payment_method)
+ {
+ return $this->where('payment_method', $payment_method);
+ }
+}
diff --git a/app/Filters/Customers/Transactions.php b/app/Filters/Customers/Transactions.php
new file mode 100755
index 0000000..fd6bc1a
--- /dev/null
+++ b/app/Filters/Customers/Transactions.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('payment.name', $query)->whereLike('revenue.name', $query);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Expenses/Bills.php b/app/Filters/Expenses/Bills.php
new file mode 100755
index 0000000..7055a1e
--- /dev/null
+++ b/app/Filters/Expenses/Bills.php
@@ -0,0 +1,31 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('vendor_name', $query);
+ }
+
+ public function vendor($vendor)
+ {
+ return $this->where('vendor_id', $vendor);
+ }
+
+ public function status($status)
+ {
+ return $this->where('bill_status_code', $status);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Expenses/Payments.php b/app/Filters/Expenses/Payments.php
new file mode 100755
index 0000000..a9345d4
--- /dev/null
+++ b/app/Filters/Expenses/Payments.php
@@ -0,0 +1,36 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('description', $query);
+ }
+
+ public function vendor($vendor)
+ {
+ return $this->where('vendor_id', $vendor);
+ }
+
+ public function category($category)
+ {
+ return $this->where('category_id', $category);
+ }
+
+ public function account($account)
+ {
+ return $this->where('account_id', $account);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Expenses/Vendors.php b/app/Filters/Expenses/Vendors.php
new file mode 100755
index 0000000..b694618
--- /dev/null
+++ b/app/Filters/Expenses/Vendors.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->where('name', 'LIKE', '%' . $query . '%')->orWhere('email', 'LIKE', '%' . $query . '%');
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Incomes/Customers.php b/app/Filters/Incomes/Customers.php
new file mode 100755
index 0000000..8af7266
--- /dev/null
+++ b/app/Filters/Incomes/Customers.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->where('name', 'LIKE', '%' . $query . '%')->orWhere('email', 'LIKE', '%' . $query . '%');
+ }
+}
diff --git a/app/Filters/Incomes/Invoices.php b/app/Filters/Incomes/Invoices.php
new file mode 100755
index 0000000..79e3ef6
--- /dev/null
+++ b/app/Filters/Incomes/Invoices.php
@@ -0,0 +1,31 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('customer_name', $query);
+ }
+
+ public function customer($customer)
+ {
+ return $this->where('customer_id', $customer);
+ }
+
+ public function status($status)
+ {
+ return $this->where('invoice_status_code', $status);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Incomes/Revenues.php b/app/Filters/Incomes/Revenues.php
new file mode 100755
index 0000000..f61bcfa
--- /dev/null
+++ b/app/Filters/Incomes/Revenues.php
@@ -0,0 +1,36 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('description', $query);
+ }
+
+ public function customer($customer)
+ {
+ return $this->where('customer_id', $customer);
+ }
+
+ public function category($category)
+ {
+ return $this->where('category_id', $category);
+ }
+
+ public function account($account)
+ {
+ return $this->where('account_id', $account);
+ }
+}
\ No newline at end of file
diff --git a/app/Filters/Settings/Categories.php b/app/Filters/Settings/Categories.php
new file mode 100755
index 0000000..ebb50b6
--- /dev/null
+++ b/app/Filters/Settings/Categories.php
@@ -0,0 +1,26 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('name', $query);
+ }
+
+ public function type($type)
+ {
+ return $this->where('type', $type);
+ }
+}
diff --git a/app/Filters/Settings/Currencies.php b/app/Filters/Settings/Currencies.php
new file mode 100755
index 0000000..ba0db14
--- /dev/null
+++ b/app/Filters/Settings/Currencies.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('name', $query);
+ }
+}
diff --git a/app/Filters/Settings/Taxes.php b/app/Filters/Settings/Taxes.php
new file mode 100755
index 0000000..d2b56d8
--- /dev/null
+++ b/app/Filters/Settings/Taxes.php
@@ -0,0 +1,21 @@
+ [input_key1, input_key2]].
+ *
+ * @var array
+ */
+ public $relations = [];
+
+ public function search($query)
+ {
+ return $this->whereLike('name', $query);
+ }
+}
diff --git a/app/Http/Controllers/Api/Auth/Permissions.php b/app/Http/Controllers/Api/Auth/Permissions.php
new file mode 100755
index 0000000..321ed8b
--- /dev/null
+++ b/app/Http/Controllers/Api/Auth/Permissions.php
@@ -0,0 +1,77 @@
+response->paginator($permissions, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Permission $permission
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Permission $permission)
+ {
+ return $this->response->item($permission, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $permission = Permission::create($request->all());
+
+ return $this->response->created(url('api/permissions/'.$permission->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $permission
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Permission $permission, Request $request)
+ {
+ $permission->update($request->all());
+
+ return $this->response->item($permission->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Permission $permission
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Permission $permission)
+ {
+ $permission->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Auth/Roles.php b/app/Http/Controllers/Api/Auth/Roles.php
new file mode 100755
index 0000000..425e3c4
--- /dev/null
+++ b/app/Http/Controllers/Api/Auth/Roles.php
@@ -0,0 +1,85 @@
+collect();
+
+ return $this->response->paginator($roles, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Role $role
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Role $role)
+ {
+ return $this->response->item($role, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $role = Role::create($request->input());
+
+ if ($request->has('permissions')) {
+ $role->permissions()->attach($request->get('permissions'));
+ }
+
+ return $this->response->created(url('api/roles/'.$role->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $role
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Role $role, Request $request)
+ {
+ $role->update($request->all());
+
+ if ($request->has('permissions')) {
+ $role->permissions()->attach($request->get('permissions'));
+ }
+
+ return $this->response->item($role->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Role $role
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Role $role)
+ {
+ $role->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Auth/Users.php b/app/Http/Controllers/Api/Auth/Users.php
new file mode 100755
index 0000000..59781a3
--- /dev/null
+++ b/app/Http/Controllers/Api/Auth/Users.php
@@ -0,0 +1,97 @@
+collect();
+
+ return $this->response->paginator($users, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int|string $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($id)
+ {
+ // Check if we're querying by id or email
+ if (is_numeric($id)) {
+ $user = User::with(['companies', 'roles', 'permissions'])->find($id);
+ } else {
+ $user = User::with(['companies', 'roles', 'permissions'])->where('email', $id)->first();
+ }
+
+ return $this->response->item($user, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $user = User::create($request->input());
+
+ // Attach roles
+ $user->roles()->attach($request->get('roles'));
+
+ // Attach companies
+ $user->companies()->attach($request->get('companies'));
+
+ return $this->response->created(url('api/users/'.$user->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $user
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(User $user, Request $request)
+ {
+ // Except password as we don't want to let the users change a password from this endpoint
+ $user->update($request->except('password'));
+
+ // Sync roles
+ $user->roles()->sync($request->get('roles'));
+
+ // Sync companies
+ $user->companies()->sync($request->get('companies'));
+
+ return $this->response->item($user->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param User $user
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(User $user)
+ {
+ $user->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Banking/Accounts.php b/app/Http/Controllers/Api/Banking/Accounts.php
new file mode 100755
index 0000000..b363b8c
--- /dev/null
+++ b/app/Http/Controllers/Api/Banking/Accounts.php
@@ -0,0 +1,77 @@
+response->paginator($accounts, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Account $account
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Account $account)
+ {
+ return $this->response->item($account, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $account = Account::create($request->all());
+
+ return $this->response->created(url('api/accounts/'.$account->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $account
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Account $account, Request $request)
+ {
+ $account->update($request->all());
+
+ return $this->response->item($account->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Account $account
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Account $account)
+ {
+ $account->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Banking/Transfers.php b/app/Http/Controllers/Api/Banking/Transfers.php
new file mode 100755
index 0000000..764da2f
--- /dev/null
+++ b/app/Http/Controllers/Api/Banking/Transfers.php
@@ -0,0 +1,84 @@
+collect('payment.paid_at');
+
+ return $this->response->paginator($transfers, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Transfer $transfer
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Transfer $transfer)
+ {
+ return $this->response->item($transfer, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $transfer = Transfer::create($request->all());
+
+ return $this->response->created(url('api/transfers/'.$transfer->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $transfer
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Transfer $transfer, Request $request)
+ {
+ $transfer->update($request->all());
+
+ return $this->response->item($transfer->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Transfer $transfer
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Transfer $transfer)
+ {
+ $payment = Payment::findOrFail($transfer['payment_id']);
+ $revenue = Revenue::findOrFail($transfer['revenue_id']);
+
+ $transfer->delete();
+ $payment->delete();
+ $revenue->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Common/Companies.php b/app/Http/Controllers/Api/Common/Companies.php
new file mode 100755
index 0000000..fe11e48
--- /dev/null
+++ b/app/Http/Controllers/Api/Common/Companies.php
@@ -0,0 +1,133 @@
+user()->companies()->get()->sortBy('name');
+
+ foreach ($companies as $company) {
+ $company->setSettings();
+ }
+
+ return $this->response->collection($companies, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Company $company
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Company $company)
+ {
+ // Check if user can access company
+ $companies = app('Dingo\Api\Auth\Auth')->user()->companies()->pluck('id')->toArray();
+ if (!in_array($company->id, $companies)) {
+ $this->response->errorUnauthorized();
+ }
+
+ $company->setSettings();
+
+ return $this->response->item($company, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ // Clear settings
+ setting()->forgetAll();
+
+ $company = Company::create($request->all());
+
+ // Create settings
+ setting()->set([
+ 'general.company_name' => $request->get('company_name'),
+ 'general.company_email' => $request->get('company_email'),
+ 'general.company_address' => $request->get('company_address'),
+ 'general.default_currency' => $request->get('default_currency'),
+ 'general.default_locale' => $request->get('default_locale', 'en-GB'),
+ ]);
+
+ setting()->setExtraColumns(['company_id' => $company->id]);
+
+ setting()->save();
+
+ return $this->response->created(url('api/companies/'.$company->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $company
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Company $company, Request $request)
+ {
+ // Check if user can access company
+ $companies = app('Dingo\Api\Auth\Auth')->user()->companies()->pluck('id')->toArray();
+ if (!in_array($company->id, $companies)) {
+ $this->response->errorUnauthorized();
+ }
+
+ // Update company
+ $company->update(['domain' => $request->get('domain')]);
+
+ // Update settings
+ setting()->forgetAll();
+ setting()->setExtraColumns(['company_id' => $company->id]);
+ setting()->load(true);
+
+ setting()->set([
+ 'general.company_name' => $request->get('company_name'),
+ 'general.company_email' => $request->get('company_email'),
+ 'general.company_address' => $request->get('company_address'),
+ 'general.default_currency' => $request->get('default_currency'),
+ 'general.default_locale' => $request->get('default_locale', 'en-GB'),
+ ]);
+
+ setting()->save();
+
+ return $this->response->item($company->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Company $company
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Company $company)
+ {
+ // Check if user can access company
+ $companies = app('Dingo\Api\Auth\Auth')->user()->companies()->pluck('id')->toArray();
+ if (!in_array($company->id, $companies)) {
+ $this->response->errorUnauthorized();
+ }
+
+ $company->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Common/Items.php b/app/Http/Controllers/Api/Common/Items.php
new file mode 100755
index 0000000..fa869af
--- /dev/null
+++ b/app/Http/Controllers/Api/Common/Items.php
@@ -0,0 +1,84 @@
+collect();
+
+ return $this->response->paginator($items, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int|string $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($id)
+ {
+ // Check if we're querying by id or sku
+ if (is_numeric($id)) {
+ $item = Item::with(['category', 'tax'])->find($id);
+ } else {
+ $item = Item::with(['category', 'tax'])->where('sku', $id)->first();
+ }
+
+ return $this->response->item($item, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $item = Item::create($request->all());
+
+ return $this->response->created(url('api/items/'.$item->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $item
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Item $item, Request $request)
+ {
+ $item->update($request->all());
+
+ return $this->response->item($item->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Item $item
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Item $item)
+ {
+ $item->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Common/Ping.php b/app/Http/Controllers/Api/Common/Ping.php
new file mode 100755
index 0000000..4f95f31
--- /dev/null
+++ b/app/Http/Controllers/Api/Common/Ping.php
@@ -0,0 +1,25 @@
+response->array([
+ 'status' => 'ok',
+ 'timestamp' => Date::now(),
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Api/Expenses/Bills.php b/app/Http/Controllers/Api/Expenses/Bills.php
new file mode 100755
index 0000000..e47a589
--- /dev/null
+++ b/app/Http/Controllers/Api/Expenses/Bills.php
@@ -0,0 +1,204 @@
+collect();
+
+ return $this->response->paginator($bills, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Bill $bill
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Bill $bill)
+ {
+ return $this->response->item($bill, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $bill = Bill::create($request->all());
+
+ $bill_item = array();
+ $bill_item['company_id'] = $request['company_id'];
+ $bill_item['bill_id'] = $bill->id;
+
+ if ($request['item']) {
+ foreach ($request['item'] as $item) {
+ $item_id = 0;
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item_id = $item['item_id'];
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+
+ // Increase stock (item bought)
+ $item_object->quantity += $item['quantity'];
+ $item_object->save();
+ } elseif (!empty($item['sku'])) {
+ $item_sku = $item['sku'];
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (($item['price'] * $item['quantity']) / 100) * $tax_object->rate;
+ } elseif (!empty($item['tax'])) {
+ $tax = $item['tax'];
+ }
+
+ $bill_item['item_id'] = $item_id;
+ $bill_item['name'] = str_limit($item['name'], 180, '');
+ $bill_item['sku'] = $item_sku;
+ $bill_item['quantity'] = $item['quantity'];
+ $bill_item['price'] = $item['price'];
+ $bill_item['tax'] = $tax;
+ $bill_item['tax_id'] = $tax_id;
+ $bill_item['total'] = ($item['price'] + $bill_item['tax']) * $item['quantity'];
+
+ $request['amount'] += $bill_item['total'];
+
+ BillItem::create($bill_item);
+ }
+ }
+
+ $bill->update($request->input());
+
+ $request['bill_id'] = $bill->id;
+ $request['status_code'] = $request['bill_status_code'];
+ $request['notify'] = 0;
+ $request['description'] = trans('messages.success.added', ['type' => $request['bill_number']]);
+
+ BillHistory::create($request->input());
+
+ // Fire the event to make it extendible
+ event(new BillCreated($bill));
+
+ return $this->response->created(url('api/bills/'.$bill->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $bill
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Bill $bill, Request $request)
+ {
+ $bill_item = array();
+ $bill_item['company_id'] = $request['company_id'];
+ $bill_item['bill_id'] = $bill->id;
+
+ if ($request['item']) {
+ BillItem::where('bill_id', $bill->id)->delete();
+
+ foreach ($request['item'] as $item) {
+ $item_id = 0;
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item_id = $item['item_id'];
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+ } elseif (!empty($item['sku'])) {
+ $item_sku = $item['sku'];
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (($item['price'] * $item['quantity']) / 100) * $tax_object->rate;
+ } elseif (!empty($item['tax'])) {
+ $tax = $item['tax'];
+ }
+
+ $bill_item['item_id'] = $item_id;
+ $bill_item['name'] = str_limit($item['name'], 180, '');
+ $bill_item['sku'] = $item_sku;
+ $bill_item['quantity'] = $item['quantity'];
+ $bill_item['price'] = $item['price'];
+ $bill_item['tax'] = $tax;
+ $bill_item['tax_id'] = $tax_id;
+ $bill_item['total'] = ($item['price'] + $bill_item['tax']) * $item['quantity'];
+
+ $request['amount'] += $bill_item['total'];
+
+ BillItem::create($bill_item);
+ }
+ }
+
+ $bill->update($request->input());
+
+ // Fire the event to make it extendible
+ event(new BillUpdated($bill));
+
+ return $this->response->item($bill->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Bill $bill
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Bill $bill)
+ {
+ $bill->delete();
+
+ BillItem::where('bill_id', $bill->id)->delete();
+ BillPayment::where('bill_id', $bill->id)->delete();
+ BillHistory::where('bill_id', $bill->id)->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Expenses/Payments.php b/app/Http/Controllers/Api/Expenses/Payments.php
new file mode 100755
index 0000000..7490fcc
--- /dev/null
+++ b/app/Http/Controllers/Api/Expenses/Payments.php
@@ -0,0 +1,77 @@
+collect();
+
+ return $this->response->paginator($payments, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Payment $payment
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Payment $payment)
+ {
+ return $this->response->item($payment, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $payment = Payment::create($request->all());
+
+ return $this->response->created(url('api/payments/'.$payment->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $payment
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Payment $payment, Request $request)
+ {
+ $payment->update($request->all());
+
+ return $this->response->item($payment->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Payment $payment
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Payment $payment)
+ {
+ $payment->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Expenses/Vendors.php b/app/Http/Controllers/Api/Expenses/Vendors.php
new file mode 100755
index 0000000..65e210d
--- /dev/null
+++ b/app/Http/Controllers/Api/Expenses/Vendors.php
@@ -0,0 +1,84 @@
+response->paginator($vendors, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int|string $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($id)
+ {
+ // Check if we're querying by id or email
+ if (is_numeric($id)) {
+ $vendor = Vendor::find($id);
+ } else {
+ $vendor = Vendor::where('email', $id)->first();
+ }
+
+ return $this->response->item($vendor, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $vendor = Vendor::create($request->all());
+
+ return $this->response->created(url('api/vendors/'.$vendor->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $vendor
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Vendor $vendor, Request $request)
+ {
+ $vendor->update($request->all());
+
+ return $this->response->item($vendor->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Vendor $vendor
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Vendor $vendor)
+ {
+ $vendor->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Incomes/Customers.php b/app/Http/Controllers/Api/Incomes/Customers.php
new file mode 100755
index 0000000..0cc37be
--- /dev/null
+++ b/app/Http/Controllers/Api/Incomes/Customers.php
@@ -0,0 +1,84 @@
+response->paginator($customers, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int|string $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($id)
+ {
+ // Check if we're querying by id or email
+ if (is_numeric($id)) {
+ $customer = Customer::find($id);
+ } else {
+ $customer = Customer::where('email', $id)->first();
+ }
+
+ return $this->response->item($customer, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $customer = Customer::create($request->all());
+
+ return $this->response->created(url('api/customers/'.$customer->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $customer
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Customer $customer, Request $request)
+ {
+ $customer->update($request->all());
+
+ return $this->response->item($customer->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Customer $customer
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Customer $customer)
+ {
+ $customer->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Incomes/InvoicePayments.php b/app/Http/Controllers/Api/Incomes/InvoicePayments.php
new file mode 100755
index 0000000..de4ebe8
--- /dev/null
+++ b/app/Http/Controllers/Api/Incomes/InvoicePayments.php
@@ -0,0 +1,126 @@
+get();
+
+ return $this->response->collection($invoice_payments, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param $invoice_id
+ * @param $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($invoice_id, $id)
+ {
+ $invoice_payment = InvoicePayment::find($id);
+
+ return $this->response->item($invoice_payment, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $invoice_id
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store($invoice_id, Request $request)
+ {
+ // Get currency object
+ $currency = Currency::where('code', $request['currency_code'])->first();
+
+ $request['currency_code'] = $currency->code;
+ $request['currency_rate'] = $currency->rate;
+
+ $request['invoice_id'] = $invoice_id;
+
+ $invoice = Invoice::find($invoice_id);
+
+ if ($request['currency_code'] == $invoice->currency_code) {
+ if ($request['amount'] > $invoice->amount) {
+ return $this->response->noContent();
+ } elseif ($request['amount'] == $invoice->amount) {
+ $invoice->invoice_status_code = 'paid';
+ } else {
+ $invoice->invoice_status_code = 'partial';
+ }
+ } else {
+ $request_invoice = new Invoice();
+
+ $request_invoice->amount = (float) $request['amount'];
+ $request_invoice->currency_code = $currency->code;
+ $request_invoice->currency_rate = $currency->rate;
+
+ $amount = $request_invoice->getConvertedAmount();
+
+ if ($amount > $invoice->amount) {
+ return $this->response->noContent();
+ } elseif ($amount == $invoice->amount) {
+ $invoice->invoice_status_code = 'paid';
+ } else {
+ $invoice->invoice_status_code = 'partial';
+ }
+ }
+
+ $invoice->save();
+
+ $invoice_payment = InvoicePayment::create($request->input());
+
+ $request['status_code'] = $invoice->invoice_status_code;
+ $request['notify'] = 0;
+
+ $desc_date = Date::parse($request['paid_at'])->format($this->getCompanyDateFormat());
+ $desc_amount = money((float) $request['amount'], $request['currency_code'], true)->format();
+ $request['description'] = $desc_date . ' ' . $desc_amount;
+
+ InvoiceHistory::create($request->input());
+
+ return $this->response->created(url('api/invoices/' . $invoice_id . '/payments' . $invoice_payment->id));
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param $invoice_id
+ * @param $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy($invoice_id, $id)
+ {
+ $invoice_payment = InvoicePayment::find($id);
+
+ $invoice_payment->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Incomes/Invoices.php b/app/Http/Controllers/Api/Incomes/Invoices.php
new file mode 100755
index 0000000..8015e0d
--- /dev/null
+++ b/app/Http/Controllers/Api/Incomes/Invoices.php
@@ -0,0 +1,360 @@
+collect();
+
+ return $this->response->paginator($invoices, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($id)
+ {
+ // Check if we're querying by id or number
+ if (is_numeric($id)) {
+ $invoice = Invoice::find($id);
+ } else {
+ $invoice = Invoice::where('invoice_number', $id)->first();
+ }
+
+ return $this->response->item($invoice, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ if (empty($request['amount'])) {
+ $request['amount'] = 0;
+ }
+
+ $invoice = Invoice::create($request->all());
+
+ $taxes = [];
+ $tax_total = 0;
+ $sub_total = 0;
+
+ $invoice_item = array();
+ $invoice_item['company_id'] = $request['company_id'];
+ $invoice_item['invoice_id'] = $invoice->id;
+
+ if ($request['item']) {
+ foreach ($request['item'] as $item) {
+ $item_id = 0;
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item_id = $item['item_id'];
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+
+ // Decrease stock (item sold)
+ $item_object->quantity -= $item['quantity'];
+ $item_object->save();
+
+ // Notify users if out of stock
+ if ($item_object->quantity == 0) {
+ foreach ($item_object->company->users as $user) {
+ if (!$user->can('read-notifications')) {
+ continue;
+ }
+
+ $user->notify(new ItemNotification($item_object));
+ }
+ }
+ } elseif (!empty($item['sku'])) {
+ $item_sku = $item['sku'];
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (($item['price'] * $item['quantity']) / 100) * $tax_object->rate;
+ } elseif (!empty($item['tax'])) {
+ $tax = $item['tax'];
+ }
+
+ $invoice_item['item_id'] = $item_id;
+ $invoice_item['name'] = str_limit($item['name'], 180, '');
+ $invoice_item['sku'] = $item_sku;
+ $invoice_item['quantity'] = $item['quantity'];
+ $invoice_item['price'] = $item['price'];
+ $invoice_item['tax'] = $tax;
+ $invoice_item['tax_id'] = $tax_id;
+ $invoice_item['total'] = $item['price'] * $item['quantity'];
+
+ InvoiceItem::create($invoice_item);
+
+ if (isset($tax_object)) {
+ if (array_key_exists($tax_object->id, $taxes)) {
+ $taxes[$tax_object->id]['amount'] += $tax;
+ } else {
+ $taxes[$tax_object->id] = [
+ 'name' => $tax_object->name,
+ 'amount' => $tax
+ ];
+ }
+ }
+
+ $tax_total += $tax;
+ $sub_total += $invoice_item['total'];
+
+ unset($item_object);
+ unset($tax_object);
+ }
+ }
+
+ if (empty($request['amount'])) {
+ $request['amount'] = $sub_total + $tax_total;
+ }
+
+ $invoice->update($request->input());
+
+ // Add invoice totals
+ $this->addTotals($invoice, $request, $taxes, $sub_total, $tax_total);
+
+ $request['invoice_id'] = $invoice->id;
+ $request['status_code'] = $request['invoice_status_code'];
+ $request['notify'] = 0;
+ $request['description'] = trans('messages.success.added', ['type' => $request['invoice_number']]);
+
+ InvoiceHistory::create($request->input());
+
+ // Update next invoice number
+ $next = setting('general.invoice_number_next', 1) + 1;
+ setting(['general.invoice_number_next' => $next]);
+ setting()->save();
+
+ // Fire the event to make it extendible
+ event(new InvoiceCreated($invoice));
+
+ return $this->response->created(url('api/invoices/'.$invoice->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $invoice
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Invoice $invoice, Request $request)
+ {
+ $taxes = [];
+ $tax_total = 0;
+ $sub_total = 0;
+
+ $invoice_item = array();
+ $invoice_item['company_id'] = $request['company_id'];
+ $invoice_item['invoice_id'] = $invoice->id;
+
+ if ($request['item']) {
+ InvoiceItem::where('invoice_id', $invoice->id)->delete();
+
+ foreach ($request['item'] as $item) {
+ $item_id = 0;
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item_id = $item['item_id'];
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+ } elseif (!empty($item['sku'])) {
+ $item_sku = $item['sku'];
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (($item['price'] * $item['quantity']) / 100) * $tax_object->rate;
+ } elseif (!empty($item['tax'])) {
+ $tax = $item['tax'];
+ }
+
+ $invoice_item['item_id'] = $item_id;
+ $invoice_item['name'] = str_limit($item['name'], 180, '');
+ $invoice_item['sku'] = $item_sku;
+ $invoice_item['quantity'] = $item['quantity'];
+ $invoice_item['price'] = $item['price'];
+ $invoice_item['tax'] = $tax;
+ $invoice_item['tax_id'] = $tax_id;
+ $invoice_item['total'] = $item['price'] * $item['quantity'];
+
+ $request['amount'] += $invoice_item['total'];
+
+ InvoiceItem::create($invoice_item);
+
+ if (isset($tax_object)) {
+ if (array_key_exists($tax_object->id, $taxes)) {
+ $taxes[$tax_object->id]['amount'] += $tax;
+ } else {
+ $taxes[$tax_object->id] = [
+ 'name' => $tax_object->name,
+ 'amount' => $tax
+ ];
+ }
+ }
+
+ $tax_total += $tax;
+ $sub_total += $invoice_item['total'];
+
+ unset($item_object);
+ unset($tax_object);
+ }
+ }
+
+ if (empty($request['amount'])) {
+ $request['amount'] = $sub_total + $tax_total;
+ }
+
+ $invoice->update($request->input());
+
+ // Delete previous invoice totals
+ InvoiceTotal::where('invoice_id', $invoice->id)->delete();
+
+ $this->addTotals($invoice, $request, $taxes, $sub_total, $tax_total);
+
+ // Fire the event to make it extendible
+ event(new InvoiceUpdated($invoice));
+
+ return $this->response->item($invoice->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Invoice $invoice
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Invoice $invoice)
+ {
+ $invoice->delete();
+
+ InvoiceItem::where('invoice_id', $invoice->id)->delete();
+ InvoicePayment::where('invoice_id', $invoice->id)->delete();
+ InvoiceHistory::where('invoice_id', $invoice->id)->delete();
+
+ return $this->response->noContent();
+ }
+
+ protected function addTotals($invoice, $request, $taxes, $sub_total, $tax_total) {
+ // Add invoice total taxes
+ if ($request['totals']) {
+ $sort_order = 1;
+
+ foreach ($request['totals'] as $total) {
+ if (!empty($total['sort_order'])) {
+ $sort_order = $total['sort_order'];
+ }
+
+ $invoice_total = [
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => $total['code'],
+ 'name' => $total['name'],
+ 'amount' => $total['amount'],
+ 'sort_order' => $sort_order,
+ ];
+
+ InvoiceTotal::create($invoice_total);
+
+ if (empty($total['sort_order'])) {
+ $sort_order++;
+ }
+ }
+ } else {
+ // Added invoice total sub total
+ $invoice_sub_total = [
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'sub_total',
+ 'name' => 'invoices.sub_total',
+ 'amount' => $sub_total,
+ 'sort_order' => 1,
+ ];
+
+ InvoiceTotal::create($invoice_sub_total);
+
+ $sort_order = 2;
+
+ // Added invoice total taxes
+ if ($taxes) {
+ foreach ($taxes as $tax) {
+ $invoice_tax_total = [
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'tax',
+ 'name' => $tax['name'],
+ 'amount' => $tax['amount'],
+ 'sort_order' => $sort_order,
+ ];
+
+ InvoiceTotal::create($invoice_tax_total);
+
+ $sort_order++;
+ }
+ }
+
+ // Added invoice total total
+ $invoice_total = [
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'total',
+ 'name' => 'invoices.total',
+ 'amount' => $sub_total + $tax_total,
+ 'sort_order' => $sort_order,
+ ];
+
+ InvoiceTotal::create($invoice_total);
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/Incomes/Revenues.php b/app/Http/Controllers/Api/Incomes/Revenues.php
new file mode 100755
index 0000000..b0a9595
--- /dev/null
+++ b/app/Http/Controllers/Api/Incomes/Revenues.php
@@ -0,0 +1,77 @@
+collect();
+
+ return $this->response->paginator($revenues, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Revenue $revenue
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Revenue $revenue)
+ {
+ return $this->response->item($revenue, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $revenue = Revenue::create($request->all());
+
+ return $this->response->created(url('api/revenues/'.$revenue->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $revenue
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Revenue $revenue, Request $request)
+ {
+ $revenue->update($request->all());
+
+ return $this->response->item($revenue->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Revenue $revenue
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Revenue $revenue)
+ {
+ $revenue->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Settings/Categories.php b/app/Http/Controllers/Api/Settings/Categories.php
new file mode 100755
index 0000000..4fc3bd7
--- /dev/null
+++ b/app/Http/Controllers/Api/Settings/Categories.php
@@ -0,0 +1,77 @@
+response->paginator($categories, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Category $category
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Category $category)
+ {
+ return $this->response->item($category, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $category = Category::create($request->all());
+
+ return $this->response->created(url('api/categories/'.$category->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $category
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Category $category, Request $request)
+ {
+ $category->update($request->all());
+
+ return $this->response->item($category->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Category $category
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Category $category)
+ {
+ $category->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Settings/Currencies.php b/app/Http/Controllers/Api/Settings/Currencies.php
new file mode 100755
index 0000000..2b59b2c
--- /dev/null
+++ b/app/Http/Controllers/Api/Settings/Currencies.php
@@ -0,0 +1,77 @@
+response->paginator($currencies, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Currency $currency
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Currency $currency)
+ {
+ return $this->response->item($currency, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $currency = Currency::create($request->all());
+
+ return $this->response->created(url('api/currencies/'.$currency->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $currency
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Currency $currency, Request $request)
+ {
+ $currency->update($request->all());
+
+ return $this->response->item($currency->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Currency $currency
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Currency $currency)
+ {
+ $currency->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Settings/Settings.php b/app/Http/Controllers/Api/Settings/Settings.php
new file mode 100755
index 0000000..d2a75f1
--- /dev/null
+++ b/app/Http/Controllers/Api/Settings/Settings.php
@@ -0,0 +1,84 @@
+response->collection($settings, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int|string $id
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show($id)
+ {
+ // Check if we're querying by id or key
+ if (is_numeric($id)) {
+ $setting = Setting::find($id);
+ } else {
+ $setting = Setting::where('key', $id)->first();
+ }
+
+ return $this->response->item($setting, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $setting = Setting::create($request->all());
+
+ return $this->response->created(url('api/settings/'.$setting->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $setting
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Setting $setting, Request $request)
+ {
+ $setting->update($request->all());
+
+ return $this->response->item($setting->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Setting $setting
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Setting $setting)
+ {
+ $setting->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/Api/Settings/Taxes.php b/app/Http/Controllers/Api/Settings/Taxes.php
new file mode 100755
index 0000000..5484e60
--- /dev/null
+++ b/app/Http/Controllers/Api/Settings/Taxes.php
@@ -0,0 +1,77 @@
+response->paginator($taxes, new Transformer());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param Tax $tax
+ * @return \Dingo\Api\Http\Response
+ */
+ public function show(Tax $tax)
+ {
+ return $this->response->item($tax, new Transformer());
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $tax = Tax::create($request->all());
+
+ return $this->response->created(url('api/taxes/'.$tax->id));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $tax
+ * @param $request
+ * @return \Dingo\Api\Http\Response
+ */
+ public function update(Tax $tax, Request $request)
+ {
+ $tax->update($request->all());
+
+ return $this->response->item($tax->fresh(), new Transformer());
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Tax $tax
+ * @return \Dingo\Api\Http\Response
+ */
+ public function destroy(Tax $tax)
+ {
+ $tax->delete();
+
+ return $this->response->noContent();
+ }
+}
diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php
new file mode 100755
index 0000000..a1f7574
--- /dev/null
+++ b/app/Http/Controllers/ApiController.php
@@ -0,0 +1,25 @@
+expectsJson()) {
+ throw new ResourceException('Validation Error', $errors);
+ }
+
+ return redirect()->to($this->getRedirectUrl())->withInput($request->input())->withErrors($errors, $this->errorBag());
+ }
+}
diff --git a/app/Http/Controllers/Auth/Forgot.php b/app/Http/Controllers/Auth/Forgot.php
new file mode 100755
index 0000000..ccc5b15
--- /dev/null
+++ b/app/Http/Controllers/Auth/Forgot.php
@@ -0,0 +1,101 @@
+middleware('guest');
+ }
+
+ /**
+ * Display the form to request a password reset link.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function create()
+ {
+ return view('auth.forgot.create');
+ }
+
+ /**
+ * Send a reset link to the given user.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function store(Request $request)
+ {
+ $this->validateEmail($request);
+
+ // We will send the password reset link to this user. Once we have attempted
+ // to send the link, we will examine the response then see the message we
+ // need to show to the user. Finally, we'll send out a proper response.
+ $response = $this->broker()->sendResetLink(
+ $request->only('email')
+ );
+
+ return $response == Password::RESET_LINK_SENT
+ ? $this->sendResetLinkResponse($response)
+ : $this->sendResetLinkFailedResponse($request, $response);
+ }
+
+ /**
+ * Get the response for a successful password reset link.
+ *
+ * @param string $response
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ protected function sendResetLinkResponse($response)
+ {
+ flash(trans($response))->success();
+
+ return redirect($this->redirectTo);
+ }
+
+ /**
+ * Get the response for a failed password reset link.
+ *
+ * @param \Illuminate\Http\Request
+ * @param string $response
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ protected function sendResetLinkFailedResponse(Request $request, $response)
+ {
+ return redirect($this->redirectTo)->withErrors(
+ ['email' => trans($response)]
+ );
+ }
+}
diff --git a/app/Http/Controllers/Auth/Login.php b/app/Http/Controllers/Auth/Login.php
new file mode 100755
index 0000000..ea2aabb
--- /dev/null
+++ b/app/Http/Controllers/Auth/Login.php
@@ -0,0 +1,102 @@
+middleware('guest')->except('logout');
+ }*/
+ public function __construct()
+ {
+ $this->middleware('guest', ['except' => 'destroy']);
+ }
+
+ public function create()
+ {
+ return view('auth.login.create');
+ }
+
+ public function store()
+ {
+ // Attempt to login
+ if (!auth()->attempt(request(['email', 'password']))) {
+ flash(trans('auth.failed'))->error();
+
+ return back();
+ }
+
+ // Get user object
+ $user = auth()->user();
+
+ // Check if user is enabled
+ if (!$user->enabled) {
+ $this->logout();
+
+ flash(trans('auth.disabled'))->error();
+
+ return redirect('auth/login');
+ }
+
+ // Check if is customer
+ if ($user->customer) {
+ $path = session('url.intended', 'customers');
+
+ // Path must start with 'customers' prefix
+ if (!str_contains($path, 'customers')) {
+ $path = 'customers';
+ }
+
+ return redirect($path);
+ }
+
+ return redirect('/');
+ }
+
+ public function destroy()
+ {
+ $this->logout();
+
+ return redirect('auth/login');
+ }
+
+ public function logout()
+ {
+ auth()->logout();
+
+ // Session destroy is required if stored in database
+ if (env('SESSION_DRIVER') == 'database') {
+ $request = app('Illuminate\Http\Request');
+ $request->session()->getHandler()->destroy($request->session()->getId());
+ }
+ }
+}
diff --git a/app/Http/Controllers/Auth/Permissions.php b/app/Http/Controllers/Auth/Permissions.php
new file mode 100755
index 0000000..a51392a
--- /dev/null
+++ b/app/Http/Controllers/Auth/Permissions.php
@@ -0,0 +1,102 @@
+all());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.permissions', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/permissions');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Permission $permission
+ *
+ * @return Response
+ */
+ public function edit(Permission $permission)
+ {
+ return view('auth.permissions.edit', compact('permission'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Permission $permission
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Permission $permission, Request $request)
+ {
+ // Update permission
+ $permission->update($request->all());
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.permissions', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/permissions');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Permission $permission
+ *
+ * @return Response
+ */
+ public function destroy(Permission $permission)
+ {
+ $permission->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.permissions', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/permissions');
+ }
+}
diff --git a/app/Http/Controllers/Auth/Reset.php b/app/Http/Controllers/Auth/Reset.php
new file mode 100755
index 0000000..5644dde
--- /dev/null
+++ b/app/Http/Controllers/Auth/Reset.php
@@ -0,0 +1,114 @@
+middleware('guest');
+ }
+
+ public function create(Request $request, $token = null)
+ {
+ return view('auth.reset.create')->with(
+ ['token' => $token, 'email' => $request->email]
+ );
+ }
+
+ public function store(Request $request)
+ {
+ $this->validate($request, $this->rules(), $this->validationErrorMessages());
+
+ // Here we will attempt to reset the user's password. If it is successful we
+ // will update the password on an actual user model and persist it to the
+ // database. Otherwise we will parse the error and return the response.
+ $response = $this->broker()->reset(
+ $this->credentials($request), function ($user, $password) {
+ $this->resetPassword($user, $password);
+ }
+ );
+
+ // If the password was successfully reset, we will redirect the user back to
+ // the application's home authenticated view. If there is an error we can
+ // redirect them back to where they came from with their error message.
+ return $response == Password::PASSWORD_RESET
+ ? $this->sendResetResponse($response)
+ : $this->sendResetFailedResponse($request, $response);
+ }
+
+ /**
+ * Reset the given user's password.
+ *
+ * @param \Illuminate\Contracts\Auth\CanResetPassword $user
+ * @param string $password
+ * @return void
+ */
+ protected function resetPassword($user, $password)
+ {
+ $user->forceFill([
+ 'password' => $password,
+ 'remember_token' => Str::random(60),
+ ])->save();
+
+ $this->guard()->login($user);
+ }
+
+ /**
+ * Get the response for a successful password reset.
+ *
+ * @param string $response
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ protected function sendResetResponse($response)
+ {
+ flash(trans($response))->success();
+
+ return redirect($this->redirectTo);
+ }
+
+ /**
+ * Get the response for a failed password reset.
+ *
+ * @param \Illuminate\Http\Request
+ * @param string $response
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ protected function sendResetFailedResponse(Request $request, $response)
+ {
+ return redirect()->back()
+ ->withInput($request->only('email'))
+ ->withErrors(['email' => trans($response)]);
+ }
+}
diff --git a/app/Http/Controllers/Auth/Roles.php b/app/Http/Controllers/Auth/Roles.php
new file mode 100755
index 0000000..d371c2b
--- /dev/null
+++ b/app/Http/Controllers/Auth/Roles.php
@@ -0,0 +1,116 @@
+all());
+
+ // Attach permissions
+ $role->permissions()->attach($request['permissions']);
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.roles', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/roles');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Role $role
+ *
+ * @return Response
+ */
+ public function edit(Role $role)
+ {
+ //$permissions = Permission::all()->sortBy('display_name');
+ $permissions = Permission::all();
+
+ $rolePermissions = $role->permissions->pluck('id', 'id')->toArray();
+
+ return view('auth.roles.edit', compact('role', 'permissions', 'rolePermissions'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Role $role
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Role $role, Request $request)
+ {
+ // Update role
+ $role->update($request->all());
+
+ // Sync permissions
+ $role->permissions()->sync($request['permissions']);
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.roles', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/roles');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Role $role
+ *
+ * @return Response
+ */
+ public function destroy(Role $role)
+ {
+ $role->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.roles', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/roles');
+ }
+}
diff --git a/app/Http/Controllers/Auth/Users.php b/app/Http/Controllers/Auth/Users.php
new file mode 100755
index 0000000..f3d6a69
--- /dev/null
+++ b/app/Http/Controllers/Auth/Users.php
@@ -0,0 +1,319 @@
+collect();
+
+ $roles = collect(Role::all()->pluck('display_name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.roles', 2)]), '');
+
+ return view('auth.users.index', compact('users', 'roles'));
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $roles = Role::all()->reject(function ($r) {
+ return $r->hasPermission('read-customer-panel');
+ });
+
+ $companies = Auth::user()->companies()->get()->sortBy('name');
+
+ foreach ($companies as $company) {
+ $company->setSettings();
+ }
+
+ return view('auth.users.create', compact('roles', 'companies'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ // Create user
+ $user = User::create($request->input());
+
+ // Upload picture
+ if ($request->file('picture')) {
+ $media = $this->getMedia($request->file('picture'), 'users');
+
+ $user->attachMedia($media, 'picture');
+ }
+
+ // Attach roles
+ $user->roles()->attach($request['roles']);
+
+ // Attach companies
+ $user->companies()->attach($request['companies']);
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.users', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/users');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function edit(User $user)
+ {
+ if ($user->customer) {
+ // Show only roles with customer permission
+ $roles = Role::all()->reject(function ($r) {
+ return !$r->hasPermission('read-customer-panel');
+ });
+ } else {
+ // Don't show roles with customer permission
+ $roles = Role::all()->reject(function ($r) {
+ return $r->hasPermission('read-customer-panel');
+ });
+ }
+
+ $companies = Auth::user()->companies()->get()->sortBy('name');
+
+ foreach ($companies as $company) {
+ $company->setSettings();
+ }
+
+ return view('auth.users.edit', compact('user', 'companies', 'roles'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param User $user
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(User $user, Request $request)
+ {
+ // Do not reset password if not entered/changed
+ if (empty($request['password'])) {
+ unset($request['password']);
+ unset($request['password_confirmation']);
+ }
+
+ // Update user
+ $user->update($request->input());
+
+ // Upload picture
+ if ($request->file('picture')) {
+ $media = $this->getMedia($request->file('picture'), 'users');
+
+ $user->attachMedia($media, 'picture');
+ }
+
+ // Sync roles
+ $user->roles()->sync($request['roles']);
+
+ // Sync companies
+ $user->companies()->sync($request['companies']);
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.users', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/users');
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function enable(User $user)
+ {
+ $user->enabled = 1;
+ $user->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.users', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('users.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function disable(User $user)
+ {
+ $user->enabled = 0;
+ $user->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.users', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('users.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function destroy(User $user)
+ {
+ // Can't delete yourself
+ if ($user->id == \Auth::user()->id) {
+ $message = trans('auth.error.self_delete');
+
+ flash($message)->error();
+
+ return redirect('auth/users');
+ }
+
+ $user->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.users', 1)]);
+
+ flash($message)->success();
+
+ return redirect('auth/users');
+ }
+
+ /**
+ * Mark upcoming bills notifications are read and redirect to bills page.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function readUpcomingBills(User $user)
+ {
+ // Mark bill notifications as read
+ foreach ($user->unreadNotifications as $notification) {
+ // Not a bill notification
+ if ($notification->getAttribute('type') != 'App\Notifications\Expense\Bill') {
+ continue;
+ }
+
+ $notification->markAsRead();
+ }
+
+ // Redirect to bills
+ return redirect('expenses/bills');
+ }
+
+ /**
+ * Mark overdue invoices notifications are read and redirect to invoices page.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function readOverdueInvoices(User $user)
+ {
+ // Mark invoice notifications as read
+ foreach ($user->unreadNotifications as $notification) {
+ // Not an invoice notification
+ if ($notification->getAttribute('type') != 'App\Notifications\Income\Invoice') {
+ continue;
+ }
+
+ $notification->markAsRead();
+ }
+
+ // Redirect to invoices
+ return redirect('incomes/invoices');
+ }
+
+ /**
+ * Mark items out of stock notifications are read and redirect to items page.
+ *
+ * @param User $user
+ *
+ * @return Response
+ */
+ public function readItemsOutOfStock(User $user)
+ {
+ // Mark item notifications as read
+ foreach ($user->unreadNotifications as $notification) {
+ // Not an item notification
+ if ($notification->getAttribute('type') != 'App\Notifications\Common\Item') {
+ continue;
+ }
+
+ $notification->markAsRead();
+ }
+
+ // Redirect to items
+ return redirect('common/items');
+ }
+
+ public function autocomplete(ARequest $request)
+ {
+ $user = false;
+ $data = false;
+
+ $column = $request['column'];
+ $value = $request['value'];
+
+ if (!empty($column) && !empty($value)) {
+ switch ($column) {
+ case 'id':
+ $user = User::find((int) $value);
+ break;
+ case 'email':
+ $user = User::where('email', $value)->first();
+ break;
+ default:
+ $user = User::where($column, $value)->first();
+ }
+
+ $data = $user;
+ } elseif (!empty($column) && empty($value)) {
+ $data = trans('validation.required', ['attribute' => $column]);
+ }
+
+ return response()->json([
+ 'errors' => ($user) ? false : true,
+ 'success' => ($user) ? true : false,
+ 'data' => $data
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Banking/Accounts.php b/app/Http/Controllers/Banking/Accounts.php
new file mode 100755
index 0000000..2440014
--- /dev/null
+++ b/app/Http/Controllers/Banking/Accounts.php
@@ -0,0 +1,255 @@
+pluck('name', 'code');
+
+ $currency = Currency::where('code', '=', setting('general.default_currency', 'USD'))->first();
+
+ return view('banking.accounts.create', compact('currencies', 'currency'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $account = Account::create($request->all());
+
+ // Set default account
+ if ($request['default_account']) {
+ setting()->set('general.default_account', $account->id);
+ setting()->save();
+ }
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.accounts', 1)]);
+
+ flash($message)->success();
+
+ return redirect('banking/accounts');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Account $account
+ *
+ * @return Response
+ */
+ public function edit(Account $account)
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ $account->default_account = ($account->id == setting('general.default_account')) ? 1 : 0;
+
+ $currency = Currency::where('code', '=', $account->currency_code)->first();
+
+ return view('banking.accounts.edit', compact('account', 'currencies', 'currency'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Account $account
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Account $account, Request $request)
+ {
+ // Check if we can disable it
+ if (!$request['enabled']) {
+ if ($account->id == setting('general.default_account')) {
+ $relationships[] = strtolower(trans_choice('general.companies', 1));
+ }
+ }
+
+ if (empty($relationships)) {
+ $account->update($request->all());
+
+ // Set default account
+ if ($request['default_account']) {
+ setting()->set('general.default_account', $account->id);
+ setting()->save();
+ }
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.accounts', 1)]);
+
+ flash($message)->success();
+
+ return redirect('banking/accounts');
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $account->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect('banking/accounts/' . $account->id . '/edit');
+ }
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Account $account
+ *
+ * @return Response
+ */
+ public function enable(Account $account)
+ {
+ $account->enabled = 1;
+ $account->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.accounts', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('accounts.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Account $account
+ *
+ * @return Response
+ */
+ public function disable(Account $account)
+ {
+ if ($account->id == setting('general.default_account')) {
+ $relationships[] = strtolower(trans_choice('general.companies', 1));
+ }
+
+ if (empty($relationships)) {
+ $account->enabled = 0;
+ $account->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.accounts', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $account->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect()->route('accounts.index');
+ }
+
+ return redirect()->route('accounts.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Account $account
+ *
+ * @return Response
+ */
+ public function destroy(Account $account)
+ {
+ $relationships = $this->countRelationships($account, [
+ 'bill_payments' => 'bills',
+ 'payments' => 'payments',
+ 'invoice_payments' => 'invoices',
+ 'revenues' => 'revenues',
+ ]);
+
+ if ($account->id == setting('general.default_account')) {
+ $relationships[] = strtolower(trans_choice('general.companies', 1));
+ }
+
+ if (empty($relationships)) {
+ $account->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.accounts', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $account->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect('banking/accounts');
+ }
+
+ public function currency()
+ {
+ $account_id = (int) request('account_id');
+
+ if (empty($account_id)) {
+ return response()->json([]);
+ }
+
+ $account = Account::find($account_id);
+
+ if (empty($account)) {
+ return response()->json([]);
+ }
+
+ $currency_code = setting('general.default_currency');
+
+ if (isset($account->currency_code)) {
+ $currencies = Currency::enabled()->pluck('name', 'code')->toArray();
+
+ if (array_key_exists($account->currency_code, $currencies)) {
+ $currency_code = $account->currency_code;
+ }
+ }
+
+ // Get currency object
+ $currency = Currency::where('code', $currency_code)->first();
+
+ $account->currency_name = $currency->name;
+ $account->currency_code = $currency_code;
+ $account->currency_rate = $currency->rate;
+
+ $account->thousands_separator = $currency->thousands_separator;
+ $account->decimal_mark = $currency->decimal_mark;
+ $account->precision = (int) $currency->precision;
+ $account->symbol_first = $currency->symbol_first;
+ $account->symbol = $currency->symbol;
+
+ return response()->json($account);
+ }
+}
diff --git a/app/Http/Controllers/Banking/Transactions.php b/app/Http/Controllers/Banking/Transactions.php
new file mode 100755
index 0000000..d108eb7
--- /dev/null
+++ b/app/Http/Controllers/Banking/Transactions.php
@@ -0,0 +1,100 @@
+pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.accounts', 2)]), '');
+
+ $types = collect(['expense' => 'Expense', 'income' => 'Income'])
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.types', 2)]), '');
+
+ $categories = collect(Category::enabled()->type('income')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.categories', 2)]), '');
+
+ $type = $request->get('type');
+
+ if ($type != 'income') {
+ $this->addTransactions(Payment::collect(['paid_at'=> 'desc']), trans_choice('general.expenses', 1));
+ $this->addTransactions(BillPayment::collect(['paid_at'=> 'desc']), trans_choice('general.expenses', 1), trans_choice('general.bills', 1));
+ }
+
+ if ($type != 'expense') {
+ $this->addTransactions(Revenue::collect(['paid_at'=> 'desc']), trans_choice('general.incomes', 1));
+ $this->addTransactions(InvoicePayment::collect(['paid_at'=> 'desc']), trans_choice('general.incomes', 1), trans_choice('general.invoices', 1));
+ }
+
+ $transactions = $this->getTransactions($request);
+
+ return view('banking.transactions.index', compact('transactions', 'accounts', 'types', 'categories'));
+ }
+
+ /**
+ * Add items to transactions array.
+ *
+ * @param $items
+ * @param $type
+ * @param $category
+ */
+ protected function addTransactions($items, $type, $category = null)
+ {
+ foreach ($items as $item) {
+ $data = [
+ 'paid_at' => $item->paid_at,
+ 'account_name' => $item->account->name,
+ 'type' => $type,
+ 'description' => $item->description,
+ 'amount' => $item->amount,
+ 'currency_code' => $item->currency_code,
+ ];
+
+ if (!is_null($category)) {
+ $data['category_name'] = $category;
+ } else {
+ $data['category_name'] = $item->category->name;
+ }
+
+ $this->transactions[] = (object) $data;
+ }
+ }
+
+ protected function getTransactions($request)
+ {
+ // Sort items
+ if (isset($request['sort'])) {
+ if ($request['order'] == 'asc') {
+ $f = 'sortBy';
+ } else {
+ $f = 'sortByDesc';
+ }
+
+ $transactions = collect($this->transactions)->$f($request['sort']);
+ } else {
+ $transactions = collect($this->transactions)->sortByDesc('paid_at');
+ }
+
+ return $transactions;
+ }
+}
diff --git a/app/Http/Controllers/Banking/Transfers.php b/app/Http/Controllers/Banking/Transfers.php
new file mode 100755
index 0000000..e7e42d9
--- /dev/null
+++ b/app/Http/Controllers/Banking/Transfers.php
@@ -0,0 +1,346 @@
+collect(['payment.paid_at' => 'desc']);
+
+ $accounts = collect(Account::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.accounts', 2)]), '');
+
+ $transfers = array();
+
+ foreach ($items as $item) {
+ $revenue = $item->revenue;
+ $payment = $item->payment;
+
+ $name = trans('transfers.messages.delete', [
+ 'from' => $payment->account->name,
+ 'to' => $revenue->account->name,
+ 'amount' => money($payment->amount, $payment->currency_code, true)
+ ]);
+
+ $transfers[] = (object)[
+ 'id' => $item->id,
+ 'name' => $name,
+ 'from_account' => $payment->account->name,
+ 'to_account' => $revenue->account->name,
+ 'amount' => $payment->amount,
+ 'currency_code' => $payment->currency_code,
+ 'paid_at' => $payment->paid_at,
+ ];
+ }
+
+ $special_key = array(
+ 'payment.name' => 'from_account',
+ 'revenue.name' => 'to_account',
+ );
+
+ if (isset($request['sort']) && array_key_exists($request['sort'], $special_key)) {
+ $sort_order = array();
+
+ foreach ($transfers as $key => $value) {
+ $sort = $request['sort'];
+
+ if (array_key_exists($request['sort'], $special_key)) {
+ $sort = $special_key[$request['sort']];
+ }
+
+ $sort_order[$key] = $value->{$sort};
+ }
+
+ $sort_type = (isset($request['order']) && $request['order'] == 'asc') ? SORT_ASC : SORT_DESC;
+
+ array_multisort($sort_order, $sort_type, $transfers);
+ }
+
+ return view('banking.transfers.index', compact('transfers', 'items', 'accounts'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function show()
+ {
+ return redirect('banking/transfers');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ $currency = Currency::where('code', '=', setting('general.default_currency', 'USD'))->first();
+
+ return view('banking.transfers.create', compact('accounts', 'payment_methods', 'currency'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+
+ $payment_currency_code = Account::where('id', $request['from_account_id'])->pluck('currency_code')->first();
+ $revenue_currency_code = Account::where('id', $request['to_account_id'])->pluck('currency_code')->first();
+
+ $payment_request = [
+ 'company_id' => $request['company_id'],
+ 'account_id' => $request['from_account_id'],
+ 'paid_at' => $request['transferred_at'],
+ 'currency_code' => $payment_currency_code,
+ 'currency_rate' => $currencies[$payment_currency_code],
+ 'amount' => $request['amount'],
+ 'vendor_id' => 0,
+ 'description' => $request['description'],
+ 'category_id' => Category::transfer(), // Transfer Category ID
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference'],
+ ];
+
+ $payment = Payment::create($payment_request);
+
+ // Convert amount if not same currency
+ if ($payment_currency_code != $revenue_currency_code) {
+ $default_currency = setting('general.default_currency', 'USD');
+
+ $default_amount = $request['amount'];
+
+ if ($default_currency != $payment_currency_code) {
+ $default_amount_model = new Transfer();
+
+ $default_amount_model->default_currency_code = $default_currency;
+ $default_amount_model->amount = $request['amount'];
+ $default_amount_model->currency_code = $payment_currency_code;
+ $default_amount_model->currency_rate = $currencies[$payment_currency_code];
+
+ $default_amount = $default_amount_model->getDivideConvertedAmount();
+ }
+
+ $transfer_amount = new Transfer();
+
+ $transfer_amount->default_currency_code = $payment_currency_code;
+ $transfer_amount->amount = $default_amount;
+ $transfer_amount->currency_code = $revenue_currency_code;
+ $transfer_amount->currency_rate = $currencies[$revenue_currency_code];
+
+ $amount = $transfer_amount->getDynamicConvertedAmount();
+ } else {
+ $amount = $request['amount'];
+ }
+
+ $revenue_request = [
+ 'company_id' => $request['company_id'],
+ 'account_id' => $request['to_account_id'],
+ 'paid_at' => $request['transferred_at'],
+ 'currency_code' => $revenue_currency_code,
+ 'currency_rate' => $currencies[$revenue_currency_code],
+ 'amount' => $amount,
+ 'customer_id' => 0,
+ 'description' => $request['description'],
+ 'category_id' => Category::transfer(), // Transfer Category ID
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference'],
+ ];
+
+ $revenue = Revenue::create($revenue_request);
+
+ $transfer_request = [
+ 'company_id' => $request['company_id'],
+ 'payment_id' => $payment->id,
+ 'revenue_id' => $revenue->id,
+ ];
+
+ Transfer::create($transfer_request);
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.transfers', 1)]);
+
+ flash($message)->success();
+
+ return redirect('banking/transfers');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function edit(Transfer $transfer)
+ {
+ $payment = Payment::findOrFail($transfer->payment_id);
+ $revenue = Revenue::findOrFail($transfer->revenue_id);
+
+ $transfer['from_account_id'] = $payment->account_id;
+ $transfer['to_account_id'] = $revenue->account_id;
+ $transfer['transferred_at'] = Date::parse($payment->paid_at)->format('Y-m-d');
+ $transfer['description'] = $payment->description;
+ $transfer['amount'] = $payment->amount;
+ $transfer['payment_method'] = $payment->payment_method;
+ $transfer['reference'] = $payment->reference;
+
+ $account = Account::find($payment->account_id);
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ $currency = Currency::where('code', '=', $account->currency_code)->first();
+
+ return view('banking.transfers.edit', compact('transfer', 'accounts', 'payment_methods', 'currency'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Transfer $transfer
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Transfer $transfer, Request $request)
+ {
+ $currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+
+ $payment_currency_code = Account::where('id', $request['from_account_id'])->pluck('currency_code')->first();
+ $revenue_currency_code = Account::where('id', $request['to_account_id'])->pluck('currency_code')->first();
+
+ $payment = Payment::findOrFail($transfer->payment_id);
+ $revenue = Revenue::findOrFail($transfer->revenue_id);
+
+ $payment_request = [
+ 'company_id' => $request['company_id'],
+ 'account_id' => $request['from_account_id'],
+ 'paid_at' => $request['transferred_at'],
+ 'currency_code' => $payment_currency_code,
+ 'currency_rate' => $currencies[$payment_currency_code],
+ 'amount' => $request['amount'],
+ 'vendor_id' => 0,
+ 'description' => $request['description'],
+ 'category_id' => Category::transfer(), // Transfer Category ID
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference'],
+ ];
+
+ $payment->update($payment_request);
+
+ // Convert amount if not same currency
+ if ($payment_currency_code != $revenue_currency_code) {
+ $default_currency = setting('general.default_currency', 'USD');
+
+ $default_amount = $request['amount'];
+
+ if ($default_currency != $payment_currency_code) {
+ $default_amount_model = new Transfer();
+
+ $default_amount_model->default_currency_code = $default_currency;
+ $default_amount_model->amount = $request['amount'];
+ $default_amount_model->currency_code = $payment_currency_code;
+ $default_amount_model->currency_rate = $currencies[$payment_currency_code];
+
+ $default_amount = $default_amount_model->getDivideConvertedAmount();
+ }
+
+ $transfer_amount = new Transfer();
+
+ $transfer_amount->default_currency_code = $payment_currency_code;
+ $transfer_amount->amount = $default_amount;
+ $transfer_amount->currency_code = $revenue_currency_code;
+ $transfer_amount->currency_rate = $currencies[$revenue_currency_code];
+
+ $amount = $transfer_amount->getDynamicConvertedAmount();
+ } else {
+ $amount = $request['amount'];
+ }
+
+ $revenue_request = [
+ 'company_id' => $request['company_id'],
+ 'account_id' => $request['to_account_id'],
+ 'paid_at' => $request['transferred_at'],
+ 'currency_code' => $revenue_currency_code,
+ 'currency_rate' => $currencies[$revenue_currency_code],
+ 'amount' => $amount,
+ 'customer_id' => 0,
+ 'description' => $request['description'],
+ 'category_id' => Category::transfer(), // Transfer Category ID
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference'],
+ ];
+
+ $revenue->update($revenue_request);
+
+ $transfer_request = [
+ 'company_id' => $request['company_id'],
+ 'payment_id' => $payment->id,
+ 'revenue_id' => $revenue->id,
+ ];
+
+ $transfer->update($transfer_request);
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.transfers', 1)]);
+
+ flash($message)->success();
+
+ return redirect('banking/transfers');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Transfer $transfer
+ *
+ * @return Response
+ */
+ public function destroy(Transfer $transfer)
+ {
+ $payment = Payment::findOrFail($transfer->payment_id);
+ $revenue = Revenue::findOrFail($transfer->revenue_id);
+
+ $payment->delete();
+ $revenue->delete();
+ $transfer->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.transfers', 1)]);
+
+ flash($message)->success();
+
+ return redirect('banking/transfers');
+ }
+}
diff --git a/app/Http/Controllers/Common/Companies.php b/app/Http/Controllers/Common/Companies.php
new file mode 100755
index 0000000..9ed22af
--- /dev/null
+++ b/app/Http/Controllers/Common/Companies.php
@@ -0,0 +1,287 @@
+setSettings();
+ }
+
+ return view('common.companies.index', compact('companies'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function show()
+ {
+ return redirect('common/companies');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ return view('common.companies.create', compact('currencies'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ setting()->forgetAll();
+
+ // Create company
+ $company = Company::create($request->input());
+
+ // Create settings
+ setting()->set('general.company_name', $request->get('company_name'));
+ setting()->set('general.company_email', $request->get('company_email'));
+ setting()->set('general.company_address', $request->get('company_address'));
+
+ if ($request->file('company_logo')) {
+ $company_logo = $this->getMedia($request->file('company_logo'), 'settings', $company->id);
+
+ if ($company_logo) {
+ $company->attachMedia($company_logo, 'company_logo');
+
+ setting()->set('general.company_logo', $company_logo->id);
+ }
+ }
+
+ setting()->set('general.default_currency', $request->get('default_currency'));
+ setting()->set('general.default_locale', session('locale'));
+
+ setting()->setExtraColumns(['company_id' => $company->id]);
+ setting()->save();
+
+ // Redirect
+ $message = trans('messages.success.added', ['type' => trans_choice('general.companies', 1)]);
+
+ flash($message)->success();
+
+ return redirect('common/companies');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Company $company
+ *
+ * @return Response
+ */
+ public function edit(Company $company)
+ {
+ // Check if user can edit company
+ if (!$this->isUserCompany($company)) {
+ $message = trans('companies.error.not_user_company');
+
+ flash($message)->error();
+
+ return redirect('common/companies');
+ }
+
+ $company->setSettings();
+
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ return view('common.companies.edit', compact('company', 'currencies'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Company $company
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Company $company, Request $request)
+ {
+ // Check if user can update company
+ if (!$this->isUserCompany($company)) {
+ $message = trans('companies.error.not_user_company');
+
+ flash($message)->error();
+
+ return redirect('common/companies');
+ }
+
+ // Update company
+ $company->update($request->input());
+
+ // Get the company settings
+ setting()->forgetAll();
+ setting()->setExtraColumns(['company_id' => $company->id]);
+ setting()->load(true);
+
+ // Update settings
+ setting()->set('general.company_name', $request->get('company_name'));
+ setting()->set('general.company_email', $request->get('company_email'));
+ setting()->set('general.company_address', $request->get('company_address'));
+
+ if ($request->file('company_logo')) {
+ $company_logo = $this->getMedia($request->file('company_logo'), 'settings', $company->id);
+
+ if ($company_logo) {
+ $company->attachMedia($company_logo, 'company_logo');
+
+ setting()->set('general.company_logo', $company_logo->id);
+ }
+ }
+
+ setting()->set('general.default_payment_method', 'offlinepayment.cash.1');
+ setting()->set('general.default_currency', $request->get('default_currency'));
+
+ setting()->save();
+
+ // Redirect
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.companies', 1)]);
+
+ flash($message)->success();
+
+ return redirect('common/companies');
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Company $company
+ *
+ * @return Response
+ */
+ public function enable(Company $company)
+ {
+ $company->enabled = 1;
+ $company->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.companies', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('companies.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Company $company
+ *
+ * @return Response
+ */
+ public function disable(Company $company)
+ {
+ // Check if user can update company
+ if (!$this->isUserCompany($company)) {
+ $message = trans('companies.error.not_user_company');
+
+ flash($message)->error();
+
+ return redirect()->route('companies.index');
+ }
+
+ $company->enabled = 0;
+ $company->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.companies', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('companies.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Company $company
+ *
+ * @return Response
+ */
+ public function destroy(Company $company)
+ {
+ // Can't delete active company
+ if ($company->id == session('company_id')) {
+ $message = trans('companies.error.delete_active');
+
+ flash($message)->error();
+
+ return redirect('common/companies');
+ }
+
+ $company->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.companies', 1)]);
+
+ flash($message)->success();
+
+ return redirect('common/companies');
+ }
+
+ /**
+ * Change the active company.
+ *
+ * @param Company $company
+ *
+ * @return Response
+ */
+ public function set(Company $company)
+ {
+ // Check if user can manage company
+ if ($this->isUserCompany($company)) {
+ session(['company_id' => $company->id]);
+
+ event(new CompanySwitched($company));
+ }
+
+ return redirect('/');
+ }
+
+ /**
+ * Check user company assignment
+ *
+ * @param Company $company
+ *
+ * @return boolean
+ */
+ public function isUserCompany(Company $company)
+ {
+ $companies = auth()->user()->companies()->pluck('id')->toArray();
+
+ if (in_array($company->id, $companies)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Http/Controllers/Common/Dashboard.php b/app/Http/Controllers/Common/Dashboard.php
new file mode 100755
index 0000000..50ec8bb
--- /dev/null
+++ b/app/Http/Controllers/Common/Dashboard.php
@@ -0,0 +1,439 @@
+ [], 'labels' => [], 'values' => []];
+
+ public $expense_donut = ['colors' => [], 'labels' => [], 'values' => []];
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return Response
+ */
+ public function index()
+ {
+ $this->today = Date::today();
+
+ list($total_incomes, $total_expenses, $total_profit) = $this->getTotals();
+
+ $cashflow = $this->getCashFlow();
+
+ list($donut_incomes, $donut_expenses) = $this->getDonuts();
+
+ $accounts = Account::enabled()->take(6)->get();
+
+ $latest_incomes = $this->getLatestIncomes();
+
+ $latest_expenses = $this->getLatestExpenses();
+
+ return view('common.dashboard.index', compact(
+ 'total_incomes',
+ 'total_expenses',
+ 'total_profit',
+ 'cashflow',
+ 'donut_incomes',
+ 'donut_expenses',
+ 'accounts',
+ 'latest_incomes',
+ 'latest_expenses'
+ ));
+ }
+
+ public function cashFlow()
+ {
+ $this->today = Date::today();
+
+ $content = $this->getCashFlow()->render();
+
+ //return response()->setContent($content)->send();
+
+ echo $content;
+ }
+
+ private function getTotals()
+ {
+ list($incomes_amount, $open_invoice, $overdue_invoice, $expenses_amount, $open_bill, $overdue_bill) = $this->calculateAmounts();
+
+ $incomes_progress = 100;
+
+ if (!empty($open_invoice) && !empty($overdue_invoice)) {
+ $incomes_progress = (int) ($open_invoice * 100) / ($open_invoice + $overdue_invoice);
+ }
+
+ // Totals
+ $total_incomes = array(
+ 'total' => $incomes_amount,
+ 'open_invoice' => money($open_invoice, setting('general.default_currency'), true),
+ 'overdue_invoice' => money($overdue_invoice, setting('general.default_currency'), true),
+ 'progress' => $incomes_progress
+ );
+
+ $expenses_progress = 100;
+
+ if (!empty($open_bill) && !empty($overdue_bill)) {
+ $expenses_progress = (int) ($open_bill * 100) / ($open_bill + $overdue_bill);
+ }
+
+ $total_expenses = array(
+ 'total' => $expenses_amount,
+ 'open_bill' => money($open_bill, setting('general.default_currency'), true),
+ 'overdue_bill' => money($overdue_bill, setting('general.default_currency'), true),
+ 'progress' => $expenses_progress
+ );
+
+ $amount_profit = $incomes_amount - $expenses_amount;
+ $open_profit = $open_invoice - $open_bill;
+ $overdue_profit = $overdue_invoice - $overdue_bill;
+
+ $total_progress = 100;
+
+ if (!empty($open_profit) && !empty($overdue_profit)) {
+ $total_progress = (int) ($open_profit * 100) / ($open_profit + $overdue_profit);
+ }
+
+ $total_profit = array(
+ 'total' => $amount_profit,
+ 'open' => money($open_profit, setting('general.default_currency'), true),
+ 'overdue' => money($overdue_profit, setting('general.default_currency'), true),
+ 'progress' => $total_progress
+ );
+
+ return array($total_incomes, $total_expenses, $total_profit);
+ }
+
+ private function getCashFlow()
+ {
+ $start = Date::parse(request('start', $this->today->startOfYear()->format('Y-m-d')));
+ $end = Date::parse(request('end', $this->today->endOfYear()->format('Y-m-d')));
+ $period = request('period', 'month');
+
+ $start_month = $start->month;
+ $end_month = $end->month;
+
+ // Monthly
+ $labels = array();
+
+ $s = clone $start;
+
+ for ($j = $end_month; $j >= $start_month; $j--) {
+ $labels[$end_month - $j] = $s->format('M Y');
+
+ if ($period == 'month') {
+ $s->addMonth();
+ } else {
+ $s->addMonths(3);
+ $j -= 2;
+ }
+ }
+
+ $income = $this->calculateCashFlowTotals('income', $start, $end, $period);
+ $expense = $this->calculateCashFlowTotals('expense', $start, $end, $period);
+
+ $profit = $this->calculateCashFlowProfit($income, $expense);
+
+ $chart = Charts::multi('line', 'chartjs')
+ ->dimensions(0, 300)
+ ->colors(['#6da252', '#00c0ef', '#F56954'])
+ ->dataset(trans_choice('general.profits', 1), $profit)
+ ->dataset(trans_choice('general.incomes', 1), $income)
+ ->dataset(trans_choice('general.expenses', 1), $expense)
+ ->labels($labels)
+ ->credits(false)
+ ->view('vendor.consoletvs.charts.chartjs.multi.line');
+
+ return $chart;
+ }
+
+ private function getDonuts()
+ {
+ // Show donut prorated if there is no income
+ if (array_sum($this->income_donut['values']) == 0) {
+ foreach ($this->income_donut['values'] as $key => $value) {
+ $this->income_donut['values'][$key] = 1;
+ }
+ }
+
+ // Get 6 categories by amount
+ $colors = $labels = [];
+ $values = collect($this->income_donut['values'])->sort()->reverse()->take(6)->all();
+ foreach ($values as $id => $val) {
+ $colors[$id] = $this->income_donut['colors'][$id];
+ $labels[$id] = $this->income_donut['labels'][$id];
+ }
+
+ $donut_incomes = Charts::create('donut', 'chartjs')
+ ->colors($colors)
+ ->labels($labels)
+ ->values($values)
+ ->dimensions(0, 160)
+ ->credits(false)
+ ->view('vendor.consoletvs.charts.chartjs.donut');
+
+ // Show donut prorated if there is no expense
+ if (array_sum($this->expense_donut['values']) == 0) {
+ foreach ($this->expense_donut['values'] as $key => $value) {
+ $this->expense_donut['values'][$key] = 1;
+ }
+ }
+
+ // Get 6 categories by amount
+ $colors = $labels = [];
+ $values = collect($this->expense_donut['values'])->sort()->reverse()->take(6)->all();
+ foreach ($values as $id => $val) {
+ $colors[$id] = $this->expense_donut['colors'][$id];
+ $labels[$id] = $this->expense_donut['labels'][$id];
+ }
+
+ $donut_expenses = Charts::create('donut', 'chartjs')
+ ->colors($colors)
+ ->labels($labels)
+ ->values($values)
+ ->dimensions(0, 160)
+ ->credits(false)
+ ->view('vendor.consoletvs.charts.chartjs.donut');
+
+ return array($donut_incomes, $donut_expenses);
+ }
+
+ private function getLatestIncomes()
+ {
+ $invoices = collect(Invoice::orderBy('invoiced_at', 'desc')->accrued()->take(10)->get())->each(function ($item) {
+ $item->paid_at = $item->invoiced_at;
+ });
+
+ $revenues = collect(Revenue::orderBy('paid_at', 'desc')->isNotTransfer()->take(10)->get());
+
+ $latest = $revenues->merge($invoices)->take(5)->sortByDesc('paid_at');
+
+ return $latest;
+ }
+
+ private function getLatestExpenses()
+ {
+ $bills = collect(Bill::orderBy('billed_at', 'desc')->accrued()->take(10)->get())->each(function ($item) {
+ $item->paid_at = $item->billed_at;
+ });
+
+ $payments = collect(Payment::orderBy('paid_at', 'desc')->isNotTransfer()->take(10)->get());
+
+ $latest = $payments->merge($bills)->take(5)->sortByDesc('paid_at');
+
+ return $latest;
+ }
+
+ private function calculateAmounts()
+ {
+ $incomes_amount = $open_invoice = $overdue_invoice = 0;
+ $expenses_amount = $open_bill = $overdue_bill = 0;
+
+ // Get categories
+ $categories = Category::with(['bills', 'invoices', 'payments', 'revenues'])->orWhere('type', 'income')->orWhere('type', 'expense')->enabled()->get();
+
+ foreach ($categories as $category) {
+ switch ($category->type) {
+ case 'income':
+ $amount = 0;
+
+ // Revenues
+ foreach ($category->revenues as $revenue) {
+ $amount += $revenue->getConvertedAmount();
+ }
+
+ $incomes_amount += $amount;
+
+ // Invoices
+ $invoices = $category->invoices()->accrued()->get();
+ foreach ($invoices as $invoice) {
+ list($paid, $open, $overdue) = $this->calculateInvoiceBillTotals($invoice, 'invoice');
+
+ $incomes_amount += $paid;
+ $open_invoice += $open;
+ $overdue_invoice += $overdue;
+
+ $amount += $paid;
+ }
+
+ $this->addToIncomeDonut($category->color, $amount, $category->name);
+
+ break;
+ case 'expense':
+ $amount = 0;
+
+ // Payments
+ foreach ($category->payments as $payment) {
+ $amount += $payment->getConvertedAmount();
+ }
+
+ $expenses_amount += $amount;
+
+ // Bills
+ $bills = $category->bills()->accrued()->get();
+ foreach ($bills as $bill) {
+ list($paid, $open, $overdue) = $this->calculateInvoiceBillTotals($bill, 'bill');
+
+ $expenses_amount += $paid;
+ $open_bill += $open;
+ $overdue_bill += $overdue;
+
+ $amount += $paid;
+ }
+
+ $this->addToExpenseDonut($category->color, $amount, $category->name);
+
+ break;
+ }
+ }
+
+ return array($incomes_amount, $open_invoice, $overdue_invoice, $expenses_amount, $open_bill, $overdue_bill);
+ }
+
+ private function calculateCashFlowTotals($type, $start, $end, $period)
+ {
+ $totals = array();
+
+ if ($type == 'income') {
+ $m1 = '\App\Models\Income\Revenue';
+ $m2 = '\App\Models\Income\InvoicePayment';
+ } else {
+ $m1 = '\App\Models\Expense\Payment';
+ $m2 = '\App\Models\Expense\BillPayment';
+ }
+
+ $date_format = 'Y-m';
+
+ if ($period == 'month') {
+ $n = 1;
+ $start_date = $start->format($date_format);
+ $end_date = $end->format($date_format);
+ $next_date = $start_date;
+ } else {
+ $n = 3;
+ $start_date = $start->quarter;
+ $end_date = $end->quarter;
+ $next_date = $start_date;
+ }
+
+ $s = clone $start;
+
+ //$totals[$start_date] = 0;
+ while ($next_date <= $end_date) {
+ $totals[$next_date] = 0;
+
+ if ($period == 'month') {
+ $next_date = $s->addMonths($n)->format($date_format);
+ } else {
+ if (isset($totals[4])) {
+ break;
+ }
+
+ $next_date = $s->addMonths($n)->quarter;
+ }
+ }
+
+ $items_1 = $m1::whereBetween('paid_at', [$start, $end])->isNotTransfer()->get();
+
+ $this->setCashFlowTotals($totals, $items_1, $date_format, $period);
+
+ $items_2 = $m2::whereBetween('paid_at', [$start, $end])->get();
+
+ $this->setCashFlowTotals($totals, $items_2, $date_format, $period);
+
+ return $totals;
+ }
+
+ private function setCashFlowTotals(&$totals, $items, $date_format, $period)
+ {
+ foreach ($items as $item) {
+ if ($period == 'month') {
+ $i = Date::parse($item->paid_at)->format($date_format);
+ } else {
+ $i = Date::parse($item->paid_at)->quarter;
+ }
+
+ if (!isset($totals[$i])) {
+ continue;
+ }
+
+ $totals[$i] += $item->getConvertedAmount();
+ }
+ }
+
+ private function calculateCashFlowProfit($incomes, $expenses)
+ {
+ $profit = [];
+
+ foreach ($incomes as $key => $income) {
+ if ($income > 0 && $income > $expenses[$key]) {
+ $profit[$key] = $income - $expenses[$key];
+ } else {
+ $profit[$key] = 0;
+ }
+ }
+
+ return $profit;
+ }
+
+ private function calculateInvoiceBillTotals($item, $type)
+ {
+ $paid = $open = $overdue = 0;
+
+ $today = $this->today->toDateString();
+
+ $paid += $item->getConvertedAmount();
+
+ $code_field = $type . '_status_code';
+
+ if ($item->$code_field != 'paid') {
+ $payments = 0;
+
+ if ($item->$code_field == 'partial') {
+ foreach ($item->payments as $payment) {
+ $payments += $payment->getConvertedAmount();
+ }
+ }
+
+ // Check if it's open or overdue invoice
+ if ($item->due_at > $today) {
+ $open += $item->getConvertedAmount() - $payments;
+ } else {
+ $overdue += $item->getConvertedAmount() - $payments;
+ }
+ }
+
+ return array($paid, $open, $overdue);
+ }
+
+ private function addToIncomeDonut($color, $amount, $text)
+ {
+ $this->income_donut['colors'][] = $color;
+ $this->income_donut['labels'][] = money($amount, setting('general.default_currency'), true)->format() . ' - ' . $text;
+ $this->income_donut['values'][] = (int) $amount;
+ }
+
+ private function addToExpenseDonut($color, $amount, $text)
+ {
+ $this->expense_donut['colors'][] = $color;
+ $this->expense_donut['labels'][] = money($amount, setting('general.default_currency'), true)->format() . ' - ' . $text;
+ $this->expense_donut['values'][] = (int) $amount;
+ }
+}
diff --git a/app/Http/Controllers/Common/Import.php b/app/Http/Controllers/Common/Import.php
new file mode 100755
index 0000000..3ea718b
--- /dev/null
+++ b/app/Http/Controllers/Common/Import.php
@@ -0,0 +1,22 @@
+collect();
+
+ $categories = Category::enabled()->orderBy('name')->type('item')->pluck('name', 'id')
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.categories', 2)]), '');
+
+ return view('common.items.index', compact('items', 'categories'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function show()
+ {
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $categories = Category::enabled()->orderBy('name')->type('item')->pluck('name', 'id');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $currency = Currency::where('code', '=', setting('general.default_currency', 'USD'))->first();
+
+ return view('common.items.create', compact('categories', 'taxes', 'currency'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $item = Item::create($request->input());
+
+ // Upload picture
+ if ($request->file('picture')) {
+ $media = $this->getMedia($request->file('picture'), 'items');
+
+ $item->attachMedia($media, 'picture');
+ }
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.items', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Item $item
+ *
+ * @return Response
+ */
+ public function duplicate(Item $item)
+ {
+ $clone = $item->duplicate();
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.items', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('items.edit', $item->id);
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ if (!Import::createFromFile($import, 'Common\Item')) {
+ return redirect('common/import/common/items');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.items', 2)]);
+
+ flash($message)->success();
+
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Item $item
+ *
+ * @return Response
+ */
+ public function edit(Item $item)
+ {
+ $categories = Category::enabled()->orderBy('name')->type('item')->pluck('name', 'id');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $currency = Currency::where('code', '=', setting('general.default_currency', 'USD'))->first();
+
+ return view('common.items.edit', compact('item', 'categories', 'taxes', 'currency'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Item $item
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Item $item, Request $request)
+ {
+ $item->update($request->input());
+
+ // Upload picture
+ if ($request->file('picture')) {
+ $media = $this->getMedia($request->file('picture'), 'items');
+
+ $item->attachMedia($media, 'picture');
+ }
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.items', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Item $item
+ *
+ * @return Response
+ */
+ public function enable(Item $item)
+ {
+ $item->enabled = 1;
+ $item->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.items', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Item $item
+ *
+ * @return Response
+ */
+ public function disable(Item $item)
+ {
+ $item->enabled = 0;
+ $item->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.items', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Item $item
+ *
+ * @return Response
+ */
+ public function destroy(Item $item)
+ {
+ $relationships = $this->countRelationships($item, [
+ 'invoice_items' => 'invoices',
+ 'bill_items' => 'bills',
+ ]);
+
+ if (empty($relationships)) {
+ $item->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.items', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $item->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect()->route('items.index');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('items', function($excel) {
+ $excel->sheet('items', function($sheet) {
+ $sheet->fromModel(Item::filter(request()->input())->get()->makeHidden([
+ 'id', 'company_id', 'item_id', 'created_at', 'updated_at', 'deleted_at'
+ ]));
+ });
+ })->download('xlsx');
+ }
+
+ public function autocomplete()
+ {
+ $type = request('type');
+ $query = request('query');
+ $currency_code = request('currency_code');
+
+ if (empty($currency_code) || (strtolower($currency_code) == 'null')) {
+ $currency_code = setting('general.default_currency');
+ }
+
+ $currency = Currency::where('code', $currency_code)->first();
+
+ $autocomplete = Item::autocomplete([
+ 'name' => $query,
+ 'sku' => $query,
+ ]);
+
+ if ($type == 'invoice') {
+ $autocomplete->quantity();
+ }
+
+ $items = $autocomplete->get();
+
+ if ($items) {
+ foreach ($items as $item) {
+ $tax = Tax::find($item->tax_id);
+
+ $item_tax_price = 0;
+
+ if (!empty($tax)) {
+ $item_tax_price = ($item->sale_price / 100) * $tax->rate;
+ }
+
+ //$item->sale_price = $this->convertPrice($item->sale_price, $currency_code, $currency->rate);
+ //$item->purchase_price = $this->convertPrice($item->purchase_price, $currency_code, $currency->rate);
+
+ switch ($type) {
+ case 'bill':
+ $total = $item->purchase_price + $item_tax_price;
+ break;
+ case 'invoice':
+ default:
+ $total = $item->sale_price + $item_tax_price;
+ break;
+ }
+
+ $item->total = money($total, $currency_code, true)->format();
+ }
+ }
+
+ return response()->json($items);
+ }
+
+ public function totalItem(TRequest $request)
+ {
+ $input_items = $request->input('item');
+ $currency_code = $request->input('currency_code');
+ $discount = $request->input('discount');
+
+ if (empty($currency_code)) {
+ $currency_code = setting('general.default_currency');
+ }
+
+ $json = new \stdClass;
+
+ $sub_total = 0;
+ $tax_total = 0;
+
+ $items = array();
+
+ if ($input_items) {
+ foreach ($input_items as $key => $item) {
+ $price = (double) $item['price'];
+ $quantity = (double) $item['quantity'];
+
+ $item_tax_total= 0;
+ $item_sub_total = ($price * $quantity);
+
+ if (!empty($item['tax_id'])) {
+ $tax = Tax::find($item['tax_id']);
+
+ $item_tax_total = (($price * $quantity) / 100) * $tax->rate;
+ }
+
+ $sub_total += $item_sub_total;
+
+ // Apply discount to tax
+ if ($discount) {
+ $item_tax_total = $item_tax_total - ($item_tax_total * ($discount / 100));
+ }
+
+ $tax_total += $item_tax_total;
+
+ $items[$key] = money($item_sub_total, $currency_code, true)->format();
+ }
+ }
+
+ $json->items = $items;
+
+ $json->sub_total = money($sub_total, $currency_code, true)->format();
+
+ $json->discount_text= trans('invoices.add_discount');
+ $json->discount_total = '';
+
+ $json->tax_total = money($tax_total, $currency_code, true)->format();
+
+ // Apply discount to total
+ if ($discount) {
+ $json->discount_text= trans('invoices.show_discount', ['discount' => $discount]);
+ $json->discount_total = money($sub_total * ($discount / 100), $currency_code, true)->format();
+
+ $sub_total = $sub_total - ($sub_total * ($discount / 100));
+ }
+
+ $grand_total = $sub_total + $tax_total;
+
+ $json->grand_total = money($grand_total, $currency_code, true)->format();
+
+ // Get currency object
+ $currency = Currency::where('code', $currency_code)->first();
+
+ $json->currency_name = $currency->name;
+ $json->currency_code = $currency_code;
+ $json->currency_rate = $currency->rate;
+
+ $json->thousands_separator = $currency->thousands_separator;
+ $json->decimal_mark = $currency->decimal_mark;
+ $json->precision = (int) $currency->precision;
+ $json->symbol_first = $currency->symbol_first;
+ $json->symbol = $currency->symbol;
+
+ return response()->json($json);
+ }
+
+ protected function convertPrice($amount, $currency_code, $currency_rate, $format = false, $reverse = false)
+ {
+ $item = new Item();
+
+ $item->amount = $amount;
+ $item->currency_code = $currency_code;
+ $item->currency_rate = $currency_rate;
+
+ if ($reverse) {
+ return $item->getReverseConvertedAmount($format);
+ }
+
+ return $item->getConvertedAmount($format);
+ }
+}
diff --git a/app/Http/Controllers/Common/Search.php b/app/Http/Controllers/Common/Search.php
new file mode 100755
index 0000000..d8c6d0f
--- /dev/null
+++ b/app/Http/Controllers/Common/Search.php
@@ -0,0 +1,130 @@
+with('category')->get()->sortBy('name');
+
+ return view('items.items.index', compact('items'));
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return Response
+ */
+ public function search()
+ {
+ $results = array();
+
+ $keyword = request('keyword');
+
+ $accounts = Account::enabled()->search($keyword)->get();
+
+ if ($accounts->count()) {
+ foreach ($accounts as $account) {
+ $results[] = (object)[
+ 'id' => $account->id,
+ 'name' => $account->name,
+ 'type' => trans_choice('general.accounts', 1),
+ 'color' => '#337ab7',
+ 'href' => url('banking/accounts/' . $account->id . '/edit'),
+ ];
+ }
+ }
+
+ $items = Item::enabled()->search($keyword)->get();
+
+ if ($items->count()) {
+ foreach ($items as $item) {
+ $results[] = (object)[
+ 'id' => $item->id,
+ 'name' => $item->name,
+ 'type' => trans_choice('general.items', 1),
+ 'color' => '#f5bd65',
+ 'href' => url('common/items/' . $item->id . '/edit'),
+ ];
+ }
+ }
+
+ $invoices = Invoice::search($keyword)->get();
+
+ if ($invoices->count()) {
+ foreach ($invoices as $invoice) {
+ $results[] = (object)[
+ 'id' => $invoice->id,
+ 'name' => $invoice->invoice_number . ' - ' . $invoice->customer_name,
+ 'type' => trans_choice('general.invoices', 1),
+ 'color' => '#00c0ef',
+ 'href' => url('incomes/invoices/' . $invoice->id),
+ ];
+ }
+ }
+
+ //$revenues = Revenue::search($keyword)->get();
+
+ $customers = Customer::enabled()->search($keyword)->get();
+
+ if ($customers->count()) {
+ foreach ($customers as $customer) {
+ $results[] = (object)[
+ 'id' => $customer->id,
+ 'name' => $customer->name,
+ 'type' => trans_choice('general.customers', 1),
+ 'color' => '#03d876',
+ 'href' => url('incomes/customers/' . $customer->id),
+ ];
+ }
+ }
+
+ $bills = Bill::search($keyword)->get();
+
+ if ($bills->count()) {
+ foreach ($bills as $bill) {
+ $results[] = (object)[
+ 'id' => $bill->id,
+ 'name' => $bill->bill_number . ' - ' . $bill->vendor_name,
+ 'type' => trans_choice('general.bills', 1),
+ 'color' => '#dd4b39',
+ 'href' => url('expenses/bills/' . $bill->id),
+ ];
+ }
+ }
+
+ //$payments = Payment::search($keyword)->get();
+
+ $vendors = Vendor::enabled()->search($keyword)->get();
+
+ if ($vendors->count()) {
+ foreach ($vendors as $vendor) {
+ $results[] = (object)[
+ 'id' => $vendor->id,
+ 'name' => $vendor->name,
+ 'type' => trans_choice('general.vendors', 1),
+ 'color' => '#ff8373',
+ 'href' => url('expenses/vendors/' . $vendor->id),
+ ];
+ }
+ }
+
+ return response()->json((object) $results);
+ }
+}
diff --git a/app/Http/Controllers/Common/Uploads.php b/app/Http/Controllers/Common/Uploads.php
new file mode 100755
index 0000000..3c8bb73
--- /dev/null
+++ b/app/Http/Controllers/Common/Uploads.php
@@ -0,0 +1,115 @@
+getPath($media)) {
+ return false;
+ }
+
+ return response()->file($path);
+ }
+
+ /**
+ * Download the specified resource.
+ *
+ * @param $id
+ * @return mixed
+ */
+ public function download($id)
+ {
+ $media = Media::find($id);
+
+ // Get file path
+ if (!$path = $this->getPath($media)) {
+ return false;
+ }
+
+ return response()->download($path);
+ }
+
+ /**
+ * Destroy the specified resource.
+ *
+ * @param $id
+ * @return callable
+ */
+ public function destroy($id, Request $request)
+ {
+ $media = Media::find($id);
+
+ // Get file path
+ if (!$path = $this->getPath($media)) {
+ $message = trans('messages.warning.deleted', ['name' => $media->basename, 'text' => $media->basename]);
+
+ flash($message)->warning();
+
+ return back();
+ }
+
+ $media->delete(); //will not delete files
+
+ File::delete($path);
+
+ if (!empty($request->input('page'))) {
+ switch ($request->input('page')) {
+ case 'setting':
+ setting()->set($request->input('key'), '');
+
+ setting()->save();
+ break;
+ default;
+ }
+ }
+
+ return back();
+ }
+
+ /**
+ * Get the full path of resource.
+ *
+ * @param $media
+ * @return boolean|string
+ */
+ protected function getPath($media)
+ {
+ $path = $media->basename;
+
+ if (!empty($media->directory)) {
+ $folders = explode('/', $media->directory);
+
+ // Check if company can access media
+ if ($folders[0] != session('company_id')) {
+ return false;
+ }
+
+ $path = $media->directory . '/' . $media->basename;
+ }
+
+ if (!Storage::exists($path)) {
+ return false;
+ }
+
+ $full_path = Storage::path($path);
+
+ return $full_path;
+ }
+}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
new file mode 100755
index 0000000..d050ae3
--- /dev/null
+++ b/app/Http/Controllers/Controller.php
@@ -0,0 +1,112 @@
+runningInConsole()) {
+ return;
+ }
+
+ $route = app(Route::class);
+
+ // Get the controller array
+ $arr = array_reverse(explode('\\', explode('@', $route->getAction()['uses'])[0]));
+
+ $controller = '';
+
+ // Add folder
+ if (strtolower($arr[1]) != 'controllers') {
+ $controller .= kebab_case($arr[1]) . '-';
+ }
+
+ // Add module
+ if (isset($arr[3]) && isset($arr[4]) && (strtolower($arr[4]) == 'modules')) {
+ $controller .= kebab_case($arr[3]) . '-';
+ }
+
+ // Add file
+ $controller .= kebab_case($arr[0]);
+
+ // Skip ACL
+ $skip = ['common-dashboard', 'customers-dashboard'];
+ if (in_array($controller, $skip)) {
+ return;
+ }
+
+ // Add CRUD permission check
+ $this->middleware('permission:create-' . $controller)->only(['create', 'store', 'duplicate', 'import']);
+ $this->middleware('permission:read-' . $controller)->only(['index', 'show', 'edit', 'export']);
+ $this->middleware('permission:update-' . $controller)->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-' . $controller)->only('destroy');
+ }
+
+ public function countRelationships($model, $relationships)
+ {
+ $counter = array();
+
+ foreach ($relationships as $relationship => $text) {
+ if ($c = $model->$relationship()->count()) {
+ $counter[] = $c . ' ' . strtolower(trans_choice('general.' . $text, ($c > 1) ? 2 : 1));
+ }
+ }
+
+ return $counter;
+ }
+
+ /**
+ * Check for api token and redirect if empty.
+ *
+ * @return mixed
+ */
+ public function checkApiToken()
+ {
+ if (setting('general.api_token')) {
+ return;
+ }
+
+ redirect('apps/token/create')->send();
+ }
+
+ /**
+ * Mass delete relationships with events being fired.
+ *
+ * @param $model
+ * @param $relationships
+ *
+ * @return void
+ */
+ public function deleteRelationships($model, $relationships)
+ {
+ foreach ((array) $relationships as $relationship) {
+ if (empty($model->$relationship)) {
+ continue;
+ }
+
+ $items = $model->$relationship->all();
+
+ if ($items instanceof Collection) {
+ $items = $items->all();
+ }
+
+ foreach ((array) $items as $item) {
+ $item->delete();
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Customers/Dashboard.php b/app/Http/Controllers/Customers/Dashboard.php
new file mode 100755
index 0000000..ac3014b
--- /dev/null
+++ b/app/Http/Controllers/Customers/Dashboard.php
@@ -0,0 +1,125 @@
+user()->customer;
+
+ $invoices = Invoice::with('status')->accrued()->where('customer_id', $customer->id)->get();
+
+ $start = Date::parse(request('start', Date::today()->startOfYear()->format('Y-m-d')));
+ $end = Date::parse(request('end', Date::today()->endOfYear()->format('Y-m-d')));
+
+ $start_month = $start->month;
+ $end_month = $end->month;
+
+ // Monthly
+ $labels = [];
+
+ $s = clone $start;
+
+ for ($j = $end_month; $j >= $start_month; $j--) {
+ $labels[$end_month - $j] = $s->format('M Y');
+
+ $s->addMonth();
+ }
+
+ $unpaid = $paid = $overdue = $partial_paid = [];
+
+ foreach ($invoices as $invoice) {
+ switch ($invoice->invoice_status_code) {
+ case 'paid':
+ $paid[] = $invoice;
+ break;
+ case 'partial':
+ $partial_paid[] = $invoice;
+ break;
+ case 'sent':
+ default:
+ if (Date::today()->format('Y-m-d') > $invoice->due_at->format('Y-m-d')) {
+ $overdue[] = $invoice;
+ } else {
+ $unpaid[] = $invoice;
+ }
+ }
+ }
+
+ $total = count($unpaid) + count($paid) + count($partial_paid) + count($overdue);
+
+ $progress = [
+ 'unpaid' => count($unpaid),
+ 'paid' => count($paid),
+ 'overdue' => count($overdue),
+ 'partially_paid' => count($partial_paid),
+ 'total' => $total,
+ ];
+
+ $unpaid = $this->calculateTotals($unpaid, $start, $end, 'unpaid');
+ $paid = $this->calculateTotals($paid, $start, $end, 'paid');
+ $partial_paid = $this->calculateTotals($partial_paid, $start, $end, 'partial');
+ $overdue = $this->calculateTotals($overdue, $start, $end, 'overdue');
+
+ $chart = Charts::multi('line', 'chartjs')
+ ->dimensions(0, 300)
+ ->colors(['#dd4b39', '#6da252', '#f39c12', '#00c0ef'])
+ ->dataset(trans('general.unpaid'), $unpaid)
+ ->dataset(trans('general.paid'), $paid)
+ ->dataset(trans('general.overdue'), $overdue)
+ ->dataset(trans('general.partially_paid'), $partial_paid)
+ ->labels($labels)
+ ->credits(false)
+ ->view('vendor.consoletvs.charts.chartjs.multi.line');
+
+ return view('customers.dashboard.index', compact('customer', 'invoices', 'progress', 'chart'));
+ }
+
+ private function calculateTotals($items, $start, $end, $type)
+ {
+ $totals = [];
+
+ $date_format = 'Y-m';
+
+ $n = 1;
+ $start_date = $start->format($date_format);
+ $end_date = $end->format($date_format);
+ $next_date = $start_date;
+
+ $s = clone $start;
+
+ while ($next_date <= $end_date) {
+ $totals[$next_date] = 0;
+
+ $next_date = $s->addMonths($n)->format($date_format);
+ }
+
+ $this->setTotals($totals, $items, $date_format, $type);
+
+ return $totals;
+ }
+
+ private function setTotals(&$totals, $items, $date_format, $type)
+ {
+ foreach ($items as $item) {
+ if ($type == 'partial') {
+ $item->amount = $item->payments()->paid();
+ }
+
+ $i = Date::parse($item->paid_at)->format($date_format);
+
+ $totals[$i] += $item->getConvertedAmount();
+ }
+ }
+}
diff --git a/app/Http/Controllers/Customers/Invoices.php b/app/Http/Controllers/Customers/Invoices.php
new file mode 100755
index 0000000..aea447a
--- /dev/null
+++ b/app/Http/Controllers/Customers/Invoices.php
@@ -0,0 +1,181 @@
+accrued()->where('customer_id', auth()->user()->customer->id)->paginate();
+
+ $status = collect(InvoiceStatus::all()->pluck('name', 'code'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.statuses', 2)]), '');
+
+ return view('customers.invoices.index', compact('invoices', 'status'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function show(Invoice $invoice)
+ {
+ $paid = 0;
+
+ foreach ($invoice->payments as $item) {
+ $amount = $item->amount;
+
+ if ($invoice->currency_code != $item->currency_code) {
+ $item->default_currency_code = $invoice->currency_code;
+
+ $amount = $item->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+
+ $invoice->paid = $paid;
+
+ $accounts = Account::enabled()->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $customers = Customer::enabled()->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('income')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('customers.invoices.show', compact('invoice', 'accounts', 'currencies', 'account_currency_code', 'customers', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function printInvoice(Invoice $invoice)
+ {
+ $invoice = $this->prepareInvoice($invoice);
+
+ $logo = $this->getLogo();
+
+ return view($invoice->template_path, compact('invoice', 'logo'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function pdfInvoice(Invoice $invoice)
+ {
+ $invoice = $this->prepareInvoice($invoice);
+
+ $logo = $this->getLogo();
+
+ $html = view($invoice->template_path, compact('invoice', 'logo'))->render();
+
+ $pdf = \App::make('dompdf.wrapper');
+ $pdf->loadHTML($html);
+
+ //$pdf->setPaper('A4', 'portrait');
+
+ $file_name = 'invoice_' . time() . '.pdf';
+
+ return $pdf->download($file_name);
+ }
+
+ protected function prepareInvoice(Invoice $invoice)
+ {
+ $paid = 0;
+
+ foreach ($invoice->payments as $item) {
+ $amount = $item->amount;
+
+ if ($invoice->currency_code != $item->currency_code) {
+ $item->default_currency_code = $invoice->currency_code;
+
+ $amount = $item->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+
+ $invoice->paid = $paid;
+
+ $invoice->template_path = 'incomes.invoices.invoice';
+
+ event(new InvoicePrinting($invoice));
+
+ return $invoice;
+ }
+
+ protected function getLogo()
+ {
+ $logo = '';
+
+ $media_id = setting('general.company_logo');
+
+ if (setting('general.invoice_logo')) {
+ $media_id = setting('general.invoice_logo');
+ }
+
+ $media = Media::find($media_id);
+
+ if (!empty($media)) {
+ $path = Storage::path($media->getDiskPath());
+
+ if (!is_file($path)) {
+ return $logo;
+ }
+ } else {
+ $path = asset('public/img/company.png');
+ }
+
+ $image = Image::make($path)->encode()->getEncoded();
+
+ if (empty($image)) {
+ return $logo;
+ }
+
+ $extension = File::extension($path);
+
+ $logo = 'data:image/' . $extension . ';base64,' . base64_encode($image);
+
+ return $logo;
+ }
+}
diff --git a/app/Http/Controllers/Customers/Payments.php b/app/Http/Controllers/Customers/Payments.php
new file mode 100755
index 0000000..8ddadde
--- /dev/null
+++ b/app/Http/Controllers/Customers/Payments.php
@@ -0,0 +1,50 @@
+where('customer_id', '=', Auth::user()->customer->id)->paginate();
+
+ $payment_methods = Modules::getPaymentMethods('all');
+
+ $categories = collect(Category::enabled()->type('income')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.categories', 2)]), '');
+
+ $accounts = collect(Account::enabled()->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.accounts', 2)]), '');
+
+ return view('customers.payments.index', compact('payments', 'payment_methods', 'categories', 'accounts'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param Payment $payment
+ *
+ * @return Response
+ */
+ public function show(Payment $payment)
+ {
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('customers.payments.show', compact('payment', 'payment_methods'));
+ }
+}
diff --git a/app/Http/Controllers/Customers/Profile.php b/app/Http/Controllers/Customers/Profile.php
new file mode 100755
index 0000000..7fea1c0
--- /dev/null
+++ b/app/Http/Controllers/Customers/Profile.php
@@ -0,0 +1,95 @@
+edit();
+ }
+
+ public function show()
+ {
+ return $this->edit();
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @return Response
+ */
+ public function edit()
+ {
+ $user = auth()->user();
+
+ return view('customers.profile.edit', compact('user'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Request $request)
+ {
+ $user = auth()->user();
+
+ // Do not reset password if not entered/changed
+ if (empty($request['password'])) {
+ unset($request['password']);
+ unset($request['password_confirmation']);
+ }
+
+ // Update user
+ $user->update($request->input());
+
+ // Upload picture
+ if ($request->file('picture')) {
+ $media = $this->getMedia($request->file('picture'), 'users');
+
+ $user->attachMedia($media, 'picture');
+ }
+
+ // Update customer
+ $user->customer->update($request->input());
+
+ $message = trans('messages.success.updated', ['type' => trans('auth.profile')]);
+
+ flash($message)->success();
+
+ return redirect('customers/profile/edit');
+ }
+
+ /**
+ * Mark overdue invoices notifications are read and redirect to invoices page.
+ *
+ * @return Response
+ */
+ public function readOverdueInvoices()
+ {
+ $user = auth()->user();
+
+ // Mark invoice notifications as read
+ foreach ($user->unreadNotifications as $notification) {
+ // Not an invoice notification
+ if ($notification->getAttribute('type') != 'App\Notifications\Income\Invoice') {
+ continue;
+ }
+
+ $notification->markAsRead();
+ }
+
+ // Redirect to invoices
+ return redirect('customers/invoices');
+ }
+}
diff --git a/app/Http/Controllers/Customers/Transactions.php b/app/Http/Controllers/Customers/Transactions.php
new file mode 100755
index 0000000..f04d78a
--- /dev/null
+++ b/app/Http/Controllers/Customers/Transactions.php
@@ -0,0 +1,24 @@
+customer->id, 'revenues');
+
+ return view('customers.transactions.index', compact('transactions'));
+ }
+}
diff --git a/app/Http/Controllers/Expenses/Bills.php b/app/Http/Controllers/Expenses/Bills.php
new file mode 100755
index 0000000..9beb38e
--- /dev/null
+++ b/app/Http/Controllers/Expenses/Bills.php
@@ -0,0 +1,854 @@
+collect(['billed_at'=> 'desc']);
+
+ $vendors = collect(Vendor::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.vendors', 2)]), '');
+
+ $statuses = collect(BillStatus::all()->pluck('name', 'code'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.statuses', 2)]), '');
+
+ return view('expenses.bills.index', compact('bills', 'vendors', 'statuses'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function show(Bill $bill)
+ {
+ $paid = 0;
+
+ // Get Bill Payments
+ if ($bill->payments->count()) {
+ $_currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+
+ foreach ($bill->payments as $item) {
+ $default_amount = (double) $item->amount;
+
+ if ($bill->currency_code == $item->currency_code) {
+ $amount = $default_amount;
+ } else {
+ $default_amount_model = new BillPayment();
+
+ $default_amount_model->default_currency_code = $bill->currency_code;
+ $default_amount_model->amount = $default_amount;
+ $default_amount_model->currency_code = $item->currency_code;
+ $default_amount_model->currency_rate = $_currencies[$item->currency_code];
+
+ $default_amount = (double) $default_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new BillPayment();
+
+ $convert_amount->default_currency_code = $item->currency_code;
+ $convert_amount->amount = $default_amount;
+ $convert_amount->currency_code = $bill->currency_code;
+ $convert_amount->currency_rate = $_currencies[$bill->currency_code];
+
+ $amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+ }
+
+ $bill->paid = $paid;
+
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $vendors = Vendor::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('expenses.bills.show', compact('bill', 'accounts', 'currencies', 'account_currency_code', 'vendors', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $vendors = Vendor::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code');
+
+ $currency = Currency::where('code', '=', setting('general.default_currency'))->first();
+
+ $items = Item::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $categories = Category::enabled()->type('expense')->orderBy('name')->pluck('name', 'id');
+
+ return view('expenses.bills.create', compact('vendors', 'currencies', 'currency', 'items', 'taxes', 'categories'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $bill = Bill::create($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'bills');
+
+ $bill->attachMedia($media, 'attachment');
+ }
+
+ $taxes = [];
+
+ $tax_total = 0;
+ $sub_total = 0;
+ $discount_total = 0;
+ $discount = $request['discount'];
+
+ $bill_item = [];
+ $bill_item['company_id'] = $request['company_id'];
+ $bill_item['bill_id'] = $bill->id;
+
+ if ($request['item']) {
+ foreach ($request['item'] as $item) {
+ unset($tax_object);
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+
+ // Increase stock (item bought)
+ $item_object->quantity += $item['quantity'];
+ $item_object->save();
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (((double) $item['price'] * (double) $item['quantity']) / 100) * $tax_object->rate;
+
+ // Apply discount to tax
+ if ($discount) {
+ $tax = $tax - ($tax * ($discount / 100));
+ }
+ }
+
+ $bill_item['item_id'] = $item['item_id'];
+ $bill_item['name'] = str_limit($item['name'], 180, '');
+ $bill_item['sku'] = $item_sku;
+ $bill_item['quantity'] = (double) $item['quantity'];
+ $bill_item['price'] = (double) $item['price'];
+ $bill_item['tax'] = $tax;
+ $bill_item['tax_id'] = $tax_id;
+ $bill_item['total'] = (double) $item['price'] * (double) $item['quantity'];
+
+ BillItem::create($bill_item);
+
+ // Set taxes
+ if (isset($tax_object)) {
+ if (array_key_exists($tax_object->id, $taxes)) {
+ $taxes[$tax_object->id]['amount'] += $tax;
+ } else {
+ $taxes[$tax_object->id] = [
+ 'name' => $tax_object->name,
+ 'amount' => $tax
+ ];
+ }
+ }
+
+ // Calculate totals
+ $tax_total += $tax;
+ $sub_total += $bill_item['total'];
+
+ unset($tax_object);
+ }
+ }
+
+ $s_total = $sub_total;
+
+ // Apply discount to total
+ if ($discount) {
+ $s_discount = $s_total * ($discount / 100);
+ $discount_total += $s_discount;
+ $s_total = $s_total - $s_discount;
+ }
+
+ $amount = $s_total + $tax_total;
+
+ $request['amount'] = money($amount, $request['currency_code'])->getAmount();
+
+ $bill->update($request->input());
+
+ // Add bill totals
+ $this->addTotals($bill, $request, $taxes, $sub_total, $discount_total, $tax_total);
+
+ // Add bill history
+ BillHistory::create([
+ 'company_id' => session('company_id'),
+ 'bill_id' => $bill->id,
+ 'status_code' => 'draft',
+ 'notify' => 0,
+ 'description' => trans('messages.success.added', ['type' => $bill->bill_number]),
+ ]);
+
+ // Recurring
+ $bill->createRecurring();
+
+ // Fire the event to make it extendible
+ event(new BillCreated($bill));
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.bills', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/bills/' . $bill->id);
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function duplicate(Bill $bill)
+ {
+ $clone = $bill->duplicate();
+
+ // Add bill history
+ BillHistory::create([
+ 'company_id' => session('company_id'),
+ 'bill_id' => $clone->id,
+ 'status_code' => 'draft',
+ 'notify' => 0,
+ 'description' => trans('messages.success.added', ['type' => $clone->bill_number]),
+ ]);
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.bills', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/bills/' . $clone->id . '/edit');
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ $success = true;
+
+ $allowed_sheets = ['bills', 'bill_items', 'bill_histories', 'bill_payments', 'bill_totals'];
+
+ // Loop through all sheets
+ $import->each(function ($sheet) use (&$success, $allowed_sheets) {
+ $sheet_title = $sheet->getTitle();
+
+ if (!in_array($sheet_title, $allowed_sheets)) {
+ $message = trans('messages.error.import_sheet');
+
+ flash($message)->error()->important();
+
+ return false;
+ }
+
+ $slug = 'Expense\\' . str_singular(studly_case($sheet_title));
+
+ if (!$success = Import::createFromSheet($sheet, $slug)) {
+ return false;
+ }
+ });
+
+ if (!$success) {
+ return redirect('common/import/expenses/bills');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.bills', 2)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/bills');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function edit(Bill $bill)
+ {
+ $vendors = Vendor::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code');
+
+ $currency = Currency::where('code', '=', $bill->currency_code)->first();
+
+ $items = Item::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $categories = Category::enabled()->type('expense')->orderBy('name')->pluck('name', 'id');
+
+ return view('expenses.bills.edit', compact('bill', 'vendors', 'currencies', 'currency', 'items', 'taxes', 'categories'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Bill $bill
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Bill $bill, Request $request)
+ {
+ $taxes = [];
+ $tax_total = 0;
+ $sub_total = 0;
+ $discount_total = 0;
+ $discount = $request['discount'];
+
+ $bill_item = [];
+ $bill_item['company_id'] = $request['company_id'];
+ $bill_item['bill_id'] = $bill->id;
+
+ if ($request['item']) {
+ $this->deleteRelationships($bill, 'items');
+
+ foreach ($request['item'] as $item) {
+ unset($tax_object);
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (((double) $item['price'] * (double) $item['quantity']) / 100) * $tax_object->rate;
+
+ // Apply discount to tax
+ if ($discount) {
+ $tax = $tax - ($tax * ($discount / 100));
+ }
+ }
+
+ $bill_item['item_id'] = $item['item_id'];
+ $bill_item['name'] = str_limit($item['name'], 180, '');
+ $bill_item['sku'] = $item_sku;
+ $bill_item['quantity'] = (double) $item['quantity'];
+ $bill_item['price'] = (double) $item['price'];
+ $bill_item['tax'] = $tax;
+ $bill_item['tax_id'] = $tax_id;
+ $bill_item['total'] = (double) $item['price'] * (double) $item['quantity'];
+
+ if (isset($tax_object)) {
+ if (array_key_exists($tax_object->id, $taxes)) {
+ $taxes[$tax_object->id]['amount'] += $tax;
+ } else {
+ $taxes[$tax_object->id] = [
+ 'name' => $tax_object->name,
+ 'amount' => $tax
+ ];
+ }
+ }
+
+ $tax_total += $tax;
+ $sub_total += $bill_item['total'];
+
+ BillItem::create($bill_item);
+ }
+ }
+
+ $s_total = $sub_total;
+
+ // Apply discount to total
+ if ($discount) {
+ $s_discount = $s_total * ($discount / 100);
+ $discount_total += $s_discount;
+ $s_total = $s_total - $s_discount;
+ }
+
+ $amount = $s_total + $tax_total;
+
+ $request['amount'] = money($amount, $request['currency_code'])->getAmount();
+
+ $bill->update($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'bills');
+
+ $bill->attachMedia($media, 'attachment');
+ }
+
+ // Delete previous bill totals
+ $this->deleteRelationships($bill, 'totals');
+
+ // Add bill totals
+ $this->addTotals($bill, $request, $taxes, $sub_total, $discount_total, $tax_total);
+
+ // Recurring
+ $bill->updateRecurring();
+
+ // Fire the event to make it extendible
+ event(new BillUpdated($bill));
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.bills', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/bills/' . $bill->id);
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function destroy(Bill $bill)
+ {
+ $this->deleteRelationships($bill, ['items', 'histories', 'payments', 'recurring', 'totals']);
+ $bill->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.bills', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/bills');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('bills', function ($excel) {
+ $bills = Bill::with(['items', 'histories', 'payments', 'totals'])->filter(request()->input())->get();
+
+ $excel->sheet('invoices', function ($sheet) use ($bills) {
+ $sheet->fromModel($bills->makeHidden([
+ 'company_id', 'parent_id', 'created_at', 'updated_at', 'deleted_at', 'attachment', 'discount', 'items', 'histories', 'payments', 'totals', 'media'
+ ]));
+ });
+
+ $tables = ['items', 'histories', 'payments', 'totals'];
+ foreach ($tables as $table) {
+ $excel->sheet('bill_' . $table, function ($sheet) use ($bills, $table) {
+ $hidden_fields = ['id', 'company_id', 'created_at', 'updated_at', 'deleted_at', 'title'];
+
+ $i = 1;
+
+ foreach ($bills as $bill) {
+ $model = $bill->$table->makeHidden($hidden_fields);
+
+ if ($i == 1) {
+ $sheet->fromModel($model, null, 'A1', false);
+ } else {
+ // Don't put multiple heading columns
+ $sheet->fromModel($model, null, 'A1', false, false);
+ }
+
+ $i++;
+ }
+ });
+ }
+ })->download('xlsx');
+ }
+
+ /**
+ * Mark the bill as received.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function markReceived(Bill $bill)
+ {
+ $bill->bill_status_code = 'received';
+ $bill->save();
+
+ flash(trans('bills.messages.received'))->success();
+
+ return redirect()->back();
+ }
+
+ /**
+ * Print the bill.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function printBill(Bill $bill)
+ {
+ $bill = $this->prepareBill($bill);
+
+ return view($bill->template_path, compact('bill'));
+ }
+
+ /**
+ * Download the PDF file of bill.
+ *
+ * @param Bill $bill
+ *
+ * @return Response
+ */
+ public function pdfBill(Bill $bill)
+ {
+ $bill = $this->prepareBill($bill);
+
+ $html = view($bill->template_path, compact('bill'))->render();
+
+ $pdf = \App::make('dompdf.wrapper');
+ $pdf->loadHTML($html);
+
+ $file_name = 'bill_' . time() . '.pdf';
+
+ return $pdf->download($file_name);
+ }
+
+ /**
+ * Add payment to the bill.
+ *
+ * @param PaymentRequest $request
+ *
+ * @return Response
+ */
+ public function payment(PaymentRequest $request)
+ {
+ // Get currency object
+ $currency = Currency::where('code', $request['currency_code'])->first();
+
+ $request['currency_code'] = $currency->code;
+ $request['currency_rate'] = $currency->rate;
+
+ $bill = Bill::find($request['bill_id']);
+
+ $total_amount = $bill->amount;
+
+ $amount = (double) $request['amount'];
+
+ if ($request['currency_code'] != $bill->currency_code) {
+ $request_bill = new Bill();
+
+ $request_bill->amount = (float) $request['amount'];
+ $request_bill->currency_code = $currency->code;
+ $request_bill->currency_rate = $currency->rate;
+
+ $amount = $request_bill->getConvertedAmount();
+ }
+
+ if ($bill->payments()->count()) {
+ $total_amount -= $bill->payments()->paid();
+ }
+
+ // For amount cover integer
+ $multiplier = 1;
+
+ for ($i = 0; $i < $currency->precision; $i++) {
+ $multiplier *= 10;
+ }
+
+ $amount *= $multiplier;
+ $total_amount *= $multiplier;
+
+ if ($amount > $total_amount) {
+ $message = trans('messages.error.over_payment');
+
+ return response()->json([
+ 'success' => false,
+ 'error' => true,
+ 'message' => $message,
+ ]);
+ } elseif ($amount == $total_amount) {
+ $bill->bill_status_code = 'paid';
+ } else {
+ $bill->bill_status_code = 'partial';
+ }
+
+ $bill->save();
+
+ $bill_payment_request = [
+ 'company_id' => $request['company_id'],
+ 'bill_id' => $request['bill_id'],
+ 'account_id' => $request['account_id'],
+ 'paid_at' => $request['paid_at'],
+ 'amount' => $request['amount'],
+ 'currency_code' => $request['currency_code'],
+ 'currency_rate' => $request['currency_rate'],
+ 'description' => $request['description'],
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference']
+ ];
+
+ $bill_payment = BillPayment::create($bill_payment_request);
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'bills');
+
+ $bill_payment->attachMedia($media, 'attachment');
+ }
+
+ $request['status_code'] = $bill->bill_status_code;
+ $request['notify'] = 0;
+
+ $desc_amount = money((float) $request['amount'], (string) $request['currency_code'], true)->format();
+
+ $request['description'] = $desc_amount . ' ' . trans_choice('general.payments', 1);
+
+ BillHistory::create($request->input());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => $message,
+ ]);
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param BillPayment $payment
+ *
+ * @return Response
+ */
+ public function paymentDestroy(BillPayment $payment)
+ {
+ $bill = Bill::find($payment->bill_id);
+
+ if ($bill->payments()->count() > 1) {
+ $bill->bill_status_code = 'partial';
+ } else {
+ $bill->bill_status_code = 'received';
+ }
+
+ $bill->save();
+
+ $desc_amount = money((float) $payment->amount, (string) $payment->currency_code, true)->format();
+
+ $description = $desc_amount . ' ' . trans_choice('general.payments', 1);
+
+ // Add bill history
+ BillHistory::create([
+ 'company_id' => $bill->company_id,
+ 'bill_id' => $bill->id,
+ 'status_code' => $bill->bill_status_code,
+ 'notify' => 0,
+ 'description' => trans('messages.success.deleted', ['type' => $description]),
+ ]);
+
+ $payment->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.bills', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->back();
+ }
+
+ public function addItem(ItemRequest $request)
+ {
+ if ($request['item_row']) {
+ $item_row = $request['item_row'];
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $currency = Currency::where('code', '=', $request['currency_code'])->first();
+
+ // it should be integer for amount mask
+ $currency->precision = (int) $currency->precision;
+
+ $html = view('expenses.bills.item', compact('item_row', 'taxes', 'currency'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => [
+ 'currency' => $currency
+ ],
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ return response()->json([
+ 'success' => false,
+ 'error' => true,
+ 'data' => 'null',
+ 'message' => trans('issue'),
+ 'html' => 'null',
+ ]);
+ }
+
+ protected function prepareBill(Bill $bill)
+ {
+ $paid = 0;
+
+ foreach ($bill->payments as $item) {
+ $amount = $item->amount;
+
+ if ($bill->currency_code != $item->currency_code) {
+ $item->default_currency_code = $bill->currency_code;
+
+ $amount = $item->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+
+ $bill->paid = $paid;
+
+ $bill->template_path = 'expenses.bills.bill';
+
+ //event(new BillPrinting($bill));
+
+ return $bill;
+ }
+
+ protected function addTotals($bill, $request, $taxes, $sub_total, $discount_total, $tax_total)
+ {
+ $sort_order = 1;
+
+ // Added bill sub total
+ BillTotal::create([
+ 'company_id' => $request['company_id'],
+ 'bill_id' => $bill->id,
+ 'code' => 'sub_total',
+ 'name' => 'bills.sub_total',
+ 'amount' => $sub_total,
+ 'sort_order' => $sort_order,
+ ]);
+
+ $sort_order++;
+
+ // Added bill discount
+ if ($discount_total) {
+ BillTotal::create([
+ 'company_id' => $request['company_id'],
+ 'bill_id' => $bill->id,
+ 'code' => 'discount',
+ 'name' => 'bills.discount',
+ 'amount' => $discount_total,
+ 'sort_order' => $sort_order,
+ ]);
+
+ // This is for total
+ $sub_total = $sub_total - $discount_total;
+ }
+
+ $sort_order++;
+
+ // Added bill taxes
+ if ($taxes) {
+ foreach ($taxes as $tax) {
+ BillTotal::create([
+ 'company_id' => $request['company_id'],
+ 'bill_id' => $bill->id,
+ 'code' => 'tax',
+ 'name' => $tax['name'],
+ 'amount' => $tax['amount'],
+ 'sort_order' => $sort_order,
+ ]);
+
+ $sort_order++;
+ }
+ }
+
+ // Added bill total
+ BillTotal::create([
+ 'company_id' => $request['company_id'],
+ 'bill_id' => $bill->id,
+ 'code' => 'total',
+ 'name' => 'bills.total',
+ 'amount' => $sub_total + $tax_total,
+ 'sort_order' => $sort_order,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Expenses/Payments.php b/app/Http/Controllers/Expenses/Payments.php
new file mode 100755
index 0000000..6a7fb1e
--- /dev/null
+++ b/app/Http/Controllers/Expenses/Payments.php
@@ -0,0 +1,238 @@
+isNotTransfer()->collect(['paid_at'=> 'desc']);
+
+ $vendors = collect(Vendor::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.vendors', 2)]), '');
+
+ $categories = collect(Category::enabled()->type('expense')->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.categories', 2)]), '');
+
+ $accounts = collect(Account::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.accounts', 2)]), '');
+
+ $transfer_cat_id = Category::transfer();
+
+ return view('expenses.payments.index', compact('payments', 'vendors', 'categories', 'accounts', 'transfer_cat_id'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function show()
+ {
+ return redirect('expenses/payments');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $currency = Currency::where('code', '=', $account_currency_code)->first();
+
+ $vendors = Vendor::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('expense')->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('expenses.payments.create', compact('accounts', 'currencies', 'account_currency_code', 'currency', 'vendors', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $payment = Payment::create($request->input());
+
+ // Upload attachment
+ $media = $this->getMedia($request->file('attachment'), 'payments');
+
+ if ($media) {
+ $payment->attachMedia($media, 'attachment');
+ }
+
+ // Recurring
+ $payment->createRecurring();
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/payments');
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Payment $payment
+ *
+ * @return Response
+ */
+ public function duplicate(Payment $payment)
+ {
+ $clone = $payment->duplicate();
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.payments', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/payments/' . $clone->id . '/edit');
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ if (!Import::createFromFile($import, 'Expense\Payment')) {
+ return redirect('common/import/expenses/payments');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.payments', 2)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/payments');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Payment $payment
+ *
+ * @return Response
+ */
+ public function edit(Payment $payment)
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', $payment->account_id)->pluck('currency_code')->first();
+
+ $currency = Currency::where('code', '=', $account_currency_code)->first();
+
+ $vendors = Vendor::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('expense')->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('expenses.payments.edit', compact('payment', 'accounts', 'currencies', 'account_currency_code', 'currency', 'vendors', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Payment $payment
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Payment $payment, Request $request)
+ {
+ $payment->update($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'payments');
+
+ $payment->attachMedia($media, 'attachment');
+ }
+
+ // Recurring
+ $payment->updateRecurring();
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.payments', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/payments');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Payment $payment
+ *
+ * @return Response
+ */
+ public function destroy(Payment $payment)
+ {
+ // Can't delete transfer payment
+ if ($payment->category->id == Category::transfer()) {
+ return redirect('expenses/payments');
+ }
+
+ $payment->recurring()->delete();
+ $payment->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.payments', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/payments');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('payments', function($excel) {
+ $excel->sheet('payments', function($sheet) {
+ $sheet->fromModel(Payment::filter(request()->input())->get()->makeHidden([
+ 'id', 'company_id', 'parent_id', 'created_at', 'updated_at', 'deleted_at'
+ ]));
+ });
+ })->download('xlsx');
+ }
+}
diff --git a/app/Http/Controllers/Expenses/Vendors.php b/app/Http/Controllers/Expenses/Vendors.php
new file mode 100755
index 0000000..d8506dc
--- /dev/null
+++ b/app/Http/Controllers/Expenses/Vendors.php
@@ -0,0 +1,363 @@
+ 0,
+ 'open' => 0,
+ 'overdue' => 0,
+ ];
+
+ $counts = [
+ 'bills' => 0,
+ 'payments' => 0,
+ ];
+
+ // Handle bills
+ $bills = Bill::with(['status', 'payments'])->where('vendor_id', $vendor->id)->get();
+
+ $counts['bills'] = $bills->count();
+
+ $bill_payments = [];
+
+ $today = Date::today()->toDateString();
+
+ foreach ($bills as $item) {
+ $payments = 0;
+
+ foreach ($item->payments as $payment) {
+ $payment->category = $item->category;
+
+ $bill_payments[] = $payment;
+
+ $amount = $payment->getConvertedAmount();
+
+ $amounts['paid'] += $amount;
+
+ $payments += $amount;
+ }
+
+ if ($item->bill_status_code == 'paid') {
+ continue;
+ }
+
+ // Check if it's open or overdue invoice
+ if ($item->due_at > $today) {
+ $amounts['open'] += $item->getConvertedAmount() - $payments;
+ } else {
+ $amounts['overdue'] += $item->getConvertedAmount() - $payments;
+ }
+ }
+
+ // Handle payments
+ $payments = Payment::with(['account', 'category'])->where('vendor_id', $vendor->id)->get();
+
+ $counts['payments'] = $payments->count();
+
+ // Prepare data
+ $items = collect($payments)->each(function ($item) use (&$amounts) {
+ $amounts['paid'] += $item->getConvertedAmount();
+ });
+
+ $limit = request('limit', setting('general.list_limit', '25'));
+ $transactions = $this->paginate($items->merge($bill_payments)->sortByDesc('paid_at'), $limit);
+
+ return view('expenses.vendors.show', compact('vendor', 'counts', 'amounts', 'transactions'));
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ return view('expenses.vendors.create', compact('currencies'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $vendor = Vendor::create($request->all());
+
+ // Upload logo
+ if ($request->file('logo')) {
+ $media = $this->getMedia($request->file('logo'), 'vendors');
+
+ $vendor->attachMedia($media, 'logo');
+ }
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.vendors', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/vendors');
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Vendor $vendor
+ *
+ * @return Response
+ */
+ public function duplicate(Vendor $vendor)
+ {
+ $clone = $vendor->duplicate();
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.vendors', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/vendors/' . $clone->id . '/edit');
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ if (!Import::createFromFile($import, 'Expense\Vendor')) {
+ return redirect('common/import/expenses/vendors');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.vendors', 2)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/vendors');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Vendor $vendor
+ *
+ * @return Response
+ */
+ public function edit(Vendor $vendor)
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ return view('expenses.vendors.edit', compact('vendor', 'currencies'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Vendor $vendor
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Vendor $vendor, Request $request)
+ {
+ $vendor->update($request->all());
+
+ // Upload logo
+ if ($request->file('logo')) {
+ $media = $this->getMedia($request->file('logo'), 'vendors');
+
+ $vendor->attachMedia($media, 'logo');
+ }
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.vendors', 1)]);
+
+ flash($message)->success();
+
+ return redirect('expenses/vendors');
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Vendor $vendor
+ *
+ * @return Response
+ */
+ public function enable(Vendor $vendor)
+ {
+ $vendor->enabled = 1;
+ $vendor->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.vendors', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('vendors.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Vendor $vendor
+ *
+ * @return Response
+ */
+ public function disable(Vendor $vendor)
+ {
+ $vendor->enabled = 0;
+ $vendor->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.vendors', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('vendors.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Vendor $vendor
+ *
+ * @return Response
+ */
+ public function destroy(Vendor $vendor)
+ {
+ $relationships = $this->countRelationships($vendor, [
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if (empty($relationships)) {
+ $vendor->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.vendors', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $vendor->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect('expenses/vendors');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('vendors', function($excel) {
+ $excel->sheet('vendors', function($sheet) {
+ $sheet->fromModel(Vendor::filter(request()->input())->get()->makeHidden([
+ 'id', 'company_id', 'created_at', 'updated_at', 'deleted_at'
+ ]));
+ });
+ })->download('xlsx');
+ }
+
+ public function currency()
+ {
+ $vendor_id = (int) request('vendor_id');
+
+ if (empty($vendor_id)) {
+ return response()->json([]);
+ }
+
+ $vendor = Vendor::find($vendor_id);
+
+ if (empty($vendor)) {
+ return response()->json([]);
+ }
+
+ $currency_code = setting('general.default_currency');
+
+ if (isset($vendor->currency_code)) {
+ $currencies = Currency::enabled()->pluck('name', 'code')->toArray();
+
+ if (array_key_exists($vendor->currency_code, $currencies)) {
+ $currency_code = $vendor->currency_code;
+ }
+ }
+
+ // Get currency object
+ $currency = Currency::where('code', $currency_code)->first();
+
+ $vendor->currency_code = $currency_code;
+ $vendor->currency_rate = $currency->rate;
+
+ return response()->json($vendor);
+ }
+
+ public function vendor(Request $request)
+ {
+ $vendor = Vendor::create($request->all());
+
+ return response()->json($vendor);
+ }
+
+ /**
+ * Generate a pagination collection.
+ *
+ * @param array|Collection $items
+ * @param int $perPage
+ * @param int $page
+ * @param array $options
+ *
+ * @return LengthAwarePaginator
+ */
+ public function paginate($items, $perPage = 15, $page = null, $options = [])
+ {
+ $page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
+
+ $items = $items instanceof Collection ? $items : Collection::make($items);
+
+ return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options);
+ }
+}
diff --git a/app/Http/Controllers/Incomes/Customers.php b/app/Http/Controllers/Incomes/Customers.php
new file mode 100755
index 0000000..8bb67dc
--- /dev/null
+++ b/app/Http/Controllers/Incomes/Customers.php
@@ -0,0 +1,430 @@
+ 0,
+ 'open' => 0,
+ 'overdue' => 0,
+ ];
+
+ $counts = [
+ 'invoices' => 0,
+ 'revenues' => 0,
+ ];
+
+ // Handle invoices
+ $invoices = Invoice::with(['status', 'payments'])->where('customer_id', $customer->id)->get();
+
+ $counts['invoices'] = $invoices->count();
+
+ $invoice_payments = [];
+
+ $today = Date::today()->toDateString();
+
+ foreach ($invoices as $item) {
+ $payments = 0;
+
+ foreach ($item->payments as $payment) {
+ $payment->category = $item->category;
+
+ $invoice_payments[] = $payment;
+
+ $amount = $payment->getConvertedAmount();
+
+ $amounts['paid'] += $amount;
+
+ $payments += $amount;
+ }
+
+ if ($item->invoice_status_code == 'paid') {
+ continue;
+ }
+
+ // Check if it's open or overdue invoice
+ if ($item->due_at > $today) {
+ $amounts['open'] += $item->getConvertedAmount() - $payments;
+ } else {
+ $amounts['overdue'] += $item->getConvertedAmount() - $payments;
+ }
+ }
+
+ // Handle revenues
+ $revenues = Revenue::with(['account', 'category'])->where('customer_id', $customer->id)->get();
+
+ $counts['revenues'] = $revenues->count();
+
+ // Prepare data
+ $items = collect($revenues)->each(function ($item) use (&$amounts) {
+ $amounts['paid'] += $item->getConvertedAmount();
+ });
+
+ $limit = request('limit', setting('general.list_limit', '25'));
+ $transactions = $this->paginate($items->merge($invoice_payments)->sortByDesc('paid_at'), $limit);
+
+ return view('incomes.customers.show', compact('customer', 'counts', 'amounts', 'transactions'));
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ return view('incomes.customers.create', compact('currencies'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ if (empty($request->input('create_user'))) {
+ Customer::create($request->all());
+ } else {
+ // Check if user exist
+ $user = User::where('email', $request['email'])->first();
+ if (!empty($user)) {
+ $message = trans('messages.error.customer', ['name' => $user->name]);
+
+ flash($message)->error();
+
+ return redirect()->back()->withInput($request->except('create_user'))->withErrors(
+ ['email' => trans('customers.error.email')]
+ );
+ }
+
+ // Create user first
+ $data = $request->all();
+ $data['locale'] = setting('general.default_locale', 'en-GB');
+
+ $user = User::create($data);
+ $user->roles()->attach(['3']);
+ $user->companies()->attach([session('company_id')]);
+
+ // Finally create customer
+ $request['user_id'] = $user->id;
+
+ Customer::create($request->all());
+ }
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.customers', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/customers');
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Customer $customer
+ *
+ * @return Response
+ */
+ public function duplicate(Customer $customer)
+ {
+ $clone = $customer->duplicate();
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.customers', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/customers/' . $clone->id . '/edit');
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ if (!Import::createFromFile($import, 'Income\Customer')) {
+ return redirect('common/import/incomes/customers');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.customers', 2)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/customers');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Customer $customer
+ *
+ * @return Response
+ */
+ public function edit(Customer $customer)
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ return view('incomes.customers.edit', compact('customer', 'currencies'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Customer $customer
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Customer $customer, Request $request)
+ {
+ if (empty($request->input('create_user'))) {
+ $customer->update($request->all());
+ } else {
+ // Check if user exist
+ $user = User::where('email', $request['email'])->first();
+ if (!empty($user)) {
+ $message = trans('messages.error.customer', ['name' => $user->name]);
+
+ flash($message)->error();
+
+ return redirect()->back()->withInput($request->except('create_user'))->withErrors(
+ ['email' => trans('customers.error.email')]
+ );
+ }
+
+ // Create user first
+ $user = User::create($request->all());
+ $user->roles()->attach(['3']);
+ $user->companies()->attach([session('company_id')]);
+
+ $request['user_id'] = $user->id;
+
+ $customer->update($request->all());
+ }
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.customers', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/customers');
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Customer $customer
+ *
+ * @return Response
+ */
+ public function enable(Customer $customer)
+ {
+ $customer->enabled = 1;
+ $customer->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.customers', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('customers.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Customer $customer
+ *
+ * @return Response
+ */
+ public function disable(Customer $customer)
+ {
+ $customer->enabled = 0;
+ $customer->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.customers', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('customers.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Customer $customer
+ *
+ * @return Response
+ */
+ public function destroy(Customer $customer)
+ {
+ $relationships = $this->countRelationships($customer, [
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ ]);
+
+ if (empty($relationships)) {
+ $customer->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.customers', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $customer->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect('incomes/customers');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('customers', function($excel) {
+ $excel->sheet('customers', function($sheet) {
+ $sheet->fromModel(Customer::filter(request()->input())->get()->makeHidden([
+ 'id', 'company_id', 'created_at', 'updated_at', 'deleted_at'
+ ]));
+ });
+ })->download('xlsx');
+ }
+
+ public function currency()
+ {
+ $customer_id = (int) request('customer_id');
+
+ if (empty($customer_id)) {
+ return response()->json([]);
+ }
+
+ $customer = Customer::find($customer_id);
+
+ if (empty($customer)) {
+ return response()->json([]);
+ }
+
+ $currency_code = setting('general.default_currency');
+
+ if (isset($customer->currency_code)) {
+ $currencies = Currency::enabled()->pluck('name', 'code')->toArray();
+
+ if (array_key_exists($customer->currency_code, $currencies)) {
+ $currency_code = $customer->currency_code;
+ }
+ }
+
+ // Get currency object
+ $currency = Currency::where('code', $currency_code)->first();
+
+ $customer->currency_name = $currency->name;
+ $customer->currency_code = $currency_code;
+ $customer->currency_rate = $currency->rate;
+
+ $customer->thousands_separator = $currency->thousands_separator;
+ $customer->decimal_mark = $currency->decimal_mark;
+ $customer->precision = (int) $currency->precision;
+ $customer->symbol_first = $currency->symbol_first;
+ $customer->symbol = $currency->symbol;
+
+ return response()->json($customer);
+ }
+
+ public function customer(Request $request)
+ {
+ $customer = Customer::create($request->all());
+
+ return response()->json($customer);
+ }
+
+ public function field(FRequest $request)
+ {
+ $html = '';
+
+ if ($request['fields']) {
+ foreach ($request['fields'] as $field) {
+ switch ($field) {
+ case 'password':
+ $html .= \Form::passwordGroup('password', trans('auth.password.current'), 'key', [], null, 'col-md-6 password');
+ break;
+ case 'password_confirmation':
+ $html .= \Form::passwordGroup('password_confirmation', trans('auth.password.current_confirm'), 'key', [], null, 'col-md-6 password');
+ break;
+ }
+ }
+ }
+
+ $json = [
+ 'html' => $html
+ ];
+
+ return response()->json($json);
+ }
+
+ /**
+ * Generate a pagination collection.
+ *
+ * @param array|Collection $items
+ * @param int $perPage
+ * @param int $page
+ * @param array $options
+ *
+ * @return LengthAwarePaginator
+ */
+ public function paginate($items, $perPage = 15, $page = null, $options = [])
+ {
+ $page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
+
+ $items = $items instanceof Collection ? $items : Collection::make($items);
+
+ return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options);
+ }
+}
diff --git a/app/Http/Controllers/Incomes/Invoices.php b/app/Http/Controllers/Incomes/Invoices.php
new file mode 100755
index 0000000..4fb9089
--- /dev/null
+++ b/app/Http/Controllers/Incomes/Invoices.php
@@ -0,0 +1,991 @@
+collect(['invoice_number'=> 'desc']);
+
+ $customers = collect(Customer::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.customers', 2)]), '');
+
+ $status = collect(InvoiceStatus::all()->pluck('name', 'code'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.statuses', 2)]), '');
+
+ return view('incomes.invoices.index', compact('invoices', 'customers', 'status'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function show(Invoice $invoice)
+ {
+ $paid = 0;
+
+ // Get Invoice Payments
+ if ($invoice->payments->count()) {
+ $_currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+
+ foreach ($invoice->payments as $item) {
+ $default_amount = $item->amount;
+
+ if ($invoice->currency_code == $item->currency_code) {
+ $amount = (double)$default_amount;
+ } else {
+ $default_amount_model = new InvoicePayment();
+
+ $default_amount_model->default_currency_code = $invoice->currency_code;
+ $default_amount_model->amount = $default_amount;
+ $default_amount_model->currency_code = $item->currency_code;
+ $default_amount_model->currency_rate = $_currencies[$item->currency_code];
+
+ $default_amount = (double) $default_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new InvoicePayment();
+
+ $convert_amount->default_currency_code = $item->currency_code;
+ $convert_amount->amount = $default_amount;
+ $convert_amount->currency_code = $invoice->currency_code;
+ $convert_amount->currency_rate = $_currencies[$invoice->currency_code];
+
+ $amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+ }
+
+ $invoice->paid = $paid;
+
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $customers = Customer::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('incomes.invoices.show', compact('invoice', 'accounts', 'currencies', 'account_currency_code', 'customers', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $customers = Customer::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code');
+
+ $currency = Currency::where('code', '=', setting('general.default_currency'))->first();
+
+ $items = Item::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $categories = Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id');
+
+ $number = $this->getNextInvoiceNumber();
+
+ return view('incomes.invoices.create', compact('customers', 'currencies', 'currency', 'items', 'taxes', 'categories', 'number'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $invoice = Invoice::create($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'invoices');
+
+ $invoice->attachMedia($media, 'attachment');
+ }
+
+ $taxes = [];
+
+ $tax_total = 0;
+ $sub_total = 0;
+ $discount_total = 0;
+ $discount = $request['discount'];
+
+ $invoice_item = [];
+ $invoice_item['company_id'] = $request['company_id'];
+ $invoice_item['invoice_id'] = $invoice->id;
+
+ if ($request['item']) {
+ foreach ($request['item'] as $item) {
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+
+ // Decrease stock (item sold)
+ $item_object->quantity -= $item['quantity'];
+ $item_object->save();
+
+ // Notify users if out of stock
+ if ($item_object->quantity == 0) {
+ foreach ($item_object->company->users as $user) {
+ if (!$user->can('read-notifications')) {
+ continue;
+ }
+
+ $user->notify(new ItemNotification($item_object));
+ }
+ }
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (((double) $item['price'] * (double) $item['quantity']) / 100) * $tax_object->rate;
+
+ // Apply discount to tax
+ if ($discount) {
+ $tax = $tax - ($tax * ($discount / 100));
+ }
+ }
+
+ $invoice_item['item_id'] = $item['item_id'];
+ $invoice_item['name'] = str_limit($item['name'], 180, '');
+ $invoice_item['sku'] = $item_sku;
+ $invoice_item['quantity'] = (double) $item['quantity'];
+ $invoice_item['price'] = (double) $item['price'];
+ $invoice_item['tax'] = $tax;
+ $invoice_item['tax_id'] = $tax_id;
+ $invoice_item['total'] = (double) $item['price'] * (double) $item['quantity'];
+
+ InvoiceItem::create($invoice_item);
+
+ // Set taxes
+ if (isset($tax_object)) {
+ if (array_key_exists($tax_object->id, $taxes)) {
+ $taxes[$tax_object->id]['amount'] += $tax;
+ } else {
+ $taxes[$tax_object->id] = [
+ 'name' => $tax_object->name,
+ 'amount' => $tax
+ ];
+ }
+ }
+
+ // Calculate totals
+ $tax_total += $tax;
+ $sub_total += $invoice_item['total'];
+
+ unset($tax_object);
+ }
+ }
+
+ $s_total = $sub_total;
+
+ // Apply discount to total
+ if ($discount) {
+ $s_discount = $s_total * ($discount / 100);
+ $discount_total += $s_discount;
+ $s_total = $s_total - $s_discount;
+ }
+
+ $amount = $s_total + $tax_total;
+
+ $request['amount'] = money($amount, $request['currency_code'])->getAmount();
+
+ $invoice->update($request->input());
+
+ // Add invoice totals
+ $this->addTotals($invoice, $request, $taxes, $sub_total, $discount_total, $tax_total);
+
+ // Add invoice history
+ InvoiceHistory::create([
+ 'company_id' => session('company_id'),
+ 'invoice_id' => $invoice->id,
+ 'status_code' => 'draft',
+ 'notify' => 0,
+ 'description' => trans('messages.success.added', ['type' => $invoice->invoice_number]),
+ ]);
+
+ // Update next invoice number
+ $this->increaseNextInvoiceNumber();
+
+ // Recurring
+ $invoice->createRecurring();
+
+ // Fire the event to make it extendible
+ event(new InvoiceCreated($invoice));
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.invoices', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/invoices/' . $invoice->id);
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function duplicate(Invoice $invoice)
+ {
+ $clone = $invoice->duplicate();
+
+ // Add invoice history
+ InvoiceHistory::create([
+ 'company_id' => session('company_id'),
+ 'invoice_id' => $clone->id,
+ 'status_code' => 'draft',
+ 'notify' => 0,
+ 'description' => trans('messages.success.added', ['type' => $clone->invoice_number]),
+ ]);
+
+ // Update next invoice number
+ $this->increaseNextInvoiceNumber();
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.invoices', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/invoices/' . $clone->id . '/edit');
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ $success = true;
+
+ $allowed_sheets = ['invoices', 'invoice_items', 'invoice_histories', 'invoice_payments', 'invoice_totals'];
+
+ // Loop through all sheets
+ $import->each(function ($sheet) use (&$success, $allowed_sheets) {
+ $sheet_title = $sheet->getTitle();
+
+ if (!in_array($sheet_title, $allowed_sheets)) {
+ $message = trans('messages.error.import_sheet');
+
+ flash($message)->error()->important();
+
+ return false;
+ }
+
+ $slug = 'Income\\' . str_singular(studly_case($sheet_title));
+
+ if (!$success = Import::createFromSheet($sheet, $slug)) {
+ return false;
+ }
+ });
+
+ if (!$success) {
+ return redirect('common/import/incomes/invoices');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.invoices', 2)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/invoices');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function edit(Invoice $invoice)
+ {
+ $customers = Customer::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code');
+
+ $currency = Currency::where('code', '=', $invoice->currency_code)->first();
+
+ $items = Item::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $categories = Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id');
+
+ return view('incomes.invoices.edit', compact('invoice', 'customers', 'currencies', 'currency', 'items', 'taxes', 'categories'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Invoice $invoice
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Invoice $invoice, Request $request)
+ {
+ $taxes = [];
+ $tax_total = 0;
+ $sub_total = 0;
+ $discount_total = 0;
+ $discount = $request['discount'];
+
+ $invoice_item = [];
+ $invoice_item['company_id'] = $request['company_id'];
+ $invoice_item['invoice_id'] = $invoice->id;
+
+ if ($request['item']) {
+ $this->deleteRelationships($invoice, 'items');
+
+ foreach ($request['item'] as $item) {
+ unset($tax_object);
+ $item_sku = '';
+
+ if (!empty($item['item_id'])) {
+ $item_object = Item::find($item['item_id']);
+
+ $item['name'] = $item_object->name;
+ $item_sku = $item_object->sku;
+ }
+
+ $tax = $tax_id = 0;
+
+ if (!empty($item['tax_id'])) {
+ $tax_object = Tax::find($item['tax_id']);
+
+ $tax_id = $item['tax_id'];
+
+ $tax = (((double) $item['price'] * (double) $item['quantity']) / 100) * $tax_object->rate;
+
+ // Apply discount to tax
+ if ($discount) {
+ $tax = $tax - ($tax * ($discount / 100));
+ }
+ }
+
+ $invoice_item['item_id'] = $item['item_id'];
+ $invoice_item['name'] = str_limit($item['name'], 180, '');
+ $invoice_item['sku'] = $item_sku;
+ $invoice_item['quantity'] = (double) $item['quantity'];
+ $invoice_item['price'] = (double) $item['price'];
+ $invoice_item['tax'] = $tax;
+ $invoice_item['tax_id'] = $tax_id;
+ $invoice_item['total'] = (double) $item['price'] * (double) $item['quantity'];
+
+ if (isset($tax_object)) {
+ if (array_key_exists($tax_object->id, $taxes)) {
+ $taxes[$tax_object->id]['amount'] += $tax;
+ } else {
+ $taxes[$tax_object->id] = [
+ 'name' => $tax_object->name,
+ 'amount' => $tax
+ ];
+ }
+ }
+
+ $tax_total += $tax;
+ $sub_total += $invoice_item['total'];
+
+ InvoiceItem::create($invoice_item);
+ }
+ }
+
+ $s_total = $sub_total;
+
+ // Apply discount to total
+ if ($discount) {
+ $s_discount = $s_total * ($discount / 100);
+ $discount_total += $s_discount;
+ $s_total = $s_total - $s_discount;
+ }
+
+ $amount = $s_total + $tax_total;
+
+ $request['amount'] = money($amount, $request['currency_code'])->getAmount();
+
+ $invoice->update($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'invoices');
+
+ $invoice->attachMedia($media, 'attachment');
+ }
+
+ // Delete previous invoice totals
+ $this->deleteRelationships($invoice, 'totals');
+
+ // Add invoice totals
+ $this->addTotals($invoice, $request, $taxes, $sub_total, $discount_total, $tax_total);
+
+ // Recurring
+ $invoice->updateRecurring();
+
+ // Fire the event to make it extendible
+ event(new InvoiceUpdated($invoice));
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.invoices', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/invoices/' . $invoice->id);
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function destroy(Invoice $invoice)
+ {
+ $this->deleteRelationships($invoice, ['items', 'histories', 'payments', 'recurring', 'totals']);
+ $invoice->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.invoices', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/invoices');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('invoices', function ($excel) {
+ $invoices = Invoice::with(['items', 'histories', 'payments', 'totals'])->filter(request()->input())->get();
+
+ $excel->sheet('invoices', function ($sheet) use ($invoices) {
+ $sheet->fromModel($invoices->makeHidden([
+ 'company_id', 'parent_id', 'created_at', 'updated_at', 'deleted_at', 'attachment', 'discount', 'items', 'histories', 'payments', 'totals', 'media'
+ ]));
+ });
+
+ $tables = ['items', 'histories', 'payments', 'totals'];
+ foreach ($tables as $table) {
+ $excel->sheet('invoice_' . $table, function ($sheet) use ($invoices, $table) {
+ $hidden_fields = ['id', 'company_id', 'created_at', 'updated_at', 'deleted_at', 'title'];
+
+ $i = 1;
+
+ foreach ($invoices as $invoice) {
+ $model = $invoice->$table->makeHidden($hidden_fields);
+
+ if ($i == 1) {
+ $sheet->fromModel($model, null, 'A1', false);
+ } else {
+ // Don't put multiple heading columns
+ $sheet->fromModel($model, null, 'A1', false, false);
+ }
+
+ $i++;
+ }
+ });
+ }
+ })->download('xlsx');
+ }
+
+ /**
+ * Mark the invoice as sent.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function markSent(Invoice $invoice)
+ {
+ $invoice->invoice_status_code = 'sent';
+
+ $invoice->save();
+
+ // Add invoice history
+ InvoiceHistory::create([
+ 'company_id' => $invoice->company_id,
+ 'invoice_id' => $invoice->id,
+ 'status_code' => 'sent',
+ 'notify' => 0,
+ 'description' => trans('invoices.mark_sent'),
+ ]);
+
+ flash(trans('invoices.messages.marked_sent'))->success();
+
+ return redirect()->back();
+ }
+
+ /**
+ * Download the PDF file of invoice.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function emailInvoice(Invoice $invoice)
+ {
+ if (empty($invoice->customer_email)) {
+ return redirect()->back();
+ }
+
+ $invoice = $this->prepareInvoice($invoice);
+
+ $html = view($invoice->template_path, compact('invoice'))->render();
+
+ $pdf = \App::make('dompdf.wrapper');
+ $pdf->loadHTML($html);
+
+ $file = storage_path('app/temp/invoice_'.time().'.pdf');
+
+ $invoice->pdf_path = $file;
+
+ // Save the PDF file into temp folder
+ $pdf->save($file);
+
+ // Notify the customer
+ $invoice->customer->notify(new Notification($invoice));
+
+ // Delete temp file
+ File::delete($file);
+
+ unset($invoice->paid);
+ unset($invoice->template_path);
+ unset($invoice->pdf_path);
+
+ // Mark invoice as sent
+ if ($invoice->invoice_status_code != 'partial') {
+ $invoice->invoice_status_code = 'sent';
+
+ $invoice->save();
+ }
+
+ // Add invoice history
+ InvoiceHistory::create([
+ 'company_id' => $invoice->company_id,
+ 'invoice_id' => $invoice->id,
+ 'status_code' => 'sent',
+ 'notify' => 1,
+ 'description' => trans('invoices.send_mail'),
+ ]);
+
+ flash(trans('invoices.messages.email_sent'))->success();
+
+ return redirect()->back();
+ }
+
+ /**
+ * Print the invoice.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function printInvoice(Invoice $invoice)
+ {
+ $invoice = $this->prepareInvoice($invoice);
+
+ return view($invoice->template_path, compact('invoice'));
+ }
+
+ /**
+ * Download the PDF file of invoice.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function pdfInvoice(Invoice $invoice)
+ {
+ $invoice = $this->prepareInvoice($invoice);
+
+ $html = view($invoice->template_path, compact('invoice'))->render();
+
+ $pdf = app('dompdf.wrapper');
+ $pdf->loadHTML($html);
+
+ //$pdf->setPaper('A4', 'portrait');
+
+ $file_name = 'invoice_'.time().'.pdf';
+
+ return $pdf->download($file_name);
+ }
+
+ /**
+ * Mark the invoice as paid.
+ *
+ * @param Invoice $invoice
+ *
+ * @return Response
+ */
+ public function markPaid(Invoice $invoice)
+ {
+ $paid = 0;
+
+ foreach ($invoice->payments as $item) {
+ $amount = $item->amount;
+
+ if ($invoice->currency_code != $item->currency_code) {
+ $item->default_currency_code = $invoice->currency_code;
+
+ $amount = $item->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+
+ $amount = $invoice->amount - $paid;
+
+ if (!empty($amount)) {
+ $request = new PaymentRequest();
+
+ $request['company_id'] = $invoice->company_id;
+ $request['invoice_id'] = $invoice->id;
+ $request['account_id'] = setting('general.default_account');
+ $request['payment_method'] = setting('general.default_payment_method', 'offlinepayment.cash.1');
+ $request['currency_code'] = $invoice->currency_code;
+ $request['amount'] = $amount;
+ $request['paid_at'] = Date::now()->format('Y-m-d');
+ $request['_token'] = csrf_token();
+
+ $this->payment($request);
+ } else {
+ $invoice->invoice_status_code = 'paid';
+ $invoice->save();
+ }
+
+ return redirect()->back();
+ }
+
+ /**
+ * Add payment to the invoice.
+ *
+ * @param PaymentRequest $request
+ *
+ * @return Response
+ */
+ public function payment(PaymentRequest $request)
+ {
+ // Get currency object
+ $currency = Currency::where('code', $request['currency_code'])->first();
+
+ $request['currency_code'] = $currency->code;
+ $request['currency_rate'] = $currency->rate;
+
+ $invoice = Invoice::find($request['invoice_id']);
+
+ $total_amount = $invoice->amount;
+
+ $amount = (double) $request['amount'];
+
+ if ($request['currency_code'] != $invoice->currency_code) {
+ $request_invoice = new Invoice();
+
+ $request_invoice->amount = (float) $request['amount'];
+ $request_invoice->currency_code = $currency->code;
+ $request_invoice->currency_rate = $currency->rate;
+
+ $amount = $request_invoice->getConvertedAmount();
+ }
+
+ if ($invoice->payments()->count()) {
+ $total_amount -= $invoice->payments()->paid();
+ }
+
+ // For amount cover integer
+ $multiplier = 1;
+
+ for ($i = 0; $i < $currency->precision; $i++) {
+ $multiplier *= 10;
+ }
+
+ $amount *= $multiplier;
+ $total_amount *= $multiplier;
+
+ if ($amount > $total_amount) {
+ $message = trans('messages.error.over_payment');
+
+ return response()->json([
+ 'success' => false,
+ 'error' => true,
+ 'message' => $message,
+ ]);
+ } elseif ($amount == $total_amount) {
+ $invoice->invoice_status_code = 'paid';
+ } else {
+ $invoice->invoice_status_code = 'partial';
+ }
+
+ $invoice->save();
+
+ $invoice_payment_request = [
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $request['invoice_id'],
+ 'account_id' => $request['account_id'],
+ 'paid_at' => $request['paid_at'],
+ 'amount' => $request['amount'],
+ 'currency_code' => $request['currency_code'],
+ 'currency_rate' => $request['currency_rate'],
+ 'description' => $request['description'],
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference']
+ ];
+
+ $invoice_payment = InvoicePayment::create($invoice_payment_request);
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'invoices');
+
+ $invoice_payment->attachMedia($media, 'attachment');
+ }
+
+ $request['status_code'] = $invoice->invoice_status_code;
+ $request['notify'] = 0;
+
+ $desc_amount = money((float) $request['amount'], (string) $request['currency_code'], true)->format();
+
+ $request['description'] = $desc_amount . ' ' . trans_choice('general.payments', 1);
+
+ InvoiceHistory::create($request->input());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => $message,
+ ]);
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param InvoicePayment $payment
+ *
+ * @return Response
+ */
+ public function paymentDestroy(InvoicePayment $payment)
+ {
+ $invoice = Invoice::find($payment->invoice_id);
+
+ if ($invoice->payments()->count() > 1) {
+ $invoice->invoice_status_code = 'partial';
+ } else {
+ $invoice->invoice_status_code = 'sent';
+ }
+
+ $invoice->save();
+
+ $desc_amount = money((float) $payment->amount, (string) $payment->currency_code, true)->format();
+
+ $description = $desc_amount . ' ' . trans_choice('general.payments', 1);
+
+ // Add invoice history
+ InvoiceHistory::create([
+ 'company_id' => $invoice->company_id,
+ 'invoice_id' => $invoice->id,
+ 'status_code' => $invoice->invoice_status_code,
+ 'notify' => 0,
+ 'description' => trans('messages.success.deleted', ['type' => $description]),
+ ]);
+
+ $payment->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.invoices', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->back();
+ }
+
+ public function addItem(ItemRequest $request)
+ {
+ if ($request['item_row']) {
+ $item_row = $request['item_row'];
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $currency = Currency::where('code', '=', $request['currency_code'])->first();
+
+ // it should be integer for amount mask
+ $currency->precision = (int) $currency->precision;
+
+ $html = view('incomes.invoices.item', compact('item_row', 'taxes', 'currency'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => [
+ 'currency' => $currency
+ ],
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ return response()->json([
+ 'success' => false,
+ 'error' => true,
+ 'data' => 'null',
+ 'message' => trans('issue'),
+ 'html' => 'null',
+ ]);
+ }
+
+ protected function prepareInvoice(Invoice $invoice)
+ {
+ $paid = 0;
+
+ foreach ($invoice->payments as $item) {
+ $amount = $item->amount;
+
+ if ($invoice->currency_code != $item->currency_code) {
+ $item->default_currency_code = $invoice->currency_code;
+
+ $amount = $item->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+
+ $invoice->paid = $paid;
+
+ $invoice->template_path = 'incomes.invoices.invoice';
+
+ event(new InvoicePrinting($invoice));
+
+ return $invoice;
+ }
+
+ protected function addTotals($invoice, $request, $taxes, $sub_total, $discount_total, $tax_total)
+ {
+ $sort_order = 1;
+
+ // Added invoice sub total
+ InvoiceTotal::create([
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'sub_total',
+ 'name' => 'invoices.sub_total',
+ 'amount' => $sub_total,
+ 'sort_order' => $sort_order,
+ ]);
+
+ $sort_order++;
+
+ // Added invoice discount
+ if ($discount_total) {
+ InvoiceTotal::create([
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'discount',
+ 'name' => 'invoices.discount',
+ 'amount' => $discount_total,
+ 'sort_order' => $sort_order,
+ ]);
+
+ // This is for total
+ $sub_total = $sub_total - $discount_total;
+ }
+
+ $sort_order++;
+
+ // Added invoice taxes
+ if ($taxes) {
+ foreach ($taxes as $tax) {
+ InvoiceTotal::create([
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'tax',
+ 'name' => $tax['name'],
+ 'amount' => $tax['amount'],
+ 'sort_order' => $sort_order,
+ ]);
+
+ $sort_order++;
+ }
+ }
+
+ // Added invoice total
+ InvoiceTotal::create([
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $invoice->id,
+ 'code' => 'total',
+ 'name' => 'invoices.total',
+ 'amount' => $sub_total + $tax_total,
+ 'sort_order' => $sort_order,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Incomes/Revenues.php b/app/Http/Controllers/Incomes/Revenues.php
new file mode 100755
index 0000000..a8a87d5
--- /dev/null
+++ b/app/Http/Controllers/Incomes/Revenues.php
@@ -0,0 +1,240 @@
+isNotTransfer()->collect(['paid_at'=> 'desc']);
+
+ $customers = collect(Customer::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.customers', 2)]), '');
+
+ $categories = collect(Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.categories', 2)]), '');
+
+ $accounts = collect(Account::enabled()->orderBy('name')->pluck('name', 'id'))
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.accounts', 2)]), '');
+
+ $transfer_cat_id = Category::transfer();
+
+ return view('incomes.revenues.index', compact('revenues', 'customers', 'categories', 'accounts', 'transfer_cat_id'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function show()
+ {
+ return redirect('incomes/revenues');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $currency = Currency::where('code', '=', $account_currency_code)->first();
+
+ $customers = Customer::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('incomes.revenues.create', compact('accounts', 'currencies', 'account_currency_code', 'currency', 'customers', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $revenue = Revenue::create($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'revenues');
+
+ $revenue->attachMedia($media, 'attachment');
+ }
+
+ // Recurring
+ $revenue->createRecurring();
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.revenues', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/revenues');
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param Revenue $revenue
+ *
+ * @return Response
+ */
+ public function duplicate(Revenue $revenue)
+ {
+ $clone = $revenue->duplicate();
+
+ $message = trans('messages.success.duplicated', ['type' => trans_choice('general.revenues', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/revenues/' . $clone->id . '/edit');
+ }
+
+ /**
+ * Import the specified resource.
+ *
+ * @param ImportFile $import
+ *
+ * @return Response
+ */
+ public function import(ImportFile $import)
+ {
+ if (!Import::createFromFile($import, 'Income\Revenue')) {
+ return redirect('common/import/incomes/revenues');
+ }
+
+ $message = trans('messages.success.imported', ['type' => trans_choice('general.revenues', 2)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/revenues');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Revenue $revenue
+ *
+ * @return Response
+ */
+ public function edit(Revenue $revenue)
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', $revenue->account_id)->pluck('currency_code')->first();
+
+ $currency = Currency::where('code', '=', $account_currency_code)->first();
+
+ $customers = Customer::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $categories = Category::enabled()->type('income')->orderBy('name')->pluck('name', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ return view('incomes.revenues.edit', compact('revenue', 'accounts', 'currencies', 'account_currency_code', 'currency', 'customers', 'categories', 'payment_methods'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Revenue $revenue
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Revenue $revenue, Request $request)
+ {
+ $revenue->update($request->input());
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'revenues');
+
+ $revenue->attachMedia($media, 'attachment');
+ }
+
+ // Recurring
+ $revenue->updateRecurring();
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.revenues', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/revenues');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Revenue $revenue
+ *
+ * @return Response
+ */
+ public function destroy(Revenue $revenue)
+ {
+ // Can't delete transfer revenue
+ if ($revenue->category->id == Category::transfer()) {
+ return redirect('incomes/revenues');
+ }
+
+ $revenue->recurring()->delete();
+ $revenue->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.revenues', 1)]);
+
+ flash($message)->success();
+
+ return redirect('incomes/revenues');
+ }
+
+ /**
+ * Export the specified resource.
+ *
+ * @return Response
+ */
+ public function export()
+ {
+ \Excel::create('revenues', function($excel) {
+ $excel->sheet('revenues', function($sheet) {
+ $sheet->fromModel(Revenue::filter(request()->input())->get()->makeHidden([
+ 'id', 'company_id', 'parent_id', 'created_at', 'updated_at', 'deleted_at'
+ ]));
+ });
+ })->download('xlsx');
+ }
+}
diff --git a/app/Http/Controllers/Install/Database.php b/app/Http/Controllers/Install/Database.php
new file mode 100755
index 0000000..ef8bb43
--- /dev/null
+++ b/app/Http/Controllers/Install/Database.php
@@ -0,0 +1,48 @@
+error()->important();
+
+ return redirect('install/database')->withInput();
+ }
+
+ return redirect('install/settings');
+ }
+}
diff --git a/app/Http/Controllers/Install/Language.php b/app/Http/Controllers/Install/Language.php
new file mode 100755
index 0000000..dc21d2c
--- /dev/null
+++ b/app/Http/Controllers/Install/Language.php
@@ -0,0 +1,35 @@
+send();
+ } else {
+ foreach ($requirements as $requirement) {
+ flash($requirement)->error()->important();
+ }
+
+ return view('install.requirements.show');
+ }
+ }
+}
diff --git a/app/Http/Controllers/Install/Settings.php b/app/Http/Controllers/Install/Settings.php
new file mode 100755
index 0000000..4162562
--- /dev/null
+++ b/app/Http/Controllers/Install/Settings.php
@@ -0,0 +1,42 @@
+get('company_name'), $request->get('company_email'), session('locale'));
+
+ // Create user
+ Installer::createUser($request->get('user_email'), $request->get('user_password'), session('locale'));
+
+ // Make the final touches
+ Installer::finalTouches();
+
+ // Redirect to dashboard
+ return redirect('auth/login');
+ }
+}
diff --git a/app/Http/Controllers/Install/Updates.php b/app/Http/Controllers/Install/Updates.php
new file mode 100755
index 0000000..855050e
--- /dev/null
+++ b/app/Http/Controllers/Install/Updates.php
@@ -0,0 +1,120 @@
+get('alias');
+
+ if (!isset($updates[$alias])) {
+ continue;
+ }
+
+ $m = new \stdClass();
+ $m->name = $row->get('name');
+ $m->alias = $row->get('alias');
+ $m->category = $row->get('category');
+ $m->installed = $row->get('version');
+ $m->latest = $updates[$alias];
+
+ $modules[] = $m;
+ }
+ }
+
+ return view('install.updates.index', compact('core', 'modules'));
+ }
+
+ public function changelog()
+ {
+ return Versions::changelog();
+ }
+
+ /**
+ * Check for updates.
+ *
+ * @return Response
+ */
+ public function check()
+ {
+ // Clear cache in order to check for updates
+ Updater::clear();
+
+ return redirect()->back();
+ }
+
+ /**
+ * Update the core or modules.
+ *
+ * @param $alias
+ * @param $version
+ * @return Response
+ */
+ public function update($alias, $version)
+ {
+ set_time_limit(600); // 10 minutes
+
+ if (Updater::update($alias, $version)) {
+ return redirect('install/updates/post/' . $alias . '/' . version('short') . '/' . $version);
+ }
+
+ flash(trans('updates.error'))->error()->important();
+
+ return redirect()->back();
+ }
+
+ /**
+ * Final actions post update.
+ *
+ * @param $alias
+ * @param $old
+ * @param $new
+ * @return Response
+ */
+ public function post($alias, $old, $new)
+ {
+ // Check if the file mirror was successful
+ if (($alias == 'core') && (version('short') != $new)) {
+ flash(trans('updates.error'))->error()->important();
+
+ return redirect('install/updates');
+ }
+
+ // Clear cache after update
+ Artisan::call('cache:clear');
+
+ event(new UpdateFinished($alias, $old, $new));
+
+ flash(trans('updates.success'))->success();
+
+ return redirect('install/updates');
+ }
+}
diff --git a/app/Http/Controllers/Modals/BillPayments.php b/app/Http/Controllers/Modals/BillPayments.php
new file mode 100755
index 0000000..0446ea9
--- /dev/null
+++ b/app/Http/Controllers/Modals/BillPayments.php
@@ -0,0 +1,250 @@
+middleware('permission:create-expenses-bills')->only(['create', 'store', 'duplicate', 'import']);
+ $this->middleware('permission:read-expenses-bills')->only(['index', 'show', 'edit', 'export']);
+ $this->middleware('permission:update-expenses-bills')->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-expenses-bills')->only('destroy');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create(Bill $bill)
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $currency = Currency::where('code', setting('general.default_currency'))->first();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ $bill->paid = $this->getPaid($bill);
+
+ // Get Bill Totals
+ foreach ($bill->totals as $bill_total) {
+ $bill->{$bill_total->code} = $bill_total->amount;
+ }
+
+ $bill->grand_total = $bill->total;
+
+ if (!empty($paid)) {
+ $bill->grand_total = $bill->total - $paid;
+ }
+
+ $html = view('modals.bills.payment', compact('bill', 'accounts', 'account_currency_code', 'currencies', 'currency', 'payment_methods'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Bill $bill, Request $request)
+ {
+ // Get currency object
+ $currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+ $currency = Currency::where('code', $request['currency_code'])->first();
+
+ $request['currency_code'] = $currency->code;
+ $request['currency_rate'] = $currency->rate;
+
+ $total_amount = $bill->amount;
+
+ $default_amount = (double) $request['amount'];
+
+ if ($bill->currency_code == $request['currency_code']) {
+ $amount = $default_amount;
+ } else {
+ $default_amount_model = new BillPayment();
+
+ $default_amount_model->default_currency_code = $bill->currency_code;
+ $default_amount_model->amount = $default_amount;
+ $default_amount_model->currency_code = $request['currency_code'];
+ $default_amount_model->currency_rate = $currencies[$request['currency_code']];
+
+ $default_amount = (double) $default_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new BillPayment();
+
+ $convert_amount->default_currency_code = $request['currency_code'];
+ $convert_amount->amount = $default_amount;
+ $convert_amount->currency_code = $bill->currency_code;
+ $convert_amount->currency_rate = $currencies[$bill->currency_code];
+
+ $amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ if ($bill->payments()->count()) {
+ $total_amount -= $this->getPaid($bill);
+ }
+
+ // For amount cover integer
+ $multiplier = 1;
+
+ for ($i = 0; $i < $currency->precision; $i++) {
+ $multiplier *= 10;
+ }
+
+ $amount_check = $amount * $multiplier;
+ $total_amount_check = $total_amount * $multiplier;
+
+ if ($amount_check > $total_amount_check) {
+ $error_amount = $total_amount;
+
+ if ($bill->currency_code != $request['currency_code']) {
+ $error_amount_model = new BillPayment();
+
+ $error_amount_model->default_currency_code = $request['currency_code'];
+ $error_amount_model->amount = $error_amount;
+ $error_amount_model->currency_code = $bill->currency_code;
+ $error_amount_model->currency_rate = $currencies[$bill->currency_code];
+
+ $error_amount = (double) $error_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new BillPayment();
+
+ $convert_amount->default_currency_code = $bill->currency_code;
+ $convert_amount->amount = $error_amount;
+ $convert_amount->currency_code = $request['currency_code'];
+ $convert_amount->currency_rate = $currencies[$request['currency_code']];
+
+ $error_amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ $message = trans('messages.error.over_payment', ['amount' => money($error_amount, $request['currency_code'], true)]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => true,
+ 'data' => [
+ 'amount' => $error_amount
+ ],
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ } elseif ($amount == $total_amount) {
+ $bill->bill_status_code = 'paid';
+ } else {
+ $bill->bill_status_code = 'partial';
+ }
+
+ $bill->save();
+
+ $bill_payment_request = [
+ 'company_id' => $request['company_id'],
+ 'bill_id' => $request['bill_id'],
+ 'account_id' => $request['account_id'],
+ 'paid_at' => $request['paid_at'],
+ 'amount' => $request['amount'],
+ 'currency_code' => $request['currency_code'],
+ 'currency_rate' => $request['currency_rate'],
+ 'description' => $request['description'],
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference']
+ ];
+
+ $bill_payment = BillPayment::create($bill_payment_request);
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'bills');
+
+ $bill_payment->attachMedia($media, 'attachment');
+ }
+
+ $request['status_code'] = $bill->bill_status_code;
+ $request['notify'] = 0;
+
+ $desc_amount = money((float) $request['amount'], (string) $request['currency_code'], true)->format();
+
+ $request['description'] = $desc_amount . ' ' . trans_choice('general.payments', 1);
+
+ BillHistory::create($request->input());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => $bill_payment,
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ }
+
+ protected function getPaid($bill)
+ {
+ $paid = 0;
+
+ // Get Bill Payments
+ if ($bill->payments->count()) {
+ $_currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+
+ foreach ($bill->payments as $item) {
+ $default_amount = (double) $item->amount;
+
+ if ($bill->currency_code == $item->currency_code) {
+ $amount = $default_amount;
+ } else {
+ $default_amount_model = new BillPayment();
+
+ $default_amount_model->default_currency_code = $bill->currency_code;
+ $default_amount_model->amount = $default_amount;
+ $default_amount_model->currency_code = $item->currency_code;
+ $default_amount_model->currency_rate = $_currencies[$item->currency_code];
+
+ $default_amount = (double) $default_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new BillPayment();
+
+ $convert_amount->default_currency_code = $item->currency_code;
+ $convert_amount->amount = $default_amount;
+ $convert_amount->currency_code = $bill->currency_code;
+ $convert_amount->currency_rate = $_currencies[$bill->currency_code];
+
+ $amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+ }
+
+ return $paid;
+ }
+}
diff --git a/app/Http/Controllers/Modals/Categories.php b/app/Http/Controllers/Modals/Categories.php
new file mode 100755
index 0000000..5fd1891
--- /dev/null
+++ b/app/Http/Controllers/Modals/Categories.php
@@ -0,0 +1,64 @@
+middleware('permission:create-settings-categories')->only(['create', 'store', 'duplicate', 'import']);
+ $this->middleware('permission:read-settings-categories')->only(['index', 'show', 'edit', 'export']);
+ $this->middleware('permission:update-settings-categories')->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-settings-categories')->only('destroy');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create(CRequest $request)
+ {
+ $type = $request['type'];
+
+ $html = view('modals.categories.create', compact('currencies', 'type'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $category = Category::create($request->all());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.categories', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => $category,
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Modals/Customers.php b/app/Http/Controllers/Modals/Customers.php
new file mode 100755
index 0000000..b0af0b6
--- /dev/null
+++ b/app/Http/Controllers/Modals/Customers.php
@@ -0,0 +1,74 @@
+middleware('permission:create-incomes-customers')->only(['create', 'store', 'duplicate', 'import']);
+ $this->middleware('permission:read-incomes-customers')->only(['index', 'show', 'edit', 'export']);
+ $this->middleware('permission:update-incomes-customers')->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-incomes-customers')->only('destroy');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ $html = view('modals.customers.create', compact('currencies'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $customer = Customer::create($request->all());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.customers', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => $customer,
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Modals/InvoicePayments.php b/app/Http/Controllers/Modals/InvoicePayments.php
new file mode 100755
index 0000000..737dd13
--- /dev/null
+++ b/app/Http/Controllers/Modals/InvoicePayments.php
@@ -0,0 +1,250 @@
+middleware('permission:create-incomes-invoices')->only(['create', 'store', 'duplicate', 'import']);
+ $this->middleware('permission:read-incomes-invoices')->only(['index', 'show', 'edit', 'export']);
+ $this->middleware('permission:update-incomes-invoices')->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-incomes-invoices')->only('destroy');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create(Invoice $invoice)
+ {
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code')->toArray();
+
+ $account_currency_code = Account::where('id', setting('general.default_account'))->pluck('currency_code')->first();
+
+ $currency = Currency::where('code', $account_currency_code)->first();
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ $paid = $this->getPaid($invoice);
+
+ // Get Invoice Totals
+ foreach ($invoice->totals as $invoice_total) {
+ $invoice->{$invoice_total->code} = $invoice_total->amount;
+ }
+
+ $invoice->grand_total = $invoice->total;
+
+ if (!empty($paid)) {
+ $invoice->grand_total = $invoice->total - $paid;
+ }
+
+ $html = view('modals.invoices.payment', compact('invoice', 'accounts', 'account_currency_code', 'currencies', 'currency', 'payment_methods'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Invoice $invoice, Request $request)
+ {
+ // Get currency object
+ $currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+ $currency = Currency::where('code', $request['currency_code'])->first();
+
+ $request['currency_code'] = $currency->code;
+ $request['currency_rate'] = $currency->rate;
+
+ $total_amount = $invoice->amount;
+
+ $default_amount = (double) $request['amount'];
+
+ if ($invoice->currency_code == $request['currency_code']) {
+ $amount = $default_amount;
+ } else {
+ $default_amount_model = new InvoicePayment();
+
+ $default_amount_model->default_currency_code = $invoice->currency_code;
+ $default_amount_model->amount = $default_amount;
+ $default_amount_model->currency_code = $request['currency_code'];
+ $default_amount_model->currency_rate = $currencies[$request['currency_code']];
+
+ $default_amount = (double) $default_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new InvoicePayment();
+
+ $convert_amount->default_currency_code = $request['currency_code'];
+ $convert_amount->amount = $default_amount;
+ $convert_amount->currency_code = $invoice->currency_code;
+ $convert_amount->currency_rate = $currencies[$invoice->currency_code];
+
+ $amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ if ($invoice->payments()->count()) {
+ $total_amount -= $this->getPaid($invoice);
+ }
+
+ // For amount cover integer
+ $multiplier = 1;
+
+ for ($i = 0; $i < $currency->precision; $i++) {
+ $multiplier *= 10;
+ }
+
+ $amount_check = $amount * $multiplier;
+ $total_amount_check = $total_amount * $multiplier;
+
+ if ($amount_check > $total_amount_check) {
+ $error_amount = $total_amount;
+
+ if ($invoice->currency_code != $request['currency_code']) {
+ $error_amount_model = new InvoicePayment();
+
+ $error_amount_model->default_currency_code = $request['currency_code'];
+ $error_amount_model->amount = $error_amount;
+ $error_amount_model->currency_code = $invoice->currency_code;
+ $error_amount_model->currency_rate = $currencies[$invoice->currency_code];
+
+ $error_amount = (double) $error_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new InvoicePayment();
+
+ $convert_amount->default_currency_code = $invoice->currency_code;
+ $convert_amount->amount = $error_amount;
+ $convert_amount->currency_code = $request['currency_code'];
+ $convert_amount->currency_rate = $currencies[$request['currency_code']];
+
+ $error_amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ $message = trans('messages.error.over_payment', ['amount' => money($error_amount, $request['currency_code'],true)]);
+
+ return response()->json([
+ 'success' => false,
+ 'error' => true,
+ 'data' => [
+ 'amount' => $error_amount
+ ],
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ } elseif ($amount == $total_amount) {
+ $invoice->invoice_status_code = 'paid';
+ } else {
+ $invoice->invoice_status_code = 'partial';
+ }
+
+ $invoice->save();
+
+ $invoice_payment_request = [
+ 'company_id' => $request['company_id'],
+ 'invoice_id' => $request['invoice_id'],
+ 'account_id' => $request['account_id'],
+ 'paid_at' => $request['paid_at'],
+ 'amount' => $request['amount'],
+ 'currency_code' => $request['currency_code'],
+ 'currency_rate' => $request['currency_rate'],
+ 'description' => $request['description'],
+ 'payment_method' => $request['payment_method'],
+ 'reference' => $request['reference']
+ ];
+
+ $invoice_payment = InvoicePayment::create($invoice_payment_request);
+
+ // Upload attachment
+ if ($request->file('attachment')) {
+ $media = $this->getMedia($request->file('attachment'), 'invoices');
+
+ $invoice_payment->attachMedia($media, 'attachment');
+ }
+
+ $request['status_code'] = $invoice->invoice_status_code;
+ $request['notify'] = 0;
+
+ $desc_amount = money((float) $request['amount'], (string) $request['currency_code'], true)->format();
+
+ $request['description'] = $desc_amount . ' ' . trans_choice('general.payments', 1);
+
+ InvoiceHistory::create($request->input());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => $invoice_payment,
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ }
+
+ protected function getPaid($invoice)
+ {
+ $paid = 0;
+
+ // Get Invoice Payments
+ if ($invoice->payments->count()) {
+ $_currencies = Currency::enabled()->pluck('rate', 'code')->toArray();
+
+ foreach ($invoice->payments as $item) {
+ $default_amount = $item->amount;
+
+ if ($invoice->currency_code == $item->currency_code) {
+ $amount = (double) $default_amount;
+ } else {
+ $default_amount_model = new InvoicePayment();
+
+ $default_amount_model->default_currency_code = $invoice->currency_code;
+ $default_amount_model->amount = $default_amount;
+ $default_amount_model->currency_code = $item->currency_code;
+ $default_amount_model->currency_rate = $_currencies[$item->currency_code];
+
+ $default_amount = (double) $default_amount_model->getDivideConvertedAmount();
+
+ $convert_amount = new InvoicePayment();
+
+ $convert_amount->default_currency_code = $item->currency_code;
+ $convert_amount->amount = $default_amount;
+ $convert_amount->currency_code = $invoice->currency_code;
+ $convert_amount->currency_rate = $_currencies[$invoice->currency_code];
+
+ $amount = (double) $convert_amount->getDynamicConvertedAmount();
+ }
+
+ $paid += $amount;
+ }
+ }
+
+ return $paid;
+ }
+}
diff --git a/app/Http/Controllers/Modals/Vendors.php b/app/Http/Controllers/Modals/Vendors.php
new file mode 100755
index 0000000..ccafd28
--- /dev/null
+++ b/app/Http/Controllers/Modals/Vendors.php
@@ -0,0 +1,82 @@
+middleware('permission:create-expenses-vendors')->only(['create', 'store', 'duplicate', 'import']);
+ $this->middleware('permission:read-expenses-vendors')->only(['index', 'show', 'edit', 'export']);
+ $this->middleware('permission:update-expenses-vendors')->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-expenses-vendors')->only('destroy');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $currencies = Currency::enabled()->pluck('name', 'code');
+
+ $html = view('modals.vendors.create', compact('currencies'))->render();
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'message' => 'null',
+ 'html' => $html,
+ ]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $vendor = Vendor::create($request->all());
+
+ // Upload logo
+ if ($request->file('logo')) {
+ $media = $this->getMedia($request->file('logo'), 'vendors');
+
+ $vendor->attachMedia($media, 'logo');
+ }
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.vendors', 1)]);
+
+ return response()->json([
+ 'success' => true,
+ 'error' => false,
+ 'data' => $vendor,
+ 'message' => $message,
+ 'html' => 'null',
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Modules/Home.php b/app/Http/Controllers/Modules/Home.php
new file mode 100755
index 0000000..e1ad914
--- /dev/null
+++ b/app/Http/Controllers/Modules/Home.php
@@ -0,0 +1,36 @@
+checkApiToken();
+
+ $data = [
+ 'query' => [
+ 'limit' => 4
+ ]
+ ];
+
+ $paid = $this->getPaidModules($data);
+ $new = $this->getNewModules($data);
+ $free = $this->getFreeModules($data);
+ $installed = Module::all()->pluck('status', 'alias')->toArray();
+
+ return view('modules.home.index', compact('paid', 'new', 'free', 'installed'));
+ }
+}
diff --git a/app/Http/Controllers/Modules/Item.php b/app/Http/Controllers/Modules/Item.php
new file mode 100755
index 0000000..016e912
--- /dev/null
+++ b/app/Http/Controllers/Modules/Item.php
@@ -0,0 +1,305 @@
+middleware('permission:create-modules-item')->only(['install']);
+ $this->middleware('permission:update-modules-item')->only(['update', 'enable', 'disable']);
+ $this->middleware('permission:delete-modules-item')->only(['uninstall']);
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param $alias
+ *
+ * @return Response
+ */
+ public function show($alias)
+ {
+ $this->checkApiToken();
+
+ $enable = false;
+ $installed = false;
+
+ $module = $this->getModule($alias);
+
+ if (empty($module)) {
+ return redirect('apps/home')->send();
+ }
+
+ $check = Module::alias($alias)->first();
+
+ if ($check) {
+ $installed = true;
+
+ if ($check->status) {
+ $enable = true;
+ }
+ }
+
+ if (request()->get('utm_source')) {
+ $parameters = request()->all();
+
+ $character = '?';
+
+ if (strpos($module->action_url, '?') !== false) {
+ $character = '&';
+ }
+
+ $module->action_url .= $character . http_build_query($parameters);
+ }
+
+ return view('modules.item.show', compact('module', 'about', 'installed', 'enable'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param $request
+ *
+ * @return Response
+ */
+ public function steps(Request $request)
+ {
+ $this->checkApiToken();
+
+ $json = [];
+ $json['step'] = [];
+
+ $name = $request['name'];
+ $version = $request['version'];
+
+ // Download
+ $json['step'][] = [
+ 'text' => trans('modules.installation.download', ['module' => $name]),
+ 'url' => url('apps/download')
+ ];
+
+ // Unzip
+ $json['step'][] = [
+ 'text' => trans('modules.installation.unzip', ['module' => $name]),
+ 'url' => url('apps/unzip')
+ ];
+
+ // Download
+ $json['step'][] = [
+ 'text' => trans('modules.installation.install', ['module' => $name]),
+ 'url' => url('apps/install')
+ ];
+
+ return response()->json($json);
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param $request
+ *
+ * @return Response
+ */
+ public function download(Request $request)
+ {
+ $this->checkApiToken();
+
+ $path = $request['path'];
+
+ $version = $request['version'];
+
+ $path .= '/' . $version . '/' . version('short') . '/' . setting('general.api_token');
+
+ $json = $this->downloadModule($path);
+
+ return response()->json($json);
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param $request
+ *
+ * @return Response
+ */
+ public function unzip(Request $request)
+ {
+ $this->checkApiToken();
+
+ $path = $request['path'];
+
+ $json = $this->unzipModule($path);
+
+ return response()->json($json);
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @param $request
+ *
+ * @return Response
+ */
+ public function install(Request $request)
+ {
+ $this->checkApiToken();
+
+ $path = $request['path'];
+
+ $json = $this->installModule($path);
+
+ if ($json['success']) {
+ $message = trans('modules.installed', ['module' => $json['data']['name']]);
+
+ flash($message)->success();
+ }
+
+ return response()->json($json);
+ }
+
+ public function uninstall($alias)
+ {
+ $this->checkApiToken();
+
+ $json = $this->uninstallModule($alias);
+
+ $module = Module::alias($alias)->first();
+
+ $data = [
+ 'company_id' => session('company_id'),
+ 'module_id' => $module->id,
+ 'category' => $json['data']['category'],
+ 'version' => $json['data']['version'],
+ 'description' => trans('modules.uninstalled', ['module' => $json['data']['name']]),
+ ];
+
+ ModuleHistory::create($data);
+
+ $module->delete();
+
+ $message = trans('modules.uninstalled', ['module' => $json['data']['name']]);
+
+ flash($message)->success();
+
+ return redirect('apps/' . $alias)->send();
+ }
+
+ public function update($alias)
+ {
+ $this->checkApiToken();
+
+ $json = $this->updateModule($alias);
+
+ $module = Module::alias($alias)->first();
+
+ $data = [
+ 'company_id' => session('company_id'),
+ 'module_id' => $module->id,
+ 'category' => $json['data']['category'],
+ 'version' => $json['data']['version'],
+ 'description' => trans_choice('modules.updated', $json['data']['name']),
+ ];
+
+ ModuleHistory::create($data);
+
+ $message = trans('modules.updated', ['module' => $json['data']['name']]);
+
+ flash($message)->success();
+
+ return redirect('apps/' . $alias)->send();
+ }
+
+ public function enable($alias)
+ {
+ $this->checkApiToken();
+
+ $json = $this->enableModule($alias);
+
+ $module = Module::alias($alias)->first();
+
+ $data = [
+ 'company_id' => session('company_id'),
+ 'module_id' => $module->id,
+ 'category' => $json['data']['category'],
+ 'version' => $json['data']['version'],
+ 'description' => trans('modules.enabled', ['module' => $json['data']['name']]),
+ ];
+
+ $module->status = 1;
+
+ $module->save();
+
+ ModuleHistory::create($data);
+
+ $message = trans('modules.enabled', ['module' => $json['data']['name']]);
+
+ flash($message)->success();
+
+ return redirect('apps/' . $alias)->send();
+ }
+
+ public function disable($alias)
+ {
+ $this->checkApiToken();
+
+ $json = $this->disableModule($alias);
+
+ $module = Module::alias($alias)->first();
+
+ $data = [
+ 'company_id' => session('company_id'),
+ 'module_id' => $module->id,
+ 'category' => $json['data']['category'],
+ 'version' => $json['data']['version'],
+ 'description' => trans('modules.disabled', ['module' => $json['data']['name']]),
+ ];
+
+ $module->status = 0;
+
+ $module->save();
+
+ ModuleHistory::create($data);
+
+ $message = trans('modules.disabled', ['module' => $json['data']['name']]);
+
+ flash($message)->success();
+
+ return redirect('apps/' . $alias)->send();
+ }
+
+ /**
+ * Final actions post update.
+ *
+ * @param $alias
+ * @param $old
+ * @param $new
+ * @return Response
+ */
+ public function post($alias)
+ {
+ Artisan::call('module:install', ['alias' => $alias, 'company_id' => session('company_id')]);
+
+ $module = LModule::findByAlias($alias);
+
+ $message = trans('modules.installed', ['module' => $module->get('name')]);
+
+ flash($message)->success();
+
+ return redirect('apps/' . $alias);
+ }
+}
diff --git a/app/Http/Controllers/Modules/My.php b/app/Http/Controllers/Modules/My.php
new file mode 100755
index 0000000..5d261dc
--- /dev/null
+++ b/app/Http/Controllers/Modules/My.php
@@ -0,0 +1,29 @@
+checkApiToken();
+
+ $purchased = $this->getMyModules();
+ $modules = $this->getInstalledModules();
+ $installed = Module::where('company_id', '=', session('company_id'))->pluck('status', 'alias')->toArray();
+
+ return view('modules.my.index', compact('purchased', 'modules', 'installed'));
+ }
+}
diff --git a/app/Http/Controllers/Modules/Tiles.php b/app/Http/Controllers/Modules/Tiles.php
new file mode 100755
index 0000000..f910bea
--- /dev/null
+++ b/app/Http/Controllers/Modules/Tiles.php
@@ -0,0 +1,106 @@
+checkApiToken();
+
+ $data = $this->getModulesByCategory($alias);
+
+ $title = $data->category->name;
+ $modules = $data->modules;
+ $installed = Module::all()->pluck('status', 'alias')->toArray();
+
+ return view('modules.tiles.index', compact('title', 'modules', 'installed'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function paidModules()
+ {
+ $this->checkApiToken();
+
+ $title = trans('modules.top_paid');
+ $modules = $this->getPaidModules();
+ $installed = Module::all()->pluck('status', 'alias')->toArray();
+
+ return view('modules.tiles.index', compact('title', 'modules', 'installed'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function newModules()
+ {
+ $this->checkApiToken();
+
+ $title = trans('modules.new');
+ $modules = $this->getNewModules();
+ $installed = Module::all()->pluck('status', 'alias')->toArray();
+
+ return view('modules.tiles.index', compact('title', 'modules', 'installed'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function freeModules()
+ {
+ $this->checkApiToken();
+
+ $title = trans('modules.top_free');
+ $modules = $this->getFreeModules();
+ $installed = Module::all()->pluck('status', 'alias')->toArray();
+
+ return view('modules.tiles.index', compact('title', 'modules', 'installed'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function searchModules(Request $request)
+ {
+ $this->checkApiToken();
+
+ $keyword = $request['keyword'];
+
+ $data = [
+ 'query' => [
+ 'keyword' => $keyword,
+ ]
+ ];
+
+ $title = trans('modules.search');
+ $modules = $this->getSearchModules($data);
+ $installed = Module::all()->pluck('status', 'alias')->toArray();
+
+ return view('modules.tiles.index', compact('title', 'modules', 'keyword', 'installed'));
+ }
+}
diff --git a/app/Http/Controllers/Modules/Token.php b/app/Http/Controllers/Modules/Token.php
new file mode 100755
index 0000000..13e1290
--- /dev/null
+++ b/app/Http/Controllers/Modules/Token.php
@@ -0,0 +1,37 @@
+set('general.api_token', $request['api_token']);
+
+ setting()->save();
+
+ return redirect('apps/home');
+ }
+}
diff --git a/app/Http/Controllers/Reports/ExpenseSummary.php b/app/Http/Controllers/Reports/ExpenseSummary.php
new file mode 100755
index 0000000..ff19da3
--- /dev/null
+++ b/app/Http/Controllers/Reports/ExpenseSummary.php
@@ -0,0 +1,134 @@
+type('expense')->pluck('name', 'id')->toArray();
+
+ // Get year
+ $year = request('year');
+ if (empty($year)) {
+ $year = Date::now()->year;
+ }
+
+ // Dates
+ for ($j = 1; $j <= 12; $j++) {
+ $dates[$j] = Date::parse($year . '-' . $j)->format('F');
+
+ $expenses_graph[Date::parse($year . '-' . $j)->format('F-Y')] = 0;
+
+ // Totals
+ $totals[$dates[$j]] = array(
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+
+ foreach ($categories as $category_id => $category_name) {
+ $expenses[$category_id][$dates[$j]] = array(
+ 'category_id' => $category_id,
+ 'name' => $category_name,
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+ }
+ }
+
+ // Bills
+ switch ($status) {
+ case 'paid':
+ $bills = BillPayment::monthsOfYear('paid_at')->get();
+ $this->setAmount($expenses_graph, $totals, $expenses, $bills, 'bill', 'paid_at');
+ break;
+ case 'upcoming':
+ $bills = Bill::accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($expenses_graph, $totals, $expenses, $bills, 'bill', 'due_at');
+ break;
+ default:
+ $bills = Bill::accrued()->monthsOfYear('billed_at')->get();
+ $this->setAmount($expenses_graph, $totals, $expenses, $bills, 'bill', 'billed_at');
+ break;
+ }
+
+ // Payments
+ if ($status != 'upcoming') {
+ $payments = Payment::monthsOfYear('paid_at')->isNotTransfer()->get();
+ $this->setAmount($expenses_graph, $totals, $expenses, $payments, 'payment', 'paid_at');
+ }
+
+ // Check if it's a print or normal request
+ if (request('print')) {
+ $chart_template = 'vendor.consoletvs.charts.chartjs.multi.line_print';
+ $view_template = 'reports.expense_summary.print';
+ } else {
+ $chart_template = 'vendor.consoletvs.charts.chartjs.multi.line';
+ $view_template = 'reports.expense_summary.index';
+ }
+
+ // Expenses chart
+ $chart = Charts::multi('line', 'chartjs')
+ ->dimensions(0, 300)
+ ->colors(['#F56954'])
+ ->dataset(trans_choice('general.expenses', 1), $expenses_graph)
+ ->labels($dates)
+ ->credits(false)
+ ->view($chart_template);
+
+ return view($view_template, compact('chart', 'dates', 'categories', 'expenses', 'totals'));
+ }
+
+ private function setAmount(&$graph, &$totals, &$expenses, $items, $type, $date_field)
+ {
+ foreach ($items as $item) {
+ if ($item['table'] == 'bill_payments') {
+ $bill = $item->bill;
+
+ $item->category_id = $bill->category_id;
+ }
+
+ $date = Date::parse($item->$date_field)->format('F');
+
+ if (!isset($expenses[$item->category_id])) {
+ continue;
+ }
+
+ $amount = $item->getConvertedAmount();
+
+ // Forecasting
+ if (($type == 'bill') && ($date_field == 'due_at')) {
+ foreach ($item->payments as $payment) {
+ $amount -= $payment->getConvertedAmount();
+ }
+ }
+
+ $expenses[$item->category_id][$date]['amount'] += $amount;
+ $expenses[$item->category_id][$date]['currency_code'] = $item->currency_code;
+ $expenses[$item->category_id][$date]['currency_rate'] = $item->currency_rate;
+
+ $graph[Date::parse($item->$date_field)->format('F-Y')] += $amount;
+
+ $totals[$date]['amount'] += $amount;
+ }
+ }
+}
diff --git a/app/Http/Controllers/Reports/IncomeExpenseSummary.php b/app/Http/Controllers/Reports/IncomeExpenseSummary.php
new file mode 100755
index 0000000..b09cc7d
--- /dev/null
+++ b/app/Http/Controllers/Reports/IncomeExpenseSummary.php
@@ -0,0 +1,179 @@
+type('income')->pluck('name', 'id')->toArray();
+
+ $expense_categories = Category::enabled()->type('expense')->pluck('name', 'id')->toArray();
+
+ // Get year
+ $year = request('year');
+ if (empty($year)) {
+ $year = Date::now()->year;
+ }
+
+ // Dates
+ for ($j = 1; $j <= 12; $j++) {
+ $dates[$j] = Date::parse($year . '-' . $j)->format('F');
+
+ $profit_graph[Date::parse($year . '-' . $j)->format('F-Y')] = 0;
+
+ // Totals
+ $totals[$dates[$j]] = array(
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+
+ foreach ($income_categories as $category_id => $category_name) {
+ $compares['income'][$category_id][$dates[$j]] = array(
+ 'category_id' => $category_id,
+ 'name' => $category_name,
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+ }
+
+ foreach ($expense_categories as $category_id => $category_name) {
+ $compares['expense'][$category_id][$dates[$j]] = array(
+ 'category_id' => $category_id,
+ 'name' => $category_name,
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+ }
+ }
+
+ // Invoices
+ switch ($status) {
+ case 'paid':
+ $invoices = InvoicePayment::monthsOfYear('paid_at')->get();
+ $this->setAmount($profit_graph, $totals, $compares, $invoices, 'invoice', 'paid_at');
+ break;
+ case 'upcoming':
+ $invoices = Invoice::accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($profit_graph, $totals, $compares, $invoices, 'invoice', 'due_at');
+ break;
+ default:
+ $invoices = Invoice::accrued()->monthsOfYear('invoiced_at')->get();
+ $this->setAmount($profit_graph, $totals, $compares, $invoices, 'invoice', 'invoiced_at');
+ break;
+ }
+
+ // Revenues
+ if ($status != 'upcoming') {
+ $revenues = Revenue::monthsOfYear('paid_at')->isNotTransfer()->get();
+ $this->setAmount($profit_graph, $totals, $compares, $revenues, 'revenue', 'paid_at');
+ }
+
+ // Bills
+ switch ($status) {
+ case 'paid':
+ $bills = BillPayment::monthsOfYear('paid_at')->get();
+ $this->setAmount($profit_graph, $totals, $compares, $bills, 'bill', 'paid_at');
+ break;
+ case 'upcoming':
+ $bills = Bill::accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($profit_graph, $totals, $compares, $bills, 'bill', 'due_at');
+ break;
+ default:
+ $bills = Bill::accrued()->monthsOfYear('billed_at')->get();
+ $this->setAmount($profit_graph, $totals, $compares, $bills, 'bill', 'billed_at');
+ break;
+ }
+
+ // Payments
+ if ($status != 'upcoming') {
+ $payments = Payment::monthsOfYear('paid_at')->isNotTransfer()->get();
+ $this->setAmount($profit_graph, $totals, $compares, $payments, 'payment', 'paid_at');
+ }
+
+ // Check if it's a print or normal request
+ if (request('print')) {
+ $chart_template = 'vendor.consoletvs.charts.chartjs.multi.line_print';
+ $view_template = 'reports.income_expense_summary.print';
+ } else {
+ $chart_template = 'vendor.consoletvs.charts.chartjs.multi.line';
+ $view_template = 'reports.income_expense_summary.index';
+ }
+
+ // Profit chart
+ $chart = Charts::multi('line', 'chartjs')
+ ->dimensions(0, 300)
+ ->colors(['#6da252'])
+ ->dataset(trans_choice('general.profits', 1), $profit_graph)
+ ->labels($dates)
+ ->credits(false)
+ ->view($chart_template);
+
+ return view($view_template, compact('chart', 'dates', 'income_categories', 'expense_categories', 'compares', 'totals'));
+ }
+
+ private function setAmount(&$graph, &$totals, &$compares, $items, $type, $date_field)
+ {
+ foreach ($items as $item) {
+ if ($item['table'] == 'bill_payments' || $item['table'] == 'invoice_payments') {
+ $type_item = $item->$type;
+
+ $item->category_id = $type_item->category_id;
+ }
+
+ $date = Date::parse($item->$date_field)->format('F');
+
+ $group = (($type == 'invoice') || ($type == 'revenue')) ? 'income' : 'expense';
+
+ if (!isset($compares[$group][$item->category_id])) {
+ continue;
+ }
+
+ $amount = $item->getConvertedAmount();
+
+ // Forecasting
+ if ((($type == 'invoice') || ($type == 'bill')) && ($date_field == 'due_at')) {
+ foreach ($item->payments as $payment) {
+ $amount -= $payment->getConvertedAmount();
+ }
+ }
+
+ $compares[$group][$item->category_id][$date]['amount'] += $amount;
+ $compares[$group][$item->category_id][$date]['currency_code'] = $item->currency_code;
+ $compares[$group][$item->category_id][$date]['currency_rate'] = $item->currency_rate;
+
+ if ($group == 'income') {
+ $graph[Date::parse($item->$date_field)->format('F-Y')] += $amount;
+
+ $totals[$date]['amount'] += $amount;
+ } else {
+ $graph[Date::parse($item->$date_field)->format('F-Y')] -= $amount;
+
+ $totals[$date]['amount'] -= $amount;
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Reports/IncomeSummary.php b/app/Http/Controllers/Reports/IncomeSummary.php
new file mode 100755
index 0000000..0d72c6e
--- /dev/null
+++ b/app/Http/Controllers/Reports/IncomeSummary.php
@@ -0,0 +1,134 @@
+type('income')->pluck('name', 'id')->toArray();
+
+ // Get year
+ $year = request('year');
+ if (empty($year)) {
+ $year = Date::now()->year;
+ }
+
+ // Dates
+ for ($j = 1; $j <= 12; $j++) {
+ $dates[$j] = Date::parse($year . '-' . $j)->format('F');
+
+ $incomes_graph[Date::parse($year . '-' . $j)->format('F-Y')] = 0;
+
+ // Totals
+ $totals[$dates[$j]] = array(
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+
+ foreach ($categories as $category_id => $category_name) {
+ $incomes[$category_id][$dates[$j]] = array(
+ 'category_id' => $category_id,
+ 'name' => $category_name,
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+ }
+ }
+
+ // Invoices
+ switch ($status) {
+ case 'paid':
+ $invoices = InvoicePayment::monthsOfYear('paid_at')->get();
+ $this->setAmount($incomes_graph, $totals, $incomes, $invoices, 'invoice', 'paid_at');
+ break;
+ case 'upcoming':
+ $invoices = Invoice::accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($incomes_graph, $totals, $incomes, $invoices, 'invoice', 'due_at');
+ break;
+ default:
+ $invoices = Invoice::accrued()->monthsOfYear('invoiced_at')->get();
+ $this->setAmount($incomes_graph, $totals, $incomes, $invoices, 'invoice', 'invoiced_at');
+ break;
+ }
+
+ // Revenues
+ if ($status != 'upcoming') {
+ $revenues = Revenue::monthsOfYear('paid_at')->isNotTransfer()->get();
+ $this->setAmount($incomes_graph, $totals, $incomes, $revenues, 'revenue', 'paid_at');
+ }
+
+ // Check if it's a print or normal request
+ if (request('print')) {
+ $chart_template = 'vendor.consoletvs.charts.chartjs.multi.line_print';
+ $view_template = 'reports.income_summary.print';
+ } else {
+ $chart_template = 'vendor.consoletvs.charts.chartjs.multi.line';
+ $view_template = 'reports.income_summary.index';
+ }
+
+ // Incomes chart
+ $chart = Charts::multi('line', 'chartjs')
+ ->dimensions(0, 300)
+ ->colors(['#00c0ef'])
+ ->dataset(trans_choice('general.incomes', 1), $incomes_graph)
+ ->labels($dates)
+ ->credits(false)
+ ->view($chart_template);
+
+ return view($view_template, compact('chart', 'dates', 'categories', 'incomes', 'totals'));
+ }
+
+ private function setAmount(&$graph, &$totals, &$incomes, $items, $type, $date_field)
+ {
+ foreach ($items as $item) {
+ if ($item['table'] == 'invoice_payments') {
+ $invoice = $item->invoice;
+
+ $item->category_id = $invoice->category_id;
+ }
+
+ $date = Date::parse($item->$date_field)->format('F');
+
+ if (!isset($incomes[$item->category_id])) {
+ continue;
+ }
+
+ $amount = $item->getConvertedAmount();
+
+ // Forecasting
+ if (($type == 'invoice') && ($date_field == 'due_at')) {
+ foreach ($item->payments as $payment) {
+ $amount -= $payment->getConvertedAmount();
+ }
+ }
+
+ $incomes[$item->category_id][$date]['amount'] += $amount;
+ $incomes[$item->category_id][$date]['currency_code'] = $item->currency_code;
+ $incomes[$item->category_id][$date]['currency_rate'] = $item->currency_rate;
+
+ $graph[Date::parse($item->$date_field)->format('F-Y')] += $amount;
+
+ $totals[$date]['amount'] += $amount;
+ }
+ }
+}
diff --git a/app/Http/Controllers/Reports/ProfitLoss.php b/app/Http/Controllers/Reports/ProfitLoss.php
new file mode 100755
index 0000000..96cecdd
--- /dev/null
+++ b/app/Http/Controllers/Reports/ProfitLoss.php
@@ -0,0 +1,195 @@
+type('income')->pluck('name', 'id')->toArray();
+
+ $expense_categories = Category::enabled()->type('expense')->pluck('name', 'id')->toArray();
+
+ // Get year
+ $year = request('year');
+ if (empty($year)) {
+ $year = Date::now()->year;
+ }
+
+ // Dates
+ for ($j = 1; $j <= 12; $j++) {
+ $dates[$j] = Date::parse($year . '-' . $j)->quarter;
+
+ // Totals
+ $totals[$dates[$j]] = array(
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ );
+
+ foreach ($income_categories as $category_id => $category_name) {
+ $compares['income'][$category_id][$dates[$j]] = [
+ 'category_id' => $category_id,
+ 'name' => $category_name,
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ ];
+ }
+
+ foreach ($expense_categories as $category_id => $category_name) {
+ $compares['expense'][$category_id][$dates[$j]] = [
+ 'category_id' => $category_id,
+ 'name' => $category_name,
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ ];
+ }
+
+ $j += 2;
+ }
+
+ $totals['total'] = [
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ ];
+
+ $gross['income'] = $gross['expense'] = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 'total' => 0];
+
+ foreach ($income_categories as $category_id => $category_name) {
+ $compares['income'][$category_id]['total'] = [
+ 'category_id' => $category_id,
+ 'name' => trans_choice('general.totals', 1),
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ ];
+ }
+
+ foreach ($expense_categories as $category_id => $category_name) {
+ $compares['expense'][$category_id]['total'] = [
+ 'category_id' => $category_id,
+ 'name' => trans_choice('general.totals', 1),
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1
+ ];
+ }
+
+ // Invoices
+ switch ($status) {
+ case 'paid':
+ $invoices = InvoicePayment::monthsOfYear('paid_at')->get();
+ $this->setAmount($totals, $compares, $invoices, 'invoice', 'paid_at');
+ break;
+ case 'upcoming':
+ $invoices = Invoice::accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($totals, $compares, $invoices, 'invoice', 'due_at');
+ break;
+ default:
+ $invoices = Invoice::accrued()->monthsOfYear('invoiced_at')->get();
+ $this->setAmount($totals, $compares, $invoices, 'invoice', 'invoiced_at');
+ break;
+ }
+
+ // Revenues
+ if ($status != 'upcoming') {
+ $revenues = Revenue::monthsOfYear('paid_at')->isNotTransfer()->get();
+ $this->setAmount($totals, $compares, $revenues, 'revenue', 'paid_at');
+ }
+
+ // Bills
+ switch ($status) {
+ case 'paid':
+ $bills = BillPayment::monthsOfYear('paid_at')->get();
+ $this->setAmount($totals, $compares, $bills, 'bill', 'paid_at');
+ break;
+ case 'upcoming':
+ $bills = Bill::accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($totals, $compares, $bills, 'bill', 'due_at');
+ break;
+ default:
+ $bills = Bill::accrued()->monthsOfYear('billed_at')->get();
+ $this->setAmount($totals, $compares, $bills, 'bill', 'billed_at');
+ break;
+ }
+
+ // Payments
+ if ($status != 'upcoming') {
+ $payments = Payment::monthsOfYear('paid_at')->isNotTransfer()->get();
+ $this->setAmount($totals, $compares, $payments, 'payment', 'paid_at');
+ }
+
+ // Check if it's a print or normal request
+ if (request('print')) {
+ $view_template = 'reports.profit_loss.print';
+ } else {
+ $view_template = 'reports.profit_loss.index';
+ }
+
+ return view($view_template, compact('dates', 'income_categories', 'expense_categories', 'compares', 'totals', 'gross'));
+ }
+
+ private function setAmount(&$totals, &$compares, $items, $type, $date_field)
+ {
+ foreach ($items as $item) {
+ if ($item['table'] == 'bill_payments' || $item['table'] == 'invoice_payments') {
+ $type_item = $item->$type;
+
+ $item->category_id = $type_item->category_id;
+ }
+
+ $date = Date::parse($item->$date_field)->quarter;
+
+ $group = (($type == 'invoice') || ($type == 'revenue')) ? 'income' : 'expense';
+
+ if (!isset($compares[$group][$item->category_id])) {
+ continue;
+ }
+
+ $amount = $item->getConvertedAmount();
+
+ // Forecasting
+ if ((($type == 'invoice') || ($type == 'bill')) && ($date_field == 'due_at')) {
+ foreach ($item->payments as $payment) {
+ $amount -= $payment->getConvertedAmount();
+ }
+ }
+
+ $compares[$group][$item->category_id][$date]['amount'] += $amount;
+ $compares[$group][$item->category_id][$date]['currency_code'] = $item->currency_code;
+ $compares[$group][$item->category_id][$date]['currency_rate'] = $item->currency_rate;
+ $compares[$group][$item->category_id]['total']['amount'] += $amount;
+
+ if ($group == 'income') {
+ $totals[$date]['amount'] += $amount;
+ $totals['total']['amount'] += $amount;
+ } else {
+ $totals[$date]['amount'] -= $amount;
+ $totals['total']['amount'] -= $amount;
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Reports/TaxSummary.php b/app/Http/Controllers/Reports/TaxSummary.php
new file mode 100755
index 0000000..5638c05
--- /dev/null
+++ b/app/Http/Controllers/Reports/TaxSummary.php
@@ -0,0 +1,141 @@
+where('rate', '<>', '0')->pluck('name')->toArray();
+
+ $taxes = array_combine($t, $t);
+
+ // Get year
+ $year = request('year');
+ if (empty($year)) {
+ $year = Date::now()->year;
+ }
+
+ // Dates
+ for ($j = 1; $j <= 12; $j++) {
+ $dates[$j] = Date::parse($year . '-' . $j)->format('M');
+
+ foreach ($taxes as $tax_name) {
+ $incomes[$tax_name][$dates[$j]] = [
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1,
+ ];
+
+ $expenses[$tax_name][$dates[$j]] = [
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1,
+ ];
+
+ $totals[$tax_name][$dates[$j]] = [
+ 'amount' => 0,
+ 'currency_code' => setting('general.default_currency'),
+ 'currency_rate' => 1,
+ ];
+ }
+ }
+
+ switch ($status) {
+ case 'paid':
+ // Invoices
+ $invoices = InvoicePayment::with(['invoice', 'invoice.totals'])->monthsOfYear('paid_at')->get();
+ $this->setAmount($incomes, $totals, $invoices, 'invoice', 'paid_at');
+ // Bills
+ $bills = BillPayment::with(['bill', 'bill.totals'])->monthsOfYear('paid_at')->get();
+ $this->setAmount($expenses, $totals, $bills, 'bill', 'paid_at');
+ break;
+ case 'upcoming':
+ // Invoices
+ $invoices = Invoice::with(['totals'])->accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($incomes, $totals, $invoices, 'invoice', 'due_at');
+ // Bills
+ $bills = Bill::with(['totals'])->accrued()->monthsOfYear('due_at')->get();
+ $this->setAmount($expenses, $totals, $bills, 'bill', 'due_at');
+ break;
+ default:
+ // Invoices
+ $invoices = Invoice::with(['totals'])->accrued()->monthsOfYear('invoiced_at')->get();
+ $this->setAmount($incomes, $totals, $invoices, 'invoice', 'invoiced_at');
+ // Bills
+ $bills = Bill::with(['totals'])->accrued()->monthsOfYear('billed_at')->get();
+ $this->setAmount($expenses, $totals, $bills, 'bill', 'billed_at');
+ break;
+ }
+
+ // Check if it's a print or normal request
+ if (request('print')) {
+ $view_template = 'reports.tax_summary.print';
+ } else {
+ $view_template = 'reports.tax_summary.index';
+ }
+
+ return view($view_template, compact('dates', 'taxes', 'incomes', 'expenses', 'totals'));
+ }
+
+ private function setAmount(&$items, &$totals, $rows, $type, $date_field)
+ {
+ foreach ($rows as $row) {
+ if ($row['table'] == 'bill_payments' || $row['table'] == 'invoice_payments') {
+ $type_row = $row->$type;
+
+ $row->category_id = $type_row->category_id;
+ }
+
+ $date = Date::parse($row->$date_field)->format('M');
+
+ if ($date_field == 'paid_at') {
+ $row_totals = $row->$type->totals;
+ } else {
+ $row_totals = $row->totals;
+ }
+
+ foreach ($row_totals as $row_total) {
+ if ($row_total->code != 'tax') {
+ continue;
+ }
+
+ if (!isset($items[$row_total->name])) {
+ continue;
+ }
+
+ $amount = $this->convert($row_total->amount, $row->currency_code, $row->currency_rate);
+
+ $items[$row_total->name][$date]['amount'] += $amount;
+
+ if ($type == 'invoice') {
+ $totals[$row_total->name][$date]['amount'] += $amount;
+ } else {
+ $totals[$row_total->name][$date]['amount'] -= $amount;
+ }
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Settings/Categories.php b/app/Http/Controllers/Settings/Categories.php
new file mode 100755
index 0000000..5b856b9
--- /dev/null
+++ b/app/Http/Controllers/Settings/Categories.php
@@ -0,0 +1,235 @@
+ trans_choice('general.expenses', 1),
+ 'income' => trans_choice('general.incomes', 1),
+ 'item' => trans_choice('general.items', 1),
+ 'other' => trans_choice('general.others', 1),
+ ])->prepend(trans('general.all_type', ['type' => trans_choice('general.types', 2)]), '');
+
+ return view('settings.categories.index', compact('categories', 'types', 'transfer_id'));
+ }
+
+ /**
+ * Show the form for viewing the specified resource.
+ *
+ * @return Response
+ */
+ public function show()
+ {
+ return redirect('settings/categories');
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ $types = [
+ 'expense' => trans_choice('general.expenses', 1),
+ 'income' => trans_choice('general.incomes', 1),
+ 'item' => trans_choice('general.items', 1),
+ 'other' => trans_choice('general.others', 1),
+ ];
+
+ return view('settings.categories.create', compact('types'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ Category::create($request->all());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.categories', 1)]);
+
+ flash($message)->success();
+
+ return redirect('settings/categories');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Category $category
+ *
+ * @return Response
+ */
+ public function edit(Category $category)
+ {
+ $types = [
+ 'expense' => trans_choice('general.expenses', 1),
+ 'income' => trans_choice('general.incomes', 1),
+ 'item' => trans_choice('general.items', 1),
+ 'other' => trans_choice('general.others', 1),
+ ];
+
+ $type_disabled = (Category::where('type', $category->type)->count() == 1) ?: false;
+
+ return view('settings.categories.edit', compact('category', 'types', 'type_disabled'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Category $category
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Category $category, Request $request)
+ {
+ $relationships = $this->countRelationships($category, [
+ 'items' => 'items',
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if (empty($relationships) || $request['enabled']) {
+ $category->update($request->all());
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.categories', 1)]);
+
+ flash($message)->success();
+
+ return redirect('settings/categories');
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $category->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect('settings/categories/' . $category->id . '/edit');
+ }
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Category $category
+ *
+ * @return Response
+ */
+ public function enable(Category $category)
+ {
+ $category->enabled = 1;
+ $category->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.categories', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('categories.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Category $category
+ *
+ * @return Response
+ */
+ public function disable(Category $category)
+ {
+ $relationships = $this->countRelationships($category, [
+ 'items' => 'items',
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if (empty($relationships)) {
+ $category->enabled = 0;
+ $category->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.categories', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $category->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect()->route('categories.index');
+ }
+
+ return redirect()->route('categories.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Category $category
+ *
+ * @return Response
+ */
+ public function destroy(Category $category)
+ {
+ // Can not delete the last category by type
+ if (Category::where('type', $category->type)->count() == 1) {
+ $message = trans('messages.error.last_category', ['type' => strtolower(trans_choice('general.' . $category->type . 's', 1))]);
+
+ flash($message)->warning();
+
+ return redirect('settings/categories');
+ }
+
+ $relationships = $this->countRelationships($category, [
+ 'items' => 'items',
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if (empty($relationships)) {
+ $category->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.categories', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $category->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect('settings/categories');
+ }
+
+ public function category(Request $request)
+ {
+ $category = Category::create($request->all());
+
+ return response()->json($category);
+ }
+}
diff --git a/app/Http/Controllers/Settings/Currencies.php b/app/Http/Controllers/Settings/Currencies.php
new file mode 100755
index 0000000..f7c51b7
--- /dev/null
+++ b/app/Http/Controllers/Settings/Currencies.php
@@ -0,0 +1,299 @@
+toArray();
+
+ // Prepare codes
+ $codes = array();
+ $currencies = MoneyCurrency::getCurrencies();
+ foreach ($currencies as $key => $item) {
+ // Don't show if already available
+ if (in_array($key, $current)) {
+ continue;
+ }
+
+ $codes[$key] = $key;
+ }
+
+ return view('settings.currencies.create', compact('codes'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ // Force the rate to be 1 for default currency
+ if ($request['default_currency']) {
+ $request['rate'] = '1';
+ }
+
+ Currency::create($request->all());
+
+ // Update default currency setting
+ if ($request['default_currency']) {
+ setting()->set('general.default_currency', $request['code']);
+ setting()->save();
+ }
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.currencies', 1)]);
+
+ flash($message)->success();
+
+ return redirect('settings/currencies');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Currency $currency
+ *
+ * @return Response
+ */
+ public function edit(Currency $currency)
+ {
+ // Get current currencies
+ $current = Currency::pluck('code')->toArray();
+
+ // Prepare codes
+ $codes = array();
+ $currencies = MoneyCurrency::getCurrencies();
+ foreach ($currencies as $key => $item) {
+ // Don't show if already available
+ if (($key != $currency->code) && in_array($key, $current)) {
+ continue;
+ }
+
+ $codes[$key] = $key;
+ }
+
+ // Set default currency
+ $currency->default_currency = ($currency->code == setting('general.default_currency')) ? 1 : 0;
+
+ return view('settings.currencies.edit', compact('currency', 'codes'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Currency $currency
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Currency $currency, Request $request)
+ {
+ // Check if we can disable or change the code
+ if (!$request['enabled'] || ($currency->code != $request['code'])) {
+ $relationships = $this->countRelationships($currency, [
+ 'accounts' => 'accounts',
+ 'customers' => 'customers',
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if ($currency->code == setting('general.default_currency')) {
+ $relationships[] = strtolower(trans_choice('general.companies', 1));
+ }
+ }
+
+ if (empty($relationships)) {
+ // Force the rate to be 1 for default currency
+ if ($request['default_currency']) {
+ $request['rate'] = '1';
+ }
+
+ $currency->update($request->all());
+
+ // Update default currency setting
+ if ($request['default_currency']) {
+ setting()->set('general.default_currency', $request['code']);
+ setting()->save();
+ }
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.currencies', 1)]);
+
+ flash($message)->success();
+
+ return redirect('settings/currencies');
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $currency->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect('settings/currencies/' . $currency->id . '/edit');
+ }
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Currency $currency
+ *
+ * @return Response
+ */
+ public function enable(Currency $currency)
+ {
+ $currency->enabled = 1;
+ $currency->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.currencies', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('currencies.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Currency $currency
+ *
+ * @return Response
+ */
+ public function disable(Currency $currency)
+ {
+ $relationships = $this->countRelationships($currency, [
+ 'accounts' => 'accounts',
+ 'customers' => 'customers',
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if ($currency->code == setting('general.default_currency')) {
+ $relationships[] = strtolower(trans_choice('general.companies', 1));
+ }
+
+ if (empty($relationships)) {
+ $currency->enabled = 0;
+ $currency->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.currencies', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $currency->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect()->route('currencies.index');
+ }
+
+ return redirect()->route('currencies.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Currency $currency
+ *
+ * @return Response
+ */
+ public function destroy(Currency $currency)
+ {
+ $relationships = $this->countRelationships($currency, [
+ 'accounts' => 'accounts',
+ 'customers' => 'customers',
+ 'invoices' => 'invoices',
+ 'revenues' => 'revenues',
+ 'bills' => 'bills',
+ 'payments' => 'payments',
+ ]);
+
+ if ($currency->code == setting('general.default_currency')) {
+ $relationships[] = strtolower(trans_choice('general.companies', 1));
+ }
+
+ if (empty($relationships)) {
+ $currency->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.currencies', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $currency->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect('settings/currencies');
+ }
+
+ public function currency()
+ {
+ $json = new \stdClass();
+
+ $code = request('code');
+
+ // Get currency object
+ $currency = Currency::where('code', $code)->first();
+
+ // it should be integer for amount mask
+ $currency->precision = (int) $currency->precision;
+
+ return response()->json($currency);
+ }
+
+ public function config()
+ {
+ $json = new \stdClass();
+
+ $code = request('code');
+
+ if ($code) {
+ $currency = config('money.' . $code);
+ $currency['symbol_first'] = $currency['symbol_first'] ? 1 : 0;
+
+ $json = (object) $currency;
+ }
+
+ return response()->json($json);
+ }
+}
diff --git a/app/Http/Controllers/Settings/Modules.php b/app/Http/Controllers/Settings/Modules.php
new file mode 100755
index 0000000..ae5a2be
--- /dev/null
+++ b/app/Http/Controllers/Settings/Modules.php
@@ -0,0 +1,61 @@
+pluck('value', 'key');*/
+ $setting = Setting::all($alias)->map(function($s) use($alias) {
+ $s->key = str_replace($alias . '.', '', $s->key);
+ return $s;
+ })->pluck('value', 'key');
+
+ $module = Module::findByAlias($alias);
+
+ return view('settings.modules.edit', compact('setting', 'module'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param $alias
+ *
+ * @return Response
+ */
+ public function update($alias)
+ {
+ $fields = request()->all();
+
+ $skip_keys = ['company_id', '_method', '_token'];
+
+ foreach ($fields as $key => $value) {
+ // Don't process unwanted keys
+ if (in_array($key, $skip_keys)) {
+ continue;
+ }
+
+ setting()->set($alias . '.' . $key, $value);
+ }
+
+ // Save all settings
+ setting()->save();
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]);
+
+ flash($message)->success();
+
+ return redirect('settings/apps/' . $alias);
+ }
+}
diff --git a/app/Http/Controllers/Settings/Settings.php b/app/Http/Controllers/Settings/Settings.php
new file mode 100755
index 0000000..59dddca
--- /dev/null
+++ b/app/Http/Controllers/Settings/Settings.php
@@ -0,0 +1,177 @@
+pluck('value', 'key');*/
+ $setting = Setting::all()->map(function ($s) {
+ $s->key = str_replace('general.', '', $s->key);
+
+ return $s;
+ })->pluck('value', 'key');
+
+ $company_logo = $setting->pull('company_logo');
+
+ $setting['company_logo'] = Media::find($company_logo);
+
+ $invoice_logo = $setting->pull('invoice_logo');
+
+ $setting['invoice_logo'] = Media::find($invoice_logo);
+
+ $timezones = $this->getTimezones();
+
+ $accounts = Account::enabled()->orderBy('name')->pluck('name', 'id');
+
+ $currencies = Currency::enabled()->orderBy('name')->pluck('name', 'code');
+
+ $taxes = Tax::enabled()->orderBy('rate')->get()->pluck('title', 'id');
+
+ $payment_methods = Modules::getPaymentMethods();
+
+ $date_formats = [
+ 'd M Y' => '31 Dec 2017',
+ 'd F Y' => '31 December 2017',
+ 'd m Y' => '31 12 2017',
+ 'm d Y' => '12 31 2017',
+ 'Y m d' => '2017 12 31',
+ ];
+
+ $date_separators = [
+ 'dash' => trans('settings.localisation.date.dash'),
+ 'slash' => trans('settings.localisation.date.slash'),
+ 'dot' => trans('settings.localisation.date.dot'),
+ 'comma' => trans('settings.localisation.date.comma'),
+ 'space' => trans('settings.localisation.date.space'),
+ ];
+
+ $email_protocols = [
+ 'mail' => trans('settings.email.php'),
+ 'smtp' => trans('settings.email.smtp.name'),
+ 'sendmail' => trans('settings.email.sendmail'),
+ 'log' => trans('settings.email.log'),
+ ];
+
+ $percent_positions = [
+ 'before' => trans('settings.localisation.percent.before'),
+ 'after' => trans('settings.localisation.percent.after'),
+ ];
+
+ return view('settings.settings.edit', compact(
+ 'setting',
+ 'timezones',
+ 'accounts',
+ 'currencies',
+ 'taxes',
+ 'payment_methods',
+ 'date_formats',
+ 'date_separators',
+ 'email_protocols',
+ 'percent_positions'
+ ));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Request $request)
+ {
+ $fields = $request->all();
+ $company_id = $request->get('company_id');
+
+ if (empty($company_id)) {
+ $company_id = session('company_id');
+ }
+
+ $company = Company::find($company_id);
+
+ $skip_keys = ['company_id', '_method', '_token'];
+ $file_keys = ['company_logo', 'invoice_logo'];
+
+ $companies = Company::all()->count();
+
+ foreach ($fields as $key => $value) {
+ // Don't process unwanted keys
+ if (in_array($key, $skip_keys)) {
+ continue;
+ }
+
+ // Process file uploads
+ if (in_array($key, $file_keys)) {
+ // Upload attachment
+ if ($request->file($key)) {
+ $media = $this->getMedia($request->file($key), 'settings');
+
+ $company->attachMedia($media, $key);
+
+ $value = $media->id;
+ }
+
+ // Prevent reset
+ if (empty($value)) {
+ continue;
+ }
+ }
+
+ // If only 1 company
+ if ($companies == 1) {
+ $this->oneCompany($key, $value);
+ }
+
+ setting()->set('general.' . $key, $value);
+ }
+
+ // Save all settings
+ setting()->save();
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.settings', 2)]);
+
+ flash($message)->success();
+
+ return redirect('settings/settings');
+ }
+
+ protected function oneCompany($key, $value)
+ {
+ switch ($key) {
+ case 'default_locale':
+ // Change default locale
+ Installer::updateEnv([
+ 'APP_LOCALE' => $value
+ ]);
+ break;
+ case 'session_handler':
+ // Change session handler
+ Installer::updateEnv([
+ 'SESSION_DRIVER' => $value
+ ]);
+ break;
+ }
+ }
+}
diff --git a/app/Http/Controllers/Settings/Taxes.php b/app/Http/Controllers/Settings/Taxes.php
new file mode 100755
index 0000000..89cfa59
--- /dev/null
+++ b/app/Http/Controllers/Settings/Taxes.php
@@ -0,0 +1,188 @@
+all());
+
+ $message = trans('messages.success.added', ['type' => trans_choice('general.tax_rates', 1)]);
+
+ flash($message)->success();
+
+ return redirect('settings/taxes');
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param Tax $tax
+ *
+ * @return Response
+ */
+ public function edit(Tax $tax)
+ {
+ return view('settings.taxes.edit', compact('tax'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param Tax $tax
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function update(Tax $tax, Request $request)
+ {
+ $relationships = $this->countRelationships($tax, [
+ 'items' => 'items',
+ 'invoice_items' => 'invoices',
+ 'bill_items' => 'bills',
+ ]);
+
+ if (empty($relationships) || $request['enabled']) {
+ $tax->update($request->all());
+
+ $message = trans('messages.success.updated', ['type' => trans_choice('general.tax_rates', 1)]);
+
+ flash($message)->success();
+
+ return redirect('settings/taxes');
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $tax->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect('settings/taxes/' . $tax->id . '/edit');
+ }
+ }
+
+ /**
+ * Enable the specified resource.
+ *
+ * @param Tax $tax
+ *
+ * @return Response
+ */
+ public function enable(Tax $tax)
+ {
+ $tax->enabled = 1;
+ $tax->save();
+
+ $message = trans('messages.success.enabled', ['type' => trans_choice('general.tax_rates', 1)]);
+
+ flash($message)->success();
+
+ return redirect()->route('taxes.index');
+ }
+
+ /**
+ * Disable the specified resource.
+ *
+ * @param Tax $tax
+ *
+ * @return Response
+ */
+ public function disable(Tax $tax)
+ {
+ $relationships = $this->countRelationships($tax, [
+ 'items' => 'items',
+ 'invoice_items' => 'invoices',
+ 'bill_items' => 'bills',
+ ]);
+
+ if (empty($relationships)) {
+ $tax->enabled = 0;
+ $tax->save();
+
+ $message = trans('messages.success.disabled', ['type' => trans_choice('general.tax_rates', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.disabled', ['name' => $tax->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+
+ return redirect()->route('taxes.index');
+ }
+
+ return redirect()->route('taxes.index');
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param Tax $tax
+ *
+ * @return Response
+ */
+ public function destroy(Tax $tax)
+ {
+ $relationships = $this->countRelationships($tax, [
+ 'items' => 'items',
+ 'invoice_items' => 'invoices',
+ 'bill_items' => 'bills',
+ ]);
+
+ if (empty($relationships)) {
+ $tax->delete();
+
+ $message = trans('messages.success.deleted', ['type' => trans_choice('general.taxes', 1)]);
+
+ flash($message)->success();
+ } else {
+ $message = trans('messages.warning.deleted', ['name' => $tax->name, 'text' => implode(', ', $relationships)]);
+
+ flash($message)->warning();
+ }
+
+ return redirect('settings/taxes');
+ }
+}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
new file mode 100755
index 0000000..7ce07c8
--- /dev/null
+++ b/app/Http/Kernel.php
@@ -0,0 +1,97 @@
+ [
+ \App\Http\Middleware\EncryptCookies::class,
+ \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+ \Illuminate\Session\Middleware\StartSession::class,
+ // \Illuminate\Session\Middleware\AuthenticateSession::class,
+ \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \App\Http\Middleware\VerifyCsrfToken::class,
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ \App\Http\Middleware\RedirectIfNotInstalled::class,
+ \App\Http\Middleware\AddXHeader::class,
+ 'company.settings',
+ 'company.currencies',
+ ],
+
+ 'admin' => [
+ 'web',
+ 'language',
+ 'auth',
+ 'adminmenu',
+ 'permission:read-admin-panel',
+ ],
+
+ 'customer' => [
+ 'web',
+ 'language',
+ 'auth',
+ 'customermenu',
+ 'permission:read-customer-panel',
+ ],
+
+ 'api' => [
+ 'api.auth',
+ 'throttle:60,1',
+ 'bindings',
+ 'api.company',
+ 'permission:read-api',
+ 'company.settings',
+ 'company.currencies',
+ ],
+ ];
+
+ /**
+ * The application's route middleware.
+ *
+ * These middleware may be assigned to groups or used individually.
+ *
+ * @var array
+ */
+ protected $routeMiddleware = [
+ 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
+ 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+ 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ 'can' => \Illuminate\Auth\Middleware\Authorize::class,
+ 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
+ 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ 'adminmenu' => \App\Http\Middleware\AdminMenu::class,
+ 'customermenu' => \App\Http\Middleware\CustomerMenu::class,
+ 'role' => \Laratrust\Middleware\LaratrustRole::class,
+ 'permission' => \Laratrust\Middleware\LaratrustPermission::class,
+ 'ability' => \Laratrust\Middleware\LaratrustAbility::class,
+ 'api.company' => \App\Http\Middleware\ApiCompany::class,
+ 'install' => \App\Http\Middleware\CanInstall::class,
+ 'company.settings' => \App\Http\Middleware\LoadSettings::class,
+ 'company.currencies' => \App\Http\Middleware\LoadCurrencies::class,
+ 'dateformat' => \App\Http\Middleware\DateFormat::class,
+ 'money' => \App\Http\Middleware\Money::class,
+ ];
+}
diff --git a/app/Http/Middleware/AddXHeader.php b/app/Http/Middleware/AddXHeader.php
new file mode 100755
index 0000000..037fd1e
--- /dev/null
+++ b/app/Http/Middleware/AddXHeader.php
@@ -0,0 +1,27 @@
+header('X-Akaunting', 'Free Accounting Software');
+ }
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Middleware/AdminMenu.php b/app/Http/Middleware/AdminMenu.php
new file mode 100755
index 0000000..817204b
--- /dev/null
+++ b/app/Http/Middleware/AdminMenu.php
@@ -0,0 +1,207 @@
+style('adminlte');
+
+ $user = Auth::user();
+ $attr = ['icon' => 'fa fa-angle-double-right'];
+
+ // Dashboard
+ $menu->add([
+ 'url' => '/',
+ 'title' => trans('general.dashboard'),
+ 'icon' => 'fa fa-dashboard',
+ 'order' => 1,
+ ]);
+
+ // Items
+ if ($user->can('read-common-items')) {
+ $menu->add([
+ 'url' => 'common/items',
+ 'title' => trans_choice('general.items', 2),
+ 'icon' => 'fa fa-cubes',
+ 'order' => 2,
+ ]);
+ }
+
+ // Incomes
+ if ($user->can(['read-incomes-invoices', 'read-incomes-revenues', 'read-incomes-customers'])) {
+ $menu->dropdown(trans_choice('general.incomes', 2), function ($sub) use($user, $attr) {
+ if ($user->can('read-incomes-invoices')) {
+ $sub->url('incomes/invoices', trans_choice('general.invoices', 2), 1, $attr);
+ }
+
+ if ($user->can('read-incomes-revenues')) {
+ $sub->url('incomes/revenues', trans_choice('general.revenues', 2), 2, $attr);
+ }
+
+ if ($user->can('read-incomes-customers')) {
+ $sub->url('incomes/customers', trans_choice('general.customers', 2), 3, $attr);
+ }
+ }, 3, [
+ 'title' => trans_choice('general.incomes', 2),
+ 'icon' => 'fa fa-money',
+ ]);
+ }
+
+ // Expences
+ if ($user->can(['read-expenses-bills', 'read-expenses-payments', 'read-expenses-vendors'])) {
+ $menu->dropdown(trans_choice('general.expenses', 2), function ($sub) use($user, $attr) {
+ if ($user->can('read-expenses-bills')) {
+ $sub->url('expenses/bills', trans_choice('general.bills', 2), 1, $attr);
+ }
+
+ if ($user->can('read-expenses-payments')) {
+ $sub->url('expenses/payments', trans_choice('general.payments', 2), 2, $attr);
+ }
+
+ if ($user->can('read-expenses-vendors')) {
+ $sub->url('expenses/vendors', trans_choice('general.vendors', 2), 3, $attr);
+ }
+ }, 4, [
+ 'title' => trans_choice('general.expenses', 2),
+ 'icon' => 'fa fa-shopping-cart',
+ ]);
+ }
+
+ // Banking
+ if ($user->can(['read-banking-accounts', 'read-banking-transfers', 'read-banking-transactions'])) {
+ $menu->dropdown(trans('general.banking'), function ($sub) use($user, $attr) {
+ if ($user->can('read-banking-accounts')) {
+ $sub->url('banking/accounts', trans_choice('general.accounts', 2), 1, $attr);
+ }
+
+ if ($user->can('read-banking-transfers')) {
+ $sub->url('banking/transfers', trans_choice('general.transfers', 2), 2, $attr);
+ }
+
+ if ($user->can('read-banking-transactions')) {
+ $sub->url('banking/transactions', trans_choice('general.transactions', 2), 3, $attr);
+ }
+ }, 5, [
+ 'title' => trans('general.banking'),
+ 'icon' => 'fa fa-university',
+ ]);
+ }
+
+ // Reports
+ if ($user->can([
+ 'read-reports-income-summary',
+ 'read-reports-expense-summary',
+ 'read-reports-income-expense-summary',
+ 'read-reports-tax-summary',
+ 'read-reports-profit-loss',
+ ])) {
+ $menu->dropdown(trans_choice('general.reports', 2), function ($sub) use($user, $attr) {
+ if ($user->can('read-reports-income-summary')) {
+ $sub->url('reports/income-summary', trans('reports.summary.income'), 1, $attr);
+ }
+
+ if ($user->can('read-reports-expense-summary')) {
+ $sub->url('reports/expense-summary', trans('reports.summary.expense'), 2, $attr);
+ }
+
+ if ($user->can('read-reports-income-expense-summary')) {
+ $sub->url('reports/income-expense-summary', trans('reports.summary.income_expense'), 3, $attr);
+ }
+
+ if ($user->can('read-reports-tax-summary')) {
+ $sub->url('reports/tax-summary', trans('reports.summary.tax'), 4, $attr);
+ }
+
+ if ($user->can('read-reports-profit-loss')) {
+ $sub->url('reports/profit-loss', trans('reports.profit_loss'), 5, $attr);
+ }
+ }, 6, [
+ 'title' => trans_choice('general.reports', 2),
+ 'icon' => 'fa fa-bar-chart',
+ ]);
+ }
+
+ // Settings
+ if ($user->can(['read-settings-settings', 'read-settings-categories', 'read-settings-currencies', 'read-settings-taxes'])) {
+ $menu->dropdown(trans_choice('general.settings', 2), function ($sub) use($user, $attr) {
+ if ($user->can('read-settings-settings')) {
+ $sub->url('settings/settings', trans('general.general'), 1, $attr);
+ }
+
+ if ($user->can('read-settings-categories')) {
+ $sub->url('settings/categories', trans_choice('general.categories', 2), 2, $attr);
+ }
+
+ if ($user->can('read-settings-currencies')) {
+ $sub->url('settings/currencies', trans_choice('general.currencies', 2), 3, $attr);
+ }
+
+ if ($user->can('read-settings-taxes')) {
+ $sub->url('settings/taxes', trans_choice('general.tax_rates', 2), 4, $attr);
+ }
+
+ // Modules
+ $modules = Module::all();
+ $position = 5;
+ foreach ($modules as $module) {
+ if (!$module->status) {
+ continue;
+ }
+
+ $m = LaravelModule::findByAlias($module->alias);
+
+ // Check if the module exists and has settings
+ if (!$m || empty($m->get('settings'))) {
+ continue;
+ }
+
+ $sub->url('settings/apps/' . $module->alias, title_case(str_replace('_', ' ', snake_case($m->getName()))), $position, $attr);
+
+ $position++;
+ }
+ }, 7, [
+ 'title' => trans_choice('general.settings', 2),
+ 'icon' => 'fa fa-gears',
+ ]);
+ }
+
+ // Apps
+ if ($user->can('read-modules-home')) {
+ $menu->add([
+ 'url' => 'apps/home',
+ 'title' => trans_choice('general.modules', 2),
+ 'icon' => 'fa fa-rocket',
+ 'order' => 8,
+ ]);
+ }
+
+ // Fire the event to extend the menu
+ event(new AdminMenuCreated($menu));
+ });
+
+ return $next($request);
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Middleware/ApiCompany.php b/app/Http/Middleware/ApiCompany.php
new file mode 100755
index 0000000..b054ddd
--- /dev/null
+++ b/app/Http/Middleware/ApiCompany.php
@@ -0,0 +1,40 @@
+get('company_id');
+
+ if (empty($company_id)) {
+ return $next($request);
+ }
+
+ // Check if user can access company
+ $companies = app('Dingo\Api\Auth\Auth')->user()->companies()->pluck('id')->toArray();
+ if (!in_array($company_id, $companies)) {
+ return $next($request);
+ }
+
+ // Set company id
+ session(['company_id' => $company_id]);
+
+ // Set the company settings
+ setting()->setExtraColumns(['company_id' => $company_id]);
+ setting()->load(true);
+
+ return $next($request);
+ }
+
+}
\ No newline at end of file
diff --git a/app/Http/Middleware/CanInstall.php b/app/Http/Middleware/CanInstall.php
new file mode 100755
index 0000000..1ef2161
--- /dev/null
+++ b/app/Http/Middleware/CanInstall.php
@@ -0,0 +1,26 @@
+send();
+ }
+}
diff --git a/app/Http/Middleware/CustomerMenu.php b/app/Http/Middleware/CustomerMenu.php
new file mode 100755
index 0000000..fdf842a
--- /dev/null
+++ b/app/Http/Middleware/CustomerMenu.php
@@ -0,0 +1,69 @@
+style('adminlte');
+
+ $user = Auth::user();
+
+ // Dashboard
+ $menu->add([
+ 'url' => 'customers/',
+ 'title' => trans('general.dashboard'),
+ 'icon' => 'fa fa-dashboard',
+ 'order' => 1,
+ ]);
+
+ // Invoices
+ $menu->add([
+ 'url' => 'customers/invoices',
+ 'title' => trans_choice('general.invoices', 2),
+ 'icon' => 'fa fa-wpforms',
+ 'order' => 2,
+ ]);
+
+ // Payments
+ $menu->add([
+ 'url' => 'customers/payments',
+ 'title' => trans_choice('general.payments', 2),
+ 'icon' => 'fa fa-money',
+ 'order' => 3,
+ ]);
+
+ // Payments
+ $menu->add([
+ 'url' => 'customers/transactions',
+ 'title' => trans_choice('general.transactions', 2),
+ 'icon' => 'fa fa-list',
+ 'order' => 4,
+ ]);
+
+ // Fire the event to extend the menu
+ event(new CustomerMenuCreated($menu));
+ });
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/DateFormat.php b/app/Http/Middleware/DateFormat.php
new file mode 100755
index 0000000..b308637
--- /dev/null
+++ b/app/Http/Middleware/DateFormat.php
@@ -0,0 +1,37 @@
+method() == 'POST') || ($request->method() == 'PATCH')) {
+ $fields = ['paid_at', 'due_at', 'billed_at', 'invoiced_at'];
+
+ foreach ($fields as $field) {
+ $date = $request->get($field);
+
+ if (empty($date)) {
+ continue;
+ }
+
+ $new_date = Date::parse($date)->format('Y-m-d') . ' ' . Date::now()->format('H:i:s');
+
+ $request->request->set($field, $new_date);
+ }
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php
new file mode 100755
index 0000000..3aa15f8
--- /dev/null
+++ b/app/Http/Middleware/EncryptCookies.php
@@ -0,0 +1,17 @@
+method() == 'POST' || $request->method() == 'PATCH') {
+ $amount = $request->get('amount');
+ $bill_number = $request->get('bill_number');
+ $invoice_number = $request->get('invoice_number');
+ $sale_price = $request->get('sale_price');
+ $purchase_price = $request->get('purchase_price');
+ $opening_balance = $request->get('opening_balance');
+ $currency_code = $request->get('currency_code');
+ $items = $request->get('item');
+
+ if (empty($currency_code)) {
+ $currency_code = setting('general.default_currency');
+ }
+
+ if (!empty($amount)) {
+ $amount = money($request->get('amount'), $currency_code)->getAmount();
+
+ $request->request->set('amount', $amount);
+ }
+
+ if (isset($bill_number) || isset($invoice_number) || !empty($items)) {
+ if (!empty($items)) {
+ foreach ($items as $key => $item) {
+ if (!isset($item['price'])) {
+ continue;
+ }
+
+ if (isset($item['currency']) && $item['currency'] != $currency_code) {
+ $items[$key]['price'] = money($item['price'], $item['currency'])->getAmount();
+ } else {
+ $items[$key]['price'] = money($item['price'], $currency_code)->getAmount();
+ }
+ }
+
+ $request->request->set('item', $items);
+ }
+ }
+
+ if (isset($opening_balance)) {
+ $opening_balance = money($opening_balance, $currency_code)->getAmount();
+
+ $request->request->set('opening_balance', $opening_balance);
+ }
+
+ /* check item price use money
+ if (isset($sale_price)) {
+ $sale_price = money($sale_price, $currency_code)->getAmount();
+
+ $request->request->set('sale_price', $sale_price);
+ }
+
+ if (isset($purchase_price)) {
+ $purchase_price = money($purchase_price, $currency_code)->getAmount();
+
+ $request->request->set('purchase_price', $purchase_price);
+ }
+ */
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php
new file mode 100755
index 0000000..95d4172
--- /dev/null
+++ b/app/Http/Middleware/RedirectIfAuthenticated.php
@@ -0,0 +1,30 @@
+check()) {
+ if (Auth::user()->customer) {
+ return redirect('/customers');
+ }
+
+ return redirect('/');
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/RedirectIfNotInstalled.php b/app/Http/Middleware/RedirectIfNotInstalled.php
new file mode 100755
index 0000000..cff90ac
--- /dev/null
+++ b/app/Http/Middleware/RedirectIfNotInstalled.php
@@ -0,0 +1,32 @@
+getPathInfo(), '/install')) {
+ return $next($request);
+ }
+
+ // Not installed, redirect to installation wizard
+ redirect('install/requirements')->send();
+ }
+}
diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php
new file mode 100755
index 0000000..943e9a4
--- /dev/null
+++ b/app/Http/Middleware/TrimStrings.php
@@ -0,0 +1,18 @@
+getMethod() == 'PATCH') {
+ $id = $this->role->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ return [
+ 'name' => 'required|string|unique:permissions,name,' . $id,
+ 'display_name' => 'required|string',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Auth/Role.php b/app/Http/Requests/Auth/Role.php
new file mode 100755
index 0000000..31873e5
--- /dev/null
+++ b/app/Http/Requests/Auth/Role.php
@@ -0,0 +1,39 @@
+getMethod() == 'PATCH') {
+ $id = $this->role->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ return [
+ 'name' => 'required|string|unique:roles,name,' . $id,
+ 'display_name' => 'required|string',
+ 'permissions' => 'required'
+ ];
+ }
+}
diff --git a/app/Http/Requests/Auth/User.php b/app/Http/Requests/Auth/User.php
new file mode 100755
index 0000000..a1414f1
--- /dev/null
+++ b/app/Http/Requests/Auth/User.php
@@ -0,0 +1,44 @@
+getMethod() == 'PATCH') {
+ $id = $this->user->getAttribute('id');
+ $required = '';
+ } else {
+ $id = null;
+ $required = 'required|';
+ }
+
+ return [
+ 'name' => 'required|string',
+ 'email' => 'required|email|unique:users,email,' . $id . ',id,deleted_at,NULL',
+ 'password' => $required . 'confirmed',
+ 'companies' => 'required',
+ 'roles' => 'required',
+ 'picture' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+}
diff --git a/app/Http/Requests/Banking/Account.php b/app/Http/Requests/Banking/Account.php
new file mode 100755
index 0000000..25e0f53
--- /dev/null
+++ b/app/Http/Requests/Banking/Account.php
@@ -0,0 +1,34 @@
+ 'required|string',
+ 'number' => 'required|string',
+ 'currency_code' => 'required|string|currency',
+ 'opening_balance' => 'required',
+ 'enabled' => 'integer|boolean',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Banking/Transfer.php b/app/Http/Requests/Banking/Transfer.php
new file mode 100755
index 0000000..405d097
--- /dev/null
+++ b/app/Http/Requests/Banking/Transfer.php
@@ -0,0 +1,34 @@
+ 'required|integer',
+ 'to_account_id' => 'required|integer',
+ 'amount' => 'required|amount',
+ 'transferred_at' => 'required|date_format:Y-m-d',
+ 'payment_method' => 'required|string',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Common/Company.php b/app/Http/Requests/Common/Company.php
new file mode 100755
index 0000000..70c6443
--- /dev/null
+++ b/app/Http/Requests/Common/Company.php
@@ -0,0 +1,34 @@
+ 'required|string',
+ 'company_name' => 'required|string',
+ 'company_email' => 'required|email',
+ 'company_logo' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ 'default_currency' => 'required|string',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Common/Item.php b/app/Http/Requests/Common/Item.php
new file mode 100755
index 0000000..3b2f4a5
--- /dev/null
+++ b/app/Http/Requests/Common/Item.php
@@ -0,0 +1,48 @@
+getMethod() == 'PATCH') {
+ $id = $this->item->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ // Get company id
+ $company_id = $this->request->get('company_id');
+
+ return [
+ 'name' => 'required|string',
+ 'sku' => 'required|string|unique:items,NULL,' . $id . ',id,company_id,' . $company_id . ',deleted_at,NULL',
+ 'sale_price' => 'required',
+ 'purchase_price' => 'required',
+ 'quantity' => 'required|integer',
+ 'tax_id' => 'nullable|integer',
+ 'category_id' => 'nullable|integer',
+ 'enabled' => 'integer|boolean',
+ 'picture' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+}
diff --git a/app/Http/Requests/Common/TotalItem.php b/app/Http/Requests/Common/TotalItem.php
new file mode 100755
index 0000000..9e6f816
--- /dev/null
+++ b/app/Http/Requests/Common/TotalItem.php
@@ -0,0 +1,42 @@
+ 'required',
+ 'item.*.price' => 'required|amount',
+ 'item.*.currency' => 'required|string|currency',
+ ];
+ }
+
+ public function messages()
+ {
+ return [
+ 'item.*.quantity.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('invoices.quantity'))]),
+ 'item.*.price.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('invoices.price'))]),
+ 'item.*.currency.required' => trans('validation.custom.invalid_currency'),
+ 'item.*.currency.string' => trans('validation.custom.invalid_currency'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Customer/InvoiceConfirm.php b/app/Http/Requests/Customer/InvoiceConfirm.php
new file mode 100755
index 0000000..0199f62
--- /dev/null
+++ b/app/Http/Requests/Customer/InvoiceConfirm.php
@@ -0,0 +1,30 @@
+ 'required|string',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Customer/InvoicePayment.php b/app/Http/Requests/Customer/InvoicePayment.php
new file mode 100755
index 0000000..263a75f
--- /dev/null
+++ b/app/Http/Requests/Customer/InvoicePayment.php
@@ -0,0 +1,30 @@
+ 'required|string',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Customer/Profile.php b/app/Http/Requests/Customer/Profile.php
new file mode 100755
index 0000000..45b7297
--- /dev/null
+++ b/app/Http/Requests/Customer/Profile.php
@@ -0,0 +1,35 @@
+user()->getAttribute('id');
+
+ return [
+ 'name' => 'required|string',
+ 'email' => 'required|email|unique:users,email,' . $id . ',id,deleted_at,NULL',
+ 'password' => 'confirmed',
+ 'picture' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+}
diff --git a/app/Http/Requests/Expense/Bill.php b/app/Http/Requests/Expense/Bill.php
new file mode 100755
index 0000000..bfcc8aa
--- /dev/null
+++ b/app/Http/Requests/Expense/Bill.php
@@ -0,0 +1,78 @@
+getMethod() == 'PATCH') {
+ $id = $this->bill->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ // Get company id
+ $company_id = $this->request->get('company_id');
+
+ return [
+ 'bill_number' => 'required|string|unique:bills,NULL,' . $id . ',id,company_id,' . $company_id . ',deleted_at,NULL',
+ 'bill_status_code' => 'required|string',
+ 'billed_at' => 'required|date_format:Y-m-d H:i:s',
+ 'due_at' => 'required|date_format:Y-m-d H:i:s',
+ 'amount' => 'required',
+ 'item.*.name' => 'required|string',
+ 'item.*.quantity' => 'required',
+ 'item.*.price' => 'required|amount',
+ 'item.*.currency' => 'required|string|currency',
+ 'currency_code' => 'required|string|currency',
+ 'currency_rate' => 'required',
+ 'vendor_id' => 'required|integer',
+ 'vendor_name' => 'required|string',
+ 'category_id' => 'required|integer',
+ 'attachment' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+
+ public function withValidator($validator)
+ {
+ if ($validator->errors()->count()) {
+ // Set date
+ $billed_at = Date::parse($this->request->get('billed_at'))->format('Y-m-d');
+ $due_at = Date::parse($this->request->get('due_at'))->format('Y-m-d');
+
+ $this->request->set('billed_at', $billed_at);
+ $this->request->set('due_at', $due_at);
+ }
+ }
+
+ public function messages()
+ {
+ return [
+ 'item.*.name.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('general.name'))]),
+ 'item.*.quantity.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('bills.quantity'))]),
+ 'item.*.price.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('bills.price'))]),
+ 'item.*.currency.required' => trans('validation.custom.invalid_currency'),
+ 'item.*.currency.string' => trans('validation.custom.invalid_currency'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Expense/BillHistory.php b/app/Http/Requests/Expense/BillHistory.php
new file mode 100755
index 0000000..6c58f3d
--- /dev/null
+++ b/app/Http/Requests/Expense/BillHistory.php
@@ -0,0 +1,32 @@
+ 'required|integer',
+ 'status_code' => 'required|string',
+ 'notify' => 'required|integer',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Expense/BillItem.php b/app/Http/Requests/Expense/BillItem.php
new file mode 100755
index 0000000..316b3b7
--- /dev/null
+++ b/app/Http/Requests/Expense/BillItem.php
@@ -0,0 +1,37 @@
+ 'required|integer',
+ 'name' => 'required|string',
+ 'quantity' => 'required|integer',
+ 'price' => 'required',
+ 'price' => 'required',
+ 'total' => 'required',
+ 'tax' => 'required',
+ 'tax_id' => 'required',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Expense/BillPayment.php b/app/Http/Requests/Expense/BillPayment.php
new file mode 100755
index 0000000..dd88c4d
--- /dev/null
+++ b/app/Http/Requests/Expense/BillPayment.php
@@ -0,0 +1,45 @@
+ 'required|integer',
+ 'paid_at' => 'required|date_format:Y-m-d H:i:s',
+ 'amount' => 'required|amount',
+ 'currency_code' => 'required|string|currency',
+ 'payment_method' => 'required|string',
+ 'attachment' => 'mimes:' . setting('general.file_types', 'pdf,jpeg,jpg,png'),
+ ];
+ }
+
+ public function withValidator($validator)
+ {
+ if ($validator->errors()->count()) {
+ $paid_at = Date::parse($this->request->get('paid_at'))->format('Y-m-d');
+
+ $this->request->set('paid_at', $paid_at);
+ }
+ }
+}
diff --git a/app/Http/Requests/Expense/BillTotal.php b/app/Http/Requests/Expense/BillTotal.php
new file mode 100755
index 0000000..657f1fb
--- /dev/null
+++ b/app/Http/Requests/Expense/BillTotal.php
@@ -0,0 +1,33 @@
+ 'required|integer',
+ 'name' => 'required|string',
+ 'amount' => 'required|amount',
+ 'sort_order' => 'required|integer',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Expense/Payment.php b/app/Http/Requests/Expense/Payment.php
new file mode 100755
index 0000000..984c13f
--- /dev/null
+++ b/app/Http/Requests/Expense/Payment.php
@@ -0,0 +1,48 @@
+ 'required|integer',
+ 'paid_at' => 'required|date_format:Y-m-d H:i:s',
+ 'amount' => 'required|amount',
+ 'currency_code' => 'required|string|currency',
+ 'currency_rate' => 'required',
+ 'vendor_id' => 'nullable|integer',
+ 'category_id' => 'required|integer',
+ 'payment_method' => 'required|string',
+ 'attachment' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+
+ public function withValidator($validator)
+ {
+ if ($validator->errors()->count()) {
+ $paid_at = Date::parse($this->request->get('paid_at'))->format('Y-m-d');
+
+ $this->request->set('paid_at', $paid_at);
+ }
+ }
+}
diff --git a/app/Http/Requests/Expense/Vendor.php b/app/Http/Requests/Expense/Vendor.php
new file mode 100755
index 0000000..9b4d1ea
--- /dev/null
+++ b/app/Http/Requests/Expense/Vendor.php
@@ -0,0 +1,50 @@
+request->get('company_id');
+
+ // Check if store or update
+ if ($this->getMethod() == 'PATCH') {
+ $id = $this->vendor->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ if (!empty($this->request->get('email'))) {
+ $email = 'email|unique:vendors,NULL,' . $id . ',id,company_id,' . $company_id . ',deleted_at,NULL';
+ }
+
+ return [
+ 'user_id' => 'nullable|integer',
+ 'name' => 'required|string',
+ 'email' => $email,
+ 'currency_code' => 'required|string|currency',
+ 'enabled' => 'integer|boolean',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Income/Customer.php b/app/Http/Requests/Income/Customer.php
new file mode 100755
index 0000000..f57b4ab
--- /dev/null
+++ b/app/Http/Requests/Income/Customer.php
@@ -0,0 +1,56 @@
+request->get('company_id');
+
+ // Check if store or update
+ if ($this->getMethod() == 'PATCH') {
+ $id = $this->customer->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ if (!empty($this->request->get('create_user')) && empty($this->request->get('user_id'))) {
+ $required = 'required|';
+ }
+
+ if (!empty($this->request->get('email'))) {
+ $email = 'email|unique:customers,NULL,' . $id . ',id,company_id,' . $company_id . ',deleted_at,NULL';
+ }
+
+ return [
+ 'user_id' => 'nullable|integer',
+ 'name' => 'required|string',
+ 'email' => $email,
+ 'currency_code' => 'required|string|currency',
+ 'password' => $required . 'confirmed',
+ 'enabled' => 'integer|boolean',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Income/Invoice.php b/app/Http/Requests/Income/Invoice.php
new file mode 100755
index 0000000..ea61e15
--- /dev/null
+++ b/app/Http/Requests/Income/Invoice.php
@@ -0,0 +1,78 @@
+getMethod() == 'PATCH') {
+ $id = $this->invoice->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ // Get company id
+ $company_id = $this->request->get('company_id');
+
+ return [
+ 'invoice_number' => 'required|string|unique:invoices,NULL,' . $id . ',id,company_id,' . $company_id . ',deleted_at,NULL',
+ 'invoice_status_code' => 'required|string',
+ 'invoiced_at' => 'required|date_format:Y-m-d H:i:s',
+ 'due_at' => 'required|date_format:Y-m-d H:i:s',
+ 'amount' => 'required',
+ 'item.*.name' => 'required|string',
+ 'item.*.quantity' => 'required',
+ 'item.*.price' => 'required|amount',
+ 'item.*.currency' => 'required|string|currency',
+ 'currency_code' => 'required|string|currency',
+ 'currency_rate' => 'required',
+ 'customer_id' => 'required|integer',
+ 'customer_name' => 'required|string',
+ 'category_id' => 'required|integer',
+ 'attachment' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+
+ public function withValidator($validator)
+ {
+ if ($validator->errors()->count()) {
+ // Set date
+ $invoiced_at = Date::parse($this->request->get('invoiced_at'))->format('Y-m-d');
+ $due_at = Date::parse($this->request->get('due_at'))->format('Y-m-d');
+
+ $this->request->set('invoiced_at', $invoiced_at);
+ $this->request->set('due_at', $due_at);
+ }
+ }
+
+ public function messages()
+ {
+ return [
+ 'item.*.name.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('general.name'))]),
+ 'item.*.quantity.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('invoices.quantity'))]),
+ 'item.*.price.required' => trans('validation.required', ['attribute' => mb_strtolower(trans('invoices.price'))]),
+ 'item.*.currency.required' => trans('validation.custom.invalid_currency'),
+ 'item.*.currency.string' => trans('validation.custom.invalid_currency'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Income/InvoiceHistory.php b/app/Http/Requests/Income/InvoiceHistory.php
new file mode 100755
index 0000000..6381055
--- /dev/null
+++ b/app/Http/Requests/Income/InvoiceHistory.php
@@ -0,0 +1,32 @@
+ 'required|integer',
+ 'status_code' => 'required|string',
+ 'notify' => 'required|integer',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Income/InvoiceItem.php b/app/Http/Requests/Income/InvoiceItem.php
new file mode 100755
index 0000000..77776c2
--- /dev/null
+++ b/app/Http/Requests/Income/InvoiceItem.php
@@ -0,0 +1,36 @@
+ 'required|integer',
+ 'name' => 'required|string',
+ 'quantity' => 'required|integer',
+ 'price' => 'required',
+ 'total' => 'required',
+ 'tax' => 'required',
+ 'tax_id' => 'required',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Income/InvoicePayment.php b/app/Http/Requests/Income/InvoicePayment.php
new file mode 100755
index 0000000..40b11aa
--- /dev/null
+++ b/app/Http/Requests/Income/InvoicePayment.php
@@ -0,0 +1,45 @@
+ 'required|integer',
+ 'paid_at' => 'required|date_format:Y-m-d H:i:s',
+ 'amount' => 'required|amount',
+ 'currency_code' => 'required|string|currency',
+ 'payment_method' => 'required|string',
+ 'attachment' => 'mimes:jpeg,jpg,png,pdf',
+ ];
+ }
+
+ public function withValidator($validator)
+ {
+ if ($validator->errors()->count()) {
+ $paid_at = Date::parse($this->request->get('paid_at'))->format('Y-m-d');
+
+ $this->request->set('paid_at', $paid_at);
+ }
+ }
+}
diff --git a/app/Http/Requests/Income/InvoiceTotal.php b/app/Http/Requests/Income/InvoiceTotal.php
new file mode 100755
index 0000000..a85cb50
--- /dev/null
+++ b/app/Http/Requests/Income/InvoiceTotal.php
@@ -0,0 +1,33 @@
+ 'required|integer',
+ 'name' => 'required|string',
+ 'amount' => 'required|amount',
+ 'sort_order' => 'required|integer',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Income/Revenue.php b/app/Http/Requests/Income/Revenue.php
new file mode 100755
index 0000000..2aaa61d
--- /dev/null
+++ b/app/Http/Requests/Income/Revenue.php
@@ -0,0 +1,48 @@
+ 'required|integer',
+ 'paid_at' => 'required|date_format:Y-m-d H:i:s',
+ 'amount' => 'required|amount',
+ 'currency_code' => 'required|string|currency',
+ 'currency_rate' => 'required',
+ 'customer_id' => 'nullable|integer',
+ 'category_id' => 'required|integer',
+ 'payment_method' => 'required|string',
+ 'attachment' => 'mimes:' . setting('general.file_types') . '|between:0,' . setting('general.file_size') * 1024,
+ ];
+ }
+
+ public function withValidator($validator)
+ {
+ if ($validator->errors()->count()) {
+ $paid_at = Date::parse($this->request->get('paid_at'))->format('Y-m-d');
+
+ $this->request->set('paid_at', $paid_at);
+ }
+ }
+}
diff --git a/app/Http/Requests/Install/Database.php b/app/Http/Requests/Install/Database.php
new file mode 100755
index 0000000..9564fca
--- /dev/null
+++ b/app/Http/Requests/Install/Database.php
@@ -0,0 +1,32 @@
+ 'required',
+ 'username' => 'required',
+ 'database' => 'required'
+ ];
+ }
+}
diff --git a/app/Http/Requests/Install/Setting.php b/app/Http/Requests/Install/Setting.php
new file mode 100755
index 0000000..30d1842
--- /dev/null
+++ b/app/Http/Requests/Install/Setting.php
@@ -0,0 +1,33 @@
+ 'required',
+ 'company_email' => 'required',
+ 'user_email' => 'required',
+ 'user_password' => 'required'
+ ];
+ }
+}
diff --git a/app/Http/Requests/Module/Module.php b/app/Http/Requests/Module/Module.php
new file mode 100755
index 0000000..8eb31b4
--- /dev/null
+++ b/app/Http/Requests/Module/Module.php
@@ -0,0 +1,47 @@
+extend(
+ 'check',
+ function ($attribute, $value, $parameters) {
+ return $this->checkToken($value);
+ },
+ trans('messages.error.invalid_token')
+ );
+
+ }
+
+ /**
+ * Determine if the user is authorized to make this request.
+ *
+ * @return bool
+ */
+ public function authorize()
+ {
+ return true;
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules()
+ {
+ return [
+ 'api_token' => 'required|string|check',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php
new file mode 100755
index 0000000..65f4ad8
--- /dev/null
+++ b/app/Http/Requests/Request.php
@@ -0,0 +1,28 @@
+all();
+
+ // Add active company id
+ $data['company_id'] = session('company_id');
+
+ // Reset the request data
+ $this->getInputSource()->replace($data);
+
+ return parent::getValidatorInstance();
+ }
+}
diff --git a/app/Http/Requests/Setting/Category.php b/app/Http/Requests/Setting/Category.php
new file mode 100755
index 0000000..8ba8f67
--- /dev/null
+++ b/app/Http/Requests/Setting/Category.php
@@ -0,0 +1,32 @@
+ 'required|string',
+ 'type' => 'required|string',
+ 'color' => 'required|string',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Setting/Currency.php b/app/Http/Requests/Setting/Currency.php
new file mode 100755
index 0000000..3d9f9e6
--- /dev/null
+++ b/app/Http/Requests/Setting/Currency.php
@@ -0,0 +1,45 @@
+getMethod() == 'PATCH') {
+ $id = $this->currency->getAttribute('id');
+ } else {
+ $id = null;
+ }
+
+ // Get company id
+ $company_id = $this->request->get('company_id');
+
+ return [
+ 'name' => 'required|string',
+ 'code' => 'required|string|unique:currencies,NULL,' . $id . ',id,company_id,' . $company_id . ',deleted_at,NULL',
+ 'rate' => 'required',
+ 'enabled' => 'integer|boolean',
+ 'default_currency' => 'boolean',
+ 'symbol_first' => 'nullable|boolean',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Setting/Setting.php b/app/Http/Requests/Setting/Setting.php
new file mode 100755
index 0000000..9f438fe
--- /dev/null
+++ b/app/Http/Requests/Setting/Setting.php
@@ -0,0 +1,32 @@
+ 'required|string',
+ 'company_email' => 'required|email',
+ 'company_logo' => 'mimes:' . setting('general.file_types', 'pdf,jpeg,jpg,png'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Setting/Tax.php b/app/Http/Requests/Setting/Tax.php
new file mode 100755
index 0000000..15645d2
--- /dev/null
+++ b/app/Http/Requests/Setting/Tax.php
@@ -0,0 +1,31 @@
+ 'required|string',
+ 'rate' => 'required|min:0|max:100',
+ ];
+ }
+}
diff --git a/app/Http/ViewComposers/All.php b/app/Http/ViewComposers/All.php
new file mode 100755
index 0000000..afa649a
--- /dev/null
+++ b/app/Http/ViewComposers/All.php
@@ -0,0 +1,26 @@
+with(['date_format' => $this->getCompanyDateFormat()]);
+ }
+ }
+}
diff --git a/app/Http/ViewComposers/Header.php b/app/Http/ViewComposers/Header.php
new file mode 100755
index 0000000..2a6d9cb
--- /dev/null
+++ b/app/Http/ViewComposers/Header.php
@@ -0,0 +1,75 @@
+customer()) {
+ $company = (object)[
+ 'company_name' => setting('general.company_name'),
+ 'company_email' => setting('general.company_email'),
+ 'company_address' => setting('general.company_address'),
+ 'company_logo' => setting('general.company_logo'),
+ ];
+ }
+
+ $undereads = $user->unreadNotifications;
+
+ foreach ($undereads as $underead) {
+ $data = $underead->getAttribute('data');
+
+ switch ($underead->getAttribute('type')) {
+ case 'App\Notifications\Expense\Bill':
+ $bills[$data['bill_id']] = $data['amount'];
+ $notifications++;
+ break;
+ case 'App\Notifications\Income\Invoice':
+ $invoices[$data['invoice_id']] = $data['amount'];
+ $notifications++;
+ break;
+ case 'App\Notifications\Common\Item':
+ $items[$data['item_id']] = $data['name'];
+ $notifications++;
+ break;
+ }
+ }
+
+ $updates = count(Updater::all());
+
+ $this->loadSuggestions();
+
+ $view->with([
+ 'user' => $user,
+ 'notifications' => $notifications,
+ 'bills' => $bills,
+ 'invoices' => $invoices,
+ 'items' => $items,
+ 'company' => $company,
+ 'updates' => $updates,
+ ]);
+ }
+}
diff --git a/app/Http/ViewComposers/Index.php b/app/Http/ViewComposers/Index.php
new file mode 100755
index 0000000..105fb64
--- /dev/null
+++ b/app/Http/ViewComposers/Index.php
@@ -0,0 +1,33 @@
+ '10', '25' => '25', '50' => '50', '100' => '100'];
+
+ $now = Date::now();
+
+ $this_year = $now->year;
+
+ $years = [];
+ $y = $now->addYears(2);
+ for ($i = 0; $i < 10; $i++) {
+ $years[$y->year] = $y->year;
+ $y->subYear();
+ }
+
+ $view->with(['limits' => $limits, 'this_year' => $this_year, 'years' => $years]);
+ }
+}
diff --git a/app/Http/ViewComposers/Logo.php b/app/Http/ViewComposers/Logo.php
new file mode 100755
index 0000000..8d3b7f6
--- /dev/null
+++ b/app/Http/ViewComposers/Logo.php
@@ -0,0 +1,54 @@
+getDiskPath());
+
+ if (!is_file($path)) {
+ return $logo;
+ }
+ } else {
+ $path = asset('public/img/company.png');
+ }
+
+ $image = Image::make($path)->encode()->getEncoded();
+
+ if (empty($image)) {
+ return $logo;
+ }
+
+ $extension = File::extension($path);
+
+ $logo = 'data:image/' . $extension . ';base64,' . base64_encode($image);
+
+ $view->with(['logo' => $logo]);
+ }
+}
diff --git a/app/Http/ViewComposers/Menu.php b/app/Http/ViewComposers/Menu.php
new file mode 100755
index 0000000..7c8dd8d
--- /dev/null
+++ b/app/Http/ViewComposers/Menu.php
@@ -0,0 +1,33 @@
+user();
+
+ // Get all companies
+ $companies = $user->companies()->enabled()->limit(10)->get()->each(function ($com) {
+ $com->setSettings();
+ })->sortBy('name');
+
+ // Get customer
+ if ($user->customer) {
+ $customer = $user;
+ }
+
+ $view->with(['companies' => $companies, 'customer' => $customer]);
+ }
+}
diff --git a/app/Http/ViewComposers/Modules.php b/app/Http/ViewComposers/Modules.php
new file mode 100755
index 0000000..1b02c93
--- /dev/null
+++ b/app/Http/ViewComposers/Modules.php
@@ -0,0 +1,35 @@
+addHour(6), function () {
+ return collect($this->getCategories())->pluck('name', 'slug')
+ ->prepend(trans('general.all_type', ['type' => trans_choice('general.categories', 2)]), '');
+ });
+ } else {
+ $categories = collect([
+ '' => trans('general.all_type', ['type' => trans_choice('general.categories', 2)]),
+ ]);
+ }
+
+ $view->with(['categories' => $categories]);
+ }
+}
diff --git a/app/Http/ViewComposers/Recurring.php b/app/Http/ViewComposers/Recurring.php
new file mode 100755
index 0000000..81063b1
--- /dev/null
+++ b/app/Http/ViewComposers/Recurring.php
@@ -0,0 +1,36 @@
+ trans('general.no'),
+ 'daily' => trans('recurring.daily'),
+ 'weekly' => trans('recurring.weekly'),
+ 'monthly' => trans('recurring.monthly'),
+ 'yearly' => trans('recurring.yearly'),
+ 'custom' => trans('recurring.custom'),
+ ];
+
+ $recurring_custom_frequencies = [
+ 'daily' => trans('recurring.days'),
+ 'weekly' => trans('recurring.weeks'),
+ 'monthly' => trans('recurring.months'),
+ 'yearly' => trans('recurring.years'),
+ ];
+
+ $view->with(['recurring_frequencies' => $recurring_frequencies, 'recurring_custom_frequencies' => $recurring_custom_frequencies]);
+ }
+}
diff --git a/app/Http/ViewComposers/Suggestions.php b/app/Http/ViewComposers/Suggestions.php
new file mode 100755
index 0000000..f665daf
--- /dev/null
+++ b/app/Http/ViewComposers/Suggestions.php
@@ -0,0 +1,55 @@
+runningInConsole() || !env('APP_INSTALLED')) {
+ return;
+ }
+
+ $modules = false;
+
+ $path = Route::current()->uri();
+
+ if ($path) {
+ $suggestions = $this->getSuggestions($path);
+
+ if ($suggestions) {
+ $suggestion_modules = $suggestions->modules;
+
+ foreach ($suggestion_modules as $key => $module) {
+ $installed = Module::where('company_id', '=', session('company_id'))->where('alias', '=', $module->alias)->first();
+
+ if ($installed) {
+ unset($suggestion_modules[$key]);
+ }
+ }
+
+ if ($suggestion_modules) {
+ shuffle($suggestion_modules);
+
+ $modules[] = $suggestion_modules[0];
+ }
+ }
+ }
+
+ $view->with(['suggestion_modules' => $modules]);
+ }
+}
diff --git a/app/Listeners/Auth/Login.php b/app/Listeners/Auth/Login.php
new file mode 100755
index 0000000..a2dce9c
--- /dev/null
+++ b/app/Listeners/Auth/Login.php
@@ -0,0 +1,39 @@
+user->companies()->enabled()->first();
+
+ // Logout if no company assigned
+ if (!$company) {
+ app('App\Http\Controllers\Auth\Login')->logout();
+
+ flash(trans('auth.error.no_company'))->error();
+
+ return;
+ }
+
+ // Set company id
+ session(['company_id' => $company->id]);
+
+ // Save user login time
+ $event->user->last_logged_in_at = Date::now();
+
+ $event->user->save();
+ }
+}
diff --git a/app/Listeners/Auth/Logout.php b/app/Listeners/Auth/Logout.php
new file mode 100755
index 0000000..48799f8
--- /dev/null
+++ b/app/Listeners/Auth/Logout.php
@@ -0,0 +1,20 @@
+forget('company_id');
+ }
+}
\ No newline at end of file
diff --git a/app/Listeners/Incomes/Invoice/Paid.php b/app/Listeners/Incomes/Invoice/Paid.php
new file mode 100755
index 0000000..3aa8863
--- /dev/null
+++ b/app/Listeners/Incomes/Invoice/Paid.php
@@ -0,0 +1,75 @@
+invoice;
+ $request = $event->request;
+
+ $request['invoice_id'] = $invoice->id;
+ $request['account_id'] = setting('general.default_account');
+
+ if (!isset($request['amount'])) {
+ $request['amount'] = $invoice->amount;
+ }
+
+ $request['currency_code'] = $invoice->currency_code;
+ $request['currency_rate'] = $invoice->currency_rate;
+
+ $request['paid_at'] = Date::parse('now')->format('Y-m-d');
+
+ if ($request['amount'] > $invoice->amount) {
+ $message = trans('messages.error.added', ['type' => trans_choice('general.payment', 1)]);
+
+ return [
+ 'success' => false,
+ 'error' => $message,
+ ];
+ } elseif ($request['amount'] == $invoice->amount) {
+ $invoice->invoice_status_code = 'paid';
+ } else {
+ $invoice->invoice_status_code = 'partial';
+ }
+
+ $invoice->save();
+
+ InvoicePayment::create($request->input());
+
+ $request['status_code'] = $invoice->invoice_status_code;
+
+ $request['notify'] = 0;
+
+ $desc_date = Date::parse($request['paid_at'])->format($this->getCompanyDateFormat());
+
+ $desc_amount = money((float) $request['amount'], $request['currency_code'], true)->format();
+
+ $request['description'] = $desc_date . ' ' . $desc_amount;
+
+ InvoiceHistory::create($request->input());
+
+ return [
+ 'success' => true,
+ 'error' => false,
+ ];
+ }
+}
diff --git a/app/Listeners/Updates/Listener.php b/app/Listeners/Updates/Listener.php
new file mode 100755
index 0000000..0a2fbf1
--- /dev/null
+++ b/app/Listeners/Updates/Listener.php
@@ -0,0 +1,31 @@
+alias != static::ALIAS) {
+ return false;
+ }
+
+ // Do not apply to the same or newer versions
+ if (version_compare($event->old, static::VERSION, '>=')) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app/Listeners/Updates/Version106.php b/app/Listeners/Updates/Version106.php
new file mode 100755
index 0000000..7f4da50
--- /dev/null
+++ b/app/Listeners/Updates/Version106.php
@@ -0,0 +1,30 @@
+check($event)) {
+ return;
+ }
+
+ // Moved to app directory
+ File::deleteDirectory(app_path('Http' . DIRECTORY_SEPARATOR .'Transformers'));
+ }
+}
diff --git a/app/Listeners/Updates/Version107.php b/app/Listeners/Updates/Version107.php
new file mode 100755
index 0000000..acb0014
--- /dev/null
+++ b/app/Listeners/Updates/Version107.php
@@ -0,0 +1,31 @@
+check($event)) {
+ return;
+ }
+
+ $table = env('DB_PREFIX') . 'taxes';
+
+ DB::statement("ALTER TABLE `$table` MODIFY `rate` DOUBLE(15,4) NOT NULL");
+ }
+}
diff --git a/app/Listeners/Updates/Version108.php b/app/Listeners/Updates/Version108.php
new file mode 100755
index 0000000..638a087
--- /dev/null
+++ b/app/Listeners/Updates/Version108.php
@@ -0,0 +1,90 @@
+check($event)) {
+ return;
+ }
+
+ $this->updateSettings();
+ $this->updateBills();
+ }
+
+ private function updateSettings()
+ {
+ // Set new invoice settings
+ setting(['general.invoice_number_prefix' => setting('general.invoice_prefix', 'INV-')]);
+ setting(['general.invoice_number_digit' => setting('general.invoice_digit', '5')]);
+ setting(['general.invoice_number_next' => setting('general.invoice_start', '1')]);
+
+ setting()->forget('general.invoice_prefix');
+ setting()->forget('general.invoice_digit');
+ setting()->forget('general.invoice_start');
+
+ setting()->save();
+ }
+
+ private function updateBills()
+ {
+ // Create new bill statuses
+ $companies = Company::all();
+
+ foreach ($companies as $company) {
+ $rows = [
+ [
+ 'company_id' => $company->id,
+ 'name' => trans('bills.status.draft'),
+ 'code' => 'draft',
+ ],
+ [
+ 'company_id' => $company->id,
+ 'name' => trans('bills.status.received'),
+ 'code' => 'received',
+ ],
+ ];
+
+ foreach ($rows as $row) {
+ BillStatus::create($row);
+ }
+ }
+
+ $bills = Bill::all();
+
+ foreach ($bills as $bill) {
+ if (($bill->bill_status_code != 'new') || ($bill->bill_status_code != 'updated')) {
+ continue;
+ }
+
+ $bill->bill_status_code = 'draft';
+ $bill->save();
+ }
+
+ $new = BillStatus::where('code', 'new');
+ $new->delete();
+ $new->forceDelete();
+
+ $updated = BillStatus::where('code', 'updated');
+ $updated->delete();
+ $updated->forceDelete();
+ }
+}
diff --git a/app/Listeners/Updates/Version109.php b/app/Listeners/Updates/Version109.php
new file mode 100755
index 0000000..8bcfdce
--- /dev/null
+++ b/app/Listeners/Updates/Version109.php
@@ -0,0 +1,36 @@
+check($event)) {
+ return;
+ }
+
+ // Create new bill statuses
+ $companies = Company::all();
+
+ foreach ($companies as $company) {
+ Artisan::call('module:install', ['alias' => 'offlinepayment', 'company_id' => $company->id]);
+ Artisan::call('module:install', ['alias' => 'paypalstandard', 'company_id' => $company->id]);
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version110.php b/app/Listeners/Updates/Version110.php
new file mode 100755
index 0000000..e8ebe93
--- /dev/null
+++ b/app/Listeners/Updates/Version110.php
@@ -0,0 +1,48 @@
+check($event)) {
+ return;
+ }
+
+ // Create permission
+ $permission = Permission::firstOrCreate([
+ 'name' => 'create-common-import',
+ 'display_name' => 'Create Common Import',
+ 'description' => 'Create Common Import',
+ ]);
+
+ // Attach permission to roles
+ $roles = Role::all();
+
+ foreach ($roles as $role) {
+ $allowed = ['admin', 'manager'];
+
+ if (!in_array($role->name, $allowed)) {
+ continue;
+ }
+
+ $role->attachPermission($permission);
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version112.php b/app/Listeners/Updates/Version112.php
new file mode 100755
index 0000000..11cdd1e
--- /dev/null
+++ b/app/Listeners/Updates/Version112.php
@@ -0,0 +1,43 @@
+check($event)) {
+ return;
+ }
+
+ $locale = 'en-GB';
+
+ // Get default locale if only 1 company
+ if (Company::all()->count() == 1) {
+ $locale = setting('general.default_locale', 'en-GB');
+ }
+
+ // Set default locale
+ DotenvEditor::setKeys([
+ [
+ 'key' => 'APP_LOCALE',
+ 'value' => $locale,
+ ],
+ ])->save();
+ }
+}
diff --git a/app/Listeners/Updates/Version113.php b/app/Listeners/Updates/Version113.php
new file mode 100755
index 0000000..87e831d
--- /dev/null
+++ b/app/Listeners/Updates/Version113.php
@@ -0,0 +1,44 @@
+check($event)) {
+ return;
+ }
+
+ // Update database
+ Artisan::call('migrate', ['--force' => true]);
+
+ // Update currencies
+ $currencies = Currency::all();
+
+ foreach ($currencies as $currency) {
+ $currency->precision = config('money.' . $currency->code . '.precision');
+ $currency->symbol = config('money.' . $currency->code . '.symbol');
+ $currency->symbol_first = config('money.' . $currency->code . '.symbol_first') ? 1 : 0;
+ $currency->decimal_mark = config('money.' . $currency->code . '.decimal_mark');
+ $currency->thousands_separator = config('money.' . $currency->code . '.thousands_separator');
+
+ $currency->save();
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version119.php b/app/Listeners/Updates/Version119.php
new file mode 100755
index 0000000..908e022
--- /dev/null
+++ b/app/Listeners/Updates/Version119.php
@@ -0,0 +1,162 @@
+check($event)) {
+ return;
+ }
+
+ if (Schema::hasTable('mediables')) {
+ return;
+ }
+
+ if (Schema::hasTable('media')) {
+ Schema::drop('media');
+ }
+
+ // Create permission
+ if (!Permission::where('name', 'delete-common-uploads')->first()) {
+ $permission = Permission::firstOrCreate([
+ 'name' => 'delete-common-uploads',
+ 'display_name' => 'Delete Common Uploads',
+ 'description' => 'Delete Common Uploads',
+ ]);
+
+ // Attach permission to roles
+ $roles = Role::all();
+
+ $allowed = ['admin'];
+
+ foreach ($roles as $role) {
+ if (!in_array($role->name, $allowed)) {
+ continue;
+ }
+
+ $role->attachPermission($permission);
+ }
+ }
+
+ $data = [];
+
+ $migrations = [
+ '\App\Models\Auth\User' => 'picture',
+ '\App\Models\Common\Item' => 'picture',
+ '\App\Models\Expense\Bill' => 'attachment',
+ '\App\Models\Expense\BillPayment' => 'attachment',
+ '\App\Models\Expense\Payment' => 'attachment',
+ '\App\Models\Income\Invoice' => 'attachment',
+ '\App\Models\Income\InvoicePayment' => 'attachment',
+ '\App\Models\Income\Revenue' => 'attachment',
+ ];
+
+ foreach ($migrations as $model => $name) {
+ if ($model != '\App\Models\Auth\User') {
+ $items = $model::where('company_id', '<>', '0')->get();
+ } else {
+ $items = $model::all();
+ }
+
+ $data[basename($model)] = $items;
+ }
+
+ // Clear cache after update
+ Artisan::call('cache:clear');
+
+ // Update database
+ Artisan::call('migrate', ['--force' => true]);
+
+ foreach ($migrations as $model => $name) {
+ $items = $data[basename($model)];
+
+ foreach ($items as $item) {
+ if (!$item->$name) {
+ continue;
+ }
+
+ $path = explode('uploads/', $item->$name);
+
+ $path = end($path);
+
+ if (!empty($item->company_id) && (strpos($path, $item->company_id . '/') === false)) {
+ $path = $item->company_id . '/' . $path;
+ }
+
+ if (!empty($path) && Storage::exists($path)) {
+ $media = \App\Models\Common\Media::where('filename', '=', pathinfo(basename($path), PATHINFO_FILENAME))->first();
+
+ if ($media) {
+ $item->attachMedia($media, $name);
+
+ continue;
+ }
+
+ $media = MediaUploader::importPath(config('mediable.default_disk'), $path);
+
+ $item->attachMedia($media, $name);
+ }
+ }
+ }
+
+ $settings['company_logo'] = \App\Models\Setting\Setting::where('key', '=', 'general.company_logo')->where('company_id', '<>', '0')->get();
+ $settings['invoice_logo'] = \App\Models\Setting\Setting::where('key', '=', 'general.invoice_logo')->where('company_id', '<>', '0')->get();
+
+ foreach ($settings as $name => $items) {
+ foreach ($items as $item) {
+ if (!$item->value) {
+ continue;
+ }
+
+ $path = explode('uploads/', $item->value);
+
+ $path = end($path);
+
+ if (!empty($item->company_id) && (strpos($path, $item->company_id . '/') === false)) {
+ $path = $item->company_id . '/' . $path;
+ }
+
+ if (!empty($path) && Storage::exists($path)) {
+ $company = \App\Models\Common\Company::find($item->company_id);
+
+ $media = \App\Models\Common\Media::where('filename', '=', pathinfo(basename($path), PATHINFO_FILENAME))->first();
+
+ if ($company && !$media) {
+ $media = MediaUploader::importPath(config('mediable.default_disk'), $path);
+
+ $company->attachMedia($media, $name);
+
+ $item->update(['value' => $media->id]);
+ } elseif ($media) {
+ $item->update(['value' => $media->id]);
+ } else {
+ $item->update(['value' => '']);
+ }
+ } else {
+ $item->update(['value' => '']);
+ }
+ }
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version120.php b/app/Listeners/Updates/Version120.php
new file mode 100755
index 0000000..0b864f3
--- /dev/null
+++ b/app/Listeners/Updates/Version120.php
@@ -0,0 +1,137 @@
+check($event)) {
+ return;
+ }
+
+ $this->updatePermissions();
+
+ // Update database
+ Artisan::call('migrate', ['--force' => true]);
+
+ $this->updateInvoicesAndBills();
+
+ $this->changeQuantityColumn();
+ }
+
+ protected function updatePermissions()
+ {
+ $permissions = [];
+
+ // Create tax summary permission
+ $permissions[] = Permission::firstOrCreate([
+ 'name' => 'read-reports-tax-summary',
+ 'display_name' => 'Read Reports Tax Summary',
+ 'description' => 'Read Reports Tax Summary',
+ ]);
+
+ // Create profit loss permission
+ $permissions[] = Permission::firstOrCreate([
+ 'name' => 'read-reports-profit-loss',
+ 'display_name' => 'Read Reports Profit Loss',
+ 'description' => 'Read Reports Profit Loss',
+ ]);
+
+ // Attach permission to roles
+ $roles = Role::all();
+
+ foreach ($roles as $role) {
+ $allowed = ['admin', 'manager'];
+
+ if (!in_array($role->name, $allowed)) {
+ continue;
+ }
+
+ foreach ($permissions as $permission) {
+ $role->attachPermission($permission);
+ }
+ }
+ }
+
+ protected function updateInvoicesAndBills()
+ {
+ $companies = Company::all();
+
+ foreach ($companies as $company) {
+ // Invoices
+ $invoice_category = Category::create([
+ 'company_id' => $company->id,
+ 'name' => trans_choice('general.invoices', 2),
+ 'type' => 'income',
+ 'color' => '#00c0ef',
+ 'enabled' => '1'
+ ]);
+
+ foreach ($company->invoices as $invoice) {
+ $invoice->category_id = $invoice_category->id;
+ $invoice->save();
+ }
+
+ // Bills
+ $bill_category = Category::create([
+ 'company_id' => $company->id,
+ 'name' => trans_choice('general.bills', 2),
+ 'type' => 'expense',
+ 'color' => '#dd4b39',
+ 'enabled' => '1'
+ ]);
+
+ foreach ($company->bills as $bill) {
+ $bill->category_id = $bill_category->id;
+ $bill->save();
+ }
+ }
+ }
+
+ protected function changeQuantityColumn()
+ {
+ $connection = env('DB_CONNECTION');
+
+ if ($connection == 'mysql') {
+ $tables = [
+ env('DB_PREFIX') . 'invoice_items',
+ env('DB_PREFIX') . 'bill_items'
+ ];
+
+ foreach ($tables as $table) {
+ DB::statement("ALTER TABLE `$table` MODIFY `quantity` DOUBLE(7,2) NOT NULL");
+ }
+ } else {
+ Schema::table('invoice_items', function ($table) {
+ $table->decimal('quantity', 7, 2)->change();
+ });
+
+ Schema::table('bill_items', function ($table) {
+ $table->decimal('quantity', 7, 2)->change();
+ });
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version1210.php b/app/Listeners/Updates/Version1210.php
new file mode 100755
index 0000000..7d04d18
--- /dev/null
+++ b/app/Listeners/Updates/Version1210.php
@@ -0,0 +1,30 @@
+check($event)) {
+ return;
+ }
+
+ // Update database
+ Artisan::call('migrate', ['--force' => true]);
+ }
+}
diff --git a/app/Listeners/Updates/Version1211.php b/app/Listeners/Updates/Version1211.php
new file mode 100755
index 0000000..5cf0125
--- /dev/null
+++ b/app/Listeners/Updates/Version1211.php
@@ -0,0 +1,30 @@
+check($event)) {
+ return;
+ }
+
+ // Update database
+ Artisan::call('migrate', ['--force' => true]);
+ }
+}
diff --git a/app/Listeners/Updates/Version126.php b/app/Listeners/Updates/Version126.php
new file mode 100755
index 0000000..791b53b
--- /dev/null
+++ b/app/Listeners/Updates/Version126.php
@@ -0,0 +1,52 @@
+check($event)) {
+ return;
+ }
+
+ $permissions = [];
+
+ // Create permission
+ $permissions[] = Permission::firstOrCreate([
+ 'name' => 'read-modules-my',
+ 'display_name' => 'Read Modules My',
+ 'description' => 'Read Modules My',
+ ]);
+
+ // Attach permission to roles
+ $roles = Role::all();
+
+ foreach ($roles as $role) {
+ $allowed = ['admin', 'manager'];
+
+ if (!in_array($role->name, $allowed)) {
+ continue;
+ }
+
+ foreach ($permissions as $permission) {
+ $role->attachPermission($permission);
+ }
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version127.php b/app/Listeners/Updates/Version127.php
new file mode 100755
index 0000000..be1fb5b
--- /dev/null
+++ b/app/Listeners/Updates/Version127.php
@@ -0,0 +1,51 @@
+check($event)) {
+ return;
+ }
+
+ // Update permissions
+ $permissions = Permission::all();
+ foreach ($permissions as $permission) {
+ if (strstr($permission->name, '-companies-companies')) {
+ $permission->name = str_replace('-companies-companies', '-common-companies', $permission->name);
+ $permission->save();
+ }
+
+ if (strstr($permission->name, '-items-items')) {
+ $permission->name = str_replace('-items-items', '-common-items', $permission->name);
+ $permission->save();
+ }
+ }
+
+ // Delete folders
+ $dirs = ['dashboard', 'search', 'companies', 'items'];
+ foreach ($dirs as $dir) {
+ File::deleteDirectory(app_path('Filters/' . ucfirst($dir)));
+ File::deleteDirectory(app_path('Http/Controllers/' . ucfirst($dir)));
+ File::deleteDirectory(app_path('Http/Requests/' . ucfirst(str_singular($dir))));
+ File::deleteDirectory(resource_path('views/' . $dir));
+ }
+ }
+}
diff --git a/app/Listeners/Updates/Version129.php b/app/Listeners/Updates/Version129.php
new file mode 100755
index 0000000..4f2a31d
--- /dev/null
+++ b/app/Listeners/Updates/Version129.php
@@ -0,0 +1,30 @@
+check($event)) {
+ return;
+ }
+
+ // Update database
+ Artisan::call('migrate', ['--force' => true]);
+ }
+}
diff --git a/app/Models/Auth/Permission.php b/app/Models/Auth/Permission.php
new file mode 100755
index 0000000..4350d59
--- /dev/null
+++ b/app/Models/Auth/Permission.php
@@ -0,0 +1,70 @@
+currentRouteAction())[0]));
+ $folder = $arr[1];
+ $file = $arr[0];
+ } else {
+ list($folder, $file) = explode('/', Route::current()->uri());
+ }
+
+ if (empty($folder) || empty($file)) {
+ return $this->provideFilter();
+ }
+
+ $class = '\App\Filters\\' . ucfirst($folder) .'\\' . ucfirst($file);
+
+ return $this->provideFilter($class);
+ }
+
+ /**
+ * Scope to get all rows filtered, sorted and paginated.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $sort
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCollect($query, $sort = 'display_name')
+ {
+ $request = request();
+
+ $input = $request->input();
+ $limit = $request->get('limit', setting('general.list_limit', '25'));
+
+ return $query->filter($input)->sortable($sort)->paginate($limit);
+ }
+}
diff --git a/app/Models/Auth/Role.php b/app/Models/Auth/Role.php
new file mode 100755
index 0000000..ebb4a12
--- /dev/null
+++ b/app/Models/Auth/Role.php
@@ -0,0 +1,69 @@
+currentRouteAction())[0]));
+ $folder = $arr[1];
+ $file = $arr[0];
+ } else {
+ list($folder, $file) = explode('/', Route::current()->uri());
+ }
+
+ if (empty($folder) || empty($file)) {
+ return $this->provideFilter();
+ }
+
+ $class = '\App\Filters\\' . ucfirst($folder) .'\\' . ucfirst($file);
+
+ return $this->provideFilter($class);
+ }
+
+ /**
+ * Scope to get all rows filtered, sorted and paginated.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $sort
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCollect($query, $sort = 'display_name')
+ {
+ $request = request();
+
+ $input = $request->input();
+ $limit = $request->get('limit', setting('general.list_limit', '25'));
+
+ return $query->filter($input)->sortable($sort)->paginate($limit);
+ }
+}
diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php
new file mode 100755
index 0000000..ae51bef
--- /dev/null
+++ b/app/Models/Auth/User.php
@@ -0,0 +1,193 @@
+morphToMany('App\Models\Common\Company', 'user', 'user_companies', 'user_id', 'company_id');
+ }
+
+ public function customer()
+ {
+ return $this->hasOne('App\Models\Income\Customer', 'user_id', 'id');
+ }
+
+ /**
+ * Always capitalize the name when we retrieve it
+ */
+ public function getNameAttribute($value)
+ {
+ return ucfirst($value);
+ }
+
+ /**
+ * Always return a valid picture when we retrieve it
+ */
+ public function getPictureAttribute($value)
+ {
+ // Check if we should use gravatar
+ if (setting('general.use_gravatar', '0') == '1') {
+ try {
+ // Check for gravatar
+ $url = 'https://www.gravatar.com/avatar/' . md5(strtolower($this->getAttribute('email'))).'?size=90&d=404';
+
+ $client = new \GuzzleHttp\Client(['verify' => false]);
+
+ $client->request('GET', $url)->getBody()->getContents();
+
+ $value = $url;
+ } catch (RequestException $e) {
+ // 404 Not Found
+ }
+ }
+
+ if (!empty($value) && !$this->hasMedia('picture')) {
+ return $value;
+ } elseif (!$this->hasMedia('picture')) {
+ return false;
+ }
+
+ return $this->getMedia('picture')->last();
+ }
+
+ /**
+ * Always return a valid picture when we retrieve it
+ */
+ public function getLastLoggedInAtAttribute($value)
+ {
+ // Date::setLocale('tr');
+
+ if (!empty($value)) {
+ return Date::parse($value)->diffForHumans();
+ } else {
+ return trans('auth.never');
+ }
+ }
+
+ /**
+ * Send reset link to user via email
+ */
+ public function sendPasswordResetNotification($token)
+ {
+ $this->notify(new Reset($token));
+ }
+
+ /**
+ * Always capitalize the name when we save it to the database
+ */
+ public function setNameAttribute($value)
+ {
+ $this->attributes['name'] = ucfirst($value);
+ }
+
+ /**
+ * Always hash the password when we save it to the database
+ */
+ public function setPasswordAttribute($value)
+ {
+ $this->attributes['password'] = bcrypt($value);
+ }
+
+ /**
+ * Define the filter provider globally.
+ *
+ * @return ModelFilter
+ */
+ public function modelFilter()
+ {
+ // Check if is api or web
+ if (Request::is('api/*')) {
+ $arr = array_reverse(explode('\\', explode('@', app()['api.router']->currentRouteAction())[0]));
+ $folder = $arr[1];
+ $file = $arr[0];
+ } else {
+ list($folder, $file) = explode('/', Route::current()->uri());
+ }
+
+ if (empty($folder) || empty($file)) {
+ return $this->provideFilter();
+ }
+
+ //$class = '\App\Filters\Auth\Users';
+
+ $class = '\App\Filters\\' . ucfirst($folder) . '\\' . ucfirst($file);
+
+ return $this->provideFilter($class);
+ }
+
+ /**
+ * Scope to get all rows filtered, sorted and paginated.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $sort
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCollect($query, $sort = 'name')
+ {
+ $request = request();
+
+ $input = $request->input();
+ $limit = $request->get('limit', setting('general.list_limit', '25'));
+
+ return $query->filter($input)->sortable($sort)->paginate($limit);
+ }
+
+ /**
+ * Scope to only include active currencies.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeEnabled($query)
+ {
+ return $query->where('enabled', 1);
+ }
+}
diff --git a/app/Models/Banking/Account.php b/app/Models/Banking/Account.php
new file mode 100755
index 0000000..950ed29
--- /dev/null
+++ b/app/Models/Banking/Account.php
@@ -0,0 +1,116 @@
+ 10,
+ 'number' => 10,
+ 'bank_name' => 10,
+ 'bank_phone' => 5,
+ 'bank_address' => 2,
+ ];
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function invoice_payments()
+ {
+ return $this->hasMany('App\Models\Income\InvoicePayment');
+ }
+
+ public function revenues()
+ {
+ return $this->hasMany('App\Models\Income\Revenue');
+ }
+
+ public function bill_payments()
+ {
+ return $this->hasMany('App\Models\Expense\BillPayment');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Expense\Payment');
+ }
+
+ /**
+ * Convert opening balance to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setOpeningBalanceAttribute($value)
+ {
+ $this->attributes['opening_balance'] = (double) $value;
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getBalanceAttribute()
+ {
+ // Opening Balance
+ $total = $this->opening_balance;
+
+ // Sum invoices
+ foreach ($this->invoice_payments as $item) {
+ $total += $item->amount;
+ }
+
+ // Sum revenues
+ foreach ($this->revenues as $item) {
+ $total += $item->amount;
+ }
+
+ // Subtract bills
+ foreach ($this->bill_payments as $item) {
+ $total -= $item->amount;
+ }
+
+ // Subtract payments
+ foreach ($this->payments as $item) {
+ $total -= $item->amount;
+ }
+
+ return $total;
+ }
+}
diff --git a/app/Models/Banking/Transaction.php b/app/Models/Banking/Transaction.php
new file mode 100755
index 0000000..75465d7
--- /dev/null
+++ b/app/Models/Banking/Transaction.php
@@ -0,0 +1,92 @@
+get();
+
+ foreach ($bills as $bill) {
+ $bill_payments = $bill->payments;
+
+ if ($bill_payments) {
+ foreach ($bill_payments as $bill_payment) {
+ $transactions[] = (object) [
+ 'date' => $bill_payment->paid_at,
+ 'account' => $bill_payment->account->name,
+ 'type' => trans('invoices.status.partial'),
+ 'category' => trans_choice('general.invoices', 1),
+ 'description' => $bill_payment->description,
+ 'amount' => $bill_payment->amount,
+ 'currency_code' => $bill_payment->currency_code,
+ ];
+ }
+ }
+ }
+
+ $payments = Payment::where('vendor_id', $user_id)->get();
+
+ foreach ($payments as $payment) {
+ $transactions[] = (object) [
+ 'date' => $payment->paid_at,
+ 'account' => $payment->account->name,
+ 'type' => 'Expense',
+ 'category' => $payment->category->name,
+ 'description' => $payment->description,
+ 'amount' => $payment->amount,
+ 'currency_code' => $payment->currency_code,
+ ];
+ }
+ break;
+ case 'revenues':
+ $invoices = Invoice::where('customer_id', $user_id)->get();
+
+ foreach ($invoices as $invoice) {
+ $invoice_payments = $invoice->payments;
+
+ if ($invoice_payments) {
+ foreach ($invoice_payments as $invoice_payment) {
+ $transactions[] = (object) [
+ 'date' => $invoice_payment->paid_at,
+ 'account' => $invoice_payment->account->name,
+ 'type' => trans('invoices.status.partial'),
+ 'category' => trans_choice('general.invoices', 1),
+ 'description' => $invoice_payment->description,
+ 'amount' => $invoice_payment->amount,
+ 'currency_code' => $invoice_payment->currency_code,
+ ];
+ }
+ }
+ }
+
+ $revenues = Revenue::where('customer_id', $user_id)->get();
+
+ foreach ($revenues as $revenue) {
+ $transactions[] = (object) [
+ 'date' => $revenue->paid_at,
+ 'account' => $revenue->account->name,
+ 'type' => trans_choice('general.payments', 1),
+ 'category' => $revenue->category->name,
+ 'description' => $revenue->description,
+ 'amount' => $revenue->amount,
+ 'currency_code' => $revenue->currency_code,
+ ];
+ }
+ break;
+ }
+
+ return $transactions;
+ }
+}
diff --git a/app/Models/Banking/Transfer.php b/app/Models/Banking/Transfer.php
new file mode 100755
index 0000000..4f53d01
--- /dev/null
+++ b/app/Models/Banking/Transfer.php
@@ -0,0 +1,62 @@
+belongsTo('App\Models\Expense\Payment');
+ }
+
+ public function paymentAccount()
+ {
+ return $this->belongsTo('App\Models\Banking\Account', 'payment.account_id', 'id');
+ }
+
+ public function revenue()
+ {
+ return $this->belongsTo('App\Models\Income\Revenue');
+ }
+
+ public function revenueAccount()
+ {
+ return $this->belongsTo('App\Models\Banking\Account', 'revenue.account_id', 'id');
+ }
+
+ public function getDynamicConvertedAmount($format = false)
+ {
+ return $this->dynamicConvert($this->default_currency_code, $this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+
+ public function getReverseConvertedAmount($format = false)
+ {
+ return $this->reverseConvert($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+
+ public function getDivideConvertedAmount($format = false)
+ {
+ return $this->divide($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+}
diff --git a/app/Models/Common/Company.php b/app/Models/Common/Company.php
new file mode 100755
index 0000000..297c7ab
--- /dev/null
+++ b/app/Models/Common/Company.php
@@ -0,0 +1,260 @@
+hasMany('App\Models\Banking\Account');
+ }
+
+ public function bill_histories()
+ {
+ return $this->hasMany('App\Models\Expense\BillHistory');
+ }
+
+ public function bill_items()
+ {
+ return $this->hasMany('App\Models\Expense\BillItem');
+ }
+
+ public function bill_payments()
+ {
+ return $this->hasMany('App\Models\Expense\BillPayment');
+ }
+
+ public function bill_statuses()
+ {
+ return $this->hasMany('App\Models\Expense\BillStatus');
+ }
+
+ public function bills()
+ {
+ return $this->hasMany('App\Models\Expense\Bill');
+ }
+
+ public function categories()
+ {
+ return $this->hasMany('App\Models\Setting\Category');
+ }
+
+ public function currencies()
+ {
+ return $this->hasMany('App\Models\Setting\Currency');
+ }
+
+ public function customers()
+ {
+ return $this->hasMany('App\Models\Income\Customer');
+ }
+
+ public function invoice_histories()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceHistory');
+ }
+
+ public function invoice_items()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceItem');
+ }
+
+ public function invoice_payments()
+ {
+ return $this->hasMany('App\Models\Income\InvoicePayment');
+ }
+
+ public function invoice_statuses()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceStatus');
+ }
+
+ public function invoices()
+ {
+ return $this->hasMany('App\Models\Income\Invoice');
+ }
+
+ public function items()
+ {
+ return $this->hasMany('App\Models\Common\Item');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Expense\Payment');
+ }
+
+ public function recurring()
+ {
+ return $this->hasMany('App\Models\Common\Recurring');
+ }
+
+ public function revenues()
+ {
+ return $this->hasMany('App\Models\Income\Revenue');
+ }
+
+ public function settings()
+ {
+ return $this->hasMany('App\Models\Setting\Setting');
+ }
+
+ public function taxes()
+ {
+ return $this->hasMany('App\Models\Setting\Tax');
+ }
+
+ public function transfers()
+ {
+ return $this->hasMany('App\Models\Banking\Transfer');
+ }
+
+ public function users()
+ {
+ return $this->morphedByMany('App\Models\Auth\User', 'user', 'user_companies', 'company_id', 'user_id');
+ }
+
+ public function vendors()
+ {
+ return $this->hasMany('App\Models\Expense\Vendor');
+ }
+
+ public function setSettings()
+ {
+ $settings = $this->settings;
+
+ foreach ($settings as $setting) {
+ list($group, $key) = explode('.', $setting->getAttribute('key'));
+
+ // Load only general settings
+ if ($group != 'general') {
+ continue;
+ }
+
+ $value = $setting->getAttribute('value');
+
+ if (($key == 'company_logo') && empty($value)) {
+ $value = 'public/img/company.png';
+ }
+
+ $this->setAttribute($key, $value);
+ }
+
+ // Set default default company logo if empty
+ if ($this->getAttribute('company_logo') == '') {
+ $this->setAttribute('company_logo', 'public/img/company.png');
+ }
+ }
+
+ /**
+ * Define the filter provider globally.
+ *
+ * @return ModelFilter
+ */
+ public function modelFilter()
+ {
+ list($folder, $file) = explode('/', \Route::current()->uri());
+
+ if (empty($folder) || empty($file)) {
+ return $this->provideFilter();
+ }
+
+ $class = '\App\Filters\\' . ucfirst($folder) .'\\' . ucfirst($file);
+
+ return $this->provideFilter($class);
+ }
+
+ /**
+ * Scope to get all rows filtered, sorted and paginated.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $sort
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCollect($query, $sort = 'name')
+ {
+ $request = request();
+
+ $input = $request->input();
+ $limit = $request->get('limit', setting('general.list_limit', '25'));
+
+ return Auth::user()->companies()->filter($input)->sortable($sort)->paginate($limit);
+ }
+
+ /**
+ * Scope to only include companies of a given enabled value.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param mixed $value
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeEnabled($query, $value = 1)
+ {
+ return $query->where('enabled', $value);
+ }
+
+ /**
+ * Sort by company name
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $direction
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function nameSortable($query, $direction)
+ {
+ return $query->join('settings', 'companies.id', '=', 'settings.company_id')
+ ->where('key', 'general.company_name')
+ ->orderBy('value', $direction)
+ ->select('companies.*');
+ }
+
+ /**
+ * Sort by company email
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $direction
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function emailSortable($query, $direction)
+ {
+ return $query->join('settings', 'companies.id', '=', 'settings.company_id')
+ ->where('key', 'general.company_email')
+ ->orderBy('value', $direction)
+ ->select('companies.*');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getCompanyLogoAttribute()
+ {
+ return $this->getMedia('company_logo')->last();
+ }
+}
diff --git a/app/Models/Common/Item.php b/app/Models/Common/Item.php
new file mode 100755
index 0000000..04acf3a
--- /dev/null
+++ b/app/Models/Common/Item.php
@@ -0,0 +1,158 @@
+ 10,
+ 'sku' => 5,
+ 'description' => 2,
+ ];
+
+ public function category()
+ {
+ return $this->belongsTo('App\Models\Setting\Category');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ public function bill_items()
+ {
+ return $this->hasMany('App\Models\Expense\BillItem');
+ }
+
+ public function invoice_items()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceItem');
+ }
+
+ /**
+ * Convert sale price to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setSalePriceAttribute($value)
+ {
+ $this->attributes['sale_price'] = (double) $value;
+ }
+
+ /**
+ * Convert purchase price to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setPurchasePriceAttribute($value)
+ {
+ $this->attributes['purchase_price'] = (double) $value;
+ }
+
+ /**
+ * Get the item id.
+ *
+ * @return string
+ */
+ public function getItemIdAttribute()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Scope autocomplete.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param array $filter
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeAutocomplete($query, $filter)
+ {
+ return $query->where(function ($query) use ($filter) {
+ foreach ($filter as $key => $value) {
+ $query->orWhere($key, 'LIKE', "%" . $value . "%");
+ }
+ });
+ }
+
+ /**
+ * Scope quantity.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeQuantity($query)
+ {
+ return $query->where('quantity', '>', '0');
+ }
+
+ /**
+ * Sort by category name
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $direction
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function categorySortable($query, $direction)
+ {
+ return $query->join('categories', 'categories.id', '=', 'items.category_id')
+ ->orderBy('name', $direction)
+ ->select('items.*');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getPictureAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('picture')) {
+ return $value;
+ } elseif (!$this->hasMedia('picture')) {
+ return false;
+ }
+
+ return $this->getMedia('picture')->last();
+ }
+}
diff --git a/app/Models/Common/Media.php b/app/Models/Common/Media.php
new file mode 100755
index 0000000..517b123
--- /dev/null
+++ b/app/Models/Common/Media.php
@@ -0,0 +1,14 @@
+morphTo();
+ }
+}
diff --git a/app/Models/Company/Company.php b/app/Models/Company/Company.php
new file mode 100755
index 0000000..b459f91
--- /dev/null
+++ b/app/Models/Company/Company.php
@@ -0,0 +1,8 @@
+ 10,
+ 'order_number' => 10,
+ 'vendor_name' => 10,
+ 'vendor_email' => 5,
+ 'vendor_phone' => 2,
+ 'vendor_address' => 1,
+ 'notes' => 2,
+ ];
+
+ /**
+ * Clonable relationships.
+ *
+ * @var array
+ */
+ public $cloneable_relations = ['items', 'recurring', 'totals'];
+
+ public function category()
+ {
+ return $this->belongsTo('App\Models\Setting\Category');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function histories()
+ {
+ return $this->hasMany('App\Models\Expense\BillHistory');
+ }
+
+ public function items()
+ {
+ return $this->hasMany('App\Models\Expense\BillItem');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Expense\BillPayment');
+ }
+
+ public function recurring()
+ {
+ return $this->morphOne('App\Models\Common\Recurring', 'recurable');
+ }
+
+ public function status()
+ {
+ return $this->belongsTo('App\Models\Expense\BillStatus', 'bill_status_code', 'code');
+ }
+
+ public function totals()
+ {
+ return $this->hasMany('App\Models\Expense\BillTotal');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Expense\Vendor');
+ }
+
+ public function scopeDue($query, $date)
+ {
+ return $query->where('due_at', '=', $date);
+ }
+
+ public function scopeLatest($query)
+ {
+ return $query->orderBy('paid_at', 'desc');
+ }
+
+ public function scopeAccrued($query)
+ {
+ return $query->where('bill_status_code', '<>', 'draft');
+ }
+
+ public function scopePaid($query)
+ {
+ return $query->where('bill_status_code', '=', 'paid');
+ }
+
+ public function scopeNotPaid($query)
+ {
+ return $query->where('bill_status_code', '<>', 'paid');
+ }
+
+ public function onCloning($src, $child = null)
+ {
+ $this->bill_status_code = 'draft';
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Convert currency rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setCurrencyRateAttribute($value)
+ {
+ $this->attributes['currency_rate'] = (double) $value;
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getAttachmentAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('attachment')) {
+ return $value;
+ } elseif (!$this->hasMedia('attachment')) {
+ return false;
+ }
+
+ return $this->getMedia('attachment')->last();
+ }
+
+ /**
+ * Get the discount percentage.
+ *
+ * @return string
+ */
+ public function getDiscountAttribute()
+ {
+ $percent = 0;
+
+ $discount = $this->totals()->where('code', 'discount')->value('amount');
+
+ if ($discount) {
+ $sub_total = $this->totals()->where('code', 'sub_total')->value('amount');
+
+ $percent = number_format((($discount * 100) / $sub_total), 0);
+ }
+
+ return $percent;
+ }
+}
diff --git a/app/Models/Expense/BillHistory.php b/app/Models/Expense/BillHistory.php
new file mode 100755
index 0000000..548d99e
--- /dev/null
+++ b/app/Models/Expense/BillHistory.php
@@ -0,0 +1,51 @@
+belongsTo('App\Models\Expense\Bill');
+ }
+
+ public function item()
+ {
+ return $this->belongsTo('App\Models\Common\Item');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ public function payment()
+ {
+ return $this->belongsTo('App\Models\Setting\Payment');
+ }
+
+ public function status()
+ {
+ return $this->belongsTo('App\Models\Expense\BillStatus', 'status_code', 'code');
+ }
+
+ public function getConvertedAmount($format = false)
+ {
+ return $this->convert($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+}
diff --git a/app/Models/Expense/BillItem.php b/app/Models/Expense/BillItem.php
new file mode 100755
index 0000000..ae2c026
--- /dev/null
+++ b/app/Models/Expense/BillItem.php
@@ -0,0 +1,69 @@
+belongsTo('App\Models\Expense\Bill');
+ }
+
+ public function item()
+ {
+ return $this->belongsTo('App\Models\Common\Item');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ /**
+ * Convert price to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setPriceAttribute($value)
+ {
+ $this->attributes['price'] = (double) $value;
+ }
+
+ /**
+ * Convert total to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setTotalAttribute($value)
+ {
+ $this->attributes['total'] = (double) $value;
+ }
+
+ /**
+ * Convert tax to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setTaxAttribute($value)
+ {
+ $this->attributes['tax'] = (double) $value;
+ }
+}
diff --git a/app/Models/Expense/BillPayment.php b/app/Models/Expense/BillPayment.php
new file mode 100755
index 0000000..b6ba134
--- /dev/null
+++ b/app/Models/Expense/BillPayment.php
@@ -0,0 +1,109 @@
+belongsTo('App\Models\Banking\Account');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function bill()
+ {
+ return $this->belongsTo('App\Models\Expense\Bill');
+ }
+
+ public function item()
+ {
+ return $this->belongsTo('App\Models\Common\Item');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ public function scopeLatest($query)
+ {
+ return $query->orderBy('paid_at', 'desc');
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Convert currency rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setCurrencyRateAttribute($value)
+ {
+ $this->attributes['currency_rate'] = (double) $value;
+ }
+
+ /**
+ * Scope paid invoice.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopePaid($query)
+ {
+ return $query->sum('amount');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getAttachmentAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('attachment')) {
+ return $value;
+ } elseif (!$this->hasMedia('attachment')) {
+ return false;
+ }
+
+ return $this->getMedia('attachment')->last();
+ }
+
+ public function getDivideConvertedAmount($format = false)
+ {
+ return $this->divide($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+}
diff --git a/app/Models/Expense/BillStatus.php b/app/Models/Expense/BillStatus.php
new file mode 100755
index 0000000..f138d1f
--- /dev/null
+++ b/app/Models/Expense/BillStatus.php
@@ -0,0 +1,51 @@
+code) {
+ case 'paid':
+ $label = 'label-success';
+ break;
+ case 'delete':
+ $label = 'label-danger';
+ break;
+ case 'partial':
+ case 'received':
+ $label = 'label-warning';
+ break;
+ default:
+ $label = 'bg-aqua';
+ break;
+ }
+
+ return $label;
+ }
+}
diff --git a/app/Models/Expense/BillTotal.php b/app/Models/Expense/BillTotal.php
new file mode 100755
index 0000000..12b96e5
--- /dev/null
+++ b/app/Models/Expense/BillTotal.php
@@ -0,0 +1,86 @@
+belongsTo('App\Models\Expense\Bill');
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Get the formatted name.
+ *
+ * @return string
+ */
+ public function getTitleAttribute()
+ {
+ $title = $this->name;
+
+ $percent = 0;
+
+ switch ($this->code) {
+ case 'discount':
+ $title = trans($title);
+ $percent = $this->bill->discount;
+
+ break;
+ case 'tax':
+ $rate = Tax::where('name', $title)->value('rate');
+
+ if (!empty($rate)) {
+ $percent = $rate;
+ }
+
+ break;
+ }
+
+ if (!empty($percent)) {
+ $title .= ' (';
+
+ if (setting('general.percent_position', 'after') == 'after') {
+ $title .= $percent . '%';
+ } else {
+ $title .= '%' . $percent;
+ }
+
+ $title .= ')';
+ }
+
+ return $title;
+ }
+}
diff --git a/app/Models/Expense/Payment.php b/app/Models/Expense/Payment.php
new file mode 100755
index 0000000..c4d349e
--- /dev/null
+++ b/app/Models/Expense/Payment.php
@@ -0,0 +1,150 @@
+belongsTo('App\Models\Banking\Account');
+ }
+
+ public function category()
+ {
+ return $this->belongsTo('App\Models\Setting\Category');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function recurring()
+ {
+ return $this->morphOne('App\Models\Common\Recurring', 'recurable');
+ }
+
+ public function transfers()
+ {
+ return $this->hasMany('App\Models\Banking\Transfer');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Expense\Vendor');
+ }
+
+ /**
+ * Get only transfers.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeIsTransfer($query)
+ {
+ return $query->where('category_id', '=', Category::transfer());
+ }
+
+ /**
+ * Skip transfers.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeIsNotTransfer($query)
+ {
+ return $query->where('category_id', '<>', Category::transfer());
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Convert currency rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setCurrencyRateAttribute($value)
+ {
+ $this->attributes['currency_rate'] = (double) $value;
+ }
+
+ public static function scopeLatest($query)
+ {
+ return $query->orderBy('paid_at', 'desc');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getAttachmentAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('attachment')) {
+ return $value;
+ } elseif (!$this->hasMedia('attachment')) {
+ return false;
+ }
+
+ return $this->getMedia('attachment')->last();
+ }
+}
diff --git a/app/Models/Expense/Vendor.php b/app/Models/Expense/Vendor.php
new file mode 100755
index 0000000..3c5d246
--- /dev/null
+++ b/app/Models/Expense/Vendor.php
@@ -0,0 +1,73 @@
+ 10,
+ 'email' => 5,
+ 'phone' => 2,
+ 'website' => 2,
+ 'address' => 1,
+ ];
+
+ public function bills()
+ {
+ return $this->hasMany('App\Models\Expense\Bill');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Expense\Payment');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getLogoAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('logo')) {
+ return $value;
+ } elseif (!$this->hasMedia('logo')) {
+ return false;
+ }
+
+ return $this->getMedia('logo')->last();
+ }
+}
diff --git a/app/Models/Income/Customer.php b/app/Models/Income/Customer.php
new file mode 100755
index 0000000..e4aa09a
--- /dev/null
+++ b/app/Models/Income/Customer.php
@@ -0,0 +1,67 @@
+ 10,
+ 'email' => 5,
+ 'phone' => 2,
+ 'website' => 2,
+ 'address' => 1,
+ ];
+
+ public function invoices()
+ {
+ return $this->hasMany('App\Models\Income\Invoice');
+ }
+
+ public function revenues()
+ {
+ return $this->hasMany('App\Models\Income\Revenue');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\Auth\User', 'user_id', 'id');
+ }
+
+ public function onCloning($src, $child = null)
+ {
+ $this->user_id = null;
+ }
+}
diff --git a/app/Models/Income/Invoice.php b/app/Models/Income/Invoice.php
new file mode 100755
index 0000000..f07e31e
--- /dev/null
+++ b/app/Models/Income/Invoice.php
@@ -0,0 +1,199 @@
+ 10,
+ 'order_number' => 10,
+ 'customer_name' => 10,
+ 'customer_email' => 5,
+ 'customer_phone' => 2,
+ 'customer_address' => 1,
+ 'notes' => 2,
+ ];
+
+ /**
+ * Clonable relationships.
+ *
+ * @var array
+ */
+ public $cloneable_relations = ['items', 'recurring', 'totals'];
+
+ public function category()
+ {
+ return $this->belongsTo('App\Models\Setting\Category');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function customer()
+ {
+ return $this->belongsTo('App\Models\Income\Customer');
+ }
+
+ public function items()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceItem');
+ }
+
+ public function histories()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceHistory');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Income\InvoicePayment');
+ }
+
+ public function recurring()
+ {
+ return $this->morphOne('App\Models\Common\Recurring', 'recurable');
+ }
+
+ public function status()
+ {
+ return $this->belongsTo('App\Models\Income\InvoiceStatus', 'invoice_status_code', 'code');
+ }
+
+ public function totals()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceTotal');
+ }
+
+ public function scopeDue($query, $date)
+ {
+ return $query->where('due_at', '=', $date);
+ }
+
+ public function scopeLatest($query)
+ {
+ return $query->orderBy('paid_at', 'desc');
+ }
+
+ public function scopeAccrued($query)
+ {
+ return $query->where('invoice_status_code', '<>', 'draft');
+ }
+
+ public function scopePaid($query)
+ {
+ return $query->where('invoice_status_code', '=', 'paid');
+ }
+
+ public function scopeNotPaid($query)
+ {
+ return $query->where('invoice_status_code', '<>', 'paid');
+ }
+
+ public function onCloning($src, $child = null)
+ {
+ $this->invoice_status_code = 'draft';
+ $this->invoice_number = $this->getNextInvoiceNumber();
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Convert currency rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setCurrencyRateAttribute($value)
+ {
+ $this->attributes['currency_rate'] = (double) $value;
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getAttachmentAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('attachment')) {
+ return $value;
+ } elseif (!$this->hasMedia('attachment')) {
+ return false;
+ }
+
+ return $this->getMedia('attachment')->last();
+ }
+
+ /**
+ * Get the discount percentage.
+ *
+ * @return string
+ */
+ public function getDiscountAttribute()
+ {
+ $percent = 0;
+
+ $discount = $this->totals()->where('code', 'discount')->value('amount');
+
+ if ($discount) {
+ $sub_total = $this->totals()->where('code', 'sub_total')->value('amount');
+
+ $percent = number_format((($discount * 100) / $sub_total), 0);
+ }
+
+ return $percent;
+ }
+}
diff --git a/app/Models/Income/InvoiceHistory.php b/app/Models/Income/InvoiceHistory.php
new file mode 100755
index 0000000..1d61f5b
--- /dev/null
+++ b/app/Models/Income/InvoiceHistory.php
@@ -0,0 +1,51 @@
+belongsTo('App\Models\Income\Invoice');
+ }
+
+ public function item()
+ {
+ return $this->belongsTo('App\Models\Common\Item');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ public function payment()
+ {
+ return $this->belongsTo('App\Models\Setting\Payment');
+ }
+
+ public function status()
+ {
+ return $this->belongsTo('App\Models\Income\InvoiceStatus', 'status_code', 'code');
+ }
+
+ public function getConvertedAmount($format = false)
+ {
+ return $this->convert($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+}
diff --git a/app/Models/Income/InvoiceItem.php b/app/Models/Income/InvoiceItem.php
new file mode 100755
index 0000000..c6e50c9
--- /dev/null
+++ b/app/Models/Income/InvoiceItem.php
@@ -0,0 +1,69 @@
+belongsTo('App\Models\Income\Invoice');
+ }
+
+ public function item()
+ {
+ return $this->belongsTo('App\Models\Common\Item');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ /**
+ * Convert price to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setPriceAttribute($value)
+ {
+ $this->attributes['price'] = (double) $value;
+ }
+
+ /**
+ * Convert total to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setTotalAttribute($value)
+ {
+ $this->attributes['total'] = (double) $value;
+ }
+
+ /**
+ * Convert tax to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setTaxAttribute($value)
+ {
+ $this->attributes['tax'] = (double) $value;
+ }
+}
diff --git a/app/Models/Income/InvoicePayment.php b/app/Models/Income/InvoicePayment.php
new file mode 100755
index 0000000..02e724a
--- /dev/null
+++ b/app/Models/Income/InvoicePayment.php
@@ -0,0 +1,109 @@
+belongsTo('App\Models\Banking\Account');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function invoice()
+ {
+ return $this->belongsTo('App\Models\Income\Invoice');
+ }
+
+ public function item()
+ {
+ return $this->belongsTo('App\Models\Common\Item');
+ }
+
+ public function tax()
+ {
+ return $this->belongsTo('App\Models\Setting\Tax');
+ }
+
+ public function scopeLatest($query)
+ {
+ return $query->orderBy('paid_at', 'desc');
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Convert currency rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setCurrencyRateAttribute($value)
+ {
+ $this->attributes['currency_rate'] = (double) $value;
+ }
+
+ /**
+ * Scope paid invoice.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopePaid($query)
+ {
+ return $query->sum('amount');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getAttachmentAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('attachment')) {
+ return $value;
+ } elseif (!$this->hasMedia('attachment')) {
+ return false;
+ }
+
+ return $this->getMedia('attachment')->last();
+ }
+
+ public function getDivideConvertedAmount($format = false)
+ {
+ return $this->divide($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+}
diff --git a/app/Models/Income/InvoiceStatus.php b/app/Models/Income/InvoiceStatus.php
new file mode 100755
index 0000000..10a7bd1
--- /dev/null
+++ b/app/Models/Income/InvoiceStatus.php
@@ -0,0 +1,51 @@
+code) {
+ case 'paid':
+ $label = 'label-success';
+ break;
+ case 'delete':
+ $label = 'label-danger';
+ break;
+ case 'partial':
+ case 'sent':
+ $label = 'label-warning';
+ break;
+ default:
+ $label = 'bg-aqua';
+ break;
+ }
+
+ return $label;
+ }
+}
diff --git a/app/Models/Income/InvoiceTotal.php b/app/Models/Income/InvoiceTotal.php
new file mode 100755
index 0000000..30f1549
--- /dev/null
+++ b/app/Models/Income/InvoiceTotal.php
@@ -0,0 +1,86 @@
+belongsTo('App\Models\Income\Invoice');
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Get the formatted name.
+ *
+ * @return string
+ */
+ public function getTitleAttribute()
+ {
+ $title = $this->name;
+
+ $percent = 0;
+
+ switch ($this->code) {
+ case 'discount':
+ $title = trans($title);
+ $percent = $this->invoice->discount;
+
+ break;
+ case 'tax':
+ $rate = Tax::where('name', $title)->value('rate');
+
+ if (!empty($rate)) {
+ $percent = $rate;
+ }
+
+ break;
+ }
+
+ if (!empty($percent)) {
+ $title .= ' (';
+
+ if (setting('general.percent_position', 'after') == 'after') {
+ $title .= $percent . '%';
+ } else {
+ $title .= '%' . $percent;
+ }
+
+ $title .= ')';
+ }
+
+ return $title;
+ }
+}
diff --git a/app/Models/Income/Revenue.php b/app/Models/Income/Revenue.php
new file mode 100755
index 0000000..09cb52f
--- /dev/null
+++ b/app/Models/Income/Revenue.php
@@ -0,0 +1,156 @@
+ 10,
+ 'order_number' => 10,
+ 'customer_name' => 10,
+ 'customer_email' => 5,
+ 'notes' => 2,
+ ];
+
+ /**
+ * Clonable relationships.
+ *
+ * @var array
+ */
+ public $cloneable_relations = ['recurring'];
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\Auth\User', 'customer_id', 'id');
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Banking\Account');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
+ }
+
+ public function category()
+ {
+ return $this->belongsTo('App\Models\Setting\Category');
+ }
+
+ public function customer()
+ {
+ return $this->belongsTo('App\Models\Income\Customer');
+ }
+
+ public function recurring()
+ {
+ return $this->morphOne('App\Models\Common\Recurring', 'recurable');
+ }
+
+ public function transfers()
+ {
+ return $this->hasMany('App\Models\Banking\Transfer');
+ }
+
+ /**
+ * Get only transfers.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeIsTransfer($query)
+ {
+ return $query->where('category_id', '=', Category::transfer());
+ }
+
+ /**
+ * Skip transfers.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeIsNotTransfer($query)
+ {
+ return $query->where('category_id', '<>', Category::transfer());
+ }
+
+ /**
+ * Convert amount to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setAmountAttribute($value)
+ {
+ $this->attributes['amount'] = (double) $value;
+ }
+
+ /**
+ * Convert currency rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setCurrencyRateAttribute($value)
+ {
+ $this->attributes['currency_rate'] = (double) $value;
+ }
+
+ public function scopeLatest($query)
+ {
+ return $query->orderBy('paid_at', 'desc');
+ }
+
+ /**
+ * Get the current balance.
+ *
+ * @return string
+ */
+ public function getAttachmentAttribute($value)
+ {
+ if (!empty($value) && !$this->hasMedia('attachment')) {
+ return $value;
+ } elseif (!$this->hasMedia('attachment')) {
+ return false;
+ }
+
+ return $this->getMedia('attachment')->last();
+ }
+}
diff --git a/app/Models/Item/Item.php b/app/Models/Item/Item.php
new file mode 100755
index 0000000..0a24f75
--- /dev/null
+++ b/app/Models/Item/Item.php
@@ -0,0 +1,8 @@
+belongsTo('App\Models\Common\Company');
+ }
+
+ /**
+ * Define the filter provider globally.
+ *
+ * @return ModelFilter
+ */
+ public function modelFilter()
+ {
+ // Check if is api or web
+ if (Request::is('api/*')) {
+ $arr = array_reverse(explode('\\', explode('@', app()['api.router']->currentRouteAction())[0]));
+ $folder = $arr[1];
+ $file = $arr[0];
+ } else {
+ list($folder, $file) = explode('/', Route::current()->uri());
+ }
+
+ if (empty($folder) || empty($file)) {
+ return $this->provideFilter();
+ }
+
+ $class = '\App\Filters\\' . ucfirst($folder) . '\\' . ucfirst($file);
+
+ return $this->provideFilter($class);
+ }
+
+ /**
+ * Scope to only include company data.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $company_id
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCompanyId($query, $company_id)
+ {
+ return $query->where($this->table . '.company_id', '=', $company_id);
+ }
+
+ /**
+ * Scope to get all rows filtered, sorted and paginated.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $sort
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCollect($query, $sort = 'name')
+ {
+ $request = request();
+
+ $input = $request->input();
+ $limit = $request->get('limit', setting('general.list_limit', '25'));
+
+ return $query->filter($input)->sortable($sort)->paginate($limit);
+ }
+
+ /**
+ * Scope to only include active models.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeEnabled($query)
+ {
+ return $query->where('enabled', 1);
+ }
+
+ /**
+ * Scope to only include passive models.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeDisabled($query)
+ {
+ return $query->where('enabled', 0);
+ }
+}
diff --git a/app/Models/Module/Module.php b/app/Models/Module/Module.php
new file mode 100755
index 0000000..34fe6cd
--- /dev/null
+++ b/app/Models/Module/Module.php
@@ -0,0 +1,32 @@
+where('alias', $alias);
+ }
+}
diff --git a/app/Models/Module/ModuleHistory.php b/app/Models/Module/ModuleHistory.php
new file mode 100755
index 0000000..6727bb1
--- /dev/null
+++ b/app/Models/Module/ModuleHistory.php
@@ -0,0 +1,18 @@
+hasMany('App\Models\Expense\Bill');
+ }
+
+ public function invoices()
+ {
+ return $this->hasMany('App\Models\Income\Invoice');
+ }
+
+ public function items()
+ {
+ return $this->hasMany('App\Models\Common\Item');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Expense\Payment');
+ }
+
+ public function revenues()
+ {
+ return $this->hasMany('App\Models\Income\Revenue');
+ }
+
+ /**
+ * Scope to only include categories of a given type.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param mixed $type
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeType($query, $type)
+ {
+ return $query->where('type', $type);
+ }
+
+ /**
+ * Scope transfer category.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeTransfer($query)
+ {
+ return $query->where('type', 'other')->pluck('id')->first();
+ }
+}
diff --git a/app/Models/Setting/Currency.php b/app/Models/Setting/Currency.php
new file mode 100755
index 0000000..bdba355
--- /dev/null
+++ b/app/Models/Setting/Currency.php
@@ -0,0 +1,146 @@
+hasMany('App\Models\Banking\Account', 'currency_code', 'code');
+ }
+
+ public function customers()
+ {
+ return $this->hasMany('App\Models\Income\Customer', 'currency_code', 'code');
+ }
+
+ public function invoices()
+ {
+ return $this->hasMany('App\Models\Income\Invoice', 'currency_code', 'code');
+ }
+
+ public function invoice_payments()
+ {
+ return $this->hasMany('App\Models\Income\InvoicePayment', 'currency_code', 'code');
+ }
+
+ public function revenues()
+ {
+ return $this->hasMany('App\Models\Income\Revenue', 'currency_code', 'code');
+ }
+
+ public function bills()
+ {
+ return $this->hasMany('App\Models\Expense\Bill', 'currency_code', 'code');
+ }
+
+ public function bill_payments()
+ {
+ return $this->hasMany('App\Models\Expense\BillPayment', 'currency_code', 'code');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Expense\Payment', 'currency_code', 'code');
+ }
+
+ /**
+ * Convert rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setRateAttribute($value)
+ {
+ $this->attributes['rate'] = (double) $value;
+ }
+
+ /**
+ * Get the current precision.
+ *
+ * @return string
+ */
+ public function getPrecisionAttribute($value)
+ {
+ if (empty($value)) {
+ return config('money.' . $this->code . '.precision');
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the current symbol.
+ *
+ * @return string
+ */
+ public function getSymbolAttribute($value)
+ {
+ if (empty($value)) {
+ return config('money.' . $this->code . '.symbol');
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the current symbol_first.
+ *
+ * @return string
+ */
+ public function getSymbolFirstAttribute($value)
+ {
+ if (empty($value)) {
+ return config('money.' . $this->code . '.symbol_first');
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the current decimal_mark.
+ *
+ * @return string
+ */
+ public function getDecimalMarkAttribute($value)
+ {
+ if (empty($value)) {
+ return config('money.' . $this->code . '.decimal_mark');
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the current thousands_separator.
+ *
+ * @return string
+ */
+ public function getThousandsSeparatorAttribute($value)
+ {
+ if (empty($value)) {
+ return config('money.' . $this->code . '.thousands_separator');
+ }
+
+ return $value;
+ }
+}
diff --git a/app/Models/Setting/Setting.php b/app/Models/Setting/Setting.php
new file mode 100755
index 0000000..c025f58
--- /dev/null
+++ b/app/Models/Setting/Setting.php
@@ -0,0 +1,61 @@
+get();
+ }
+
+ /**
+ * Global company relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function company()
+ {
+ return $this->belongsTo('App\Models\Common\Company');
+ }
+
+ /**
+ * Scope to only include company data.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param $company_id
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeCompanyId($query, $company_id)
+ {
+ return $query->where($this->table . '.company_id', '=', $company_id);
+ }
+}
diff --git a/app/Models/Setting/Tax.php b/app/Models/Setting/Tax.php
new file mode 100755
index 0000000..9d19d87
--- /dev/null
+++ b/app/Models/Setting/Tax.php
@@ -0,0 +1,78 @@
+hasMany('App\Models\Common\Item');
+ }
+
+ public function bill_items()
+ {
+ return $this->hasMany('App\Models\Expense\BillItem');
+ }
+
+ public function invoice_items()
+ {
+ return $this->hasMany('App\Models\Income\InvoiceItem');
+ }
+
+ /**
+ * Convert rate to double.
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setRateAttribute($value)
+ {
+ $this->attributes['rate'] = (double) $value;
+ }
+
+ /**
+ * Get the name including rate.
+ *
+ * @return string
+ */
+ public function getTitleAttribute()
+ {
+ $title = $this->name . ' (';
+
+ if (setting('general.percent_position', 'after') == 'after') {
+ $title .= $this->rate . '%';
+ } else {
+ $title .= '%' . $this->rate;
+ }
+
+ $title .= ')';
+
+ return $title;
+ }
+}
diff --git a/app/Notifications/Auth/Reset.php b/app/Notifications/Auth/Reset.php
new file mode 100755
index 0000000..7353221
--- /dev/null
+++ b/app/Notifications/Auth/Reset.php
@@ -0,0 +1,53 @@
+token = $token;
+ }
+
+ /**
+ * Get the notification's channels.
+ *
+ * @param mixed $notifiable
+ * @return array|string
+ */
+ public function via($notifiable)
+ {
+ return ['mail'];
+ }
+
+ /**
+ * Build the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ setting(['general.company_name' => config('app.name')]);
+
+ return (new MailMessage)
+ ->line(trans('auth.notification.message_1'))
+ ->action(trans('auth.notification.button'), url('auth/reset', $this->token, true))
+ ->line(trans('auth.notification.message_2'));
+ }
+}
diff --git a/app/Notifications/Common/Item.php b/app/Notifications/Common/Item.php
new file mode 100755
index 0000000..208edb6
--- /dev/null
+++ b/app/Notifications/Common/Item.php
@@ -0,0 +1,69 @@
+item = $item;
+ }
+
+ /**
+ * Get the notification's channels.
+ *
+ * @param mixed $notifiable
+ * @return array|string
+ */
+ public function via($notifiable)
+ {
+ return ['mail', 'database'];
+ }
+
+ /**
+ * Build the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ $message = (new MailMessage)
+ ->line(trans('items.notification.message', ['name' => $this->item->name]))
+ ->action(trans('items.notification.button'), url('items/items', $this->item->id, true));
+
+ // Override per company as Laravel doesn't read config
+ $message->from(config('mail.from.address'), config('mail.from.name'));
+
+ return $message;
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ 'item_id' => $this->item->id,
+ 'name' => $this->item->name,
+ ];
+ }
+}
diff --git a/app/Notifications/Expense/Bill.php b/app/Notifications/Expense/Bill.php
new file mode 100755
index 0000000..85fa966
--- /dev/null
+++ b/app/Notifications/Expense/Bill.php
@@ -0,0 +1,72 @@
+queue = 'high';
+ $this->delay = config('queue.connections.database.delay');
+
+ $this->bill = $bill;
+ }
+
+ /**
+ * Get the notification's channels.
+ *
+ * @param mixed $notifiable
+ * @return array|string
+ */
+ public function via($notifiable)
+ {
+ return ['mail', 'database'];
+ }
+
+ /**
+ * Build the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ $message = (new MailMessage)
+ ->line('You are receiving this email because you have an upcoming ' . money($this->bill->amount, $this->bill->currency_code, true) . ' bill to ' . $this->bill->vendor_name . ' vendor.')
+ ->action('Add Payment', url('expenses/bills', $this->bill->id, true));
+
+ // Override per company as Laravel doesn't read config
+ $message->from(config('mail.from.address'), config('mail.from.name'));
+
+ return $message;
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ 'bill_id' => $this->bill->id,
+ 'amount' => $this->bill->amount,
+ ];
+ }
+}
diff --git a/app/Notifications/Income/Invoice.php b/app/Notifications/Income/Invoice.php
new file mode 100755
index 0000000..ad6dd01
--- /dev/null
+++ b/app/Notifications/Income/Invoice.php
@@ -0,0 +1,82 @@
+queue = 'high';
+ $this->delay = config('queue.connections.database.delay');
+
+ $this->invoice = $invoice;
+ }
+
+ /**
+ * Get the notification's channels.
+ *
+ * @param mixed $notifiable
+ * @return array|string
+ */
+ public function via($notifiable)
+ {
+ return ['mail', 'database'];
+ }
+
+ /**
+ * Build the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ $message = (new MailMessage)
+ ->line(trans('invoices.notification.message', ['amount' => money($this->invoice->amount, $this->invoice->currency_code, true), 'customer' => $this->invoice->customer_name]));
+
+ // Override per company as Laravel doesn't read config
+ $message->from(config('mail.from.address'), config('mail.from.name'));
+
+ // Attach the PDF file if available
+ if (isset($this->invoice->pdf_path)) {
+ $message->attach($this->invoice->pdf_path, [
+ 'mime' => 'application/pdf',
+ ]);
+ }
+
+ if ($this->invoice->customer->user) {
+ $message->action(trans('invoices.notification.button'), url('customers/invoices', $this->invoice->id, true));
+ }
+
+ return $message;
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ 'invoice_id' => $this->invoice->id,
+ 'amount' => $this->invoice->amount,
+ ];
+ }
+}
diff --git a/app/Observers/Company.php b/app/Observers/Company.php
new file mode 100755
index 0000000..11ba47b
--- /dev/null
+++ b/app/Observers/Company.php
@@ -0,0 +1,65 @@
+ $company->id
+ ]);
+
+ // Check if user is logged in
+ if (!Auth::check()) {
+ return;
+ }
+
+ // Attach company to user
+ Auth::user()->companies()->attach($company->id);
+ }
+
+ /**
+ * Listen to the deleted event.
+ *
+ * @param Model $company
+ * @return void
+ */
+ public function deleted(Model $company)
+ {
+ $tables = [
+ 'accounts', 'bill_histories', 'bill_items', 'bill_payments', 'bill_statuses', 'bills', 'categories',
+ 'currencies', 'customers', 'invoice_histories', 'invoice_items', 'invoice_payments', 'invoice_statuses',
+ 'invoices', 'items', 'payments', 'recurring', 'revenues', 'settings', 'taxes', 'transfers', 'vendors',
+ ];
+
+ foreach ($tables as $table) {
+ $this->deleteItems($company, $table);
+ }
+ }
+
+ /**
+ * Delete items in batch.
+ *
+ * @param Model $company
+ * @param $table
+ * @return void
+ */
+ protected function deleteItems($company, $table)
+ {
+ foreach ($company->$table as $item) {
+ $item->delete();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Overrides/Illuminate/MessageSelector.php b/app/Overrides/Illuminate/MessageSelector.php
new file mode 100755
index 0000000..66d177b
--- /dev/null
+++ b/app/Overrides/Illuminate/MessageSelector.php
@@ -0,0 +1,254 @@
+extract($segments, $number)) !== null) {
+ return trim($value);
+ }
+
+ $segments = $this->stripConditions($segments);
+
+ $pluralIndex = $this->getPluralIndex($locale, $number);
+
+ if (count($segments) == 1 || ! isset($segments[$pluralIndex])) {
+ return $segments[0];
+ }
+
+ return $segments[$pluralIndex];
+ }
+
+ /**
+ * Extract a translation string using inline conditions.
+ *
+ * @param array $segments
+ * @param int $number
+ * @return mixed
+ */
+ private function extract($segments, $number)
+ {
+ foreach ($segments as $part) {
+ if (! is_null($line = $this->extractFromString($part, $number))) {
+ return $line;
+ }
+ }
+ }
+
+ /**
+ * Get the translation string if the condition matches.
+ *
+ * @param string $part
+ * @param int $number
+ * @return mixed
+ */
+ private function extractFromString($part, $number)
+ {
+ preg_match('/^[\{\[]([^\[\]\{\}]*)[\}\]](.*)/s', $part, $matches);
+
+ if (count($matches) != 3) {
+ return;
+ }
+
+ $condition = $matches[1];
+
+ $value = $matches[2];
+
+ if (Str::contains($condition, ',')) {
+ list($from, $to) = explode(',', $condition, 2);
+
+ if ($to == '*' && $number >= $from) {
+ return $value;
+ } elseif ($from == '*' && $number <= $to) {
+ return $value;
+ } elseif ($number >= $from && $number <= $to) {
+ return $value;
+ }
+ }
+
+ return $condition == $number ? $value : null;
+ }
+
+ /**
+ * Strip the inline conditions from each segment, just leaving the text.
+ *
+ * @param array $segments
+ * @return array
+ */
+ private function stripConditions($segments)
+ {
+ return collect($segments)->map(function ($part) {
+ return preg_replace('/^[\{\[]([^\[\]\{\}]*)[\}\]]/', '', $part);
+ })->all();
+ }
+
+ /**
+ * Get the index to use for pluralization.
+ *
+ * The plural rules are derived from code of the Zend Framework (2010-09-25), which
+ * is subject to the new BSD license (http://framework.zend.com/license/new-bsd)
+ * Copyright (c) 2005-2010 - Zend Technologies USA Inc. (http://www.zend.com)
+ *
+ * @param string $locale
+ * @param int $number
+ * @return int
+ */
+ public function getPluralIndex($locale, $number)
+ {
+ $allowed_langs = config('localizer.allowed_langs');
+
+ switch ($locale) {
+ case 'az':
+ case 'bo':
+ case 'dz':
+ case 'id':
+ case 'id-ID':
+ case 'ja':
+ case 'jv':
+ case 'ka':
+ case 'km':
+ case 'kn':
+ case 'ko':
+ case 'ms':
+ case 'th':
+ case 'tr':
+ case 'vi':
+ case 'vi-VN':
+ case 'zh':
+ case 'zh-TW':
+ return 0;
+ break;
+ case 'af':
+ case 'bn':
+ case 'bg':
+ case 'bg-BG':
+ case 'ca':
+ case 'da':
+ case 'da-DK':
+ case 'de':
+ case 'de-DE':
+ case 'el':
+ case 'el-GR':
+ case 'en':
+ case 'en-AU':
+ case 'en-GB':
+ case 'en-US':
+ case 'eo':
+ case 'es':
+ case 'es-ES':
+ case 'es-MX':
+ case 'et':
+ case 'eu':
+ case 'fa':
+ case 'fi':
+ case 'fo':
+ case 'fur':
+ case 'fy':
+ case 'gl':
+ case 'gu':
+ case 'ha':
+ case 'he':
+ case 'hu':
+ case 'is':
+ case 'it':
+ case 'it-IT':
+ case 'ku':
+ case 'lb':
+ case 'ml':
+ case 'mn':
+ case 'mr':
+ case 'nah':
+ case 'nb':
+ case 'nb-NO':
+ case 'ne':
+ case 'nl':
+ case 'nl-NL':
+ case 'nn':
+ case 'no':
+ case 'om':
+ case 'or':
+ case 'pa':
+ case 'pap':
+ case 'ps':
+ case 'pt':
+ case 'so':
+ case 'sq':
+ case 'sq-AL':
+ case 'sv':
+ case 'sv-SE':
+ case 'sw':
+ case 'ta':
+ case 'te':
+ case 'tk':
+ case 'tr-TR':
+ case 'ur':
+ case 'zu':
+ return ($number == 1) ? 0 : 1;
+ case 'am':
+ case 'bh':
+ case 'fil':
+ case 'fr':
+ case 'fr-FR':
+ case 'gun':
+ case 'hi':
+ case 'hy':
+ case 'ln':
+ case 'mg':
+ case 'nso':
+ case 'pt-BR':
+ case 'xbr':
+ case 'ti':
+ case 'wa':
+ return (($number == 0) || ($number == 1)) ? 0 : 1;
+ case 'be':
+ case 'bs':
+ case 'hr':
+ case 'hr-HR':
+ case 'ru':
+ case 'ru-RU':
+ case 'sr':
+ case 'uk':
+ return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
+ case 'cs':
+ case 'cs-CZ':
+ case 'sk':
+ return ($number == 1) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2);
+ case 'ga':
+ return ($number == 1) ? 0 : (($number == 2) ? 1 : 2);
+ case 'lt':
+ return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
+ case 'sl':
+ return ($number % 100 == 1) ? 0 : (($number % 100 == 2) ? 1 : ((($number % 100 == 3) || ($number % 100 == 4)) ? 2 : 3));
+ case 'mk':
+ return ($number % 10 == 1) ? 0 : 1;
+ case 'mt':
+ return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3));
+ case 'lv':
+ return ($number == 0) ? 0 : ((($number % 10 == 1) && ($number % 100 != 11)) ? 1 : 2);
+ case 'pl':
+ return ($number == 1) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2);
+ case 'cy':
+ return ($number == 1) ? 0 : (($number == 2) ? 1 : ((($number == 8) || ($number == 11)) ? 2 : 3));
+ case 'ro':
+ return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2);
+ case 'ar':
+ return ($number == 0) ? 0 : (($number == 1) ? 1 : (($number == 2) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5))));
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
new file mode 100755
index 0000000..629383f
--- /dev/null
+++ b/app/Providers/AppServiceProvider.php
@@ -0,0 +1,36 @@
+app->register(\Barryvdh\Debugbar\ServiceProvider::class);
+ }
+
+ if (env('APP_ENV') !== 'production') {
+ $this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
+ }
+ }
+}
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php
new file mode 100755
index 0000000..9784b1a
--- /dev/null
+++ b/app/Providers/AuthServiceProvider.php
@@ -0,0 +1,30 @@
+ 'App\Policies\ModelPolicy',
+ ];
+
+ /**
+ * Register any authentication / authorization services.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ $this->registerPolicies();
+
+ //
+ }
+}
diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php
new file mode 100755
index 0000000..352cce4
--- /dev/null
+++ b/app/Providers/BroadcastServiceProvider.php
@@ -0,0 +1,21 @@
+ [
+ 'App\Listeners\Updates\Version106',
+ 'App\Listeners\Updates\Version107',
+ 'App\Listeners\Updates\Version108',
+ 'App\Listeners\Updates\Version109',
+ 'App\Listeners\Updates\Version110',
+ 'App\Listeners\Updates\Version112',
+ 'App\Listeners\Updates\Version113',
+ 'App\Listeners\Updates\Version119',
+ 'App\Listeners\Updates\Version120',
+ 'App\Listeners\Updates\Version126',
+ 'App\Listeners\Updates\Version127',
+ 'App\Listeners\Updates\Version129',
+ 'App\Listeners\Updates\Version1210',
+ 'App\Listeners\Updates\Version1211',
+ ],
+ 'Illuminate\Auth\Events\Login' => [
+ 'App\Listeners\Auth\Login',
+ ],
+ 'Illuminate\Auth\Events\Logout' => [
+ 'App\Listeners\Auth\Logout',
+ ],
+ 'App\Events\InvoicePaid' => [
+ 'App\Listeners\Incomes\Invoice\Paid',
+ ],
+ ];
+
+ /**
+ * Register any events for your application.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ parent::boot();
+
+ //
+ }
+}
diff --git a/app/Providers/FormServiceProvider.php b/app/Providers/FormServiceProvider.php
new file mode 100755
index 0000000..39e66eb
--- /dev/null
+++ b/app/Providers/FormServiceProvider.php
@@ -0,0 +1,80 @@
+ ['required' => 'required'], 'value' => null, 'col' => 'col-md-6',
+ ]);
+
+ Form::component('emailGroup', 'partials.form.email_group', [
+ 'name', 'text', 'icon', 'attributes' => ['required' => 'required'], 'value' => null, 'col' => 'col-md-6',
+ ]);
+
+ Form::component('passwordGroup', 'partials.form.password_group', [
+ 'name', 'text', 'icon', 'attributes' => ['required' => 'required'], 'value' => null, 'col' => 'col-md-6',
+ ]);
+
+ Form::component('numberGroup', 'partials.form.number_group', [
+ 'name', 'text', 'icon', 'attributes' => ['required' => 'required'], 'value' => null, 'col' => 'col-md-6',
+ ]);
+
+ Form::component('selectGroup', 'partials.form.select_group', [
+ 'name', 'text', 'icon', 'values', 'selected' => null, 'attributes' => ['required' => 'required'], 'col' => 'col-md-6',
+ ]);
+
+ Form::component('textareaGroup', 'partials.form.textarea_group', [
+ 'name', 'text', 'value' => null, 'attributes' => ['rows' => '3'], 'col' => 'col-md-12',
+ ]);
+
+ Form::component('radioGroup', 'partials.form.radio_group', [
+ 'name', 'text', 'enable' => trans('general.yes'), 'disable' => trans('general.no'), 'attributes' => [], 'col' => 'col-md-6',
+ ]);
+
+ Form::component('checkboxGroup', 'partials.form.checkbox_group', [
+ 'name', 'text', 'items' => [], 'value' => 'name', 'id' => 'id', 'attributes' => ['required' => 'required'], 'col' => 'col-md-12',
+ ]);
+
+ Form::component('fileGroup', 'partials.form.file_group', [
+ 'name', 'text', 'attributes' => [], 'value' => null, 'col' => 'col-md-6',
+ ]);
+
+ Form::component('deleteButton', 'partials.form.delete_button', [
+ 'item', 'url', 'text' => '', 'value' => 'name', 'id' => 'id',
+ ]);
+
+ Form::component('deleteLink', 'partials.form.delete_link', [
+ 'item', 'url', 'text' => '', 'value' => 'name', 'id' => 'id',
+ ]);
+
+ Form::component('saveButtons', 'partials.form.save_buttons', [
+ 'cancel', 'col' => 'col-md-12',
+ ]);
+
+ Form::component('recurring', 'partials.form.recurring', [
+ 'page', 'model' => null,
+ ]);
+ }
+
+ /**
+ * Register the service provider.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ //
+ }
+}
\ No newline at end of file
diff --git a/app/Providers/ObserverServiceProvider.php b/app/Providers/ObserverServiceProvider.php
new file mode 100755
index 0000000..a0aaef1
--- /dev/null
+++ b/app/Providers/ObserverServiceProvider.php
@@ -0,0 +1,30 @@
+mapApiRoutes();
+
+ $this->mapWebRoutes();
+
+ //
+ }
+
+ /**
+ * Define the "web" routes for the application.
+ *
+ * These routes all receive session state, CSRF protection, etc.
+ *
+ * @return void
+ */
+ protected function mapWebRoutes()
+ {
+ Route::middleware('web')
+ ->namespace($this->namespace)
+ ->group(base_path('routes/web.php'));
+ }
+
+ /**
+ * Define the "api" routes for the application.
+ *
+ * These routes are typically stateless.
+ *
+ * @return void
+ */
+ protected function mapApiRoutes()
+ {
+ Route::prefix('api')
+ ->middleware('api')
+ ->namespace($this->namespace)
+ ->group(base_path('routes/api.php'));
+ }
+}
diff --git a/app/Providers/ValidationServiceProvider.php b/app/Providers/ValidationServiceProvider.php
new file mode 100755
index 0000000..5771e75
--- /dev/null
+++ b/app/Providers/ValidationServiceProvider.php
@@ -0,0 +1,66 @@
+pluck('code')->toArray();
+
+ if (in_array($value, $currencies)) {
+ $status = true;
+ }
+
+ $currency_code = $value;
+
+ return $status;
+ },
+ trans('validation.custom.invalid_currency', ['attribute' => $currency_code])
+ );
+
+ $amount = null;
+
+ Validator::extend('amount', function ($attribute, $value, $parameters, $validator) use (&$amount) {
+ $status = false;
+
+ if ($value > 0) {
+ $status = true;
+ }
+
+ $amount = $value;
+
+ return $status;
+ },
+ trans('validation.custom.invalid_amount', ['attribute' => $amount])
+ );
+ }
+
+ /**
+ * Register any application services.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ //
+ }
+}
diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php
new file mode 100755
index 0000000..689827c
--- /dev/null
+++ b/app/Providers/ViewComposerServiceProvider.php
@@ -0,0 +1,67 @@
+getTable();
+
+ // Skip for specific tables
+ $skip_tables = ['companies', 'jobs', 'migrations', 'notifications', 'permissions', 'role_user', 'roles', 'sessions', 'users'];
+ if (in_array($table, $skip_tables)) {
+ return;
+ }
+
+ // Skip if already exists
+ if ($this->exists($builder, 'company_id')) {
+ return;
+ }
+
+ // Apply company scope
+ $builder->where($table . '.company_id', '=', $company_id);
+ }
+
+ /**
+ * Check if scope exists.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $builder
+ * @param $column
+ * @return boolean
+ */
+ protected function exists($builder, $column)
+ {
+ $query = $builder->getQuery();
+
+ foreach ((array) $query->wheres as $key => $where) {
+ if (empty($where) || empty($where['column'])) {
+ continue;
+ }
+
+ if (strstr($where['column'], '.')) {
+ $whr = explode('.', $where['column']);
+
+ $where['column'] = $whr[1];
+ }
+
+ if ($where['column'] != $column) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/app/Traits/Currencies.php b/app/Traits/Currencies.php
new file mode 100755
index 0000000..ae84cd9
--- /dev/null
+++ b/app/Traits/Currencies.php
@@ -0,0 +1,77 @@
+convert($default, (double) $rate)->format();
+ } else {
+ $money = Money::$code($amount)->convert($default, (double) $rate)->getAmount();
+ }
+
+ return $money;
+ }
+
+ public function divide($amount, $code, $rate, $format = false)
+ {
+ if ($format) {
+ $money = Money::$code($amount, true)->divide((double) $rate)->format();
+ } else {
+ $money = Money::$code($amount)->divide((double) $rate)->getAmount();
+ }
+
+ return $money;
+ }
+
+ public function reverseConvert($amount, $code, $rate, $format = false)
+ {
+ $default = setting('general.default_currency', 'USD');
+
+ $code = new Currency($code);
+
+ if ($format) {
+ $money = Money::$default($amount, true)->convert($code, (double) $rate)->format();
+ } else {
+ $money = Money::$default($amount)->convert($code, (double) $rate)->getAmount();
+ }
+
+ return $money;
+ }
+
+ public function dynamicConvert($default, $amount, $code, $rate, $format = false)
+ {
+ $code = new Currency($code);
+
+ if ($format) {
+ $money = Money::$default($amount, true)->convert($code, (double) $rate)->format();
+ } else {
+ $money = Money::$default($amount)->convert($code, (double) $rate)->getAmount();
+ }
+
+ return $money;
+ }
+
+ public function getConvertedAmount($format = false)
+ {
+ return $this->convert($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+
+ public function getReverseConvertedAmount($format = false)
+ {
+ return $this->reverseConvert($this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+
+ public function getDynamicConvertedAmount($format = false)
+ {
+ return $this->dynamicConvert($this->default_currency_code, $this->amount, $this->currency_code, $this->currency_rate, $format);
+ }
+}
\ No newline at end of file
diff --git a/app/Traits/DateTime.php b/app/Traits/DateTime.php
new file mode 100755
index 0000000..974fffe
--- /dev/null
+++ b/app/Traits/DateTime.php
@@ -0,0 +1,78 @@
+ '-', 'slash' => '/', 'dot' => '.', 'comma' => ',', 'space' => ' '];
+
+ $date_format = setting('general.date_format', 'd F Y');
+ $date_separator = $chars[setting('general.date_separator', 'space')];
+
+ return str_replace(' ', $date_separator, $date_format);
+ }
+
+ public function scopeMonthsOfYear($query, $field)
+ {
+ $year = request('year');
+
+ // Get current year if not set
+ if (empty($year)) {
+ $year = Date::now()->year;
+ }
+
+ $start = Date::parse($year . '-01-01')->format('Y-m-d');
+ $end = Date::parse($year . '-12-31')->format('Y-m-d');
+
+ return $query->whereBetween($field, [$start, $end]);
+ }
+
+ public function getTimezones()
+ {
+ // The list of available timezone groups to use.
+ $use_zones = array('Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific');
+
+ // Get the list of time zones from the server.
+ $zones = \DateTimeZone::listIdentifiers();
+
+ // Build the group lists.
+ foreach ($zones as $zone) {
+ // Time zones not in a group we will ignore.
+ if (strpos($zone, '/') === false) {
+ continue;
+ }
+
+ // Get the group/locale from the timezone.
+ list ($group, $locale) = explode('/', $zone, 2);
+
+ // Only use known groups.
+ if (in_array($group, $use_zones)) {
+ // Initialize the group if necessary.
+ if (!isset($groups[$group])) {
+ $groups[$group] = array();
+ }
+
+ // Only add options where a locale exists.
+ if (!empty($locale)) {
+ $groups[$group][$zone] = str_replace('_', ' ', $locale);
+ }
+ }
+ }
+
+ // Sort the group lists.
+ ksort($groups);
+
+ return $groups;
+ }
+}
\ No newline at end of file
diff --git a/app/Traits/Incomes.php b/app/Traits/Incomes.php
new file mode 100755
index 0000000..1e2dc80
--- /dev/null
+++ b/app/Traits/Incomes.php
@@ -0,0 +1,34 @@
+ $next]);
+ setting()->save();
+ }
+}
\ No newline at end of file
diff --git a/app/Traits/Media.php b/app/Traits/Media.php
new file mode 100755
index 0000000..6f6774b
--- /dev/null
+++ b/app/Traits/Media.php
@@ -0,0 +1,37 @@
+
+ *
+ * Whether the model should automatically reload its media relationship after modification.
+ */
+trait Media
+{
+ use Mediable;
+
+ /**
+ * Relationship for all attached media.
+ * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
+ */
+ public function media()
+ {
+ $media = $this->morphToMany(config('mediable.model'), 'mediable')
+ ->withPivot('tag', 'order')
+ ->orderBy('order');
+
+ // Skip deleted media if not detached
+ if (config('mediable.detach_on_soft_delete') == false) {
+ $media->whereNull('deleted_at');
+ }
+
+ return $media;
+ }
+}
diff --git a/app/Traits/Modules.php b/app/Traits/Modules.php
new file mode 100755
index 0000000..5ff860f
--- /dev/null
+++ b/app/Traits/Modules.php
@@ -0,0 +1,438 @@
+ [
+ 'token' => $token,
+ ]
+ ];
+
+ $response = $this->getRemote('token/check', 'POST', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ $result = json_decode($response->getBody());
+
+ return ($result->success) ? true : false;
+ }
+
+ return false;
+ }
+
+ public function getModules()
+ {
+ $response = $this->getRemote('apps/items');
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getModule($alias)
+ {
+ $response = $this->getRemote('apps/' . $alias);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getCategories()
+ {
+ $response = $this->getRemote('apps/categories');
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getModulesByCategory($alias)
+ {
+ $response = $this->getRemote('apps/categories/' . $alias);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getMyModules($data = [])
+ {
+ $response = $this->getRemote('apps/my', 'GET', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getInstalledModules($data = [])
+ {
+ $company_id = session('company_id');
+
+ $cache = 'installed.' . $company_id . '.module';
+
+ $installed = Cache::get($cache);
+
+ if ($installed) {
+ return $installed;
+ }
+
+ $installed = [];
+ $modules = Module::all();
+ $installed_modules = MModule::where('company_id', '=', session('company_id'))->pluck('status', 'alias')->toArray();
+
+ foreach ($modules as $module) {
+ if (!array_key_exists($module->alias, $installed_modules)) {
+ continue;
+ }
+
+ $result = $this->getModule($module->alias);
+
+ if ($result) {
+ $installed[] = $result;
+ }
+ }
+
+ Cache::put($cache, $installed, Date::now()->addHour(6));
+
+ return $installed;
+ }
+
+ public function getPaidModules($data = [])
+ {
+ $response = $this->getRemote('apps/paid', 'GET', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getNewModules($data = [])
+ {
+ $response = $this->getRemote('apps/new', 'GET', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getFreeModules($data = [])
+ {
+ $response = $this->getRemote('apps/free', 'GET', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getSearchModules($data = [])
+ {
+ $response = $this->getRemote('apps/search', 'GET', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return json_decode($response->getBody())->data;
+ }
+
+ return [];
+ }
+
+ public function getCoreVersion()
+ {
+ $data['query'] = Info::all();
+
+ $response = $this->getRemote('core/version', 'GET', $data);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ return $response->json();
+ }
+
+ return [];
+ }
+
+ public function downloadModule($path)
+ {
+ $response = $this->getRemote($path);
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ $file = $response->getBody()->getContents();
+
+ $path = 'temp-' . md5(mt_rand());
+ $temp_path = storage_path('app/temp') . '/' . $path;
+
+ $file_path = $temp_path . '/upload.zip';
+
+ // Create tmp directory
+ if (!File::isDirectory($temp_path)) {
+ File::makeDirectory($temp_path);
+ }
+
+ // Add content to the Zip file
+ $uploaded = is_int(file_put_contents($file_path, $file)) ? true : false;
+
+ if (!$uploaded) {
+ return false;
+ }
+
+ $data = [
+ 'path' => $path
+ ];
+
+ return [
+ 'success' => true,
+ 'errors' => false,
+ 'data' => $data,
+ ];
+ }
+
+ return [
+ 'success' => false,
+ 'errors' => true,
+ 'data' => null,
+ ];
+ }
+
+ public function unzipModule($path)
+ {
+ $temp_path = storage_path('app/temp') . '/' . $path;
+
+ $file = $temp_path . '/upload.zip';
+
+ // Unzip the file
+ $zip = new ZipArchive();
+
+ if (!$zip->open($file) || !$zip->extractTo($temp_path)) {
+ return [
+ 'success' => false,
+ 'errors' => true,
+ 'data' => null,
+ ];
+ }
+
+ $zip->close();
+
+ // Remove Zip
+ File::delete($file);
+
+ $data = [
+ 'path' => $path
+ ];
+
+ return [
+ 'success' => true,
+ 'errors' => false,
+ 'data' => $data,
+ ];
+ }
+
+ public function installModule($path)
+ {
+ $temp_path = storage_path('app/temp') . '/' . $path;
+
+ $modules_path = base_path() . '/modules';
+
+ // Create modules directory
+ if (!File::isDirectory($modules_path)) {
+ File::makeDirectory($modules_path);
+ }
+
+ $module = json_decode(file_get_contents($temp_path . '/module.json'));
+
+ $module_path = $modules_path . '/' . $module->name;
+
+ // Create module directory
+ if (!File::isDirectory($module_path)) {
+ File::makeDirectory($module_path);
+ }
+
+ // Move all files/folders from temp path then delete it
+ File::copyDirectory($temp_path, $module_path);
+ File::deleteDirectory($temp_path);
+
+ Artisan::call('cache:clear');
+
+ $data = [
+ 'path' => $path,
+ 'name' => $module->name,
+ 'alias' => $module->alias
+ ];
+
+ return [
+ 'success' => true,
+ 'installed' => url("apps/post/" . $module->alias),
+ 'errors' => false,
+ 'data' => $data,
+ ];
+ }
+
+ public function uninstallModule($alias)
+ {
+ $module = Module::findByAlias($alias);
+
+ $data = [
+ 'name' => $module->get('name'),
+ 'category' => $module->get('category'),
+ 'version' => $module->get('version'),
+ ];
+
+ $module->delete();
+
+ Artisan::call('cache:clear');
+
+ return [
+ 'success' => true,
+ 'errors' => false,
+ 'data' => $data
+ ];
+ }
+
+ public function enableModule($alias)
+ {
+ $module = Module::findByAlias($alias);
+
+ $data = [
+ 'name' => $module->get('name'),
+ 'category' => $module->get('category'),
+ 'version' => $module->get('version'),
+ ];
+
+ $module->enable();
+
+ Artisan::call('cache:clear');
+
+ return [
+ 'success' => true,
+ 'errors' => false,
+ 'data' => $data
+ ];
+ }
+
+ public function disableModule($alias)
+ {
+ $module = Module::findByAlias($alias);
+
+ $data = [
+ 'name' => $module->get('name'),
+ 'category' => $module->get('category'),
+ 'version' => $module->get('version'),
+ ];
+
+ $module->disable();
+
+ Artisan::call('cache:clear');
+
+ return [
+ 'success' => true,
+ 'errors' => false,
+ 'data' => $data
+ ];
+ }
+
+ public function loadSuggestions()
+ {
+ // Get data from cache
+ $data = Cache::get('suggestions');
+
+ if (!empty($data)) {
+ return $data;
+ }
+
+ $data = [];
+
+ $url = 'apps/suggestions';
+
+ $response = $this->getRemote($url, 'GET', ['timeout' => 30, 'referer' => true]);
+
+ // Exception
+ if ($response instanceof RequestException) {
+ return false;
+ }
+
+ // Bad response
+ if (!$response || ($response->getStatusCode() != 200)) {
+ return false;
+ }
+
+ $suggestions = json_decode($response->getBody())->data;
+
+ foreach ($suggestions as $suggestion) {
+ $data[$suggestion->path] = $suggestion;
+ }
+
+ Cache::put('suggestions', $data, Date::now()->addHour(6));
+
+ return $data;
+ }
+
+ public function getSuggestions($path)
+ {
+ // Get data from cache
+ $data = Cache::get('suggestions');
+
+ if (empty($data)) {
+ $data = $this->loadSuggestions();
+ }
+
+ if (!empty($data) && array_key_exists($path, $data)) {
+ return $data[$path];
+ }
+
+ return false;
+ }
+
+ protected function getRemote($path, $method = 'GET', $data = array())
+ {
+ $base = 'https://akaunting.com/api/';
+
+ $client = new Client(['verify' => false, 'base_uri' => $base]);
+
+ $headers['headers'] = [
+ 'Authorization' => 'Bearer ' . setting('general.api_token'),
+ 'Accept' => 'application/json',
+ 'Referer' => env('APP_URL'),
+ 'Akaunting' => version('short'),
+ ];
+
+ $data['http_errors'] = false;
+
+ $data = array_merge($data, $headers);
+
+ try {
+ $result = $client->request($method, $path, $data);
+ } catch (RequestException $e) {
+ $result = false;
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Traits/Recurring.php b/app/Traits/Recurring.php
new file mode 100755
index 0000000..5e07980
--- /dev/null
+++ b/app/Traits/Recurring.php
@@ -0,0 +1,150 @@
+get('recurring_frequency') == 'no') {
+ return;
+ }
+
+ $frequency = ($request['recurring_frequency'] != 'custom') ? $request['recurring_frequency'] : $request['recurring_custom_frequency'];
+ $interval = ($request['recurring_frequency'] != 'custom') ? 1 : (int) $request['recurring_interval'];
+ $started_at = $request->get('paid_at') ?: ($request->get('invoiced_at') ?: $request->get('billed_at'));
+
+ $this->recurring()->create([
+ 'company_id' => session('company_id'),
+ 'frequency' => $frequency,
+ 'interval' => $interval,
+ 'started_at' => $started_at,
+ 'count' => (int) $request['recurring_count'],
+ ]);
+ }
+
+ public function updateRecurring()
+ {
+ $request = request();
+
+ if ($request->get('recurring_frequency') == 'no') {
+ $this->recurring()->delete();
+ return;
+ }
+
+ $frequency = ($request['recurring_frequency'] != 'custom') ? $request['recurring_frequency'] : $request['recurring_custom_frequency'];
+ $interval = ($request['recurring_frequency'] != 'custom') ? 1 : (int) $request['recurring_interval'];
+ $started_at = $request->get('paid_at') ?: ($request->get('invoiced_at') ?: $request->get('billed_at'));
+
+ $recurring = $this->recurring();
+
+ if ($recurring->count()) {
+ $function = 'update';
+ } else {
+ $function = 'create';
+ }
+
+ $recurring->$function([
+ 'company_id' => session('company_id'),
+ 'frequency' => $frequency,
+ 'interval' => $interval,
+ 'started_at' => $started_at,
+ 'count' => (int) $request['recurring_count'],
+ ]);
+ }
+
+ public function current()
+ {
+ if (!$schedule = $this->schedule()) {
+ return false;
+ }
+
+ return $schedule->current()->getStart();
+ }
+
+ public function next()
+ {
+ if (!$schedule = $this->schedule()) {
+ return false;
+ }
+
+ if (!$next = $schedule->next()) {
+ return false;
+ }
+
+ return $next->getStart();
+ }
+
+ public function first()
+ {
+ if (!$schedule = $this->schedule()) {
+ return false;
+ }
+
+ return $schedule->first()->getStart();
+ }
+
+ public function last()
+ {
+ if (!$schedule = $this->schedule()) {
+ return false;
+ }
+
+ return $schedule->last()->getStart();
+ }
+
+ public function schedule()
+ {
+ $config = new ArrayTransformerConfig();
+ $config->enableLastDayOfMonthFix();
+
+ $transformer = new ArrayTransformer();
+ $transformer->setConfig($config);
+
+ return $transformer->transform($this->getRule());
+ }
+
+ public function getRule()
+ {
+ $rule = (new Rule())
+ ->setStartDate($this->getRuleStartDate())
+ ->setTimezone($this->getRuleTimeZone())
+ ->setFreq($this->getRuleFrequency())
+ ->setInterval($this->interval);
+
+ // 0 means infinite
+ if ($this->count != 0) {
+ $rule->setCount($this->getRuleCount());
+ }
+
+ return $rule;
+ }
+
+ public function getRuleStartDate()
+ {
+ return new \DateTime($this->started_at, new \DateTimeZone($this->getRuleTimeZone()));
+ }
+
+ public function getRuleTimeZone()
+ {
+ return setting('general.timezone');
+ }
+
+ public function getRuleCount()
+ {
+ // Fix for humans
+ return $this->count + 1;
+ }
+
+ public function getRuleFrequency()
+ {
+ return strtoupper($this->frequency);
+ }
+}
\ No newline at end of file
diff --git a/app/Traits/SiteApi.php b/app/Traits/SiteApi.php
new file mode 100755
index 0000000..3a07eb0
--- /dev/null
+++ b/app/Traits/SiteApi.php
@@ -0,0 +1,36 @@
+ false, 'base_uri' => $base]);
+
+ $headers['headers'] = array(
+ 'Authorization' => 'Bearer ' . setting('general.api_token'),
+ 'Accept' => 'application/json',
+ 'Referer' => env('APP_URL'),
+ 'Akaunting' => version('short')
+ );
+
+ $data['http_errors'] = false;
+
+ $data = array_merge($data, $headers);
+
+ try {
+ $result = $client->get($url, $data);
+ } catch (RequestException $e) {
+ $result = $e;
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Traits/Uploads.php b/app/Traits/Uploads.php
new file mode 100755
index 0000000..8adfbd2
--- /dev/null
+++ b/app/Traits/Uploads.php
@@ -0,0 +1,49 @@
+isValid()) {
+ return $path;
+ }
+
+ if (!$company_id) {
+ $company_id = session('company_id');
+ }
+
+ $file_name = $file->getClientOriginalName();
+
+ // Upload file
+ $file->storeAs($company_id . '/' . $folder, $file_name);
+
+ // Prepare db path
+ $path = $folder . '/' . $file_name;
+
+ return $path;
+ }
+
+ public function getMedia($file, $folder = 'settings', $company_id = null)
+ {
+ $path = '';
+
+ if (!$file || !$file->isValid()) {
+ return $path;
+ }
+
+ if (!$company_id) {
+ $company_id = session('company_id');
+ }
+
+ $path = $company_id . '/' . $folder;
+
+ return MediaUploader::fromSource($file)->toDirectory($path)->upload();
+ }
+}
diff --git a/app/Transformers/Auth/Permission.php b/app/Transformers/Auth/Permission.php
new file mode 100755
index 0000000..ef04d22
--- /dev/null
+++ b/app/Transformers/Auth/Permission.php
@@ -0,0 +1,24 @@
+ $model->id,
+ 'name' => $model->display_name,
+ 'code' => $model->name,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Auth/Role.php b/app/Transformers/Auth/Role.php
new file mode 100755
index 0000000..932a26f
--- /dev/null
+++ b/app/Transformers/Auth/Role.php
@@ -0,0 +1,38 @@
+ $model->id,
+ 'name' => $model->display_name,
+ 'code' => $model->name,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includePermissions(Model $model)
+ {
+ return $this->collection($model->permissions, new Permission());
+ }
+}
diff --git a/app/Transformers/Auth/User.php b/app/Transformers/Auth/User.php
new file mode 100755
index 0000000..e7c0983
--- /dev/null
+++ b/app/Transformers/Auth/User.php
@@ -0,0 +1,48 @@
+ $model->id,
+ 'name' => $model->name,
+ 'email' => $model->email,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includeCompanies(Model $model)
+ {
+ return $this->collection($model->companies, new Company());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includeRoles(Model $model)
+ {
+ return $this->collection($model->roles, new Role());
+ }
+}
diff --git a/app/Transformers/Banking/Account.php b/app/Transformers/Banking/Account.php
new file mode 100755
index 0000000..2195599
--- /dev/null
+++ b/app/Transformers/Banking/Account.php
@@ -0,0 +1,32 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'number' => $model->number,
+ 'currency_code' => $model->currency_code,
+ 'opening_balance' => $model->opening_balance,
+ 'current_balance' => $model->balance,
+ 'bank_name' => $model->bank_name,
+ 'bank_phone' => $model->bank_phone,
+ 'bank_address' => $model->bank_address,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Banking/Transfer.php b/app/Transformers/Banking/Transfer.php
new file mode 100755
index 0000000..e55beae
--- /dev/null
+++ b/app/Transformers/Banking/Transfer.php
@@ -0,0 +1,50 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'payment_id' => $model->payment_id,
+ 'revenue_id' => $model->revenue_id,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includePayment(Model $model)
+ {
+ return $this->item($model->payment, new Payment());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeRevenue(Model $model)
+ {
+ return $this->item($model->revenue, new Revenue());
+ }
+}
diff --git a/app/Transformers/Common/Company.php b/app/Transformers/Common/Company.php
new file mode 100755
index 0000000..acfdce7
--- /dev/null
+++ b/app/Transformers/Common/Company.php
@@ -0,0 +1,33 @@
+ $model->id,
+ 'name' => $model->company_name,
+ 'email' => $model->company_email,
+ 'domain' => $model->domain,
+ 'address' => $model->company_address,
+ 'logo' => $model->company_logo,
+ 'default_account' => $model->default_account,
+ 'default_currency' => $model->default_currency,
+ 'default_tax' => $model->default_tax,
+ 'default_payment_method' => $model->default_payment_method,
+ 'default_language' => $model->default_language,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Common/Item.php b/app/Transformers/Common/Item.php
new file mode 100755
index 0000000..4cc2a68
--- /dev/null
+++ b/app/Transformers/Common/Item.php
@@ -0,0 +1,66 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'sku' => $model->sku,
+ 'description' => $model->description,
+ 'sale_price' => $model->sale_price,
+ 'purchase_price' => $model->purchase_price,
+ 'quantity' => $model->quantity,
+ 'category_id' => $model->category_id,
+ 'tax_id' => $model->tax_id,
+ 'picture' => $model->picture,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return mixed
+ */
+ public function includeTax(Model $model)
+ {
+ if (!$model->tax) {
+ return $this->null();
+ }
+
+ return $this->item($model->tax, new Tax());
+ }
+
+ /**
+ * @param Model $model
+ * @return mixed
+ */
+ public function includeCategory(Model $model)
+ {
+ if (!$model->category) {
+ return $this->null();
+ }
+
+ return $this->item($model->category, new Category());
+ }
+}
diff --git a/app/Transformers/Company/Company.php b/app/Transformers/Company/Company.php
new file mode 100755
index 0000000..7726c96
--- /dev/null
+++ b/app/Transformers/Company/Company.php
@@ -0,0 +1,8 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'bill_number' => $model->bill_number,
+ 'order_number' => $model->order_number,
+ 'bill_status_code' => $model->invoice_status_code,
+ 'billed_at' => $model->billed_at->toIso8601String(),
+ 'due_at' => $model->due_at->toIso8601String(),
+ 'amount' => $model->amount,
+ 'currency_code' => $model->currency_code,
+ 'currency_rate' => $model->currency_rate,
+ 'vendor_id' => $model->vendor_id,
+ 'vendor_name' => $model->vendor_name,
+ 'vendor_email' => $model->vendor_email,
+ 'vendor_tax_number' => $model->vendor_tax_number,
+ 'vendor_phone' => $model->vendor_phone,
+ 'vendor_address' => $model->vendor_address,
+ 'notes' => $model->notes,
+ 'attachment' => $model->attachment,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCurrency(Model $model)
+ {
+ return $this->item($model->currency, new Currency());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includeHistories(Model $model)
+ {
+ return $this->collection($model->histories, new BillHistories());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includeItems(Model $model)
+ {
+ return $this->collection($model->items, new BillItems());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includePayments(Model $model)
+ {
+ return $this->collection($model->payments, new BillPayments());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeStatus(Model $model)
+ {
+ return $this->item($model->status, new BillStatus());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeVendor(Model $model)
+ {
+ return $this->item($model->vendor, new Vendor());
+ }
+}
diff --git a/app/Transformers/Expense/BillHistories.php b/app/Transformers/Expense/BillHistories.php
new file mode 100755
index 0000000..e679bb8
--- /dev/null
+++ b/app/Transformers/Expense/BillHistories.php
@@ -0,0 +1,27 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'bill_id' => $model->bill_id,
+ 'status_code' => $model->status_code,
+ 'notify' => $model->notify,
+ 'description' => $model->description,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Expense/BillItems.php b/app/Transformers/Expense/BillItems.php
new file mode 100755
index 0000000..d3488ce
--- /dev/null
+++ b/app/Transformers/Expense/BillItems.php
@@ -0,0 +1,32 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'bill_id' => $model->bill_id,
+ 'item_id' => $model->item_id,
+ 'name' => $model->name,
+ 'sku' => $model->sku,
+ 'quantity' => $model->quantity,
+ 'price' => $model->price,
+ 'total' => $model->total,
+ 'tax' => $model->tax,
+ 'tax_id' => $model->tax_id,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Expense/BillPayments.php b/app/Transformers/Expense/BillPayments.php
new file mode 100755
index 0000000..ed314cc
--- /dev/null
+++ b/app/Transformers/Expense/BillPayments.php
@@ -0,0 +1,58 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'bill_id' => $model->bill_id,
+ 'account_id' => $model->account_id,
+ 'paid_at' => $model->paid_at->toIso8601String(),
+ 'amount' => $model->amount,
+ 'currency_code' => $model->currency_code,
+ 'currency_rate' => $model->currency_rate,
+ 'description' => $model->description,
+ 'payment_method' => $model->payment_method,
+ 'reference' => $model->reference,
+ 'attachment' => $model->attachment,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeAccount(Model $model)
+ {
+ return $this->item($model->account, new Account());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCurrency(Model $model)
+ {
+ return $this->item($model->currency, new Currency());
+ }
+}
diff --git a/app/Transformers/Expense/BillStatus.php b/app/Transformers/Expense/BillStatus.php
new file mode 100755
index 0000000..216f302
--- /dev/null
+++ b/app/Transformers/Expense/BillStatus.php
@@ -0,0 +1,25 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'code' => $model->code,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Expense/BillTotals.php b/app/Transformers/Expense/BillTotals.php
new file mode 100755
index 0000000..c51b46c
--- /dev/null
+++ b/app/Transformers/Expense/BillTotals.php
@@ -0,0 +1,29 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'bill_id' => $model->bill_id,
+ 'code' => $model->code,
+ 'name' => $model->name,
+ 'amount' => $model->amount,
+ 'sort_order' => $model->sort_order,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Expense/Payment.php b/app/Transformers/Expense/Payment.php
new file mode 100755
index 0000000..41c52e4
--- /dev/null
+++ b/app/Transformers/Expense/Payment.php
@@ -0,0 +1,83 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'account_id' => $model->account_id,
+ 'paid_at' => $model->paid_at->toIso8601String(),
+ 'amount' => $model->amount,
+ 'currency_code' => $model->currency_code,
+ 'currency_rate' => $model->currency_rate,
+ 'vendor_id' => $model->vendor_id,
+ 'description' => $model->description,
+ 'category_id' => $model->category_id,
+ 'payment_method' => $model->payment_method,
+ 'reference' => $model->reference,
+ 'attachment' => $model->attachment,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeAccount(Model $model)
+ {
+ return $this->item($model->account, new Account());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCategory(Model $model)
+ {
+ return $this->item($model->category, new Category());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCurrency(Model $model)
+ {
+ return $this->item($model->currency, new Currency());
+ }
+
+ /**
+ * @param Model $model
+ * @return mixed
+ */
+ public function includeVendor(Model $model)
+ {
+ if (!$model->vendor) {
+ return $this->null();
+ }
+
+ return $this->item($model->vendor, new Vendor());
+ }
+}
diff --git a/app/Transformers/Expense/Vendor.php b/app/Transformers/Expense/Vendor.php
new file mode 100755
index 0000000..e9f7569
--- /dev/null
+++ b/app/Transformers/Expense/Vendor.php
@@ -0,0 +1,32 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'user_id' => $model->user_id,
+ 'name' => $model->name,
+ 'email' => $model->email,
+ 'tax_number' => $model->tax_number,
+ 'phone' => $model->phone,
+ 'address' => $model->address,
+ 'website' => $model->website,
+ 'currency_code' => $model->currency_code,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Income/Customer.php b/app/Transformers/Income/Customer.php
new file mode 100755
index 0000000..0d9d642
--- /dev/null
+++ b/app/Transformers/Income/Customer.php
@@ -0,0 +1,32 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'user_id' => $model->user_id,
+ 'name' => $model->name,
+ 'email' => $model->email,
+ 'tax_number' => $model->tax_number,
+ 'phone' => $model->phone,
+ 'address' => $model->address,
+ 'website' => $model->website,
+ 'currency_code' => $model->currency_code,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Income/Invoice.php b/app/Transformers/Income/Invoice.php
new file mode 100755
index 0000000..85edca9
--- /dev/null
+++ b/app/Transformers/Income/Invoice.php
@@ -0,0 +1,104 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'invoice_number' => $model->invoice_number,
+ 'order_number' => $model->order_number,
+ 'invoice_status_code' => $model->invoice_status_code,
+ 'invoiced_at' => $model->invoiced_at->toIso8601String(),
+ 'due_at' => $model->due_at->toIso8601String(),
+ 'amount' => $model->amount,
+ 'currency_code' => $model->currency_code,
+ 'currency_rate' => $model->currency_rate,
+ 'customer_id' => $model->customer_id,
+ 'customer_name' => $model->customer_name,
+ 'customer_email' => $model->customer_email,
+ 'customer_tax_number' => $model->customer_tax_number,
+ 'customer_phone' => $model->customer_phone,
+ 'customer_address' => $model->customer_address,
+ 'notes' => $model->notes,
+ 'attachment' => $model->attachment,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCurrency(Model $model)
+ {
+ return $this->item($model->currency, new Currency());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCustomer(Model $model)
+ {
+ return $this->item($model->customer, new Customer());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includeHistories(Model $model)
+ {
+ return $this->collection($model->histories, new InvoiceHistories());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includeItems(Model $model)
+ {
+ return $this->collection($model->items, new InvoiceItems());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Collection
+ */
+ public function includePayments(Model $model)
+ {
+ return $this->collection($model->payments, new InvoicePayments());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeStatus(Model $model)
+ {
+ return $this->item($model->status, new InvoiceStatus());
+ }
+}
diff --git a/app/Transformers/Income/InvoiceHistories.php b/app/Transformers/Income/InvoiceHistories.php
new file mode 100755
index 0000000..8e2ad7b
--- /dev/null
+++ b/app/Transformers/Income/InvoiceHistories.php
@@ -0,0 +1,27 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'invoice_id' => $model->invoice_id,
+ 'status_code' => $model->status_code,
+ 'notify' => $model->notify,
+ 'description' => $model->description,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Income/InvoiceItems.php b/app/Transformers/Income/InvoiceItems.php
new file mode 100755
index 0000000..8ce9c0f
--- /dev/null
+++ b/app/Transformers/Income/InvoiceItems.php
@@ -0,0 +1,32 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'invoice_id' => $model->invoice_id,
+ 'item_id' => $model->item_id,
+ 'name' => $model->name,
+ 'sku' => $model->sku,
+ 'quantity' => $model->quantity,
+ 'price' => $model->price,
+ 'total' => $model->total,
+ 'tax' => $model->tax,
+ 'tax_id' => $model->tax_id,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Income/InvoicePayments.php b/app/Transformers/Income/InvoicePayments.php
new file mode 100755
index 0000000..4e9cc72
--- /dev/null
+++ b/app/Transformers/Income/InvoicePayments.php
@@ -0,0 +1,58 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'invoice_id' => $model->invoice_id,
+ 'account_id' => $model->account_id,
+ 'paid_at' => $model->paid_at->toIso8601String(),
+ 'amount' => $model->amount,
+ 'currency_code' => $model->currency_code,
+ 'currency_rate' => $model->currency_rate,
+ 'description' => $model->description,
+ 'payment_method' => $model->payment_method,
+ 'reference' => $model->reference,
+ 'attachment' => $model->attachment,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeAccount(Model $model)
+ {
+ return $this->item($model->account, new Account());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCurrency(Model $model)
+ {
+ return $this->item($model->currency, new Currency());
+ }
+}
diff --git a/app/Transformers/Income/InvoiceStatus.php b/app/Transformers/Income/InvoiceStatus.php
new file mode 100755
index 0000000..76d029f
--- /dev/null
+++ b/app/Transformers/Income/InvoiceStatus.php
@@ -0,0 +1,25 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'code' => $model->code,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Income/InvoiceTotals.php b/app/Transformers/Income/InvoiceTotals.php
new file mode 100755
index 0000000..025c9b6
--- /dev/null
+++ b/app/Transformers/Income/InvoiceTotals.php
@@ -0,0 +1,29 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'invoice_id' => $model->invoice_id,
+ 'code' => $model->code,
+ 'name' => $model->name,
+ 'amount' => $model->amount,
+ 'sort_order' => $model->sort_order,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Income/Revenue.php b/app/Transformers/Income/Revenue.php
new file mode 100755
index 0000000..daf7f7e
--- /dev/null
+++ b/app/Transformers/Income/Revenue.php
@@ -0,0 +1,83 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'account_id' => $model->account_id,
+ 'paid_at' => $model->paid_at->toIso8601String(),
+ 'amount' => $model->amount,
+ 'currency_code' => $model->currency_code,
+ 'currency_rate' => $model->currency_rate,
+ 'customer_id' => $model->customer_id,
+ 'description' => $model->description,
+ 'category_id' => $model->category_id,
+ 'payment_method' => $model->payment_method,
+ 'reference' => $model->reference,
+ 'attachment' => $model->attachment,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeAccount(Model $model)
+ {
+ return $this->item($model->account, new Account());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCategory(Model $model)
+ {
+ return $this->item($model->category, new Category());
+ }
+
+ /**
+ * @param Model $model
+ * @return \League\Fractal\Resource\Item
+ */
+ public function includeCurrency(Model $model)
+ {
+ return $this->item($model->currency, new Currency());
+ }
+
+ /**
+ * @param Model $model
+ * @return mixed
+ */
+ public function includeCustomer(Model $model)
+ {
+ if (!$model->customer) {
+ return $this->null();
+ }
+
+ return $this->item($model->customer, new Customer());
+ }
+}
diff --git a/app/Transformers/Item/Item.php b/app/Transformers/Item/Item.php
new file mode 100755
index 0000000..12948fd
--- /dev/null
+++ b/app/Transformers/Item/Item.php
@@ -0,0 +1,8 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'type' => $model->type,
+ 'color' => $model->color,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Setting/Currency.php b/app/Transformers/Setting/Currency.php
new file mode 100755
index 0000000..5f235fe
--- /dev/null
+++ b/app/Transformers/Setting/Currency.php
@@ -0,0 +1,32 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'code' => $model->code,
+ 'rate' => $model->rate,
+ 'enabled' => $model->enabled,
+ 'precision' => $model->precision,
+ 'symbol' => $model->symbol,
+ 'symbol_first' => $model->symbol_first,
+ 'decimal_mark' => $model->decimal_mark,
+ 'thousands_separator' => $model->thousands_separator,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Transformers/Setting/Setting.php b/app/Transformers/Setting/Setting.php
new file mode 100755
index 0000000..733b061
--- /dev/null
+++ b/app/Transformers/Setting/Setting.php
@@ -0,0 +1,23 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'key' => $model->key,
+ 'value' => $model->value,
+ ];
+ }
+}
diff --git a/app/Transformers/Setting/Tax.php b/app/Transformers/Setting/Tax.php
new file mode 100755
index 0000000..be4deb6
--- /dev/null
+++ b/app/Transformers/Setting/Tax.php
@@ -0,0 +1,26 @@
+ $model->id,
+ 'company_id' => $model->company_id,
+ 'name' => $model->name,
+ 'rate' => $model->rate,
+ 'enabled' => $model->enabled,
+ 'created_at' => $model->created_at->toIso8601String(),
+ 'updated_at' => $model->updated_at->toIso8601String(),
+ ];
+ }
+}
diff --git a/app/Utilities/Import.php b/app/Utilities/Import.php
new file mode 100755
index 0000000..f39aee5
--- /dev/null
+++ b/app/Utilities/Import.php
@@ -0,0 +1,117 @@
+each(function ($sheet) use (&$success, $slug) {
+ if (!static::isValidSheetName($sheet, $slug)) {
+ $message = trans('messages.error.import_sheet');
+
+ flash($message)->error()->important();
+
+ return false;
+ }
+
+ if (!$success = static::createFromSheet($sheet, $slug)) {
+ return false;
+ }
+ });
+
+ return $success;
+ }
+
+ public static function createFromSheet($sheet, $slug)
+ {
+ $success = true;
+
+ $model = '\App\Models\\' . $slug;
+ $request = '\App\Http\Requests\\' . $slug;
+
+ if (!class_exists($model) || !class_exists($request)) {
+ return false;
+ }
+
+ // Loop through all rows
+ $sheet->each(function ($row, $index) use ($sheet, &$success, $model, $request) {
+ $data = static::fixRow($row->toArray());
+
+ // Set the line values so that request class could validate
+ request()->merge($data);
+
+ try {
+ app($request);
+
+ $data['company_id'] = session('company_id');
+
+ $model::create($data);
+ } catch (ValidationException $e) {
+ $message = trans('messages.error.import_column', [
+ 'message' => $e->validator->errors()->first(),
+ 'sheet' => $sheet->getTitle(),
+ 'line' => $index + 2,
+ ]);
+
+ flash($message)->error()->important();
+
+ $success = false;
+
+ // Break the import process
+ return false;
+ }
+
+ // Unset added line values
+ foreach ($data as $key => $value) {
+ request()->offsetUnset($key);
+ }
+ });
+
+ return $success;
+ }
+
+ public static function isValidSheetName($sheet, $slug)
+ {
+ $t = explode('\\', $slug);
+
+ if (empty($t[1])) {
+ return false;
+ }
+
+ if ($sheet->getTitle() != str_plural(snake_case($t[1]))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected static function fixRow($data)
+ {
+ // Fix the date fields
+ $date_fields = ['paid_at', 'due_at', 'billed_at', 'invoiced_at'];
+ foreach ($date_fields as $date_field) {
+ if (empty($data[$date_field])) {
+ continue;
+ }
+
+ $new_date = Date::parse($data[$date_field])->format('Y-m-d') . ' ' . Date::now()->format('H:i:s');
+
+ $data[$date_field] = $new_date;
+ }
+
+ // Make enabled field integer
+ if (isset($data['enabled'])) {
+ $data['enabled'] = (int) $data['enabled'];
+ }
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/app/Utilities/ImportFile.php b/app/Utilities/ImportFile.php
new file mode 100755
index 0000000..049c22e
--- /dev/null
+++ b/app/Utilities/ImportFile.php
@@ -0,0 +1,29 @@
+hasFile('import')) {
+ flash(trans('messages.error.no_file'))->error();
+
+ redirect()->back()->send();
+ }
+
+ $folder = session('company_id') . '/imports';
+
+ // Upload file
+ $path = Storage::path($request->file('import')->store($folder));
+
+ return $path;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Utilities/Info.php b/app/Utilities/Info.php
new file mode 100755
index 0000000..1f20c86
--- /dev/null
+++ b/app/Utilities/Info.php
@@ -0,0 +1,49 @@
+count();
+
+ return $data;
+ }
+
+ public static function phpVersion()
+ {
+ return phpversion();
+ }
+
+ public static function mysqlVersion()
+ {
+ if(env('DB_CONNECTION') === 'mysql')
+ {
+ return DB::selectOne('select version() as mversion')->mversion;
+ }
+
+ return "N/A";
+ }
+}
\ No newline at end of file
diff --git a/app/Utilities/Installer.php b/app/Utilities/Installer.php
new file mode 100755
index 0000000..5d6f811
--- /dev/null
+++ b/app/Utilities/Installer.php
@@ -0,0 +1,282 @@
+ 'Safe Mode']);
+ }
+
+ if (ini_get('register_globals')) {
+ $requirements[] = trans('install.requirements.disabled', ['feature' => 'Register Globals']);
+ }
+
+ if (ini_get('magic_quotes_gpc')) {
+ $requirements[] = trans('install.requirements.disabled', ['feature' => 'Magic Quotes']);
+ }
+
+ if (!ini_get('file_uploads')) {
+ $requirements[] = trans('install.requirements.enabled', ['feature' => 'File Uploads']);
+ }
+
+ if (!class_exists('PDO')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'MySQL PDO']);
+ }
+
+ if (!extension_loaded('openssl')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'OpenSSL']);
+ }
+
+ if (!extension_loaded('tokenizer')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'Tokenizer']);
+ }
+
+ if (!extension_loaded('mbstring')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'mbstring']);
+ }
+
+ if (!extension_loaded('curl')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'cURL']);
+ }
+
+ if (!extension_loaded('xml')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'XML']);
+ }
+
+ if (!extension_loaded('zip')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'ZIP']);
+ }
+
+ if (!extension_loaded('fileinfo')) {
+ $requirements[] = trans('install.requirements.extension', ['extension' => 'FileInfo']);
+ }
+
+ if (!is_writable(base_path('storage/app'))) {
+ $requirements[] = trans('install.requirements.directory', ['directory' => 'storage/app']);
+ }
+
+ if (!is_writable(base_path('storage/app/uploads'))) {
+ $requirements[] = trans('install.requirements.directory', ['directory' => 'storage/app/uploads']);
+ }
+
+ if (!is_writable(base_path('storage/framework'))) {
+ $requirements[] = trans('install.requirements.directory', ['directory' => 'storage/framework']);
+ }
+
+ if (!is_writable(base_path('storage/logs'))) {
+ $requirements[] = trans('install.requirements.directory', ['directory' => 'storage/logs']);
+ }
+
+ return $requirements;
+ }
+
+ /**
+ * Create a default .env file.
+ *
+ * @return void
+ */
+ public static function createDefaultEnvFile()
+ {
+ // Rename file
+ if (is_file(base_path('.env.example'))) {
+ File::move(base_path('.env.example'), base_path('.env'));
+ }
+
+ // Update .env file
+ static::updateEnv([
+ 'APP_KEY' => 'base64:'.base64_encode(random_bytes(32)),
+ 'APP_URL' => url('/'),
+ ]);
+ }
+
+ public static function createDbTables($host, $port, $database, $username, $password)
+ {
+ if (!static::isDbValid($host, $port, $database, $username, $password)) {
+ return false;
+ }
+
+ // Set database details
+ static::saveDbVariables($host, $port, $database, $username, $password);
+
+ // Try to increase the maximum execution time
+ set_time_limit(300); // 5 minutes
+
+ // Create tables
+ Artisan::call('migrate', ['--force' => true]);
+
+ // Create Roles
+ Artisan::call('db:seed', ['--class' => 'Database\Seeds\Roles', '--force' => true]);
+
+ return true;
+ }
+
+ /**
+ * Check if the database exists and is accessible.
+ *
+ * @param $host
+ * @param $port
+ * @param $database
+ * @param $host
+ * @param $database
+ * @param $username
+ * @param $password
+ *
+ * @return bool
+ */
+ public static function isDbValid($host, $port, $database, $username, $password)
+ {
+ Config::set('database.connections.install_test', [
+ 'host' => $host,
+ 'port' => $port,
+ 'database' => $database,
+ 'username' => $username,
+ 'password' => $password,
+ 'driver' => env('DB_CONNECTION', 'mysql'),
+ 'charset' => env('DB_CHARSET', 'utf8mb4'),
+ ]);
+
+ try {
+ DB::connection('install_test')->getPdo();
+ } catch (\Exception $e) {;
+ return false;
+ }
+
+ // Purge test connection
+ DB::purge('install_test');
+
+ return true;
+ }
+
+ public static function saveDbVariables($host, $port, $database, $username, $password)
+ {
+ $prefix = strtolower(str_random(3) . '_');
+
+ // Update .env file
+ static::updateEnv([
+ 'DB_HOST' => $host,
+ 'DB_PORT' => $port,
+ 'DB_DATABASE' => $database,
+ 'DB_USERNAME' => $username,
+ 'DB_PASSWORD' => $password,
+ 'DB_PREFIX' => $prefix,
+ ]);
+
+ $con = env('DB_CONNECTION', 'mysql');
+
+ // Change current connection
+ $db = Config::get('database.connections.' . $con);
+
+ $db['host'] = $host;
+ $db['database'] = $database;
+ $db['username'] = $username;
+ $db['password'] = $password;
+ $db['prefix'] = $prefix;
+
+ Config::set('database.connections.' . $con, $db);
+
+ DB::purge($con);
+ DB::reconnect($con);
+ }
+
+ public static function createCompany($name, $email, $locale)
+ {
+ // Create company
+ $company = Company::create([
+ 'domain' => '',
+ ]);
+
+ // Set settings
+ setting()->set([
+ 'general.company_name' => $name,
+ 'general.company_email' => $email,
+ 'general.default_currency' => 'USD',
+ 'general.default_locale' => $locale,
+ ]);
+ setting()->setExtraColumns(['company_id' => $company->id]);
+ setting()->save();
+ }
+
+ public static function createUser($email, $password, $locale)
+ {
+ // Create the user
+ $user = User::create([
+ 'name' => '',
+ 'email' => $email,
+ 'password' => $password,
+ 'locale' => $locale,
+ ]);
+
+ // Attach admin role
+ $user->roles()->attach('1');
+
+ // Attach company
+ $user->companies()->attach('1');
+ }
+
+ public static function finalTouches()
+ {
+ // Update .env file
+ static::updateEnv([
+ 'APP_LOCALE' => session('locale'),
+ 'APP_INSTALLED' => 'true',
+ 'APP_DEBUG' => 'false',
+ ]);
+
+ // Rename the robots.txt file
+ try {
+ File::move(base_path('robots.txt.dist'), base_path('robots.txt'));
+ } catch (\Exception $e) {
+ // nothing to do
+ }
+ }
+
+ public static function updateEnv($data)
+ {
+ if (empty($data) || !is_array($data) || !is_file(base_path('.env'))) {
+ return false;
+ }
+
+ $env = file_get_contents(base_path('.env'));
+
+ $env = explode("\n", $env);
+
+ foreach ($data as $data_key => $data_value) {
+ foreach ($env as $env_key => $env_value) {
+ $entry = explode('=', $env_value, 2);
+
+ // Check if new or old key
+ if ($entry[0] == $data_key) {
+ $env[$env_key] = $data_key . '=' . $data_value;
+ } else {
+ $env[$env_key] = $env_value;
+ }
+ }
+ }
+
+ $env = implode("\n", $env);
+
+ file_put_contents(base_path('.env'), $env);
+
+ return true;
+ }
+}
diff --git a/app/Utilities/Modules.php b/app/Utilities/Modules.php
new file mode 100755
index 0000000..f5495c2
--- /dev/null
+++ b/app/Utilities/Modules.php
@@ -0,0 +1,75 @@
+user()->customer;
+
+ if ($customer && $type != 'all') {
+ $payment_methods = Cache::get($cache_customer);
+ }
+
+ if (!empty($payment_methods)) {
+ return $payment_methods;
+ }
+
+ $gateways = [];
+ $methods = [];
+
+ // Fire the event to extend the menu
+ $results = event(new PaymentGatewayListing($gateways));
+
+ foreach ($results as $gateways) {
+ foreach ($gateways as $gateway) {
+ if (!isset($gateway['name']) || !isset($gateway['code'])) {
+ continue;
+ }
+
+ if (($customer && empty($gateway['customer'])) && $type != 'all') {
+ continue;
+ }
+
+ $methods[] = $gateway;
+ }
+ }
+
+ $sort_order = [];
+
+ if ($methods) {
+ foreach ($methods as $key => $value) {
+ $sort_order[$key] = !empty($value['order']) ? $value['order'] : 0;
+ }
+
+ array_multisort($sort_order, SORT_ASC, $methods);
+
+ foreach ($methods as $method) {
+ $payment_methods[$method['code']] = $method['name'];
+ }
+ }
+
+ if ($customer) {
+ Cache::put($cache_customer, $payment_methods, Date::now()->addHour(6));
+ } else {
+ Cache::put($cache_admin, $payment_methods, Date::now()->addHour(6));
+ }
+
+ return ($payment_methods) ? $payment_methods : [];
+ }
+}
diff --git a/app/Utilities/Overrider.php b/app/Utilities/Overrider.php
new file mode 100755
index 0000000..e9bc785
--- /dev/null
+++ b/app/Utilities/Overrider.php
@@ -0,0 +1,82 @@
+setExtraColumns(['company_id' => static::$company_id]);
+ setting()->load(true);
+
+ // Timezone
+ config(['app.timezone' => setting('general.timezone', 'UTC')]);
+
+ // Email
+ $email_protocol = setting('general.email_protocol', 'mail');
+ config(['mail.driver' => $email_protocol]);
+ config(['mail.from.name' => setting('general.company_name')]);
+ config(['mail.from.address' => setting('general.company_email')]);
+
+ if ($email_protocol == 'sendmail') {
+ config(['mail.sendmail' => setting('general.email_sendmail_path')]);
+ } elseif ($email_protocol == 'smtp') {
+ config(['mail.host' => setting('general.email_smtp_host')]);
+ config(['mail.port' => setting('general.email_smtp_port')]);
+ config(['mail.username' => setting('general.email_smtp_username')]);
+ config(['mail.password' => setting('general.email_smtp_password')]);
+ config(['mail.encryption' => setting('general.email_smtp_encryption')]);
+ }
+
+ // Session
+ config(['session.lifetime' => setting('general.session_lifetime', '30')]);
+
+ // Locale
+ if (session('locale') == '') {
+ //App::setLocale(setting('general.default_language'));
+ //Session::put('locale', setting('general.default_language'));
+ config(['app.locale' => setting('general.default_locale')]);
+ }
+ }
+
+ protected static function loadCurrencies()
+ {
+ $currencies = Currency::all();
+
+ foreach ($currencies as $currency) {
+ if (!isset($currency->precision)) {
+ continue;
+ }
+
+ config(['money.' . $currency->code . '.precision' => $currency->precision]);
+ config(['money.' . $currency->code . '.symbol' => $currency->symbol]);
+ config(['money.' . $currency->code . '.symbol_first' => $currency->symbol_first]);
+ config(['money.' . $currency->code . '.decimal_mark' => $currency->decimal_mark]);
+ config(['money.' . $currency->code . '.thousands_separator' => $currency->thousands_separator]);
+ }
+
+ // Set currencies with new settings
+ \Akaunting\Money\Currency::setCurrencies(config('money'));
+ }
+
+}
\ No newline at end of file
diff --git a/app/Utilities/Updater.php b/app/Utilities/Updater.php
new file mode 100755
index 0000000..6890f10
--- /dev/null
+++ b/app/Utilities/Updater.php
@@ -0,0 +1,162 @@
+open($file) !== true) || !$zip->extractTo($temp_path)) {
+ return false;
+ }
+
+ $zip->close();
+
+ // Delete zip file
+ File::delete($file);
+
+ if ($alias == 'core') {
+ // Move all files/folders from temp path
+ if (!File::copyDirectory($temp_path, base_path())) {
+ return false;
+ }
+ } else {
+ // Get module instance
+ $module = Module::findByAlias($alias);
+ $model = Model::where('alias', $alias)->first();
+
+ // Move all files/folders from temp path
+ if (!File::copyDirectory($temp_path, module_path($module->get('name')))) {
+ return false;
+ }
+
+ // Add history
+ ModelHistory::create([
+ 'company_id' => session('company_id'),
+ 'module_id' => $model->id,
+ 'category' => $module->get('category'),
+ 'version' => $version,
+ 'description' => trans('modules.history.updated', ['module' => $module->get('name')]),
+ ]);
+ }
+
+ // Delete temp directory
+ File::deleteDirectory($temp_path);
+
+ return true;
+ }
+
+ public static function download($alias, $version)
+ {
+ $file = null;
+
+ // Check core first
+ $info = Info::all();
+
+ if ($alias == 'core') {
+ $url = 'core/download/' . $version . '/' . $info['php'] . '/' . $info['mysql'];
+ } else {
+ $url = 'apps/' . $alias . '/download/' . $version . '/' . $info['akaunting'] . '/' . $info['token'];
+ }
+
+ $response = static::getRemote($url, ['timeout' => 50, 'track_redirects' => true]);
+
+ // Exception
+ if ($response instanceof RequestException) {
+ return false;
+ }
+
+ if ($response && ($response->getStatusCode() == 200)) {
+ $file = $response->getBody()->getContents();
+ }
+
+ return $file;
+ }
+
+ public static function all()
+ {
+ // Get data from cache
+ $data = Cache::get('updates');
+
+ if (!empty($data)) {
+ return $data;
+ }
+
+ // No data in cache, grab them from remote
+ $data = array();
+
+ $modules = Module::all();
+
+ $versions = Versions::latest($modules);
+
+ foreach ($versions as $alias => $version) {
+ // Modules come as array
+ if ($alias == 'core') {
+ if (version_compare(version('short'), $version) != 0) {
+ $data['core'] = $version;
+ }
+ } else {
+ $module = Module::findByAlias($alias);
+
+ // Up-to-date
+ if (version_compare($module->get('version'), $version) == 0) {
+ continue;
+ }
+
+ $data[$alias] = $version;
+ }
+ }
+
+ Cache::put('updates', $data, Date::now()->addHour(6));
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/app/Utilities/Versions.php b/app/Utilities/Versions.php
new file mode 100755
index 0000000..c55b3e1
--- /dev/null
+++ b/app/Utilities/Versions.php
@@ -0,0 +1,118 @@
+ false]);
+
+ $json = $http->get($url, ['timeout' => 30])->getBody()->getContents();
+
+ if (empty($json)) {
+ return $output;
+ }
+
+ $parsedown = new Parsedown();
+
+ $releases = json_decode($json);
+
+ foreach ($releases as $release) {
+ if (version_compare($release->tag_name, version('short'), '<=')) {
+ continue;
+ }
+
+ if ($release->prerelease == true) {
+ continue;
+ }
+
+ if (empty($release->body)) {
+ continue;
+ }
+
+ $output .= ''.$release->tag_name.'
';
+
+ $output .= $parsedown->text($release->body);
+
+ $output .= '
';
+ }
+
+ return $output;
+ }
+
+ public static function latest($modules = array())
+ {
+ // Get data from cache
+ $data = Cache::get('versions');
+
+ if (!empty($data)) {
+ return $data;
+ }
+
+ $info = Info::all();
+
+ // No data in cache, grab them from remote
+ $data = array();
+
+ // Check core first
+ $url = 'core/version/' . $info['akaunting'] . '/' . $info['php'] . '/' . $info['mysql'] . '/' . $info['companies'];
+
+ $data['core'] = static::getLatestVersion($url);
+
+ // Then modules
+ foreach ($modules as $module) {
+ $alias = $module->get('alias');
+ $version = $module->get('version');
+
+ $url = 'apps/' . $alias . '/version/' . $version . '/' . $info['akaunting'];
+
+ $data[$alias] = static::getLatestVersion($url);
+ }
+
+ Cache::put('versions', $data, Date::now()->addHour(6));
+
+ return $data;
+ }
+
+ public static function getLatestVersion($url)
+ {
+ $latest = '0.0.0';
+
+ $response = static::getRemote($url, ['timeout' => 10, 'referer' => true]);
+
+ // Exception
+ if ($response instanceof RequestException) {
+ return $latest;
+ }
+
+ // Bad response
+ if (!$response || ($response->getStatusCode() != 200)) {
+ return $latest;
+ }
+
+ $content = json_decode($response->getBody());
+
+ // Empty response
+ if (!is_object($content) || !is_object($content->data)) {
+ return $latest;
+ }
+
+ // Get the latest version
+ $latest = $content->data->latest;
+
+ return $latest;
+ }
+}
diff --git a/artisan b/artisan
new file mode 100755
index 0000000..df630d0
--- /dev/null
+++ b/artisan
@@ -0,0 +1,51 @@
+#!/usr/bin/env php
+make(Illuminate\Contracts\Console\Kernel::class);
+
+$status = $kernel->handle(
+ $input = new Symfony\Component\Console\Input\ArgvInput,
+ new Symfony\Component\Console\Output\ConsoleOutput
+);
+
+/*
+|--------------------------------------------------------------------------
+| Shutdown The Application
+|--------------------------------------------------------------------------
+|
+| Once Artisan has finished running. We will fire off the shutdown events
+| so that any final work may be done by the application before we shut
+| down the process. This is the last thing to happen to the request.
+|
+*/
+
+$kernel->terminate($input, $status);
+
+exit($status);
diff --git a/bootstrap/app.php b/bootstrap/app.php
new file mode 100755
index 0000000..f2801ad
--- /dev/null
+++ b/bootstrap/app.php
@@ -0,0 +1,55 @@
+singleton(
+ Illuminate\Contracts\Http\Kernel::class,
+ App\Http\Kernel::class
+);
+
+$app->singleton(
+ Illuminate\Contracts\Console\Kernel::class,
+ App\Console\Kernel::class
+);
+
+$app->singleton(
+ Illuminate\Contracts\Debug\ExceptionHandler::class,
+ App\Exceptions\Handler::class
+);
+
+/*
+|--------------------------------------------------------------------------
+| Return The Application
+|--------------------------------------------------------------------------
+|
+| This script returns the application instance. The instance is given to
+| the calling script so we can separate the building of the instances
+| from the actual running of the application and sending responses.
+|
+*/
+
+return $app;
diff --git a/bootstrap/autoload.php b/bootstrap/autoload.php
new file mode 100755
index 0000000..6299833
--- /dev/null
+++ b/bootstrap/autoload.php
@@ -0,0 +1,28 @@
+ env('API_STANDARDS_TREE', 'vnd'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | API Subtype
+ |--------------------------------------------------------------------------
+ |
+ | Your subtype will follow the standards tree you use when used in the
+ | "Accept" header to negotiate the content type and version.
+ |
+ | For example: Accept: application/x.SUBTYPE.v1+json
+ |
+ */
+
+ 'subtype' => env('API_SUBTYPE', 'api'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Default API Version
+ |--------------------------------------------------------------------------
+ |
+ | This is the default version when strict mode is disabled and your API
+ | is accessed via a web browser. It's also used as the default version
+ | when generating your APIs documentation.
+ |
+ */
+
+ 'version' => env('API_VERSION', 'v1'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Default API Prefix
+ |--------------------------------------------------------------------------
+ |
+ | A default prefix to use for your API routes so you don't have to
+ | specify it for each group.
+ |
+ */
+
+ 'prefix' => env('API_PREFIX', 'api'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Default API Domain
+ |--------------------------------------------------------------------------
+ |
+ | A default domain to use for your API routes so you don't have to
+ | specify it for each group.
+ |
+ */
+
+ 'domain' => env('API_DOMAIN', null),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Name
+ |--------------------------------------------------------------------------
+ |
+ | When documenting your API using the API Blueprint syntax you can
+ | configure a default name to avoid having to manually specify
+ | one when using the command.
+ |
+ */
+
+ 'name' => env('API_NAME', 'Akaunting'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Conditional Requests
+ |--------------------------------------------------------------------------
+ |
+ | Globally enable conditional requests so that an ETag header is added to
+ | any successful response. Subsequent requests will perform a check and
+ | will return a 304 Not Modified. This can also be enabled or disabled
+ | on certain groups or routes.
+ |
+ */
+
+ 'conditionalRequest' => env('API_CONDITIONAL_REQUEST', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Strict Mode
+ |--------------------------------------------------------------------------
+ |
+ | Enabling strict mode will require clients to send a valid Accept header
+ | with every request. This also voids the default API version, meaning
+ | your API will not be browsable via a web browser.
+ |
+ */
+
+ 'strict' => env('API_STRICT', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Debug Mode
+ |--------------------------------------------------------------------------
+ |
+ | Enabling debug mode will result in error responses caused by thrown
+ | exceptions to have a "debug" key that will be populated with
+ | more detailed information on the exception.
+ |
+ */
+
+ 'debug' => env('API_DEBUG', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Generic Error Format
+ |--------------------------------------------------------------------------
+ |
+ | When some HTTP exceptions are not caught and dealt with the API will
+ | generate a generic error response in the format provided. Any
+ | keys that aren't replaced with corresponding values will be
+ | removed from the final response.
+ |
+ */
+
+ 'errorFormat' => [
+ 'message' => ':message',
+ 'errors' => ':errors',
+ 'code' => ':code',
+ 'status_code' => ':status_code',
+ 'debug' => ':debug',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | API Middleware
+ |--------------------------------------------------------------------------
+ |
+ | Middleware that will be applied globally to all API requests.
+ |
+ */
+
+ 'middleware' => [
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Authentication Providers
+ |--------------------------------------------------------------------------
+ |
+ | The authentication providers that should be used when attempting to
+ | authenticate an incoming API request.
+ |
+ */
+
+ 'auth' => [
+ 'basic' => 'Dingo\Api\Auth\Provider\Basic',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Throttling / Rate Limiting
+ |--------------------------------------------------------------------------
+ |
+ | Consumers of your API can be limited to the amount of requests they can
+ | make. You can create your own throttles or simply change the default
+ | throttles.
+ |
+ */
+
+ 'throttling' => [
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Response Transformer
+ |--------------------------------------------------------------------------
+ |
+ | Responses can be transformed so that they are easier to format. By
+ | default a Fractal transformer will be used to transform any
+ | responses prior to formatting. You can easily replace
+ | this with your own transformer.
+ |
+ */
+
+ 'transformer' => env('API_TRANSFORMER', Dingo\Api\Transformer\Adapter\Fractal::class),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Response Formats
+ |--------------------------------------------------------------------------
+ |
+ | Responses can be returned in multiple formats by registering different
+ | response formatters. You can also customize an existing response
+ | formatter.
+ |
+ */
+
+ 'defaultFormat' => env('API_DEFAULT_FORMAT', 'json'),
+
+ 'formats' => [
+
+ 'json' => Dingo\Api\Http\Response\Format\Json::class,
+
+ ],
+
+];
diff --git a/config/app.php b/config/app.php
new file mode 100755
index 0000000..6bedfca
--- /dev/null
+++ b/config/app.php
@@ -0,0 +1,280 @@
+ env('APP_NAME', 'Akaunting'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Environment
+ |--------------------------------------------------------------------------
+ |
+ | This value determines the "environment" your application is currently
+ | running in. This may determine how you prefer to configure various
+ | services your application utilizes. Set this in your ".env" file.
+ |
+ */
+
+ 'env' => env('APP_ENV', 'local'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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' => env('APP_LOCALE', 'en-GB'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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-GB',
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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', 'JustAKeyForAkauntingInstallation'),
+
+ 'cipher' => env('APP_CIPHER', 'AES-256-CBC'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Logging Configuration
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure the log settings for your application. Out of
+ | the box, Laravel uses the Monolog PHP logging library. This gives
+ | you a variety of powerful log handlers / formatters to utilize.
+ |
+ | Available Settings: "single", "daily", "syslog", "errorlog"
+ |
+ */
+
+ 'log' => env('APP_LOG', 'single'),
+
+ 'log_level' => env('APP_LOG_LEVEL', 'debug'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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...
+ */
+ Laravel\Tinker\TinkerServiceProvider::class,
+
+ /*
+ * Application Service Providers...
+ */
+ App\Providers\AppServiceProvider::class,
+ App\Providers\AuthServiceProvider::class,
+ // App\Providers\BroadcastServiceProvider::class,
+ App\Providers\EventServiceProvider::class,
+ App\Providers\FormServiceProvider::class,
+ App\Providers\ObserverServiceProvider::class,
+ App\Providers\RouteServiceProvider::class,
+ App\Providers\ValidationServiceProvider::class,
+ App\Providers\ViewComposerServiceProvider::class,
+
+ /*
+ * Vendor Service Providers...
+ */
+ Akaunting\Language\Provider::class,
+ Akaunting\Money\Provider::class,
+ Akaunting\Setting\Provider::class,
+ Akaunting\Version\Provider::class,
+ Barryvdh\DomPDF\ServiceProvider::class,
+ Bkwld\Cloner\ServiceProvider::class,
+ Collective\Html\HtmlServiceProvider::class,
+ ConsoleTVs\Charts\ChartsServiceProvider::class,
+ Dingo\Api\Provider\LaravelServiceProvider::class,
+ EloquentFilter\ServiceProvider::class,
+ Fideloper\Proxy\TrustedProxyServiceProvider::class,
+ Intervention\Image\ImageServiceProvider::class,
+ Jenssegers\Date\DateServiceProvider::class,
+ Kyslik\ColumnSortable\ColumnSortableServiceProvider::class,
+ Laracasts\Flash\FlashServiceProvider::class,
+ Laratrust\LaratrustServiceProvider::class,
+ Maatwebsite\Excel\ExcelServiceProvider::class,
+ Nwidart\Menus\MenusServiceProvider::class,
+ Nwidart\Modules\LaravelModulesServiceProvider::class,
+ Sofa\Eloquence\ServiceProvider::class,
+ Plank\Mediable\MediableServiceProvider::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,
+ '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,
+ 'MediaUploader' => Plank\Mediable\MediaUploaderFacade::class,
+ 'Notification' => Illuminate\Support\Facades\Notification::class,
+ 'Password' => Illuminate\Support\Facades\Password::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,
+ 'URL' => Illuminate\Support\Facades\URL::class,
+ 'Validator' => Illuminate\Support\Facades\Validator::class,
+ 'View' => Illuminate\Support\Facades\View::class,
+
+ /*
+ * Vendor Aliases...
+ */
+ //'Api' => Dingo\Api\Facade\API,
+ 'Charts' => ConsoleTVs\Charts\Facades\Charts::class,
+ 'Debugbar' => Barryvdh\Debugbar\Facade::class,
+ 'Date' => Jenssegers\Date\Date::class,
+ 'Excel' => Maatwebsite\Excel\Facades\Excel::class,
+ 'Form' => Collective\Html\FormFacade::class,
+ 'Html' => Collective\Html\HtmlFacade::class,
+ 'Image' => Intervention\Image\Facades\Image::class,
+ 'Language' => Akaunting\Language\Facade::class,
+ 'Laratrust' => Laratrust\LaratrustFacade::class,
+ 'Menu' => Nwidart\Menus\Facades\Menu::class,
+ 'Module' => Nwidart\Modules\Facades\Module::class,
+ 'PDF' => Barryvdh\DomPDF\Facade::class,
+ 'Setting' => Akaunting\Setting\Facade::class,
+ 'Version' => Akaunting\Version\Facade::class,
+
+ ],
+
+];
diff --git a/config/auth.php b/config/auth.php
new file mode 100755
index 0000000..4ca9439
--- /dev/null
+++ b/config/auth.php
@@ -0,0 +1,102 @@
+ [
+ 'guard' => 'web',
+ 'passwords' => 'users',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Authentication Guards
+ |--------------------------------------------------------------------------
+ |
+ | Next, you may define every authentication guard for your application.
+ | Of course, a great default configuration has been defined for you
+ | here which uses session storage and the Eloquent user provider.
+ |
+ | All authentication drivers have a user provider. This defines how the
+ | users are actually retrieved out of your database or other storage
+ | mechanisms used by this application to persist your user's data.
+ |
+ | Supported: "session", "token"
+ |
+ */
+
+ 'guards' => [
+ 'web' => [
+ 'driver' => 'session',
+ 'provider' => 'users',
+ ],
+
+ 'api' => [
+ 'driver' => 'token',
+ 'provider' => 'users',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | User Providers
+ |--------------------------------------------------------------------------
+ |
+ | All authentication drivers have a user provider. This defines how the
+ | users are actually retrieved out of your database or other storage
+ | mechanisms used by this application to persist your user's data.
+ |
+ | If you have multiple user tables or models you may configure multiple
+ | sources which represent each model / table. These sources may then
+ | be assigned to any extra authentication guards you have defined.
+ |
+ | Supported: "database", "eloquent"
+ |
+ */
+
+ 'providers' => [
+ 'users' => [
+ 'driver' => 'eloquent',
+ 'model' => App\Models\Auth\User::class,
+ ],
+
+ // 'users' => [
+ // 'driver' => 'database',
+ // 'table' => 'users',
+ // ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Resetting Passwords
+ |--------------------------------------------------------------------------
+ |
+ | You may specify multiple password reset configurations if you have more
+ | than one user table or model in the application and you want to have
+ | separate password reset settings based on the specific user types.
+ |
+ | The expire time is the number of minutes that the reset token should be
+ | considered valid. This security feature keeps tokens short-lived so
+ | they have less time to be guessed. You may change this as needed.
+ |
+ */
+
+ 'passwords' => [
+ 'users' => [
+ 'provider' => 'users',
+ 'table' => 'password_resets',
+ 'expire' => 60,
+ ],
+ ],
+
+];
diff --git a/config/broadcasting.php b/config/broadcasting.php
new file mode 100755
index 0000000..5eecd2b
--- /dev/null
+++ b/config/broadcasting.php
@@ -0,0 +1,58 @@
+ env('BROADCAST_DRIVER', 'null'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Broadcast Connections
+ |--------------------------------------------------------------------------
+ |
+ | Here you may define all of the broadcast connections that will be used
+ | to broadcast events to other systems or over websockets. Samples of
+ | each available type of connection are provided inside this array.
+ |
+ */
+
+ 'connections' => [
+
+ 'pusher' => [
+ 'driver' => 'pusher',
+ 'key' => env('PUSHER_APP_KEY'),
+ 'secret' => env('PUSHER_APP_SECRET'),
+ 'app_id' => env('PUSHER_APP_ID'),
+ 'options' => [
+ //
+ ],
+ ],
+
+ 'redis' => [
+ 'driver' => 'redis',
+ 'connection' => 'default',
+ ],
+
+ 'log' => [
+ 'driver' => 'log',
+ ],
+
+ 'null' => [
+ 'driver' => 'null',
+ ],
+
+ ],
+
+];
diff --git a/config/cache.php b/config/cache.php
new file mode 100755
index 0000000..e87f032
--- /dev/null
+++ b/config/cache.php
@@ -0,0 +1,91 @@
+ env('CACHE_DRIVER', 'file'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache Stores
+ |--------------------------------------------------------------------------
+ |
+ | Here you may define all of the cache "stores" for your application as
+ | well as their drivers. You may even define multiple stores for the
+ | same cache driver to group types of items stored in your caches.
+ |
+ */
+
+ 'stores' => [
+
+ 'apc' => [
+ 'driver' => 'apc',
+ ],
+
+ 'array' => [
+ 'driver' => 'array',
+ ],
+
+ 'database' => [
+ 'driver' => 'database',
+ 'table' => 'cache',
+ 'connection' => null,
+ ],
+
+ 'file' => [
+ 'driver' => 'file',
+ 'path' => storage_path('framework/cache/data'),
+ ],
+
+ 'memcached' => [
+ 'driver' => 'memcached',
+ 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
+ 'sasl' => [
+ env('MEMCACHED_USERNAME'),
+ env('MEMCACHED_PASSWORD'),
+ ],
+ 'options' => [
+ // Memcached::OPT_CONNECT_TIMEOUT => 2000,
+ ],
+ 'servers' => [
+ [
+ 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
+ 'port' => env('MEMCACHED_PORT', 11211),
+ 'weight' => 100,
+ ],
+ ],
+ ],
+
+ 'redis' => [
+ 'driver' => 'redis',
+ 'connection' => 'default',
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache Key Prefix
+ |--------------------------------------------------------------------------
+ |
+ | When utilizing a RAM based store such as APC or Memcached, there might
+ | be other applications utilizing the same cache. So, we'll specify a
+ | value to get prefixed to all our keys so we can avoid collisions.
+ |
+ */
+
+ 'prefix' => 'laravel',
+
+];
diff --git a/config/charts.php b/config/charts.php
new file mode 100755
index 0000000..1c33260
--- /dev/null
+++ b/config/charts.php
@@ -0,0 +1,173 @@
+ [
+ 'type' => 'line', // The default chart type.
+ 'library' => 'material', // The default chart library.
+ 'element_label' => '', // The default chart element label.
+ 'empty_dataset_label' => 'No Data Set',
+ 'empty_dataset_value' => 0,
+ 'title' => '', // Default chart title.
+ 'height' => 400, // 0 Means it will take 100% of the division height.
+ 'width' => 0, // 0 Means it will take 100% of the division width.
+ 'responsive' => false, // Not recommended since all libraries have diferent sizes.
+ 'background_color' => 'inherit', // The chart division background color.
+ 'colors' => [], // Default chart colors if using no template is set.
+ 'one_color' => false, // Only use the first color in all values.
+ 'template' => 'material', // The default chart color template.
+ 'legend' => true, // Whether to enable the chart legend (where applicable).
+ 'x_axis_title' => false, // The title of the x-axis
+ 'y_axis_title' => null, // The title of the y-axis (When set to null will use element_label value).
+ 'loader' => [
+ 'active' => false, // Determines the if loader is active by default.
+ 'duration' => 500, // In milliseconds.
+ 'color' => '#6da252', // Determines the default loader color.
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | All the color templates available for the charts.
+ |--------------------------------------------------------------------------
+ */
+ 'templates' => [
+ 'material' => [
+ '#2196F3', '#F44336', '#FFC107',
+ ],
+ 'red-material' => [
+ '#B71C1C', '#F44336', '#E57373',
+ ],
+ 'indigo-material' => [
+ '#1A237E', '#3F51B5', '#7986CB',
+ ],
+ 'blue-material' => [
+ '#0D47A1', '#2196F3', '#64B5F6',
+ ],
+ 'teal-material' => [
+ '#004D40', '#009688', '#4DB6AC',
+ ],
+ 'green-material' => [
+ '#1B5E20', '#4CAF50', '#81C784',
+ ],
+ 'yellow-material' => [
+ '#F57F17', '#FFEB3B', '#FFF176',
+ ],
+ 'orange-material' => [
+ '#E65100', '#FF9800', '#FFB74D',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Assets required by the libraries.
+ |--------------------------------------------------------------------------
+ */
+
+ 'assets' => [
+ 'global' => [
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js',
+ ],
+ ],
+
+ 'canvas-gauges' => [
+ 'scripts' => [
+ //'https://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.2/all/gauge.min.js',
+ ],
+ ],
+
+ 'chartist' => [
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/chartist/0.10.1/chartist.min.js',
+ ],
+ 'styles' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/chartist/0.10.1/chartist.min.css',
+ ],
+ ],
+
+ 'chartjs' => [
+ 'scripts' => [
+ env('APP_URL') . '/public/js/chartjs/Chart.min.js',
+ ],
+ ],
+
+ 'fusioncharts' => [
+ 'scripts' => [
+ //'https://static.fusioncharts.com/code/latest/fusioncharts.js',
+ //'https://static.fusioncharts.com/code/latest/themes/fusioncharts.theme.fint.js',
+ ],
+ ],
+
+ 'google' => [
+ 'scripts' => [
+ //'https://www.google.com/jsapi',
+ //'https://www.gstatic.com/charts/loader.js',
+ //"google.charts.load('current', {'packages':['corechart', 'gauge', 'geochart', 'bar', 'line']})",
+ ],
+ ],
+
+ 'highcharts' => [
+ 'styles' => [
+ // The following CSS is not added due to color compatibility errors.
+ // 'https://cdnjs.cloudflare.com/ajax/libs/highcharts/5.0.7/css/highcharts.css',
+ ],
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/highcharts/5.0.7/highcharts.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/highcharts/5.0.7/js/modules/offline-exporting.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/highmaps/5.0.7/js/modules/map.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/highmaps/5.0.7/js/modules/data.js',
+ //'https://code.highcharts.com/mapdata/custom/world.js',
+ ],
+ ],
+
+ 'justgage' => [
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.6/raphael.min.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/justgage/1.2.2/justgage.min.js',
+ ],
+ ],
+
+ 'morris' => [
+ 'styles' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css',
+ ],
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.6/raphael.min.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js',
+ ],
+ ],
+
+ 'plottablejs' => [
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/plottable.js/2.8.0/plottable.min.js',
+ ],
+ 'styles' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/plottable.js/2.2.0/plottable.css',
+ ],
+ ],
+
+ 'progressbarjs' => [
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/progressbar.js/1.0.1/progressbar.min.js',
+ ],
+ ],
+
+ 'c3' => [
+ 'scripts' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js',
+ //'https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.min.js',
+ ],
+ 'styles' => [
+ //'https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.min.css',
+ ],
+ ],
+ ],
+];
diff --git a/config/columnsortable.php b/config/columnsortable.php
new file mode 100755
index 0000000..cb55aa1
--- /dev/null
+++ b/config/columnsortable.php
@@ -0,0 +1,89 @@
+ [
+ 'alpha' => [
+ 'rows' => ['name', 'customer_name', 'vendor_name', 'display_name', 'company_name', 'domain', 'email', 'description', 'code', 'type', 'status', 'vendor', 'account', 'bill_status_code', 'invoice_status_code'],
+ 'class' => 'fa fa-sort-alpha',
+ ],
+ 'amount' => [
+ 'rows' => ['amount', 'price', 'sale_price', 'purchase_price', 'total_price', 'current_balance', 'total_price', 'opening_balance'],
+ 'class' => 'fa fa-sort-amount'
+ ],
+ 'numeric' => [
+ 'rows' => ['created_at', 'updated_at', 'paid_at', 'invoiced_at', 'billed_at', 'due_at', 'id', 'quantity', 'rate', 'number', 'invoice_number', 'bill_number'],
+ 'class' => 'fa fa-sort-numeric'
+ ],
+ ],
+
+ /*
+ defines icon set to use when sorted data is none above (alpha nor amount nor numeric)
+ */
+ 'default_icon_set' => 'fa fa-long-arrow-down sort-icon',
+
+ /*
+ icon that shows when generating sortable link while column is not sorted
+ */
+ 'sortable_icon' => 'fa fa-long-arrow-down sort-icon',
+
+ /*
+ generated icon is clickable non-clickable (default)
+ */
+ 'clickable_icon' => false,
+
+ /*
+ icon and text separator (any string)
+ in case of 'clickable_icon' => true; separator creates possibility to style icon and anchor-text properly
+ */
+ 'icon_text_separator' => ' ',
+
+ /*
+ suffix class that is appended when ascending order is applied
+ */
+ 'asc_suffix' => '-asc',
+
+ /*
+ suffix class that is appended when descending order is applied
+ */
+ 'desc_suffix' => '-desc',
+
+ /*
+ default anchor class, if value is null none is added
+ */
+ 'anchor_class' => null,
+
+ /*
+ relation - column separator ex: detail.phone_number means relation "detail" and column "phone_number"
+ */
+ 'uri_relation_column_separator' => '.',
+
+ /*
+ formatting function applied to name of column, use null to turn formatting off
+ */
+ 'formatting_function' => 'ucfirst',
+
+ /*
+ inject title parameter in query strings, use null to turn injection off
+ example: 'inject_title' => 't' will result in ..user/?t="formatted title of sorted column"
+ */
+ 'inject_title_as' => null,
+
+ /*
+ allow request modification, when default sorting is set but is not in URI (first load)
+ */
+ 'allow_request_modification' => true,
+
+ /*
+ default order for: $user->sortable('id') usage
+ */
+ 'default_direction' => 'asc',
+
+ /*
+ default order for non-sorted columns
+ */
+ 'default_direction_unsorted' => 'asc'
+];
diff --git a/config/database.php b/config/database.php
new file mode 100755
index 0000000..de9839e
--- /dev/null
+++ b/config/database.php
@@ -0,0 +1,118 @@
+ env('DB_CONNECTION', 'mysql'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Database Connections
+ |--------------------------------------------------------------------------
+ |
+ | Here are each of the database connections setup for your application.
+ | Of course, examples of configuring each database platform that is
+ | supported by Laravel is shown below to make development simple.
+ |
+ |
+ | All database work in Laravel is done through the PHP PDO facilities
+ | so make sure you have the driver for your particular database of
+ | choice installed on your machine before you begin development.
+ |
+ */
+
+ 'connections' => [
+
+ 'sqlite' => [
+ 'driver' => 'sqlite',
+ 'database' => env('DB_DATABASE', database_path('database.sqlite')),
+ 'prefix' => env('DB_PREFIX', 'ak_'),
+ ],
+
+ 'mysql' => [
+ 'driver' => 'mysql',
+ 'host' => env('DB_HOST', '127.0.0.1'),
+ 'port' => env('DB_PORT', '3306'),
+ 'database' => env('DB_DATABASE', 'forge'),
+ 'username' => env('DB_USERNAME', 'forge'),
+ 'password' => env('DB_PASSWORD', ''),
+ 'unix_socket' => env('DB_SOCKET', ''),
+ 'charset' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'prefix' => env('DB_PREFIX', 'ak_'),
+ 'strict' => true,
+ 'engine' => null,
+ 'modes' => [
+ //'ONLY_FULL_GROUP_BY', // conflicts with eloquence
+ 'STRICT_TRANS_TABLES',
+ 'NO_ZERO_IN_DATE',
+ 'NO_ZERO_DATE',
+ 'ERROR_FOR_DIVISION_BY_ZERO',
+ 'NO_AUTO_CREATE_USER',
+ 'NO_ENGINE_SUBSTITUTION',
+ ],
+ ],
+
+ 'pgsql' => [
+ 'driver' => 'pgsql',
+ 'host' => env('DB_HOST', '127.0.0.1'),
+ 'port' => env('DB_PORT', '5432'),
+ 'database' => env('DB_DATABASE', 'forge'),
+ 'username' => env('DB_USERNAME', 'forge'),
+ 'password' => env('DB_PASSWORD', ''),
+ 'charset' => 'utf8',
+ 'prefix' => env('DB_PREFIX', 'ak_'),
+ 'schema' => 'public',
+ 'sslmode' => 'prefer',
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Migration Repository Table
+ |--------------------------------------------------------------------------
+ |
+ | This table keeps track of all the migrations that have already run for
+ | your application. Using this information, we can determine which of
+ | the migrations on disk haven't actually been run in the database.
+ |
+ */
+
+ 'migrations' => 'migrations',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Redis Databases
+ |--------------------------------------------------------------------------
+ |
+ | Redis is an open source, fast, and advanced key-value store that also
+ | provides a richer set of commands than a typical key-value systems
+ | such as APC or Memcached. Laravel makes it easy to dig right in.
+ |
+ */
+
+ 'redis' => [
+
+ 'client' => 'predis',
+
+ 'default' => [
+ 'host' => env('REDIS_HOST', '127.0.0.1'),
+ 'password' => env('REDIS_PASSWORD', null),
+ 'port' => env('REDIS_PORT', 6379),
+ 'database' => 0,
+ ],
+
+ ],
+
+];
diff --git a/config/debugbar.php b/config/debugbar.php
new file mode 100755
index 0000000..b343d32
--- /dev/null
+++ b/config/debugbar.php
@@ -0,0 +1,170 @@
+ true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Storage settings
+ |--------------------------------------------------------------------------
+ |
+ | DebugBar stores data for session/ajax requests.
+ | You can disable this, so the debugbar stores data in headers/session,
+ | but this can cause problems with large data collectors.
+ | By default, file storage (in the storage folder) is used. Redis and PDO
+ | can also be used. For PDO, run the package migrations first.
+ |
+ */
+ 'storage' => [
+ 'enabled' => true,
+ 'driver' => 'file', // redis, file, pdo, custom
+ 'path' => storage_path('debugbar'), // For file driver
+ 'connection' => null, // Leave null for default connection (Redis/PDO)
+ 'provider' => '' // Instance of StorageInterface for custom driver
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Vendors
+ |--------------------------------------------------------------------------
+ |
+ | Vendor files are included by default, but can be set to false.
+ | This can also be set to 'js' or 'css', to only include javascript or css vendor files.
+ | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
+ | and for js: jquery and and highlight.js
+ | So if you want syntax highlighting, set it to true.
+ | jQuery is set to not conflict with existing jQuery scripts.
+ |
+ */
+
+ 'include_vendors' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Capture Ajax Requests
+ |--------------------------------------------------------------------------
+ |
+ | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
+ | you can use this option to disable sending the data through the headers.
+ |
+ */
+
+ 'capture_ajax' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Clockwork integration
+ |--------------------------------------------------------------------------
+ |
+ | The Debugbar can emulate the Clockwork headers, so you can use the Chrome
+ | Extension, without the server-side code. It uses Debugbar collectors instead.
+ |
+ */
+ 'clockwork' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | DataCollectors
+ |--------------------------------------------------------------------------
+ |
+ | Enable/disable DataCollectors
+ |
+ */
+
+ 'collectors' => [
+ 'phpinfo' => true, // Php version
+ 'messages' => true, // Messages
+ 'time' => true, // Time Datalogger
+ 'memory' => true, // Memory usage
+ 'exceptions' => true, // Exception displayer
+ 'log' => true, // Logs from Monolog (merged in messages if enabled)
+ 'db' => true, // Show database (PDO) queries and bindings
+ 'views' => true, // Views with their data
+ 'route' => true, // Current route information
+ 'laravel' => false, // Laravel version and environment
+ 'events' => false, // All events fired
+ 'default_request' => false, // Regular or special Symfony request logger
+ 'symfony_request' => true, // Only one can be enabled..
+ 'mail' => true, // Catch mail messages
+ 'logs' => false, // Add the latest log messages
+ 'files' => false, // Show the included files
+ 'config' => false, // Display config settings
+ 'auth' => false, // Display Laravel authentication status
+ 'gate' => false, // Display Laravel Gate checks
+ 'session' => true, // Display session data
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Extra options
+ |--------------------------------------------------------------------------
+ |
+ | Configure some DataCollectors
+ |
+ */
+
+ 'options' => [
+ 'auth' => [
+ 'show_name' => false, // Also show the users name/email in the debugbar
+ ],
+ 'db' => [
+ 'with_params' => true, // Render SQL with the parameters substituted
+ 'timeline' => false, // Add the queries to the timeline
+ 'backtrace' => false, // EXPERIMENTAL: Use a backtrace to find the origin of the query in your files.
+ 'explain' => [ // EXPERIMENTAL: Show EXPLAIN output on queries
+ 'enabled' => false,
+ 'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
+ ],
+ 'hints' => true, // Show hints for common mistakes
+ ],
+ 'mail' => [
+ 'full_log' => false
+ ],
+ 'views' => [
+ 'data' => false, //Note: Can slow down the application, because the data can be quite large..
+ ],
+ 'route' => [
+ 'label' => true // show complete route on bar
+ ],
+ 'logs' => [
+ 'file' => null
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Inject Debugbar in Response
+ |--------------------------------------------------------------------------
+ |
+ | Usually, the debugbar is added just before