diff --git a/.env.example b/.env.example index e8a53eaa41c5..1dc6d99bd419 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,9 @@ APP_DEBUG=false APP_URL=http://ninja.dev APP_CIPHER=rijndael-128 APP_KEY=SomeRandomString -APP_TIMEZONE DB_TYPE=mysql +DB_STRICT=false DB_HOST=localhost DB_DATABASE=ninja DB_USERNAME @@ -19,3 +19,11 @@ MAIL_USERNAME MAIL_FROM_ADDRESS MAIL_FROM_NAME MAIL_PASSWORD + +PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address' +LOG=single +REQUIRE_HTTPS=false + +GOOGLE_CLIENT_ID +GOOGLE_CLIENT_SECRET +GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe7a9dd0cbcc..5a86589d7a21 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ /vendor /node_modules /.DS_Store -/Thumbs.db +Thumbs.db /.env /.env.development.php /.env.php @@ -29,4 +29,10 @@ /_ide_helper.php /.idea /.project -tests/_output/ \ No newline at end of file +tests/_output/ +tests/_bootstrap.php + +# composer stuff +/c3.php + +_ide_helper.php \ No newline at end of file diff --git a/.htaccess b/.htaccess index 500686664f68..27a6945d38f8 100644 --- a/.htaccess +++ b/.htaccess @@ -2,4 +2,7 @@ RewriteEngine On RewriteRule "^.env" - [F,L] RewriteRule "^storage" - [F,L] + + # https://coderwall.com/p/erbaig/laravel-s-htaccess-to-remove-public-from-url + # RewriteRule ^(.*)$ public/$1 [L] diff --git a/Gruntfile.js b/Gruntfile.js index 4de932ca45a6..26660667929c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,6 +2,39 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), + dump_dir: (function() { + var out = {}; + + grunt.file.expand({ filter: 'isDirectory'}, 'public/fonts/invoice-fonts/*').forEach(function(path) { + var fontName = /[^/]*$/.exec(path)[0], + files = {}, + license=''; + + // Add license text + grunt.file.expand({ filter: 'isFile'}, path+'/*.txt').forEach(function(path) { + var licenseText = grunt.file.read(path); + + // Fix anything that could escape from the comment + licenseText = licenseText.replace(/\*\//g,'*\\/'); + + license += "/*\n"+licenseText+"\n*/"; + }); + + // Create files list + files['public/js/vfs_fonts/'+fontName+'.js'] = [path+'/*.ttf']; + + out[fontName] = { + options: { + pre: license+'window.ninjaFontVfs=window.ninjaFontVfs||{};window.ninjaFontVfs.'+fontName+'=', + rootPath: path+'/' + }, + files: files + }; + }); + + // Return the computed object + return out; + }()), concat: { options: { process: function(src, filepath) { @@ -67,6 +100,7 @@ module.exports = function(grunt) { 'public/vendor/spectrum/spectrum.js', 'public/vendor/jspdf/dist/jspdf.min.js', 'public/vendor/moment/min/moment.min.js', + 'public/vendor/moment-timezone/builds/moment-timezone-with-data.min.js', //'public/vendor/moment-duration-format/lib/moment-duration-format.js', //'public/vendor/handsontable/dist/jquery.handsontable.full.min.js', //'public/vendor/pdfmake/build/pdfmake.min.js', @@ -119,25 +153,33 @@ module.exports = function(grunt) { src: [ 'public/vendor/bootstrap/dist/css/bootstrap.min.css', 'public/vendor/font-awesome/css/font-awesome.min.css', - /* - 'public/css/bootstrap.splash.css', - 'public/css/splash.css', - */ 'public/css/bootstrap-combobox.css', 'public/vendor/datatables/media/css/jquery.dataTables.css', 'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css', + 'public/css/public.style.css', ], dest: 'public/css/built.public.css', nonull: true, options: { process: false } + }, + js_pdf: { + src: [ + 'public/js/pdf_viewer.js', + 'public/js/compatibility.js', + 'public/js/pdfmake.min.js', + 'public/js/vfs_fonts.js', + ], + dest: 'public/js/pdf.built.js', + nonull: true } } }); grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-dump-dir'); - grunt.registerTask('default', ['concat']); + grunt.registerTask('default', ['dump_dir', 'concat']); }; diff --git a/LICENSE b/LICENSE index eaa9f1e3672c..83e6795919db 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Attribution Assurance License -Copyright (c) 2014 by Hillel Coren +Copyright (c) 2015 by Hillel Coren http://www.hillelcoren.com All Rights Reserved diff --git a/app/Commands/Command.php b/app/Commands/Command.php index 018bc2192435..5bc48501167e 100644 --- a/app/Commands/Command.php +++ b/app/Commands/Command.php @@ -1,7 +1,6 @@ -info(date('Y-m-d') . ' Running CheckData...'); - $today = new DateTime(); if (!$this->option('client_id')) { - // update client paid_to_date value - $clients = DB::table('clients') - ->join('payments', 'payments.client_id', '=', 'clients.id') - ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') - ->where('payments.is_deleted', '=', 0) - ->where('invoices.is_deleted', '=', 0) - ->groupBy('clients.id') - ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') - ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); - $this->info(count($clients) . ' clients with incorrect paid to date'); - - if ($this->option('fix') == 'true') { - foreach ($clients as $client) { - DB::table('clients') - ->where('id', $client->id) - ->update(['paid_to_date' => $client->amount]); + $this->checkPaidToDate(); + } + + $this->checkBalances(); + + $this->checkAccountData(); + + $this->info('Done'); + } + + private function checkAccountData() + { + $tables = [ + 'activities' => [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_CONTACT, + ENTITY_PAYMENT, + ENTITY_INVITATION, + ENTITY_USER + ], + 'invoices' => [ + ENTITY_CLIENT, + ENTITY_USER + ], + 'payments' => [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_USER, + ENTITY_INVITATION, + ENTITY_CONTACT + ], + 'tasks' => [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_USER + ], + 'credits' => [ + ENTITY_CLIENT, + ENTITY_USER + ], + ]; + + foreach ($tables as $table => $entityTypes) { + foreach ($entityTypes as $entityType) { + $records = DB::table($table) + ->join("{$entityType}s", "{$entityType}s.id", '=', "{$table}.{$entityType}_id"); + + if ($entityType != ENTITY_CLIENT) { + $records = $records->join('clients', 'clients.id', '=', "{$table}.client_id"); + } + + $records = $records->where("{$table}.account_id", '!=', DB::raw("{$entityType}s.account_id")) + ->get(["{$table}.id", "clients.account_id", "clients.user_id"]); + + if (count($records)) { + $this->info(count($records) . " {$table} records with incorrect {$entityType} account id"); + + if ($this->option('fix') == 'true') { + foreach ($records as $record) { + DB::table($table) + ->where('id', $record->id) + ->update([ + 'account_id' => $record->account_id, + 'user_id' => $record->user_id, + ]); + } + } } } } + } + private function checkPaidToDate() + { + // update client paid_to_date value + $clients = DB::table('clients') + ->join('payments', 'payments.client_id', '=', 'clients.id') + ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->where('payments.is_deleted', '=', 0) + ->where('invoices.is_deleted', '=', 0) + ->groupBy('clients.id') + ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') + ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); + $this->info(count($clients) . ' clients with incorrect paid to date'); + + if ($this->option('fix') == 'true') { + foreach ($clients as $client) { + DB::table('clients') + ->where('id', $client->id) + ->update(['paid_to_date' => $client->amount]); + } + } + } + + private function checkBalances() + { // find all clients where the balance doesn't equal the sum of the outstanding invoices $clients = DB::table('clients') ->join('invoices', 'invoices.client_id', '=', 'clients.id') @@ -98,7 +174,7 @@ class CheckData extends Command { $activities = DB::table('activities') ->where('client_id', '=', $client->id) ->orderBy('activities.id') - ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.message', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); + ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); //$this->info(var_dump($activities)); foreach ($activities as $activity) { @@ -111,7 +187,7 @@ class CheckData extends Command { ->first(['invoices.amount', 'invoices.is_recurring', 'invoices.is_quote', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']); // Check if this invoice was once set as recurring invoice - if (!$invoice->is_recurring && DB::table('invoices') + if ($invoice && !$invoice->is_recurring && DB::table('invoices') ->where('recurring_invoice_id', '=', $activity->invoice_id) ->first(['invoices.id'])) { $invoice->is_recurring = 1; @@ -197,7 +273,7 @@ class CheckData extends Command { $activityFix = 0; } } else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) { - // **Fix for delting payment after deleting invoice** + // **Fix for deleting payment after deleting invoice** if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) { $this->info("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); $foundProblem = true; @@ -235,7 +311,6 @@ class CheckData extends Command { 'updated_at' => new Carbon, 'account_id' => $client->account_id, 'client_id' => $client->id, - 'message' => 'Recovered update to invoice [details]', 'adjustment' => $client->actual_balance - $activity->balance, 'balance' => $client->actual_balance, ]); @@ -250,8 +325,6 @@ class CheckData extends Command { ->update($data); } } - - $this->info('Done'); } protected function getArguments() diff --git a/app/Console/Commands/CreateRandomData.php b/app/Console/Commands/CreateRandomData.php deleted file mode 100644 index 9fe826cc9b5b..000000000000 --- a/app/Console/Commands/CreateRandomData.php +++ /dev/null @@ -1,88 +0,0 @@ -info(date('Y-m-d') . ' Running CreateRandomData...'); - - $user = User::first(); - - if (!$user) { - $this->error("Error: please create user account by logging in"); - return; - } - - $productNames = ['Arkansas', 'New York', 'Arizona', 'California', 'Colorado', 'Alabama', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'Alaska', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; - $clientNames = ['IBM', 'Nestle', 'Mitsubishi UFJ Financial', 'Vodafone', 'Eni', 'Procter & Gamble', 'Johnson & Johnson', 'American International Group', 'Banco Santander', 'BHP Billiton', 'Pfizer', 'Itaú Unibanco Holding', 'Ford Motor', 'BMW Group', 'Commonwealth Bank', 'EDF', 'Statoil', 'Google', 'Siemens', 'Novartis', 'Royal Bank of Canada', 'Sumitomo Mitsui Financial', 'Comcast', 'Sberbank', 'Goldman Sachs Group', 'Westpac Banking Group', 'Nippon Telegraph & Tel', 'Ping An Insurance Group', 'Banco Bradesco', 'Anheuser-Busch InBev', 'Bank of Communications', 'China Life Insurance', 'General Motors', 'Telefónica', 'MetLife', 'Honda Motor', 'Enel', 'BASF', 'Softbank', 'National Australia Bank', 'ANZ', 'ConocoPhillips', 'TD Bank Group', 'Intel', 'UBS', 'Hewlett-Packard', 'Coca-Cola', 'Cisco Systems', 'UnitedHealth Group', 'Boeing', 'Zurich Insurance Group', 'Hyundai Motor', 'Sanofi', 'Credit Agricole', 'United Technologies', 'Roche Holding', 'Munich Re', 'PepsiCo', 'Oracle', 'Bank of Nova Scotia']; - - foreach ($productNames as $i => $value) { - $product = Product::createNew($user); - $product->id = $i+1; - $product->product_key = $value; - $product->save(); - } - - foreach ($clientNames as $i => $value) { - $client = Client::createNew($user); - $client->name = $value; - $client->save(); - - $contact = Contact::createNew($user); - $contact->email = "client@aol.com"; - $contact->is_primary = 1; - $client->contacts()->save($contact); - - $numInvoices = rand(1, 25); - if ($numInvoices == 4 || $numInvoices == 10 || $numInvoices == 25) { - // leave these - } else if ($numInvoices % 3 == 0) { - $numInvoices = 1; - } else if ($numInvoices > 10) { - $numInvoices = $numInvoices % 2; - } - - $paidUp = rand(0, 1) == 1; - - for ($j=1; $j<=$numInvoices; $j++) { - - $price = rand(10, 1000); - if ($price < 900) { - $price = rand(10, 150); - } - - $invoice = Invoice::createNew($user); - $invoice->invoice_number = $user->account->getNextInvoiceNumber(); - $invoice->amount = $invoice->balance = $price; - $invoice->created_at = date('Y-m-d', strtotime(date("Y-m-d") . ' - ' . rand(1, 100) . ' days')); - $client->invoices()->save($invoice); - - $productId = rand(0, 40); - if ($productId > 20) { - $productId = ($productId % 2) + rand(0, 2); - } - - $invoiceItem = InvoiceItem::createNew($user); - $invoiceItem->product_id = $productId+1; - $invoiceItem->product_key = $productNames[$invoiceItem->product_id]; - $invoiceItem->cost = $invoice->amount; - $invoiceItem->qty = 1; - $invoice->invoice_items()->save($invoiceItem); - - if ($paidUp || rand(0,2) > 1) { - $payment = Payment::createNew($user); - $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->amount; - $client->payments()->save($payment); - } - } - } - } -} \ No newline at end of file diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 6acd87aa0c6e..abf493d1ca54 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -33,16 +33,22 @@ class SendRecurringInvoices extends Command $today = new DateTime(); $invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user') - ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get(); + ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today)) + ->orderBy('id', 'asc') + ->get(); $this->info(count($invoices).' recurring invoice(s) found'); foreach ($invoices as $recurInvoice) { - $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); - + if (!$recurInvoice->user->confirmed) { + continue; + } + + $recurInvoice->account->loadLocalizationSettings($recurInvoice->client); + $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); - if ($invoice) { - $recurInvoice->account->loadLocalizationSettings(); + if ($invoice && !$invoice->isPaid()) { + $this->info('Sending Invoice'); $this->mailer->sendInvoice($invoice); } } diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php new file mode 100644 index 000000000000..3243ac016eaf --- /dev/null +++ b/app/Console/Commands/SendReminders.php @@ -0,0 +1,70 @@ +mailer = $mailer; + $this->invoiceRepo = $invoiceRepo; + $this->accountRepo = $accountRepo; + } + + public function fire() + { + $this->info(date('Y-m-d').' Running SendReminders...'); + $today = new DateTime(); + + $accounts = $this->accountRepo->findWithReminders(); + $this->info(count($accounts).' accounts found'); + + foreach ($accounts as $account) { + if (!$account->isPro()) { + continue; + } + + $invoices = $this->invoiceRepo->findNeedingReminding($account); + $this->info($account->name . ': ' . count($invoices).' invoices found'); + + foreach ($invoices as $invoice) { + if ($reminder = $account->getInvoiceReminder($invoice)) { + $this->info('Send to ' . $invoice->id); + $this->mailer->sendInvoice($invoice, $reminder); + } + } + } + + $this->info('Done'); + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + //array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null), + ); + } +} diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php index cf9a968d56ba..1e8ea1b49eb1 100644 --- a/app/Console/Commands/SendRenewalInvoices.php +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -28,14 +28,34 @@ class SendRenewalInvoices extends Command { $this->info(date('Y-m-d').' Running SendRenewalInvoices...'); $today = new DateTime(); + $sentTo = []; - $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355')->get(); + // get all accounts with pro plans expiring in 10 days + $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355') + ->orderBy('id') + ->get(); $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { + // don't send multiple invoices to multi-company users + if ($userAccountId = $this->accountRepo->getUserAccountId($account)) { + if (isset($sentTo[$userAccountId])) { + continue; + } else { + $sentTo[$userAccountId] = true; + } + } + $client = $this->accountRepo->getNinjaClient($account); $invitation = $this->accountRepo->createNinjaInvoice($client); - $this->mailer->sendInvoice($invitation->invoice); + + // set the due date to 10 days from now + $invoice = $invitation->invoice; + $invoice->due_date = date('Y-m-d', strtotime('+ 10 days')); + $invoice->save(); + + $this->mailer->sendInvoice($invoice); + $this->info("Sent invoice to {$client->getDisplayName()}"); } $this->info('Done'); diff --git a/app/Console/Commands/TestOFX.php b/app/Console/Commands/TestOFX.php new file mode 100644 index 000000000000..637451fbba68 --- /dev/null +++ b/app/Console/Commands/TestOFX.php @@ -0,0 +1,30 @@ +bankAccountService = $bankAccountService; + } + + public function fire() + { + $this->info(date('Y-m-d').' Running TestOFX...'); + + $bankId = env('TEST_BANK_ID'); + $username = env('TEST_BANK_USERNAME'); + $password = env('TEST_BANK_PASSWORD'); + + $data = $this->bankAccountService->loadBankAccounts($bankId, $username, $password, false); + + print "
".print_r($data, 1)."
"; + } +} \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 9235cbf87b58..e281afb926d4 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -1,33 +1,51 @@ -command('inspire') - // ->hourly(); - } + /** + * Define the application's command schedule. + * + * @param \Illuminate\Console\Scheduling\Schedule $schedule + * @return void + */ + protected function schedule(Schedule $schedule) + { + $logFile = storage_path() . '/logs/cron.log'; + $schedule + ->command('ninja:send-invoices --force') + ->sendOutputTo($logFile) + ->withoutOverlapping() + ->hourly(); + + $schedule + ->command('ninja:send-reminders --force') + ->sendOutputTo($logFile) + ->daily(); + + if (Utils::isNinja()) { + $schedule + ->command('ninja:send-renewals --force') + ->sendOutputTo($logFile) + ->daily(); + } + } } diff --git a/app/Events/ClientWasArchived.php b/app/Events/ClientWasArchived.php new file mode 100644 index 000000000000..03ebdc09cd18 --- /dev/null +++ b/app/Events/ClientWasArchived.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasCreated.php b/app/Events/ClientWasCreated.php new file mode 100644 index 000000000000..5c2d3700172b --- /dev/null +++ b/app/Events/ClientWasCreated.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasDeleted.php b/app/Events/ClientWasDeleted.php new file mode 100644 index 000000000000..b87063c4979d --- /dev/null +++ b/app/Events/ClientWasDeleted.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasRestored.php b/app/Events/ClientWasRestored.php new file mode 100644 index 000000000000..385a0472ab4c --- /dev/null +++ b/app/Events/ClientWasRestored.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasUpdated.php b/app/Events/ClientWasUpdated.php new file mode 100644 index 000000000000..7e4790da6885 --- /dev/null +++ b/app/Events/ClientWasUpdated.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/CreditWasArchived.php b/app/Events/CreditWasArchived.php new file mode 100644 index 000000000000..2c680905b33a --- /dev/null +++ b/app/Events/CreditWasArchived.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/CreditWasCreated.php b/app/Events/CreditWasCreated.php new file mode 100644 index 000000000000..bc20b312dc5f --- /dev/null +++ b/app/Events/CreditWasCreated.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/CreditWasDeleted.php b/app/Events/CreditWasDeleted.php new file mode 100644 index 000000000000..e26a5d3ab053 --- /dev/null +++ b/app/Events/CreditWasDeleted.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/CreditWasRestored.php b/app/Events/CreditWasRestored.php new file mode 100644 index 000000000000..8d17d961e7ff --- /dev/null +++ b/app/Events/CreditWasRestored.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/ExpenseWasArchived.php b/app/Events/ExpenseWasArchived.php new file mode 100644 index 000000000000..a4b2af4bdf31 --- /dev/null +++ b/app/Events/ExpenseWasArchived.php @@ -0,0 +1,22 @@ +expense = $expense; + } + +} diff --git a/app/Events/ExpenseWasCreated.php b/app/Events/ExpenseWasCreated.php new file mode 100644 index 000000000000..ab462fe60253 --- /dev/null +++ b/app/Events/ExpenseWasCreated.php @@ -0,0 +1,21 @@ +expense = $expense; + } +} diff --git a/app/Events/ExpenseWasDeleted.php b/app/Events/ExpenseWasDeleted.php new file mode 100644 index 000000000000..1549b483b497 --- /dev/null +++ b/app/Events/ExpenseWasDeleted.php @@ -0,0 +1,23 @@ +expense = $expense; + } + +} diff --git a/app/Events/ExpenseWasRestored.php b/app/Events/ExpenseWasRestored.php new file mode 100644 index 000000000000..b52a2d119a2d --- /dev/null +++ b/app/Events/ExpenseWasRestored.php @@ -0,0 +1,23 @@ +expense = $expense; + } + +} diff --git a/app/Events/ExpenseWasUpdated.php b/app/Events/ExpenseWasUpdated.php new file mode 100644 index 000000000000..1066d90de4f7 --- /dev/null +++ b/app/Events/ExpenseWasUpdated.php @@ -0,0 +1,21 @@ +expense = $expense; + } +} diff --git a/app/Events/InvoiceInvitationWasEmailed.php b/app/Events/InvoiceInvitationWasEmailed.php new file mode 100644 index 000000000000..da0031249217 --- /dev/null +++ b/app/Events/InvoiceInvitationWasEmailed.php @@ -0,0 +1,23 @@ +invitation = $invitation; + } + +} diff --git a/app/Events/InvoiceInvitationWasViewed.php b/app/Events/InvoiceInvitationWasViewed.php new file mode 100644 index 000000000000..bbf7e23c3353 --- /dev/null +++ b/app/Events/InvoiceInvitationWasViewed.php @@ -0,0 +1,25 @@ +invoice = $invoice; + $this->invitation = $invitation; + } + +} diff --git a/app/Events/InvoiceWasArchived.php b/app/Events/InvoiceWasArchived.php new file mode 100644 index 000000000000..7587c071a66e --- /dev/null +++ b/app/Events/InvoiceWasArchived.php @@ -0,0 +1,22 @@ +invoice = $invoice; + } + +} diff --git a/app/Events/InvoiceSent.php b/app/Events/InvoiceWasCreated.php similarity index 88% rename from app/Events/InvoiceSent.php rename to app/Events/InvoiceWasCreated.php index cbe08d0528f3..cfd943bcffbf 100644 --- a/app/Events/InvoiceSent.php +++ b/app/Events/InvoiceWasCreated.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class InvoiceSent extends Event { +class InvoiceWasCreated extends Event { use SerializesModels; - public $invoice; /** diff --git a/app/Events/InvoiceViewed.php b/app/Events/InvoiceWasDeleted.php similarity index 61% rename from app/Events/InvoiceViewed.php rename to app/Events/InvoiceWasDeleted.php index 8d9f129e764a..316b1b5c5001 100644 --- a/app/Events/InvoiceViewed.php +++ b/app/Events/InvoiceWasDeleted.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class InvoiceViewed extends Event { +class InvoiceWasDeleted extends Event { use SerializesModels; - public $invoice; /** @@ -15,9 +14,9 @@ class InvoiceViewed extends Event { * * @return void */ - public function __construct($invoice) - { - $this->invoice = $invoice; - } + public function __construct($invoice) + { + $this->invoice = $invoice; + } } diff --git a/app/Events/InvoiceWasEmailed.php b/app/Events/InvoiceWasEmailed.php new file mode 100644 index 000000000000..dc30f6a55869 --- /dev/null +++ b/app/Events/InvoiceWasEmailed.php @@ -0,0 +1,22 @@ +invoice = $invoice; + } + +} diff --git a/app/Events/InvoiceWasRestored.php b/app/Events/InvoiceWasRestored.php new file mode 100644 index 000000000000..5d75b4b246b4 --- /dev/null +++ b/app/Events/InvoiceWasRestored.php @@ -0,0 +1,25 @@ +invoice = $invoice; + $this->fromDeleted = $fromDeleted; + } + +} diff --git a/app/Events/QuoteApproved.php b/app/Events/InvoiceWasUpdated.php similarity index 87% rename from app/Events/QuoteApproved.php rename to app/Events/InvoiceWasUpdated.php index 12b5384b35e3..87a0f8f20136 100644 --- a/app/Events/QuoteApproved.php +++ b/app/Events/InvoiceWasUpdated.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class QuoteApproved extends Event { +class InvoiceWasUpdated extends Event { use SerializesModels; - public $invoice; /** diff --git a/app/Events/InvoicePaid.php b/app/Events/PaymentWasArchived.php similarity index 87% rename from app/Events/InvoicePaid.php rename to app/Events/PaymentWasArchived.php index 4dced73471a0..b8bb693dfc78 100644 --- a/app/Events/InvoicePaid.php +++ b/app/Events/PaymentWasArchived.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class InvoicePaid extends Event { +class PaymentWasArchived extends Event { use SerializesModels; - public $payment; /** diff --git a/app/Events/PaymentWasCreated.php b/app/Events/PaymentWasCreated.php new file mode 100644 index 000000000000..619d33e95890 --- /dev/null +++ b/app/Events/PaymentWasCreated.php @@ -0,0 +1,22 @@ +payment = $payment; + } + +} diff --git a/app/Events/PaymentWasDeleted.php b/app/Events/PaymentWasDeleted.php new file mode 100644 index 000000000000..e12647c86011 --- /dev/null +++ b/app/Events/PaymentWasDeleted.php @@ -0,0 +1,22 @@ +payment = $payment; + } + +} diff --git a/app/Events/PaymentWasRestored.php b/app/Events/PaymentWasRestored.php new file mode 100644 index 000000000000..711bdbb67fa5 --- /dev/null +++ b/app/Events/PaymentWasRestored.php @@ -0,0 +1,25 @@ +payment = $payment; + $this->fromDeleted = $fromDeleted; + } + +} diff --git a/app/Events/QuoteInvitationWasApproved.php b/app/Events/QuoteInvitationWasApproved.php new file mode 100644 index 000000000000..5e69fe9c7895 --- /dev/null +++ b/app/Events/QuoteInvitationWasApproved.php @@ -0,0 +1,27 @@ +quote = $quote; + $this->invoice = $invoice; + $this->invitation = $invitation; + } + +} diff --git a/app/Events/QuoteInvitationWasEmailed.php b/app/Events/QuoteInvitationWasEmailed.php new file mode 100644 index 000000000000..5ce1c68602fb --- /dev/null +++ b/app/Events/QuoteInvitationWasEmailed.php @@ -0,0 +1,23 @@ +invitation = $invitation; + } + +} diff --git a/app/Events/QuoteInvitationWasViewed.php b/app/Events/QuoteInvitationWasViewed.php new file mode 100644 index 000000000000..3cd84b0e1189 --- /dev/null +++ b/app/Events/QuoteInvitationWasViewed.php @@ -0,0 +1,25 @@ +quote = $quote; + $this->invitation = $invitation; + } + +} diff --git a/app/Events/QuoteWasArchived.php b/app/Events/QuoteWasArchived.php new file mode 100644 index 000000000000..285a61250c04 --- /dev/null +++ b/app/Events/QuoteWasArchived.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasCreated.php b/app/Events/QuoteWasCreated.php new file mode 100644 index 000000000000..d17ef9c1318c --- /dev/null +++ b/app/Events/QuoteWasCreated.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasDeleted.php b/app/Events/QuoteWasDeleted.php new file mode 100644 index 000000000000..ce3685d7a212 --- /dev/null +++ b/app/Events/QuoteWasDeleted.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasEmailed.php b/app/Events/QuoteWasEmailed.php new file mode 100644 index 000000000000..19b1ec12d6a5 --- /dev/null +++ b/app/Events/QuoteWasEmailed.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasRestored.php b/app/Events/QuoteWasRestored.php new file mode 100644 index 000000000000..0f13a65b437e --- /dev/null +++ b/app/Events/QuoteWasRestored.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasUpdated.php b/app/Events/QuoteWasUpdated.php new file mode 100644 index 000000000000..f01b9822601f --- /dev/null +++ b/app/Events/QuoteWasUpdated.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/UserSettingsChanged.php b/app/Events/UserSettingsChanged.php index 02c3a0195875..ead79b390898 100644 --- a/app/Events/UserSettingsChanged.php +++ b/app/Events/UserSettingsChanged.php @@ -8,14 +8,16 @@ class UserSettingsChanged extends Event { use SerializesModels; + public $user; + /** * Create a new event instance. * * @return void */ - public function __construct() + public function __construct($user = false) { - // + $this->user = $user; } } diff --git a/app/Events/UserSignedUp.php b/app/Events/UserSignedUp.php new file mode 100644 index 000000000000..99e8b22456c2 --- /dev/null +++ b/app/Events/UserSignedUp.php @@ -0,0 +1,21 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasCreated.php b/app/Events/VendorWasCreated.php new file mode 100644 index 000000000000..b2d7e81c9394 --- /dev/null +++ b/app/Events/VendorWasCreated.php @@ -0,0 +1,22 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasDeleted.php b/app/Events/VendorWasDeleted.php new file mode 100644 index 000000000000..553bece3ccdc --- /dev/null +++ b/app/Events/VendorWasDeleted.php @@ -0,0 +1,22 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasRestored.php b/app/Events/VendorWasRestored.php new file mode 100644 index 000000000000..88c24693e611 --- /dev/null +++ b/app/Events/VendorWasRestored.php @@ -0,0 +1,22 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasUpdated.php b/app/Events/VendorWasUpdated.php new file mode 100644 index 000000000000..eb90a68f46c0 --- /dev/null +++ b/app/Events/VendorWasUpdated.php @@ -0,0 +1,21 @@ +vendor = $vendor; + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index e0db4f0a89f5..ba656f6c0131 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -27,11 +27,13 @@ class Handler extends ExceptionHandler { */ public function report(Exception $e) { - Utils::logError(Utils::getErrorString($e)); - return false; - - //return parent::report($e); - } + if (Utils::isNinja()) { + Utils::logError(Utils::getErrorString($e)); + return false; + } else { + return parent::report($e); + } + } /** * Render an exception into an HTTP response. @@ -41,13 +43,21 @@ class Handler extends ExceptionHandler { * @return \Illuminate\Http\Response */ public function render($request, Exception $e) - { - + { if ($e instanceof ModelNotFoundException) { return Redirect::to('/'); + } elseif ($e instanceof \Illuminate\Session\TokenMismatchException) { + // https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e + return redirect() + ->back() + ->withInput($request->except('password', '_token')) + ->with([ + 'warning' => trans('texts.token_expired') + ]); } - if (Utils::isNinjaProd()) { + // In production, except for maintenance mode, we'll show a custom error screen + if (Utils::isNinjaProd() && !Utils::isDownForMaintenance()) { $data = [ 'error' => get_class($e), 'hideHeader' => true, diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php new file mode 100644 index 000000000000..3517be1af9f3 --- /dev/null +++ b/app/Http/Controllers/AccountApiController.php @@ -0,0 +1,104 @@ +accountRepo = $accountRepo; + } + + public function login(Request $request) + { + if ( ! env(API_SECRET) || $request->api_secret !== env(API_SECRET)) { + sleep(ERROR_DELAY); + return 'Invalid secret'; + } + + if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) { + return $this->processLogin($request); + } else { + sleep(ERROR_DELAY); + return 'Invalid credentials'; + } + } + + private function processLogin(Request $request) + { + // Create a new token only if one does not already exist + $user = Auth::user(); + $this->accountRepo->createTokens($user, $request->token_name); + + $users = $this->accountRepo->findUsers($user, 'account.account_tokens'); + $transformer = new UserAccountTransformer($user->account, $request->serializer, $request->token_name); + $data = $this->createCollection($users, $transformer, 'user_account'); + + return $this->response($data); + } + + public function show(Request $request) + { + $account = Auth::user()->account; + $updatedAt = $request->updated_at ? date('Y-m-d H:i:s', $request->updated_at) : false; + + $map = [ + 'users' => [], + 'clients' => ['contacts'], + 'invoices' => ['invoice_items', 'user', 'client', 'payments'], + 'products' => [], + 'tax_rates' => [], + ]; + + foreach ($map as $key => $values) { + $account->load([$key => function($query) use ($values, $updatedAt) { + $query->withTrashed()->with($values); + if ($updatedAt) { + $query->where('updated_at', '>=', $updatedAt); + } + }]); + } + + $transformer = new AccountTransformer(null, $request->serializer); + $account = $this->createItem($account, $transformer, 'account'); + + return $this->response($account); + } + + public function getStaticData() + { + $data = []; + + $cachedTables = unserialize(CACHED_TABLES); + foreach ($cachedTables as $name => $class) { + $data[$name] = Cache::get($name); + } + + return $this->response($data); + } + + public function getUserAccounts(Request $request) + { + return $this->processLogin($request); + } +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index cf647dcaa8c6..d03b13fa9e72 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,7 +1,6 @@ accountRepo = $accountRepo; $this->userMailer = $userMailer; $this->contactMailer = $contactMailer; + $this->referralRepository = $referralRepository; } public function demo() @@ -85,7 +75,7 @@ class AccountController extends BaseController if (!Utils::isNinja() && (Account::count() > 0 && !$prevUserId)) { return Redirect::to('/login'); } - + if ($guestKey && !$prevUserId) { $user = User::where('password', '=', $guestKey)->first(); @@ -107,9 +97,10 @@ class AccountController extends BaseController } Auth::login($user, true); - Event::fire(new UserLoggedIn()); - - $redirectTo = Input::get('redirect_to', 'invoices/create'); + event(new UserLoggedIn()); + + $redirectTo = Input::get('redirect_to') ?: 'invoices/create'; + return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up')); } @@ -117,13 +108,6 @@ class AccountController extends BaseController { $invitation = $this->accountRepo->enableProPlan(); - /* - if ($invoice) - { - $this->contactMailer->sendInvoice($invoice); - } - */ - return $invitation->invitation_key; } @@ -132,9 +116,9 @@ class AccountController extends BaseController Session::put("show_trash:{$entityType}", $visible == 'true'); if ($entityType == 'user') { - return Redirect::to('company/'.ACCOUNT_ADVANCED_SETTINGS.'/'.ACCOUNT_USER_MANAGEMENT); + return Redirect::to('settings/'.ACCOUNT_USER_MANAGEMENT); } elseif ($entityType == 'token') { - return Redirect::to('company/'.ACCOUNT_ADVANCED_SETTINGS.'/'.ACCOUNT_TOKEN_MANAGEMENT); + return Redirect::to('settings/'.ACCOUNT_API_TOKENS); } else { return Redirect::to("{$entityType}s"); } @@ -143,170 +127,417 @@ class AccountController extends BaseController public function getSearchData() { $data = $this->accountRepo->getSearchData(); + return Response::json($data); } - public function showSection($section = ACCOUNT_DETAILS, $subSection = false) + public function showSection($section = false) { - if ($section == ACCOUNT_DETAILS) { - $primaryUser = Auth::user()->account->users()->orderBy('id')->first(); - $data = [ - 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), - 'countries' => Cache::get('countries'), - 'sizes' => Cache::get('sizes'), - 'industries' => Cache::get('industries'), - 'timezones' => Cache::get('timezones'), - 'dateFormats' => Cache::get('dateFormats'), - 'datetimeFormats' => Cache::get('datetimeFormats'), - 'currencies' => Cache::get('currencies'), - 'languages' => Cache::get('languages'), - 'showUser' => Auth::user()->id === $primaryUser->id, - 'title' => trans('texts.company_details'), - 'primaryUser' => $primaryUser, - ]; + if (!$section) { + return Redirect::to('/settings/'.ACCOUNT_COMPANY_DETAILS, 301); + } - return View::make('accounts.details', $data); + if ($section == ACCOUNT_COMPANY_DETAILS) { + return self::showCompanyDetails(); + } elseif ($section == ACCOUNT_USER_DETAILS) { + return self::showUserDetails(); + } elseif ($section == ACCOUNT_LOCALIZATION) { + return self::showLocalization(); } elseif ($section == ACCOUNT_PAYMENTS) { - - $account = Auth::user()->account; - $account->load('account_gateways'); - $count = count($account->account_gateways); - - if ($count == 0) { - return Redirect::to('gateways/create'); - } else { - return View::make('accounts.payments', [ - 'showAdd' => $count < 3, - 'title' => trans('texts.online_payments') - ]); - } - } elseif ($section == ACCOUNT_NOTIFICATIONS) { - $data = [ - 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), - 'title' => trans('texts.notifications'), - ]; - - return View::make('accounts.notifications', $data); + return self::showOnlinePayments(); + } elseif ($section == ACCOUNT_BANKS) { + return self::showBankAccounts(); + } elseif ($section == ACCOUNT_INVOICE_SETTINGS) { + return self::showInvoiceSettings(); } elseif ($section == ACCOUNT_IMPORT_EXPORT) { return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); - } elseif ($section == ACCOUNT_ADVANCED_SETTINGS) { - $account = Auth::user()->account->load('country'); + } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { + return self::showInvoiceDesign($section); + } elseif ($section == ACCOUNT_CLIENT_PORTAL) { + return self::showClientViewStyling(); + } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { + return self::showTemplates(); + } elseif ($section === ACCOUNT_PRODUCTS) { + return self::showProducts(); + } elseif ($section === ACCOUNT_TAX_RATES) { + return self::showTaxRates(); + } elseif ($section === ACCOUNT_PAYMENT_TERMS) { + return self::showPaymentTerms(); + } elseif ($section === ACCOUNT_SYSTEM_SETTINGS) { + return self::showSystemSettings(); + } else { $data = [ - 'account' => $account, - 'feature' => $subSection, - 'title' => trans('texts.invoice_settings'), + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans("texts.{$section}"), + 'section' => $section, ]; - if ($subSection == ACCOUNT_INVOICE_DESIGN - || $subSection == ACCOUNT_CUSTOMIZE_DESIGN) { - $invoice = new stdClass(); - $client = new stdClass(); - $contact = new stdClass(); - $invoiceItem = new stdClass(); - - $client->name = 'Sample Client'; - $client->address1 = ''; - $client->city = ''; - $client->state = ''; - $client->postal_code = ''; - $client->work_phone = ''; - $client->work_email = ''; - - $invoice->invoice_number = $account->getNextInvoiceNumber(); - $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); - $invoice->account = json_decode($account->toJson()); - $invoice->amount = $invoice->balance = 100; - - $invoice->terms = trim($account->invoice_terms); - $invoice->invoice_footer = trim($account->invoice_footer); - - $contact->email = 'contact@gmail.com'; - $client->contacts = [$contact]; - - $invoiceItem->cost = 100; - $invoiceItem->qty = 1; - $invoiceItem->notes = 'Notes'; - $invoiceItem->product_key = 'Item'; - - $invoice->client = $client; - $invoice->invoice_items = [$invoiceItem]; - - $data['account'] = $account; - $data['invoice'] = $invoice; - $data['invoiceLabels'] = json_decode($account->invoice_labels) ?: []; - $data['title'] = trans('texts.invoice_design'); - $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); - - $design = false; - foreach ($data['invoiceDesigns'] as $item) { - if ($item->id == $account->invoice_design_id) { - $design = $item->javascript; - break; - } - } - - if ($subSection == ACCOUNT_CUSTOMIZE_DESIGN) { - $data['customDesign'] = ($account->custom_design && !$design) ? $account->custom_design : $design; - } - } else if ($subSection == ACCOUNT_EMAIL_TEMPLATES) { - $data['invoiceEmail'] = $account->getEmailTemplate(ENTITY_INVOICE); - $data['quoteEmail'] = $account->getEmailTemplate(ENTITY_QUOTE); - $data['paymentEmail'] = $account->getEmailTemplate(ENTITY_PAYMENT); - $data['emailFooter'] = $account->getEmailFooter(); - $data['title'] = trans('texts.email_templates'); - } else if ($subSection == ACCOUNT_USER_MANAGEMENT) { - $data['title'] = trans('texts.users_and_tokens'); - } - - return View::make("accounts.{$subSection}", $data); - } elseif ($section == ACCOUNT_PRODUCTS) { - $data = [ - 'account' => Auth::user()->account, - 'title' => trans('texts.product_library'), - ]; - - return View::make('accounts.products', $data); + return View::make("accounts.{$section}", $data); } } - public function doSection($section = ACCOUNT_DETAILS, $subSection = false) + private function showSystemSettings() { - if ($section == ACCOUNT_DETAILS) { - return AccountController::saveDetails(); - } elseif ($section == ACCOUNT_IMPORT_EXPORT) { - return AccountController::importFile(); - } elseif ($section == ACCOUNT_MAP) { - return AccountController::mapFile(); - } elseif ($section == ACCOUNT_NOTIFICATIONS) { - return AccountController::saveNotifications(); - } elseif ($section == ACCOUNT_EXPORT) { - return AccountController::export(); - } elseif ($section == ACCOUNT_ADVANCED_SETTINGS) { - if ($subSection == ACCOUNT_INVOICE_SETTINGS) { - return AccountController::saveInvoiceSettings(); - } elseif ($subSection == ACCOUNT_INVOICE_DESIGN) { - return AccountController::saveInvoiceDesign(); - } elseif ($subSection == ACCOUNT_CUSTOMIZE_DESIGN) { - return AccountController::saveCustomizeDesign(); - } elseif ($subSection == ACCOUNT_EMAIL_TEMPLATES) { - return AccountController::saveEmailTemplates(); + if (Utils::isNinjaProd()) { + return Redirect::to('/'); + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans("texts.system_settings"), + 'section' => ACCOUNT_SYSTEM_SETTINGS, + ]; + + return View::make("accounts.system_settings", $data); + } + + private function showInvoiceSettings() + { + $account = Auth::user()->account; + $recurringHours = []; + + for ($i = 0; $i<24; $i++) { + if ($account->military_time) { + $format = 'H:i'; + } else { + $format = 'g:i a'; } - } elseif ($section == ACCOUNT_PRODUCTS) { - return AccountController::saveProducts(); + $recurringHours[$i] = date($format, strtotime("{$i}:00")); + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans("texts.invoice_settings"), + 'section' => ACCOUNT_INVOICE_SETTINGS, + 'recurringHours' => $recurringHours, + ]; + + return View::make("accounts.invoice_settings", $data); + } + + private function showCompanyDetails() + { + // check that logo is less than the max file size + $account = Auth::user()->account; + if ($account->isLogoTooLarge()) { + Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize().'KB'])); + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'countries' => Cache::get('countries'), + 'sizes' => Cache::get('sizes'), + 'industries' => Cache::get('industries'), + 'title' => trans('texts.company_details'), + ]; + + return View::make('accounts.details', $data); + } + + private function showUserDetails() + { + $oauthLoginUrls = []; + foreach (AuthService::$providers as $provider) { + $oauthLoginUrls[] = ['label' => $provider, 'url' => '/auth/'.strtolower($provider)]; + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans('texts.user_details'), + 'user' => Auth::user(), + 'oauthProviderName' => AuthService::getProviderName(Auth::user()->oauth_provider_id), + 'oauthLoginUrls' => $oauthLoginUrls, + 'referralCounts' => $this->referralRepository->getCounts(Auth::user()->id), + ]; + + return View::make('accounts.user_details', $data); + } + + private function showLocalization() + { + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'timezones' => Cache::get('timezones'), + 'dateFormats' => Cache::get('dateFormats'), + 'datetimeFormats' => Cache::get('datetimeFormats'), + 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), + 'title' => trans('texts.localization'), + ]; + + return View::make('accounts.localization', $data); + } + + private function showBankAccounts() + { + $account = Auth::user()->account; + $account->load('bank_accounts'); + $count = count($account->bank_accounts); + + if ($count == 0) { + return Redirect::to('bank_accounts/create'); + } else { + return View::make('accounts.banks', [ + 'title' => trans('texts.bank_accounts') + ]); } } - private function saveCustomizeDesign() { + private function showOnlinePayments() + { + $account = Auth::user()->account; + $account->load('account_gateways'); + $count = count($account->account_gateways); + + if ($accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) { + if (! $accountGateway->getPublishableStripeKey()) { + Session::flash('warning', trans('texts.missing_publishable_key')); + } + } + + if ($count == 0) { + return Redirect::to('gateways/create'); + } else { + return View::make('accounts.payments', [ + 'showAdd' => $count < count(Gateway::$paymentTypes), + 'title' => trans('texts.online_payments') + ]); + } + } + + private function showProducts() + { + $columns = ['product', 'description', 'unit_cost']; + if (Auth::user()->account->invoice_item_taxes) { + $columns[] = 'tax_rate'; + } + $columns[] = 'action'; + + $data = [ + 'account' => Auth::user()->account, + 'title' => trans('texts.product_library'), + 'columns' => Utils::trans($columns), + ]; + + return View::make('accounts.products', $data); + } + + private function showTaxRates() + { + $data = [ + 'account' => Auth::user()->account, + 'title' => trans('texts.tax_rates'), + 'taxRates' => TaxRate::scope()->get(['id', 'name', 'rate']), + ]; + + return View::make('accounts.tax_rates', $data); + } + + private function showPaymentTerms() + { + $data = [ + 'account' => Auth::user()->account, + 'title' => trans('texts.payment_terms'), + 'taxRates' => PaymentTerm::scope()->get(['id', 'name', 'num_days']), + ]; + + return View::make('accounts.payment_terms', $data); + } + + private function showInvoiceDesign($section) + { + $account = Auth::user()->account->load('country'); + $invoice = new stdClass(); + $client = new stdClass(); + $contact = new stdClass(); + $invoiceItem = new stdClass(); + + $client->name = 'Sample Client'; + $client->address1 = trans('texts.address1'); + $client->city = trans('texts.city'); + $client->state = trans('texts.state'); + $client->postal_code = trans('texts.postal_code'); + $client->work_phone = trans('texts.work_phone'); + $client->work_email = trans('texts.work_id'); + + $invoice->invoice_number = '0000'; + $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); + $invoice->account = json_decode($account->toJson()); + $invoice->amount = $invoice->balance = 100; + + $invoice->terms = trim($account->invoice_terms); + $invoice->invoice_footer = trim($account->invoice_footer); + + $contact->email = 'contact@gmail.com'; + $client->contacts = [$contact]; + + $invoiceItem->cost = 100; + $invoiceItem->qty = 1; + $invoiceItem->notes = 'Notes'; + $invoiceItem->product_key = 'Item'; + + $invoice->client = $client; + $invoice->invoice_items = [$invoiceItem]; + + $data['account'] = $account; + $data['invoice'] = $invoice; + $data['invoiceLabels'] = json_decode($account->invoice_labels) ?: []; + $data['title'] = trans('texts.invoice_design'); + $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); + $data['invoiceFonts'] = Cache::get('fonts'); + $data['section'] = $section; + + $design = false; + foreach ($data['invoiceDesigns'] as $item) { + if ($item->id == $account->invoice_design_id) { + $design = $item->javascript; + break; + } + } + + if ($section == ACCOUNT_CUSTOMIZE_DESIGN) { + $data['customDesign'] = ($account->custom_design && !$design) ? $account->custom_design : $design; + } + + return View::make("accounts.{$section}", $data); + } + + private function showClientViewStyling() + { + $account = Auth::user()->account->load('country'); + $css = $account->client_view_css ? $account->client_view_css : ''; + + if (Utils::isNinja() && $css) { + // Unescape the CSS for display purposes + $css = str_replace( + array('\3C ', '\3E ', '\26 '), + array('<', '>', '&'), + $css + ); + } + + $data = [ + 'client_view_css' => $css, + 'title' => trans("texts.client_portal"), + 'section' => ACCOUNT_CLIENT_PORTAL, + ]; + + return View::make("accounts.client_portal", $data); + } + + private function showTemplates() + { + $account = Auth::user()->account->load('country'); + $data['account'] = $account; + $data['templates'] = []; + $data['defaultTemplates'] = []; + foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { + $data['templates'][$type] = [ + 'subject' => $account->getEmailSubject($type), + 'template' => $account->getEmailTemplate($type), + ]; + $data['defaultTemplates'][$type] = [ + 'subject' => $account->getDefaultEmailSubject($type), + 'template' => $account->getDefaultEmailTemplate($type), + ]; + } + $data['emailFooter'] = $account->getEmailFooter(); + $data['title'] = trans('texts.email_templates'); + + return View::make('accounts.templates_and_reminders', $data); + } + + public function doSection($section = ACCOUNT_COMPANY_DETAILS) + { + if ($section === ACCOUNT_COMPANY_DETAILS) { + return AccountController::saveDetails(); + } elseif ($section === ACCOUNT_USER_DETAILS) { + return AccountController::saveUserDetails(); + } elseif ($section === ACCOUNT_LOCALIZATION) { + return AccountController::saveLocalization(); + } elseif ($section === ACCOUNT_NOTIFICATIONS) { + return AccountController::saveNotifications(); + } elseif ($section === ACCOUNT_EXPORT) { + return AccountController::export(); + } elseif ($section === ACCOUNT_INVOICE_SETTINGS) { + return AccountController::saveInvoiceSettings(); + } elseif ($section === ACCOUNT_EMAIL_SETTINGS) { + return AccountController::saveEmailSettings(); + } elseif ($section === ACCOUNT_INVOICE_DESIGN) { + return AccountController::saveInvoiceDesign(); + } elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) { + return AccountController::saveCustomizeDesign(); + } elseif ($section === ACCOUNT_CLIENT_PORTAL) { + return AccountController::saveClientPortal(); + } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { + return AccountController::saveEmailTemplates(); + } elseif ($section === ACCOUNT_PRODUCTS) { + return AccountController::saveProducts(); + } elseif ($section === ACCOUNT_TAX_RATES) { + return AccountController::saveTaxRates(); + } elseif ($section === ACCOUNT_PAYMENT_TERMS) { + return AccountController::savePaymetTerms(); + } + } + + private function saveCustomizeDesign() + { if (Auth::user()->account->isPro()) { $account = Auth::user()->account; $account->custom_design = Input::get('custom_design'); $account->invoice_design_id = CUSTOM_DESIGN; $account->save(); - + Session::flash('message', trans('texts.updated_settings')); } - return Redirect::to('company/advanced_settings/customize_design'); + return Redirect::to('settings/'.ACCOUNT_CUSTOMIZE_DESIGN); + } + + private function saveClientPortal() + { + // Only allowed for pro Invoice Ninja users or white labeled self-hosted users + if ((Utils::isNinja() && Auth::user()->account->isPro()) || Auth::user()->account->isWhiteLabel()) { + $input_css = Input::get('client_view_css'); + if (Utils::isNinja()) { + // Allow referencing the body element + $input_css = preg_replace('/(? + // + + // Create a new configuration object + $config = \HTMLPurifier_Config::createDefault(); + $config->set('Filter.ExtractStyleBlocks', true); + $config->set('CSS.AllowImportant', true); + $config->set('CSS.AllowTricky', true); + $config->set('CSS.Trusted', true); + + // Create a new purifier instance + $purifier = new \HTMLPurifier($config); + + // Wrap our CSS in style tags and pass to purifier. + // we're not actually interested in the html response though + $html = $purifier->purify(''); + + // The "style" blocks are stored seperately + $output_css = $purifier->context->get('StyleBlocks'); + + // Get the first style block + $sanitized_css = count($output_css) ? $output_css[0] : ''; + } else { + $sanitized_css = $input_css; + } + + $account = Auth::user()->account; + $account->client_view_css = $sanitized_css; + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + } + + return Redirect::to('settings/'.ACCOUNT_CLIENT_PORTAL); } private function saveEmailTemplates() @@ -314,16 +545,48 @@ class AccountController extends BaseController if (Auth::user()->account->isPro()) { $account = Auth::user()->account; - $account->email_template_invoice = Input::get('email_template_invoice', $account->getEmailTemplate(ENTITY_INVOICE)); - $account->email_template_quote = Input::get('email_template_quote', $account->getEmailTemplate(ENTITY_QUOTE)); - $account->email_template_payment = Input::get('email_template_payment', $account->getEmailTemplate(ENTITY_PAYMENT)); + foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { + $subjectField = "email_subject_{$type}"; + $subject = Input::get($subjectField, $account->getEmailSubject($type)); + $account->$subjectField = ($subject == $account->getDefaultEmailSubject($type) ? null : $subject); + + $bodyField = "email_template_{$type}"; + $body = Input::get($bodyField, $account->getEmailTemplate($type)); + $account->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body); + } + + foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) { + $enableField = "enable_{$type}"; + $account->$enableField = Input::get($enableField) ? true : false; + + if ($account->$enableField) { + $account->{"num_days_{$type}"} = Input::get("num_days_{$type}"); + $account->{"field_{$type}"} = Input::get("field_{$type}"); + $account->{"direction_{$type}"} = Input::get("field_{$type}") == REMINDER_FIELD_INVOICE_DATE ? REMINDER_DIRECTION_AFTER : Input::get("direction_{$type}"); + } + } $account->save(); Session::flash('message', trans('texts.updated_settings')); } - - return Redirect::to('company/advanced_settings/email_templates'); + + return Redirect::to('settings/'.ACCOUNT_TEMPLATES_AND_REMINDERS); + } + + private function saveTaxRates() + { + $account = Auth::user()->account; + + $account->invoice_taxes = Input::get('invoice_taxes') ? true : false; + $account->invoice_item_taxes = Input::get('invoice_item_taxes') ? true : false; + $account->show_item_taxes = Input::get('show_item_taxes') ? true : false; + $account->default_tax_rate_id = Input::get('default_tax_rate_id'); + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_TAX_RATES); } private function saveProducts() @@ -335,48 +598,124 @@ class AccountController extends BaseController $account->save(); Session::flash('message', trans('texts.updated_settings')); - return Redirect::to('company/products'); + + return Redirect::to('settings/'.ACCOUNT_PRODUCTS); } - private function saveInvoiceSettings() + private function saveEmailSettings() { if (Auth::user()->account->isPro()) { - $account = Auth::user()->account; + $rules = []; + $user = Auth::user(); + $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); + $iframeURL = rtrim($iframeURL, "/"); - $account->custom_label1 = trim(Input::get('custom_label1')); - $account->custom_value1 = trim(Input::get('custom_value1')); - $account->custom_label2 = trim(Input::get('custom_label2')); - $account->custom_value2 = trim(Input::get('custom_value2')); - $account->custom_client_label1 = trim(Input::get('custom_client_label1')); - $account->custom_client_label2 = trim(Input::get('custom_client_label2')); - $account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1')); - $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); - $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; - $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; - - $account->invoice_number_prefix = Input::get('invoice_number_prefix'); - $account->invoice_number_counter = Input::get('invoice_number_counter'); - $account->quote_number_prefix = Input::get('quote_number_prefix'); - $account->share_counter = Input::get('share_counter') ? true : false; - - $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; - $account->auto_wrap = Input::get('auto_wrap') ? true : false; - - if (!$account->share_counter) { - $account->quote_number_counter = Input::get('quote_number_counter'); + $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); + if ($iframeURL || !$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { + $subdomain = null; + } + if ($subdomain) { + $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id"; } - if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) { - Session::flash('error', trans('texts.invalid_counter')); + $validator = Validator::make(Input::all(), $rules); - return Redirect::to('company/advanced_settings/invoice_settings')->withInput(); + if ($validator->fails()) { + return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS) + ->withErrors($validator) + ->withInput(); } else { + $account = Auth::user()->account; + $account->subdomain = $subdomain; + $account->iframe_url = $iframeURL; + $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; + $account->email_design_id = Input::get('email_design_id'); + + if (Utils::isNinja()) { + $account->enable_email_markup = Input::get('enable_email_markup') ? true : false; + } + $account->save(); Session::flash('message', trans('texts.updated_settings')); } } - return Redirect::to('company/advanced_settings/invoice_settings'); + return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS); + } + + private function saveInvoiceSettings() + { + if (Auth::user()->account->isPro()) { + $rules = [ + 'invoice_number_pattern' => 'has_counter', + 'quote_number_pattern' => 'has_counter', + ]; + + $validator = Validator::make(Input::all(), $rules); + + if ($validator->fails()) { + return Redirect::to('settings/'.ACCOUNT_INVOICE_SETTINGS) + ->withErrors($validator) + ->withInput(); + } else { + $account = Auth::user()->account; + $account->custom_label1 = trim(Input::get('custom_label1')); + $account->custom_value1 = trim(Input::get('custom_value1')); + $account->custom_label2 = trim(Input::get('custom_label2')); + $account->custom_value2 = trim(Input::get('custom_value2')); + $account->custom_client_label1 = trim(Input::get('custom_client_label1')); + $account->custom_client_label2 = trim(Input::get('custom_client_label2')); + $account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1')); + $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); + $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; + $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; + $account->custom_invoice_text_label1 = trim(Input::get('custom_invoice_text_label1')); + $account->custom_invoice_text_label2 = trim(Input::get('custom_invoice_text_label2')); + + $account->invoice_number_counter = Input::get('invoice_number_counter'); + $account->quote_number_prefix = Input::get('quote_number_prefix'); + $account->share_counter = Input::get('share_counter') ? true : false; + $account->invoice_terms = Input::get('invoice_terms'); + $account->invoice_footer = Input::get('invoice_footer'); + $account->quote_terms = Input::get('quote_terms'); + $account->auto_convert_quote = Input::get('auto_convert_quote'); + + if (Input::has('recurring_hour')) { + $account->recurring_hour = Input::get('recurring_hour'); + } + + if (!$account->share_counter) { + $account->quote_number_counter = Input::get('quote_number_counter'); + } + + if (Input::get('invoice_number_type') == 'prefix') { + $account->invoice_number_prefix = trim(Input::get('invoice_number_prefix')); + $account->invoice_number_pattern = null; + } else { + $account->invoice_number_pattern = trim(Input::get('invoice_number_pattern')); + $account->invoice_number_prefix = null; + } + + if (Input::get('quote_number_type') == 'prefix') { + $account->quote_number_prefix = trim(Input::get('quote_number_prefix')); + $account->quote_number_pattern = null; + } else { + $account->quote_number_pattern = trim(Input::get('quote_number_pattern')); + $account->quote_number_prefix = null; + } + + if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) { + Session::flash('error', trans('texts.invalid_counter')); + + return Redirect::to('settings/'.ACCOUNT_INVOICE_SETTINGS)->withInput(); + } else { + $account->save(); + Session::flash('message', trans('texts.updated_settings')); + } + } + } + + return Redirect::to('settings/'.ACCOUNT_INVOICE_SETTINGS); } private function saveInvoiceDesign() @@ -385,16 +724,19 @@ class AccountController extends BaseController $account = Auth::user()->account; $account->hide_quantity = Input::get('hide_quantity') ? true : false; $account->hide_paid_to_date = Input::get('hide_paid_to_date') ? true : false; + $account->header_font_id = Input::get('header_font_id'); + $account->body_font_id = Input::get('body_font_id'); $account->primary_color = Input::get('primary_color'); $account->secondary_color = Input::get('secondary_color'); - $account->invoice_design_id = Input::get('invoice_design_id'); + $account->invoice_design_id = Input::get('invoice_design_id'); + if (Input::has('font_size')) { $account->font_size = intval(Input::get('font_size')); } - + $labels = []; - foreach (['item', 'description', 'unit_cost', 'quantity'] as $field) { - $labels[$field] = trim(Input::get("labels_{$field}")); + foreach (['item', 'description', 'unit_cost', 'quantity', 'line_total', 'terms'] as $field) { + $labels[$field] = Input::get("labels_{$field}"); } $account->invoice_labels = json_encode($labels); @@ -403,220 +745,11 @@ class AccountController extends BaseController Session::flash('message', trans('texts.updated_settings')); } - return Redirect::to('company/advanced_settings/invoice_design'); - } - - private function export() - { - $output = fopen('php://output', 'w') or Utils::fatalError(); - header('Content-Type:application/csv'); - header('Content-Disposition:attachment;filename=export.csv'); - - $clients = Client::scope()->get(); - Utils::exportData($output, $clients->toArray()); - - $contacts = Contact::scope()->get(); - Utils::exportData($output, $contacts->toArray()); - - $invoices = Invoice::scope()->get(); - Utils::exportData($output, $invoices->toArray()); - - $invoiceItems = InvoiceItem::scope()->get(); - Utils::exportData($output, $invoiceItems->toArray()); - - $payments = Payment::scope()->get(); - Utils::exportData($output, $payments->toArray()); - - $credits = Credit::scope()->get(); - Utils::exportData($output, $credits->toArray()); - - fclose($output); - exit; - } - - private function importFile() - { - $data = Session::get('data'); - Session::forget('data'); - - $map = Input::get('map'); - $count = 0; - $hasHeaders = Input::get('header_checkbox'); - - $countries = Cache::get('countries'); - $countryMap = []; - - foreach ($countries as $country) { - $countryMap[strtolower($country->name)] = $country->id; - } - - foreach ($data as $row) { - if ($hasHeaders) { - $hasHeaders = false; - continue; - } - - $client = Client::createNew(); - $contact = Contact::createNew(); - $contact->is_primary = true; - $contact->send_invoice = true; - $count++; - - foreach ($row as $index => $value) { - $field = $map[$index]; - $value = trim($value); - - if ($field == Client::$fieldName && !$client->name) { - $client->name = $value; - } elseif ($field == Client::$fieldPhone && !$client->work_phone) { - $client->work_phone = $value; - } elseif ($field == Client::$fieldAddress1 && !$client->address1) { - $client->address1 = $value; - } elseif ($field == Client::$fieldAddress2 && !$client->address2) { - $client->address2 = $value; - } elseif ($field == Client::$fieldCity && !$client->city) { - $client->city = $value; - } elseif ($field == Client::$fieldState && !$client->state) { - $client->state = $value; - } elseif ($field == Client::$fieldPostalCode && !$client->postal_code) { - $client->postal_code = $value; - } elseif ($field == Client::$fieldCountry && !$client->country_id) { - $value = strtolower($value); - $client->country_id = isset($countryMap[$value]) ? $countryMap[$value] : null; - } elseif ($field == Client::$fieldNotes && !$client->private_notes) { - $client->private_notes = $value; - } elseif ($field == Contact::$fieldFirstName && !$contact->first_name) { - $contact->first_name = $value; - } elseif ($field == Contact::$fieldLastName && !$contact->last_name) { - $contact->last_name = $value; - } elseif ($field == Contact::$fieldPhone && !$contact->phone) { - $contact->phone = $value; - } elseif ($field == Contact::$fieldEmail && !$contact->email) { - $contact->email = strtolower($value); - } - } - - $client->save(); - $client->contacts()->save($contact); - Activity::createClient($client, false); - } - - $message = Utils::pluralize('created_client', $count); - Session::flash('message', $message); - - return Redirect::to('clients'); - } - - private function mapFile() - { - $file = Input::file('file'); - - if ($file == null) { - Session::flash('error', trans('texts.select_file')); - - return Redirect::to('company/import_export'); - } - - $name = $file->getRealPath(); - - require_once app_path().'/Includes/parsecsv.lib.php'; - $csv = new parseCSV(); - $csv->heading = false; - $csv->auto($name); - - if (count($csv->data) + Client::scope()->count() > Auth::user()->getMaxNumClients()) { - $message = trans('texts.limit_clients', ['count' => Auth::user()->getMaxNumClients()]); - Session::flash('error', $message); - - return Redirect::to('company/import_export'); - } - - Session::put('data', $csv->data); - - $headers = false; - $hasHeaders = false; - $mapped = array(); - $columns = array('', - Client::$fieldName, - Client::$fieldPhone, - Client::$fieldAddress1, - Client::$fieldAddress2, - Client::$fieldCity, - Client::$fieldState, - Client::$fieldPostalCode, - Client::$fieldCountry, - Client::$fieldNotes, - Contact::$fieldFirstName, - Contact::$fieldLastName, - Contact::$fieldPhone, - Contact::$fieldEmail, - ); - - if (count($csv->data) > 0) { - $headers = $csv->data[0]; - foreach ($headers as $title) { - if (strpos(strtolower($title), 'name') > 0) { - $hasHeaders = true; - break; - } - } - - for ($i = 0; $i Contact::$fieldFirstName, - 'last' => Contact::$fieldLastName, - 'email' => Contact::$fieldEmail, - 'mobile' => Contact::$fieldPhone, - 'phone' => Client::$fieldPhone, - 'name|organization' => Client::$fieldName, - 'street|address|address1' => Client::$fieldAddress1, - 'street2|address2' => Client::$fieldAddress2, - 'city' => Client::$fieldCity, - 'state|province' => Client::$fieldState, - 'zip|postal|code' => Client::$fieldPostalCode, - 'country' => Client::$fieldCountry, - 'note' => Client::$fieldNotes, - ); - - foreach ($map as $search => $column) { - foreach (explode("|", $search) as $string) { - if (strpos($title, 'sec') === 0) { - continue; - } - - if (strpos($title, $string) !== false) { - $mapped[$i] = $column; - break(2); - } - } - } - } - } - } - - $data = array( - 'data' => $csv->data, - 'headers' => $headers, - 'hasHeaders' => $hasHeaders, - 'columns' => $columns, - 'mapped' => $mapped, - ); - - return View::make('accounts.import_map', $data); + return Redirect::to('settings/'.ACCOUNT_INVOICE_DESIGN); } private function saveNotifications() { - $account = Auth::user()->account; - $account->invoice_terms = Input::get('invoice_terms'); - $account->invoice_footer = Input::get('invoice_footer'); - $account->email_footer = Input::get('email_footer'); - $account->save(); - $user = Auth::user(); $user->notify_sent = Input::get('notify_sent'); $user->notify_viewed = Input::get('notify_viewed'); @@ -626,43 +759,29 @@ class AccountController extends BaseController Session::flash('message', trans('texts.updated_settings')); - return Redirect::to('company/notifications'); + return Redirect::to('settings/'.ACCOUNT_NOTIFICATIONS); } private function saveDetails() { $rules = array( 'name' => 'required', - 'logo' => 'sometimes|max:1024|mimes:jpeg,gif,png', + 'logo' => 'sometimes|max:'.MAX_LOGO_FILE_SIZE.'|mimes:jpeg,gif,png', ); - $user = Auth::user()->account->users()->orderBy('id')->first(); - - if (Auth::user()->id === $user->id) { - $rules['email'] = 'email|required|unique:users,email,'.$user->id.',id'; - } - - $subdomain = preg_replace('/[^a-zA-Z0-9_\-]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); - if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { - $subdomain = null; - } - if ($subdomain) { - $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id"; - } - $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { - return Redirect::to('company/details') + return Redirect::to('settings/'.ACCOUNT_COMPANY_DETAILS) ->withErrors($validator) ->withInput(); } else { $account = Auth::user()->account; $account->name = trim(Input::get('name')); - $account->subdomain = $subdomain; $account->id_number = trim(Input::get('id_number')); $account->vat_number = trim(Input::get('vat_number')); $account->work_email = trim(Input::get('work_email')); + $account->website = trim(Input::get('website')); $account->work_phone = trim(Input::get('work_phone')); $account->address1 = trim(Input::get('address1')); $account->address2 = trim(Input::get('address2')); @@ -672,25 +791,9 @@ class AccountController extends BaseController $account->country_id = Input::get('country_id') ? Input::get('country_id') : null; $account->size_id = Input::get('size_id') ? Input::get('size_id') : null; $account->industry_id = Input::get('industry_id') ? Input::get('industry_id') : null; - $account->timezone_id = Input::get('timezone_id') ? Input::get('timezone_id') : null; - $account->date_format_id = Input::get('date_format_id') ? Input::get('date_format_id') : null; - $account->datetime_format_id = Input::get('datetime_format_id') ? Input::get('datetime_format_id') : null; - $account->currency_id = Input::get('currency_id') ? Input::get('currency_id') : 1; // US Dollar - $account->language_id = Input::get('language_id') ? Input::get('language_id') : 1; // English + $account->email_footer = Input::get('email_footer'); $account->save(); - if (Auth::user()->id === $user->id) { - $user->first_name = trim(Input::get('first_name')); - $user->last_name = trim(Input::get('last_name')); - $user->username = trim(Input::get('email')); - $user->email = trim(strtolower(Input::get('email'))); - $user->phone = trim(Input::get('phone')); - if (Utils::isNinjaDev()) { - $user->dark_mode = Input::get('dark_mode') ? true : false; - } - $user->save(); - } - /* Logo image file */ if ($file = Input::file('logo')) { $path = Input::file('logo')->getRealPath(); @@ -700,29 +803,94 @@ class AccountController extends BaseController $mimeType = $file->getMimeType(); if ($mimeType == 'image/jpeg') { - $file->move('logo/', $account->account_key . '.jpg'); - } else if ($mimeType == 'image/png') { - $file->move('logo/', $account->account_key . '.png'); + $path = 'logo/'.$account->account_key.'.jpg'; + $file->move('logo/', $account->account_key.'.jpg'); + } elseif ($mimeType == 'image/png') { + $path = 'logo/'.$account->account_key.'.png'; + $file->move('logo/', $account->account_key.'.png'); } else { if (extension_loaded('fileinfo')) { $image = Image::make($path); $image->resize(200, 120, function ($constraint) { $constraint->aspectRatio(); }); + $path = 'logo/'.$account->account_key.'.jpg'; Image::canvas($image->width(), $image->height(), '#FFFFFF') - ->insert($image)->save('logo/'.$account->account_key.'.jpg'); + ->insert($image)->save($path); } else { Session::flash('warning', 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.'); } } + + // make sure image isn't interlaced + if (extension_loaded('fileinfo')) { + $img = Image::make($path); + $img->interlace(false); + $img->save(); + } } + event(new UserSettingsChanged()); + Session::flash('message', trans('texts.updated_settings')); - return Redirect::to('company/details'); + return Redirect::to('settings/'.ACCOUNT_COMPANY_DETAILS); } } + private function saveUserDetails() + { + $user = Auth::user(); + $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id']; + $validator = Validator::make(Input::all(), $rules); + + if ($validator->fails()) { + return Redirect::to('settings/'.ACCOUNT_USER_DETAILS) + ->withErrors($validator) + ->withInput(); + } else { + $user->first_name = trim(Input::get('first_name')); + $user->last_name = trim(Input::get('last_name')); + $user->username = trim(Input::get('email')); + $user->email = trim(strtolower(Input::get('email'))); + $user->phone = trim(Input::get('phone')); + + if (Utils::isNinja()) { + if (Input::get('referral_code') && !$user->referral_code) { + $user->referral_code = $this->accountRepo->getReferralCode(); + } + } + if (Utils::isNinjaDev()) { + $user->dark_mode = Input::get('dark_mode') ? true : false; + } + + $user->save(); + + event(new UserSettingsChanged()); + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_USER_DETAILS); + } + } + + private function saveLocalization() + { + $account = Auth::user()->account; + $account->timezone_id = Input::get('timezone_id') ? Input::get('timezone_id') : null; + $account->date_format_id = Input::get('date_format_id') ? Input::get('date_format_id') : null; + $account->datetime_format_id = Input::get('datetime_format_id') ? Input::get('datetime_format_id') : null; + $account->currency_id = Input::get('currency_id') ? Input::get('currency_id') : 1; // US Dollar + $account->language_id = Input::get('language_id') ? Input::get('language_id') : 1; // English + $account->military_time = Input::get('military_time') ? true : false; + $account->save(); + + event(new UserSettingsChanged()); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_LOCALIZATION); + } + public function removeLogo() { File::delete('logo/'.Auth::user()->account->account_key.'.jpg'); @@ -730,7 +898,7 @@ class AccountController extends BaseController Session::flash('message', trans('texts.removed_logo')); - return Redirect::to('company/details'); + return Redirect::to('settings/'.ACCOUNT_COMPANY_DETAILS); } public function checkEmail() @@ -766,35 +934,22 @@ class AccountController extends BaseController $user->username = $user->email; $user->password = bcrypt(trim(Input::get('new_password'))); $user->registered = true; - $user->save(); - - if (Utils::isNinjaProd()) { - $this->userMailer->sendConfirmation($user); - } - - $activities = Activity::scope()->get(); - foreach ($activities as $activity) { - $activity->message = str_replace('Guest', $user->getFullName(), $activity->message); - $activity->save(); - } + $user->save(); if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); } - Session::set(SESSION_COUNTER, -1); - return "{$user->first_name} {$user->last_name}"; } public function doRegister() { $affiliate = Affiliate::where('affiliate_key', '=', SELF_HOST_AFFILIATE_KEY)->first(); - $email = trim(Input::get('email')); - if (!$email || $email == 'user@example.com') { - return ''; + if (!$email || $email == TEST_USERNAME) { + return RESULT_FAILURE; } $license = new License(); @@ -808,7 +963,7 @@ class AccountController extends BaseController $license->is_claimed = 1; $license->save(); - return ''; + return RESULT_SUCCESS; } public function cancelAccount() @@ -824,7 +979,10 @@ class AccountController extends BaseController $this->userMailer->sendTo(CONTACT_EMAIL, $email, $name, 'Invoice Ninja Feedback [Canceled Account]', 'contact', $data); } + $user = Auth::user(); $account = Auth::user()->account; + \Log::info("Canceled Account: {$account->name} - {$user->email}"); + $this->accountRepo->unlinkAccount($account); $account->forceDelete(); @@ -839,6 +997,26 @@ class AccountController extends BaseController $user = Auth::user(); $this->userMailer->sendConfirmation($user); - return Redirect::to('/company/details')->with('message', trans('texts.confirmation_resent')); + return Redirect::to('/settings/'.ACCOUNT_USER_DETAILS)->with('message', trans('texts.confirmation_resent')); + } + + public function redirectLegacy($section, $subSection = false) + { + if ($section === 'details') { + $section = ACCOUNT_COMPANY_DETAILS; + } elseif ($section === 'payments') { + $section = ACCOUNT_PAYMENTS; + } elseif ($section === 'advanced_settings') { + $section = $subSection; + if ($section === 'token_management') { + $section = ACCOUNT_API_TOKENS; + } + } + + if (!in_array($section, array_merge(Account::$basicSettings, Account::$advancedSettings))) { + $section = ACCOUNT_COMPANY_DETAILS; + } + + return Redirect::to("/settings/$section/", 301); } } diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index ff4c84c649be..86c76ecf2760 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -16,59 +16,43 @@ use App\Models\Account; use App\Models\AccountGateway; use App\Ninja\Repositories\AccountRepository; +use App\Services\AccountGatewayService; class AccountGatewayController extends BaseController { + protected $accountGatewayService; + + public function __construct(AccountGatewayService $accountGatewayService) + { + parent::__construct(); + + $this->accountGatewayService = $accountGatewayService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_PAYMENTS); + } + public function getDatatable() { - $query = DB::table('account_gateways') - ->join('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') - ->where('account_gateways.deleted_at', '=', null) - ->where('account_gateways.account_id', '=', Auth::user()->account_id) - ->select('account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); - - return Datatable::query($query) - ->addColumn('name', function ($model) { return link_to('gateways/'.$model->public_id.'/edit', $model->name); }) - ->addColumn('payment_type', function ($model) { return Gateway::getPrettyPaymentType($model->gateway_id); }) - ->addColumn('dropdown', function ($model) { - $actions = ''; - - return $actions; - }) - ->orderColumns(['name']) - ->make(); + return $this->accountGatewayService->getDatatable(Auth::user()->account_id); } public function edit($publicId) { $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); - $config = $accountGateway->config; - $selectedCards = $accountGateway->accepted_credit_cards; + $config = $accountGateway->getConfig(); - $configFields = json_decode($config); - - foreach ($configFields as $configField => $value) { - $configFields->$configField = str_repeat('*', strlen($value)); + foreach ($config as $field => $value) { + $config->$field = str_repeat('*', strlen($value)); } $data = self::getViewModel($accountGateway); $data['url'] = 'gateways/'.$publicId; $data['method'] = 'PUT'; $data['title'] = trans('texts.edit_gateway') . ' - ' . $accountGateway->gateway->name; - $data['config'] = $configFields; + $data['config'] = $config; $data['hiddenFields'] = Gateway::$hiddenFields; $data['paymentTypeId'] = $accountGateway->getPaymentType(); $data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get(); @@ -97,7 +81,12 @@ class AccountGatewayController extends BaseController $data['url'] = 'gateways'; $data['method'] = 'POST'; $data['title'] = trans('texts.add_gateway'); - $data['selectGateways'] = Gateway::where('payment_library_id', '=', 1)->where('id', '!=', GATEWAY_PAYPAL_EXPRESS)->where('id', '!=', GATEWAY_PAYPAL_EXPRESS)->orderBy('name')->get(); + $data['selectGateways'] = Gateway::where('payment_library_id', '=', 1) + ->where('id', '!=', GATEWAY_PAYPAL_EXPRESS) + ->where('id', '!=', GATEWAY_BITPAY) + ->where('id', '!=', GATEWAY_GOCARDLESS) + ->where('id', '!=', GATEWAY_DWOLLA) + ->orderBy('name')->get(); $data['hiddenFields'] = Gateway::$hiddenFields; return View::make('accounts.account_gateway', $data); @@ -116,6 +105,9 @@ class AccountGatewayController extends BaseController if ($type == PAYMENT_TYPE_BITCOIN) { $paymentTypes[$type] .= ' - BitPay'; } + if ($type == PAYMENT_TYPE_DIRECT_DEBIT) { + $paymentTypes[$type] .= ' - GoCardless'; + } } } @@ -155,21 +147,20 @@ class AccountGatewayController extends BaseController 'gateways' => $gateways, 'creditCardTypes' => $creditCards, 'tokenBillingOptions' => $tokenBillingOptions, - 'showBreadcrumbs' => false, 'countGateways' => count($currentGateways) ]; } - public function delete() + + public function bulk() { - $accountGatewayPublicId = Input::get('accountGatewayPublicId'); - $gateway = AccountGateway::scope($accountGatewayPublicId)->firstOrFail(); + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->accountGatewayService->bulk($ids, $action); - $gateway->delete(); + Session::flash('message', trans('texts.archived_account_gateway')); - Session::flash('message', trans('texts.deleted_gateway')); - - return Redirect::to('company/payments'); + return Redirect::to('settings/' . ACCOUNT_PAYMENTS); } /** @@ -186,6 +177,8 @@ class AccountGatewayController extends BaseController $gatewayId = GATEWAY_PAYPAL_EXPRESS; } elseif ($paymentType == PAYMENT_TYPE_BITCOIN) { $gatewayId = GATEWAY_BITPAY; + } elseif ($paymentType == PAYMENT_TYPE_DIRECT_DEBIT) { + $gatewayId = GATEWAY_GOCARDLESS; } elseif ($paymentType == PAYMENT_TYPE_DWOLLA) { $gatewayId = GATEWAY_DWOLLA; } @@ -229,7 +222,7 @@ class AccountGatewayController extends BaseController if ($accountGatewayPublicId) { $accountGateway = AccountGateway::scope($accountGatewayPublicId)->firstOrFail(); - $oldConfig = json_decode($accountGateway->config); + $oldConfig = $accountGateway->getConfig(); } else { $accountGateway = AccountGateway::createNew(); $accountGateway->gateway_id = $gatewayId; @@ -239,7 +232,7 @@ class AccountGatewayController extends BaseController foreach ($fields as $field => $details) { $value = trim(Input::get($gateway->id.'_'.$field)); // if the new value is masked use the original value - if ($value && $value === str_repeat('*', strlen($value))) { + if ($oldConfig && $value && $value === str_repeat('*', strlen($value))) { $value = $oldConfig->$field; } if (!$value && ($field == 'testMode' || $field == 'developerMode')) { @@ -249,6 +242,13 @@ class AccountGatewayController extends BaseController } } + $publishableKey = Input::get('publishable_key'); + if ($publishableKey = str_replace('*', '', $publishableKey)) { + $config->publishableKey = $publishableKey; + } elseif ($oldConfig && property_exists($oldConfig, 'publishableKey')) { + $config->publishableKey = $oldConfig->publishableKey; + } + $cardCount = 0; if ($creditcards) { foreach ($creditcards as $card => $value) { @@ -259,7 +259,7 @@ class AccountGatewayController extends BaseController $accountGateway->accepted_credit_cards = $cardCount; $accountGateway->show_address = Input::get('show_address') ? true : false; $accountGateway->update_address = Input::get('update_address') ? true : false; - $accountGateway->config = json_encode($config); + $accountGateway->setConfig($config); if ($accountGatewayPublicId) { $accountGateway->save(); diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php index 9704b3adaaad..44d87429d691 100644 --- a/app/Http/Controllers/ActivityController.php +++ b/app/Http/Controllers/ActivityController.php @@ -5,29 +5,23 @@ use DB; use Datatable; use Utils; use View; +use App\Models\Client; +use App\Models\Activity; +use App\Services\ActivityService; class ActivityController extends BaseController { + protected $activityService; + + public function __construct(ActivityService $activityService) + { + parent::__construct(); + + $this->activityService = $activityService; + } + public function getDatatable($clientPublicId) { - $query = DB::table('activities') - ->join('clients', 'clients.id', '=', 'activities.client_id') - ->where('clients.public_id', '=', $clientPublicId) - ->where('activities.account_id', '=', Auth::user()->account_id) - ->select('activities.id', 'activities.message', 'activities.created_at', 'clients.currency_id', 'activities.balance', 'activities.adjustment'); - - return Datatable::query($query) - ->addColumn('activities.id', function ($model) { return Utils::timestampToDateTimeString(strtotime($model->created_at)); }) - ->addColumn('message', function ($model) { return Utils::decodeActivity($model->message); }) - ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); }) - ->addColumn('adjustment', function ($model) { return $model->adjustment != 0 ? self::wrapAdjustment($model->adjustment, $model->currency_id) : ''; }) - ->make(); - } - - private function wrapAdjustment($adjustment, $currencyId) - { - $class = $adjustment <= 0 ? 'success' : 'default'; - $adjustment = Utils::formatMoney($adjustment, $currencyId); - return "

$adjustment

"; + return $this->activityService->getDatatable($clientPublicId); } } diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index 703bd5fdd5b7..35f1f9b0a5be 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -9,27 +9,32 @@ use Exception; use Input; use Utils; use View; +use Event; use Session; use Cookie; use Response; +use Redirect; use App\Models\User; use App\Models\Account; use App\Models\Industry; use App\Ninja\Mailers\Mailer; use App\Ninja\Repositories\AccountRepository; -use Redirect; +use App\Events\UserSettingsChanged; +use App\Services\EmailService; class AppController extends BaseController { protected $accountRepo; protected $mailer; + protected $emailService; - public function __construct(AccountRepository $accountRepo, Mailer $mailer) + public function __construct(AccountRepository $accountRepo, Mailer $mailer, EmailService $emailService) { parent::__construct(); $this->accountRepo = $accountRepo; $this->mailer = $mailer; + $this->emailService = $emailService; } public function showSetup() @@ -43,7 +48,7 @@ class AppController extends BaseController public function doSetup() { - if (Utils::isNinjaProd() || (Utils::isDatabaseSetup() && Account::count() > 0)) { + if (Utils::isNinjaProd()) { return Redirect::to('/'); } @@ -51,10 +56,11 @@ class AppController extends BaseController $test = Input::get('test'); $app = Input::get('app'); - $app['key'] = str_random(RANDOM_KEY_LENGTH); + $app['key'] = env('APP_KEY') ?: str_random(RANDOM_KEY_LENGTH); + $app['debug'] = Input::get('debug') ? 'true' : 'false'; $database = Input::get('database'); - $dbType = $database['default']; + $dbType = 'mysql'; // $database['default']; $database['connections'] = [$dbType => $database['type']]; $mail = Input::get('mail'); @@ -73,8 +79,12 @@ class AppController extends BaseController return Redirect::to('/setup')->withInput(); } + if (Utils::isDatabaseSetup() && Account::count() > 0) { + return Redirect::to('/'); + } + $config = "APP_ENV=production\n". - "APP_DEBUG=false\n". + "APP_DEBUG={$app['debug']}\n". "APP_URL={$app['url']}\n". "APP_KEY={$app['key']}\n\n". "DB_TYPE={$dbType}\n". @@ -88,7 +98,8 @@ class AppController extends BaseController "MAIL_HOST={$mail['host']}\n". "MAIL_USERNAME={$mail['username']}\n". "MAIL_FROM_NAME={$mail['from']['name']}\n". - "MAIL_PASSWORD={$mail['password']}"; + "MAIL_PASSWORD={$mail['password']}\n\n". + "PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'"; // Write Config Settings $fp = fopen(base_path()."/.env", 'w'); @@ -114,17 +125,68 @@ class AppController extends BaseController return Redirect::to('/login'); } + public function updateSetup() + { + if (Utils::isNinjaProd()) { + return Redirect::to('/'); + } + + if (!Auth::check() && Utils::isDatabaseSetup() && Account::count() > 0) { + return Redirect::to('/'); + } + + if ( ! $canUpdateEnv = @fopen(base_path()."/.env", 'w')) { + Session::flash('error', 'Warning: Permission denied to write to .env config file, try running sudo chown www-data:www-data /path/to/ninja/.env'); + return Redirect::to('/settings/system_settings'); + } + + $app = Input::get('app'); + $db = Input::get('database'); + $mail = Input::get('mail'); + + $_ENV['APP_URL'] = $app['url']; + $_ENV['APP_DEBUG'] = Input::get('debug') ? 'true' : 'false'; + + $_ENV['DB_TYPE'] = 'mysql'; // $db['default']; + $_ENV['DB_HOST'] = $db['type']['host']; + $_ENV['DB_DATABASE'] = $db['type']['database']; + $_ENV['DB_USERNAME'] = $db['type']['username']; + $_ENV['DB_PASSWORD'] = $db['type']['password']; + + if ($mail) { + $_ENV['MAIL_DRIVER'] = $mail['driver']; + $_ENV['MAIL_PORT'] = $mail['port']; + $_ENV['MAIL_ENCRYPTION'] = $mail['encryption']; + $_ENV['MAIL_HOST'] = $mail['host']; + $_ENV['MAIL_USERNAME'] = $mail['username']; + $_ENV['MAIL_FROM_NAME'] = $mail['from']['name']; + $_ENV['MAIL_PASSWORD'] = $mail['password']; + $_ENV['MAIL_FROM_ADDRESS'] = $mail['username']; + } + + $config = ''; + foreach ($_ENV as $key => $val) { + $config .= "{$key}={$val}\n"; + } + + $fp = fopen(base_path()."/.env", 'w'); + fwrite($fp, $config); + fclose($fp); + + Session::flash('message', trans('texts.updated_settings')); + return Redirect::to('/settings/system_settings'); + } + private function testDatabase($database) { - $dbType = $database['default']; - + $dbType = 'mysql'; // $database['default']; Config::set('database.default', $dbType); - foreach ($database['connections'][$dbType] as $key => $val) { Config::set("database.connections.{$dbType}.{$key}", $val); } - + try { + DB::reconnect(); $valid = DB::connection()->getDatabaseName() ? true : false; } catch (Exception $e) { return $e->getMessage(); @@ -179,10 +241,13 @@ class AppController extends BaseController { if (!Utils::isNinjaProd()) { try { + Cache::flush(); + Session::flush(); + Artisan::call('optimize', array('--force' => true)); Artisan::call('migrate', array('--force' => true)); Artisan::call('db:seed', array('--force' => true, '--class' => 'PaymentLibrariesSeeder')); - Artisan::call('optimize', array('--force' => true)); - Cache::flush(); + Artisan::call('db:seed', array('--force' => true, '--class' => 'FontsSeeder')); + Event::fire(new UserSettingsChanged()); Session::flash('message', trans('texts.processed_updates')); } catch (Exception $e) { Response::make($e->getMessage(), 500); @@ -191,4 +256,19 @@ class AppController extends BaseController return Redirect::to('/'); } + + public function emailBounced() + { + $messageId = Input::get('MessageID'); + $error = Input::get('Name') . ': ' . Input::get('Description'); + return $this->emailService->markBounced($messageId, $error) ? RESULT_SUCCESS : RESULT_FAILURE; + } + + public function emailOpened() + { + $messageId = Input::get('MessageID'); + return $this->emailService->markOpened($messageId) ? RESULT_SUCCESS : RESULT_FAILURE; + + return RESULT_SUCCESS; + } } diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index a5897ac6879d..5ed231cedc82 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -9,6 +9,7 @@ use App\Models\User; use App\Events\UserLoggedIn; use App\Http\Controllers\Controller; use App\Ninja\Repositories\AccountRepository; +use App\Services\AuthService; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Registrar; use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers; @@ -30,6 +31,7 @@ class AuthController extends Controller { protected $loginPath = '/login'; protected $redirectTo = '/dashboard'; + protected $authService; protected $accountRepo; /** @@ -39,15 +41,29 @@ class AuthController extends Controller { * @param \Illuminate\Contracts\Auth\Registrar $registrar * @return void */ - public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo) + public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo, AuthService $authService) { $this->auth = $auth; $this->registrar = $registrar; $this->accountRepo = $repo; + $this->authService = $authService; //$this->middleware('guest', ['except' => 'getLogout']); } + public function authLogin($provider, Request $request) + { + return $this->authService->execute($provider, $request->has('code')); + } + + public function authUnlink() + { + $this->accountRepo->unlinkUserFromOauth(Auth::user()); + + Session::flash('message', trans('texts.updated_settings')); + return redirect()->to('/settings/' . ACCOUNT_USER_DETAILS); + } + public function getLoginWrapper() { if (!Utils::isNinja() && !User::count()) { @@ -59,11 +75,12 @@ class AuthController extends Controller { public function postLoginWrapper(Request $request) { + $userId = Auth::check() ? Auth::user()->id : null; $user = User::where('email', '=', $request->input('email'))->first(); - if ($user && $user->failed_logins >= 3) { - Session::flash('error', 'These credentials do not match our records.'); + if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) { + Session::flash('error', trans('texts.invalid_credentials')); return redirect()->to('login'); } @@ -74,15 +91,15 @@ class AuthController extends Controller { $users = false; // we're linking a new account - if ($userId && Auth::user()->id != $userId) { + if ($request->link_accounts && $userId && Auth::user()->id != $userId) { $users = $this->accountRepo->associateAccounts($userId, Auth::user()->id); Session::flash('message', trans('texts.associated_accounts')); // check if other accounts are linked } else { $users = $this->accountRepo->loadAccounts(Auth::user()->id); } - Session::put(SESSION_USER_ACCOUNTS, $users); + } elseif ($user) { $user->failed_logins = $user->failed_logins + 1; $user->save(); @@ -91,6 +108,7 @@ class AuthController extends Controller { return $response; } + public function getLogoutWrapper() { if (Auth::check() && !Auth::user()->registered) { diff --git a/app/Http/Controllers/BankAccountController.php b/app/Http/Controllers/BankAccountController.php new file mode 100644 index 000000000000..3b0f190ef1ec --- /dev/null +++ b/app/Http/Controllers/BankAccountController.php @@ -0,0 +1,152 @@ +bankAccountService = $bankAccountService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_BANKS); + } + + public function getDatatable() + { + return $this->bankAccountService->getDatatable(Auth::user()->account_id); + } + + public function edit($publicId) + { + $bankAccount = BankAccount::scope($publicId)->firstOrFail(); + $bankAccount->username = str_repeat('*', 16); + + $data = [ + 'url' => 'bank_accounts/' . $publicId, + 'method' => 'PUT', + 'title' => trans('texts.edit_bank_account'), + 'banks' => Cache::get('banks'), + 'bankAccount' => $bankAccount, + ]; + + return View::make('accounts.bank_account', $data); + } + + public function update($publicId) + { + return $this->save($publicId); + } + + public function store() + { + return $this->save(); + } + + /** + * Displays the form for account creation + * + */ + public function create() + { + $data = [ + 'url' => 'bank_accounts', + 'method' => 'POST', + 'title' => trans('texts.add_bank_account'), + 'banks' => Cache::get('banks'), + 'bankAccount' => null, + ]; + + return View::make('accounts.bank_account', $data); + } + + public function bulk() + { + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->bankAccountService->bulk($ids, $action); + + Session::flash('message', trans('texts.archived_bank_account')); + + return Redirect::to('settings/' . ACCOUNT_BANKS); + } + + /** + * Stores new account + * + */ + public function save($bankAccountPublicId = false) + { + $account = Auth::user()->account; + $bankId = Input::get('bank_id'); + $username = Input::get('bank_username'); + + $rules = [ + 'bank_id' => $bankAccountPublicId ? '' : 'required', + 'bank_username' => 'required', + ]; + + $validator = Validator::make(Input::all(), $rules); + + if ($validator->fails()) { + return Redirect::to('bank_accounts/create') + ->withErrors($validator) + ->withInput(); + } else { + if ($bankAccountPublicId) { + $bankAccount = BankAccount::scope($bankAccountPublicId)->firstOrFail(); + } else { + $bankAccount = BankAccount::createNew(); + $bankAccount->bank_id = $bankId; + } + + if ($username != str_repeat('*', strlen($username))) { + $bankAccount->username = Crypt::encrypt(trim($username)); + } + + if ($bankAccountPublicId) { + $bankAccount->save(); + $message = trans('texts.updated_bank_account'); + } else { + $account->bank_accounts()->save($bankAccount); + $message = trans('texts.created_bank_account'); + } + + Session::flash('message', $message); + return Redirect::to("bank_accounts/{$bankAccount->public_id}/edit"); + } + } + + public function test() + { + $bankId = Input::get('bank_id'); + $username = Input::get('bank_username'); + $password = Input::get('bank_password'); + + return json_encode($this->bankAccountService->loadBankAccounts($bankId, $username, $password, false)); + } + +} diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php new file mode 100644 index 000000000000..4d783556022e --- /dev/null +++ b/app/Http/Controllers/BaseAPIController.php @@ -0,0 +1,135 @@ +manager = new Manager(); + + if ($include = Request::get('include')) { + $this->manager->parseIncludes($include); + } + + $this->serializer = Request::get('serializer') ?: API_SERIALIZER_ARRAY; + + if ($this->serializer === API_SERIALIZER_JSON) { + $this->manager->setSerializer(new JsonApiSerializer()); + } else { + $this->manager->setSerializer(new ArraySerializer()); + } + } + + protected function createItem($data, $transformer, $entityType) + { + if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) { + $entityType = null; + } + + $resource = new Item($data, $transformer, $entityType); + return $this->manager->createData($resource)->toArray(); + } + + protected function createCollection($data, $transformer, $entityType, $paginator = false) + { + if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) { + $entityType = null; + } + + $resource = new Collection($data, $transformer, $entityType); + + if ($paginator) { + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + } + + return $this->manager->createData($resource)->toArray(); + } + + protected function response($response) + { + $index = Request::get('index') ?: 'data'; + $meta = isset($response['meta']) ? $response['meta'] : null; + $response = [ + $index => $response + ]; + if ($meta) { + $response['meta'] = $meta; + unset($response[$index]['meta']); + } + + $response = json_encode($response, JSON_PRETTY_PRINT); + $headers = Utils::getApiHeaders(); + + return Response::make($response, 200, $headers); + } + + protected function getIncluded() + { + $data = ['user']; + + $included = Request::get('include'); + $included = explode(',', $included); + + foreach ($included as $include) { + if ($include == 'invoices') { + $data[] = 'invoices.invoice_items'; + $data[] = 'invoices.user'; + } elseif ($include == 'clients') { + $data[] = 'clients.contacts'; + $data[] = 'clients.user'; + } elseif ($include == 'vendors') { + $data[] = 'vendors.vendorcontacts'; + $data[] = 'vendors.user'; + } + elseif ($include) { + $data[] = $include; + } + } + + return $data; + } +} diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 0cc63c7c5a1a..1a2f6c8dc526 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -1,7 +1,11 @@ clientRepo = $clientRepo; } @@ -22,37 +28,80 @@ class ClientApiController extends Controller return Response::make('', 200, $headers); } + /** + * @SWG\Get( + * path="/clients", + * summary="List of clients", + * tags={"client"}, + * @SWG\Response( + * response=200, + * description="A list with clients", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Client")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { $clients = Client::scope() - ->with('country', 'contacts', 'industry', 'size', 'currency') - ->orderBy('created_at', 'desc') - ->get(); - $clients = Utils::remapPublicIds($clients); + ->with($this->getIncluded()) + ->orderBy('created_at', 'desc'); - $response = json_encode($clients, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(count($clients)); + // Filter by email + if (Input::has('email')) { - return Response::make($response, 200, $headers); + $email = Input::get('email'); + $clients = $clients->whereHas('contacts', function ($query) use ($email) { + $query->where('email', $email); + }); + + } + + $clients = $clients->paginate(); + + $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); + $paginator = Client::scope()->paginate(); + + $data = $this->createCollection($clients, $transformer, ENTITY_CLIENT, $paginator); + + return $this->response($data); } - public function store() + /** + * @SWG\Post( + * path="/clients", + * tags={"client"}, + * summary="Create a client", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Client") + * ), + * @SWG\Response( + * response=200, + * description="New client", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Client")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store(CreateClientRequest $request) { - $data = Input::all(); - $error = $this->clientRepo->getErrors($data); + $client = $this->clientRepo->save($request->input()); - if ($error) { - $headers = Utils::getApiHeaders(); + $client = Client::scope($client->public_id) + ->with('country', 'contacts', 'industry', 'size', 'currency') + ->first(); - return Response::make($error, 500, $headers); - } else { - $client = $this->clientRepo->save(isset($data['id']) ? $data['id'] : false, $data, false); - $client = Client::scope($client->public_id)->with('country', 'contacts', 'industry', 'size', 'currency')->first(); - $client = Utils::remapPublicIds([$client]); - $response = json_encode($client, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(); + $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($client, $transformer, ENTITY_CLIENT); - return Response::make($response, 200, $headers); - } + return $this->response($data); } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 065e3e073a33..84145afe9c1c 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -13,6 +13,7 @@ use Cache; use App\Models\Activity; use App\Models\Client; +use App\Models\Account; use App\Models\Contact; use App\Models\Invoice; use App\Models\Size; @@ -21,18 +22,23 @@ use App\Models\Industry; use App\Models\Currency; use App\Models\Country; use App\Models\Task; - use App\Ninja\Repositories\ClientRepository; +use App\Services\ClientService; + +use App\Http\Requests\CreateClientRequest; +use App\Http\Requests\UpdateClientRequest; class ClientController extends BaseController { + protected $clientService; protected $clientRepo; - public function __construct(ClientRepository $clientRepo) + public function __construct(ClientRepository $clientRepo, ClientService $clientService) { parent::__construct(); $this->clientRepo = $clientRepo; + $this->clientService = $clientService; } /** @@ -46,56 +52,22 @@ class ClientController extends BaseController 'entityType' => ENTITY_CLIENT, 'title' => trans('texts.clients'), 'sortCol' => '4', - 'columns' => Utils::trans(['checkbox', 'client', 'contact', 'email', 'date_created', 'last_login', 'balance', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'client', + 'contact', + 'email', + 'date_created', + 'last_login', + 'balance', + '' + ]), )); } public function getDatatable() { - $clients = $this->clientRepo->find(Input::get('sSearch')); - - return Datatable::query($clients) - ->addColumn('checkbox', function ($model) { return ''; }) - ->addColumn('name', function ($model) { return link_to('clients/'.$model->public_id, $model->name); }) - ->addColumn('first_name', function ($model) { return link_to('clients/'.$model->public_id, $model->first_name.' '.$model->last_name); }) - ->addColumn('email', function ($model) { return link_to('clients/'.$model->public_id, $model->email); }) - ->addColumn('clients.created_at', function ($model) { return Utils::timestampToDateString(strtotime($model->created_at)); }) - ->addColumn('last_login', function ($model) { return Utils::timestampToDateString(strtotime($model->last_login)); }) - ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); }) - ->addColumn('dropdown', function ($model) { - if ($model->is_deleted) { - return '
'; - } - - $str = ''; - }) - ->make(); + return $this->clientService->getDatatable(Input::get('sSearch')); } /** @@ -103,9 +75,13 @@ class ClientController extends BaseController * * @return Response */ - public function store() + public function store(CreateClientRequest $request) { - return $this->save(); + $client = $this->clientService->save($request->input()); + + Session::flash('message', trans('texts.created_client')); + + return redirect()->to($client->getRoute()); } /** @@ -128,8 +104,10 @@ class ClientController extends BaseController } array_push($actionLinks, + \DropdownButton::DIVIDER, ['label' => trans('texts.enter_payment'), 'url' => '/payments/create/'.$client->public_id], - ['label' => trans('texts.enter_credit'), 'url' => '/credits/create/'.$client->public_id] + ['label' => trans('texts.enter_credit'), 'url' => '/credits/create/'.$client->public_id], + ['label' => trans('texts.enter_expense'), 'url' => '/expenses/create/0/'.$client->public_id] ); $data = array( @@ -188,16 +166,25 @@ class ClientController extends BaseController $data = array_merge($data, self::getViewModel()); + if (Auth::user()->account->isNinjaAccount()) { + if ($account = Account::whereId($client->public_id)->first()) { + $data['proPlanPaid'] = $account['pro_plan_paid']; + } + } + return View::make('clients.edit', $data); } private static function getViewModel() { return [ + 'data' => Input::old('data'), + 'account' => Auth::user()->account, 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), 'countries' => Cache::get('countries'), 'customLabel1' => Auth::user()->account->custom_client_label1, 'customLabel2' => Auth::user()->account->custom_client_label2, @@ -210,97 +197,20 @@ class ClientController extends BaseController * @param int $id * @return Response */ - public function update($publicId) + public function update(UpdateClientRequest $request) { - return $this->save($publicId); - } - - private function save($publicId = null) - { - $rules = array( - 'email' => 'email|required_without:first_name', - 'first_name' => 'required_without:email', - ); - $validator = Validator::make(Input::all(), $rules); - - if ($validator->fails()) { - $url = $publicId ? 'clients/'.$publicId.'/edit' : 'clients/create'; - - return Redirect::to($url) - ->withErrors($validator) - ->withInput(Input::except('password')); - } else { - if ($publicId) { - $client = Client::scope($publicId)->firstOrFail(); - } else { - $client = Client::createNew(); - } - - $client->name = trim(Input::get('name')); - $client->id_number = trim(Input::get('id_number')); - $client->vat_number = trim(Input::get('vat_number')); - $client->work_phone = trim(Input::get('work_phone')); - $client->custom_value1 = trim(Input::get('custom_value1')); - $client->custom_value2 = trim(Input::get('custom_value2')); - $client->address1 = trim(Input::get('address1')); - $client->address2 = trim(Input::get('address2')); - $client->city = trim(Input::get('city')); - $client->state = trim(Input::get('state')); - $client->postal_code = trim(Input::get('postal_code')); - $client->country_id = Input::get('country_id') ?: null; - $client->private_notes = trim(Input::get('private_notes')); - $client->size_id = Input::get('size_id') ?: null; - $client->industry_id = Input::get('industry_id') ?: null; - $client->currency_id = Input::get('currency_id') ?: null; - $client->payment_terms = Input::get('payment_terms') ?: 0; - $client->website = trim(Input::get('website')); - - $client->save(); - - $data = json_decode(Input::get('data')); - $contactIds = []; - $isPrimary = true; - - foreach ($data->contacts as $contact) { - if (isset($contact->public_id) && $contact->public_id) { - $record = Contact::scope($contact->public_id)->firstOrFail(); - } else { - $record = Contact::createNew(); - } - - $record->email = trim($contact->email); - $record->first_name = trim($contact->first_name); - $record->last_name = trim($contact->last_name); - $record->phone = trim($contact->phone); - $record->is_primary = $isPrimary; - $isPrimary = false; - - $client->contacts()->save($record); - $contactIds[] = $record->public_id; - } - - foreach ($client->contacts as $contact) { - if (!in_array($contact->public_id, $contactIds)) { - $contact->delete(); - } - } - - if ($publicId) { - Session::flash('message', trans('texts.updated_client')); - } else { - Activity::createClient($client); - Session::flash('message', trans('texts.created_client')); - } - - return Redirect::to('clients/'.$client->public_id); - } + $client = $this->clientService->save($request->input()); + + Session::flash('message', trans('texts.updated_client')); + + return redirect()->to($client->getRoute()); } public function bulk() { $action = Input::get('action'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->clientRepo->bulk($ids, $action); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + $count = $this->clientService->bulk($ids, $action); $message = Utils::pluralize($action.'d_client', $count); Session::flash('message', $message); diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php index 505400718caf..e08c136b5267 100644 --- a/app/Http/Controllers/CreditController.php +++ b/app/Http/Controllers/CreditController.php @@ -4,22 +4,26 @@ use Datatable; use Input; use Redirect; use Session; +use URL; use Utils; use View; use Validator; use App\Models\Client; - +use App\Services\CreditService; use App\Ninja\Repositories\CreditRepository; +use App\Http\Requests\CreateCreditRequest; class CreditController extends BaseController { protected $creditRepo; + protected $creditService; - public function __construct(CreditRepository $creditRepo) + public function __construct(CreditRepository $creditRepo, CreditService $creditService) { parent::__construct(); $this->creditRepo = $creditRepo; + $this->creditService = $creditService; } /** @@ -33,46 +37,21 @@ class CreditController extends BaseController 'entityType' => ENTITY_CREDIT, 'title' => trans('texts.credits'), 'sortCol' => '4', - 'columns' => Utils::trans(['checkbox', 'client', 'credit_amount', 'credit_balance', 'credit_date', 'private_notes', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'client', + 'credit_amount', + 'credit_balance', + 'credit_date', + 'private_notes', + '' + ]), )); } public function getDatatable($clientPublicId = null) { - $credits = $this->creditRepo->find($clientPublicId, Input::get('sSearch')); - - $table = Datatable::query($credits); - - if (!$clientPublicId) { - $table->addColumn('checkbox', function ($model) { return ''; }) - ->addColumn('client_name', function ($model) { return link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)); }); - } - - return $table->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id).''; }) - ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); }) - ->addColumn('credit_date', function ($model) { return Utils::fromSqlDate($model->credit_date); }) - ->addColumn('private_notes', function ($model) { return $model->private_notes; }) - ->addColumn('dropdown', function ($model) { - if ($model->is_deleted) { - return '
'; - } - - $str = ''; - }) - ->make(); + return $this->creditService->getDatatable($clientPublicId, Input::get('sSearch')); } public function create($clientPublicId = 0) @@ -106,46 +85,20 @@ class CreditController extends BaseController return View::make('credit.edit', $data); } - public function store() + public function store(CreateCreditRequest $request) { - return $this->save(); - } - - public function update($publicId) - { - return $this->save($publicId); - } - - private function save($publicId = null) - { - $rules = array( - 'client' => 'required', - 'amount' => 'required|positive', - ); - - $validator = Validator::make(Input::all(), $rules); - - if ($validator->fails()) { - $url = $publicId ? 'credits/'.$publicId.'/edit' : 'credits/create'; - - return Redirect::to($url) - ->withErrors($validator) - ->withInput(); - } else { - $this->creditRepo->save($publicId, Input::all()); - - $message = trans('texts.created_credit'); - Session::flash('message', $message); - - return Redirect::to('clients/'.Input::get('client')); - } + $credit = $this->creditRepo->save($request->input()); + + Session::flash('message', trans('texts.created_credit')); + + return redirect()->to($credit->client->getRoute()); } public function bulk() { $action = Input::get('action'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->creditRepo->bulk($ids, $action); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + $count = $this->creditService->bulk($ids, $action); if ($count > 0) { $message = Utils::pluralize($action.'d_credit', $count); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 189b277c2c77..483b73a6c2ef 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -11,6 +11,7 @@ class DashboardController extends BaseController { public function index() { + // total_income, billed_clients, invoice_sent and active_clients $select = DB::raw('COUNT(DISTINCT CASE WHEN invoices.id IS NOT NULL THEN clients.id ELSE null END) billed_clients, SUM(CASE WHEN invoices.invoice_status_id >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent, @@ -62,6 +63,7 @@ class DashboardController extends BaseController ->get(); $activities = Activity::where('activities.account_id', '=', Auth::user()->account_id) + ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account') ->where('activity_type_id', '>', 0) ->orderBy('created_at', 'desc') ->take(50) @@ -74,12 +76,13 @@ class DashboardController extends BaseController ->where('clients.deleted_at', '=', null) ->where('contacts.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) - ->where('invoices.is_quote', '=', false) + //->where('invoices.is_quote', '=', false) ->where('invoices.balance', '>', 0) ->where('invoices.is_deleted', '=', false) + ->where('invoices.deleted_at', '=', null) ->where('contacts.is_primary', '=', true) ->where('invoices.due_date', '<', date('Y-m-d')) - ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id']) + ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'is_quote']) ->orderBy('invoices.due_date', 'asc') ->take(50) ->get(); @@ -90,15 +93,16 @@ class DashboardController extends BaseController ->where('invoices.account_id', '=', Auth::user()->account_id) ->where('clients.deleted_at', '=', null) ->where('contacts.deleted_at', '=', null) + ->where('invoices.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) - ->where('invoices.is_quote', '=', false) + //->where('invoices.is_quote', '=', false) ->where('invoices.balance', '>', 0) ->where('invoices.is_deleted', '=', false) ->where('contacts.is_primary', '=', true) ->where('invoices.due_date', '>=', date('Y-m-d')) ->orderBy('invoices.due_date', 'asc') ->take(50) - ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id']) + ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'is_quote']) ->get(); $payments = DB::table('payments') @@ -106,14 +110,24 @@ class DashboardController extends BaseController ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') ->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id') ->where('payments.account_id', '=', Auth::user()->account_id) + ->where('payments.is_deleted', '=', false) + ->where('invoices.is_deleted', '=', false) ->where('clients.deleted_at', '=', null) ->where('contacts.deleted_at', '=', null) ->where('contacts.is_primary', '=', true) ->select(['payments.payment_date', 'payments.amount', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id']) - ->orderBy('payments.id', 'desc') + ->orderBy('payments.payment_date', 'desc') ->take(50) ->get(); + $hasQuotes = false; + foreach ([$upcoming, $pastDue] as $data) { + foreach ($data as $invoice) { + if ($invoice->is_quote) { + $hasQuotes = true; + } + } + } $data = [ 'account' => Auth::user()->account, @@ -127,6 +141,7 @@ class DashboardController extends BaseController 'upcoming' => $upcoming, 'payments' => $payments, 'title' => trans('texts.dashboard'), + 'hasQuotes' => $hasQuotes, ]; return View::make('dashboard', $data); diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php new file mode 100644 index 000000000000..c3dc77b23ec2 --- /dev/null +++ b/app/Http/Controllers/ExpenseController.php @@ -0,0 +1,252 @@ +expenseRepo = $expenseRepo; + $this->expenseService = $expenseService; + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + return View::make('list', array( + 'entityType' => ENTITY_EXPENSE, + 'title' => trans('texts.expenses'), + 'sortCol' => '1', + 'columns' => Utils::trans([ + 'checkbox', + 'vendor', + 'client', + 'expense_date', + 'amount', + 'public_notes', + 'status', + '' + ]), + )); + } + + public function getDatatable($expensePublicId = null) + { + return $this->expenseService->getDatatable($expensePublicId, Input::get('sSearch')); + } + + public function getDatatableVendor($vendorPublicId = null) + { + return $this->expenseService->getDatatableVendor($vendorPublicId); + } + + public function create($vendorPublicId = null, $clientPublicId = null) + { + if($vendorPublicId != 0) { + $vendor = Vendor::scope($vendorPublicId)->with('vendorcontacts')->firstOrFail(); + } else { + $vendor = null; + } + $data = array( + 'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $vendorPublicId, + 'expense' => null, + 'method' => 'POST', + 'url' => 'expenses', + 'title' => trans('texts.new_expense'), + 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(), + 'vendor' => $vendor, + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + 'clientPublicId' => $clientPublicId, + ); + + $data = array_merge($data, self::getViewModel()); + + return View::make('expenses.edit', $data); + } + + public function edit($publicId) + { + $expense = Expense::scope($publicId)->firstOrFail(); + $expense->expense_date = Utils::fromSqlDate($expense->expense_date); + + $actions = []; + if ($expense->invoice) { + $actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans("texts.view_invoice")]; + } else { + $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.invoice_expense")]; + + /* + // check for any open invoices + $invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : []; + + foreach ($invoices as $invoice) { + $actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans("texts.add_to_invoice", ["invoice" => $invoice->invoice_number])]; + } + */ + } + + $actions[] = \DropdownButton::DIVIDER; + if (!$expense->trashed()) { + $actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_expense')]; + $actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans('texts.delete_expense')]; + } else { + $actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_expense')]; + } + + $data = array( + 'vendor' => null, + 'expense' => $expense, + 'method' => 'PUT', + 'url' => 'expenses/'.$publicId, + 'title' => 'Edit Expense', + 'actions' => $actions, + 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(), + 'vendorPublicId' => $expense->vendor ? $expense->vendor->public_id : null, + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + 'clientPublicId' => $expense->client ? $expense->client->public_id : null, + ); + + $data = array_merge($data, self::getViewModel()); + + if (Auth::user()->account->isNinjaAccount()) { + if ($account = Account::whereId($client->public_id)->first()) { + $data['proPlanPaid'] = $account['pro_plan_paid']; + } + } + + return View::make('expenses.edit', $data); + } + + /** + * Update the specified resource in storage. + * + * @param int $id + * @return Response + */ + public function update(UpdateExpenseRequest $request) + { + $expense = $this->expenseRepo->save($request->input()); + + Session::flash('message', trans('texts.updated_expense')); + + $action = Input::get('action'); + if (in_array($action, ['archive', 'delete', 'restore', 'invoice'])) { + return self::bulk(); + } + + return redirect()->to("expenses/{$expense->public_id}/edit"); + } + + public function store(CreateExpenseRequest $request) + { + $expense = $this->expenseRepo->save($request->input()); + + Session::flash('message', trans('texts.created_expense')); + + return redirect()->to("expenses/{$expense->public_id}/edit"); + } + + public function bulk() + { + $action = Input::get('action'); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + + switch($action) + { + case 'invoice': + $expenses = Expense::scope($ids)->get(); + $clientPublicId = null; + $data = []; + + // Validate that either all expenses do not have a client or if there is a client, it is the same client + foreach ($expenses as $expense) + { + if ($expense->client_id) { + if (!$clientPublicId) { + $clientPublicId = $expense->client_id; + } elseif ($clientPublicId != $expense->client_id) { + Session::flash('error', trans('texts.expense_error_multiple_clients')); + return Redirect::to('expenses'); + } + } + + if ($expense->invoice_id) { + Session::flash('error', trans('texts.expense_error_invoiced')); + return Redirect::to('expenses'); + } + + $account = Auth::user()->account; + $data[] = [ + 'publicId' => $expense->public_id, + 'description' => $expense->public_notes, + 'qty' => 1, + 'cost' => $expense->present()->converted_amount, + ]; + } + + return Redirect::to("invoices/create/{$clientPublicId}")->with('expenses', $data); + break; + + default: + $count = $this->expenseService->bulk($ids, $action); + } + + if ($count > 0) { + $message = Utils::pluralize($action.'d_expense', $count); + Session::flash('message', $message); + } + + return Redirect::to('expenses'); + } + + private static function getViewModel() + { + return [ + 'data' => Input::old('data'), + 'account' => Auth::user()->account, + 'sizes' => Cache::get('sizes'), + 'paymentTerms' => Cache::get('paymentTerms'), + 'industries' => Cache::get('industries'), + 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), + 'countries' => Cache::get('countries'), + 'customLabel1' => Auth::user()->account->custom_vendor_label1, + 'customLabel2' => Auth::user()->account->custom_vendor_label2, + ]; + } + + public function show($publicId) + { + Session::reflash(); + + return Redirect::to("expenses/{$publicId}/edit"); + } +} diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php new file mode 100644 index 000000000000..540e38d97296 --- /dev/null +++ b/app/Http/Controllers/ExportController.php @@ -0,0 +1,181 @@ +input('format'); + $date = date('Y-m-d'); + $fileName = "invoice-ninja-{$date}"; + + if ($format === 'JSON') { + return $this->returnJSON($request, $fileName); + } elseif ($format === 'CSV') { + return $this->returnCSV($request, $fileName); + } else { + return $this->returnXLS($request, $fileName); + } + } + + private function returnJSON($request, $fileName) + { + $output = fopen('php://output', 'w') or Utils::fatalError(); + header('Content-Type:application/json'); + header("Content-Disposition:attachment;filename={$fileName}.json"); + + $manager = new Manager(); + $manager->setSerializer(new ArraySerializer()); + + $account = Auth::user()->account; + $account->loadAllData(); + + $resource = new Item($account, new AccountTransformer); + $data = $manager->createData($resource)->toArray(); + + return response()->json($data); + } + + + private function returnCSV($request, $fileName) + { + $data = $this->getData($request); + + return Excel::create($fileName, function($excel) use ($data) { + $excel->sheet('', function($sheet) use ($data) { + $sheet->loadView('export', $data); + }); + })->download('csv'); + } + + private function returnXLS($request, $fileName) + { + $user = Auth::user(); + $data = $this->getData($request); + + return Excel::create($fileName, function($excel) use ($user, $data) { + + $excel->setTitle($data['title']) + ->setCreator($user->getDisplayName()) + ->setLastModifiedBy($user->getDisplayName()) + ->setDescription('') + ->setSubject('') + ->setKeywords('') + ->setCategory('') + ->setManager('') + ->setCompany($user->account->getDisplayName()); + + foreach ($data as $key => $val) { + if ($key === 'account' || $key === 'title' || $key === 'multiUser') { + continue; + } + $label = trans("texts.{$key}"); + $excel->sheet($label, function($sheet) use ($key, $data) { + if ($key === 'quotes') { + $key = 'invoices'; + $data['entityType'] = ENTITY_QUOTE; + } + $sheet->loadView("export.{$key}", $data); + }); + } + })->download('xls'); + } + + private function getData($request) + { + $account = Auth::user()->account; + + $data = [ + 'account' => $account, + 'title' => 'Invoice Ninja v' . NINJA_VERSION . ' - ' . $account->formatDateTime($account->getDateTime()), + 'multiUser' => $account->users->count() > 1 + ]; + + if ($request->input(ENTITY_CLIENT)) { + $data['clients'] = Client::scope() + ->with('user', 'contacts', 'country') + ->withTrashed() + ->where('is_deleted', '=', false) + ->get(); + + $data['contacts'] = Contact::scope() + ->with('user', 'client.contacts') + ->withTrashed() + ->get(); + + $data['credits'] = Credit::scope() + ->with('user', 'client.contacts') + ->get(); + } + + if ($request->input(ENTITY_TASK)) { + $data['tasks'] = Task::scope() + ->with('user', 'client.contacts') + ->withTrashed() + ->where('is_deleted', '=', false) + ->get(); + } + + if ($request->input(ENTITY_INVOICE)) { + $data['invoices'] = Invoice::scope() + ->with('user', 'client.contacts', 'invoice_status') + ->withTrashed() + ->where('is_deleted', '=', false) + ->where('is_quote', '=', false) + ->where('is_recurring', '=', false) + ->get(); + + $data['quotes'] = Invoice::scope() + ->with('user', 'client.contacts', 'invoice_status') + ->withTrashed() + ->where('is_deleted', '=', false) + ->where('is_quote', '=', true) + ->where('is_recurring', '=', false) + ->get(); + } + + if ($request->input(ENTITY_PAYMENT)) { + $data['payments'] = Payment::scope() + ->withTrashed() + ->where('is_deleted', '=', false) + ->with('user', 'client.contacts', 'payment_type', 'invoice', 'account_gateway.gateway') + ->get(); + } + + + if ($request->input(ENTITY_VENDOR)) { + $data['clients'] = Vendor::scope() + ->with('user', 'vendorcontacts', 'country') + ->withTrashed() + ->where('is_deleted', '=', false) + ->get(); + + $data['vendor_contacts'] = VendorContact::scope() + ->with('user', 'vendor.contacts') + ->withTrashed() + ->get(); + /* + $data['expenses'] = Credit::scope() + ->with('user', 'client.contacts') + ->get(); + */ + } + + return $data; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 1308a4378b26..571ac731938c 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -1,7 +1,6 @@ true]); } + + public function viewLogo() + { + return View::make('public.logo'); + } public function invoiceNow() { @@ -49,11 +53,18 @@ class HomeController extends BaseController Auth::logout(); } + // Track the referral/campaign code + foreach (['rc', 'utm_campaign'] as $code) { + if (Input::has($code)) { + Session::set(SESSION_REFERRAL_CODE, Input::get($code)); + } + } + if (Auth::check()) { $redirectTo = Input::get('redirect_to', 'invoices/create'); return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up')); } else { - return View::make('public.header', ['invoiceNow' => true]); + return View::make('public.invoice_now'); } } diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php new file mode 100644 index 000000000000..b078fb6f955e --- /dev/null +++ b/app/Http/Controllers/ImportController.php @@ -0,0 +1,92 @@ +importService = $importService; + } + + public function doImport() + { + $source = Input::get('source'); + $files = []; + + foreach (ImportService::$entityTypes as $entityType) { + if (Input::file("{$entityType}_file")) { + $files[$entityType] = Input::file("{$entityType}_file")->getRealPath(); + if ($source === IMPORT_CSV) { + Session::forget("{$entityType}-data"); + } + } + } + + try { + if ($source === IMPORT_CSV) { + $data = $this->importService->mapCSV($files); + return View::make('accounts.import_map', ['data' => $data]); + } else { + $results = $this->importService->import($source, $files); + return $this->showResult($results); + } + } catch (Exception $exception) { + Utils::logError($exception); + Session::flash('error', $exception->getMessage()); + return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); + } + } + + public function doImportCSV() + { + $map = Input::get('map'); + $headers = Input::get('headers'); + + try { + $results = $this->importService->importCSV($map, $headers); + return $this->showResult($results); + } catch (Exception $exception) { + Utils::logError($exception); + Session::flash('error', $exception->getMessage()); + return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); + } + } + + private function showResult($results) + { + $message = ''; + $skipped = []; + + foreach ($results as $entityType => $entityResults) { + if ($count = count($entityResults[RESULT_SUCCESS])) { + $message .= trans("texts.created_{$entityType}s", ['count' => $count]) . '
'; + } + if (count($entityResults[RESULT_FAILURE])) { + $skipped = array_merge($skipped, $entityResults[RESULT_FAILURE]); + } + } + + if (count($skipped)) { + $message .= '

' . trans('texts.failed_to_import') . '
'; + foreach ($skipped as $skip) { + $message .= json_encode($skip) . '
'; + } + } + + if ($message) { + Session::flash('warning', $message); + } + + return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); + } +} diff --git a/app/Http/Controllers/IntegrationController.php b/app/Http/Controllers/IntegrationController.php index de30afdfaf86..740c91e36127 100644 --- a/app/Http/Controllers/IntegrationController.php +++ b/app/Http/Controllers/IntegrationController.php @@ -13,10 +13,11 @@ class IntegrationController extends Controller $eventId = Utils::lookupEventId(trim(Input::get('event'))); if (!$eventId) { - return Response::json('', 500); + return Response::json('Event is invalid', 500); } - $subscription = Subscription::where('account_id', '=', Auth::user()->account_id)->where('event_id', '=', $eventId)->first(); + $subscription = Subscription::where('account_id', '=', Auth::user()->account_id) + ->where('event_id', '=', $eventId)->first(); if (!$subscription) { $subscription = new Subscription(); @@ -27,6 +28,10 @@ class IntegrationController extends Controller $subscription->target_url = trim(Input::get('target_url')); $subscription->save(); + if (!$subscription->id) { + return Response::json('Failed to create subscription', 500); + } + return Response::json('{"id":'.$subscription->id.'}', 201); } } diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 4c13259aecb2..52b1811ca21f 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -1,9 +1,11 @@ invoiceRepo = $invoiceRepo; $this->clientRepo = $clientRepo; $this->mailer = $mailer; } + /** + * @SWG\Get( + * path="/invoices", + * summary="List of invoices", + * tags={"invoice"}, + * @SWG\Response( + * response=200, + * description="A list with invoices", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { - $invoices = Invoice::scope() - ->with('client', 'invitations.account') - ->where('invoices.is_quote', '=', false) - ->orderBy('created_at', 'desc') - ->get(); + $paginator = Invoice::scope()->withTrashed(); + $invoices = Invoice::scope()->withTrashed() + ->with(array_merge(['invoice_items'], $this->getIncluded())); + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $invoices->whereHas('client', $filter); + $paginator->whereHas('client', $filter); + } + + $invoices = $invoices->orderBy('created_at', 'desc')->paginate(); + + /* // Add the first invitation link to the data foreach ($invoices as $key => $invoice) { foreach ($invoice->invitations as $subKey => $invitation) { @@ -39,39 +72,57 @@ class InvoiceApiController extends Controller } unset($invoice['invitations']); } + */ - $invoices = Utils::remapPublicIds($invoices); - - $response = json_encode($invoices, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(count($invoices)); + $transformer = new InvoiceTransformer(Auth::user()->account, Input::get('serializer')); + $paginator = $paginator->paginate(); - return Response::make($response, 200, $headers); + $data = $this->createCollection($invoices, $transformer, 'invoices', $paginator); + + return $this->response($data); } - public function store() + + /** + * @SWG\Post( + * path="/invoices", + * tags={"invoice"}, + * summary="Create an invoice", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Invoice") + * ), + * @SWG\Response( + * response=200, + * description="New invoice", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store(CreateInvoiceRequest $request) { $data = Input::all(); $error = null; - - // check if the invoice number is set and unique - if (!isset($data['invoice_number']) && !isset($data['id'])) { - $data['invoice_number'] = Auth::user()->account->getNextInvoiceNumber(); - } else if (isset($data['invoice_number'])) { - $invoice = Invoice::scope()->where('invoice_number', '=', $data['invoice_number'])->first(); - if ($invoice) { - $error = trans('validation.unique', ['attribute' => 'texts.invoice_number']); - } else { - $data['id'] = $invoice->public_id; - } - } if (isset($data['email'])) { - $client = Client::scope()->whereHas('contacts', function($query) use ($data) { - $query->where('email', '=', $data['email']); + $email = $data['email']; + $client = Client::scope()->whereHas('contacts', function($query) use ($email) { + $query->where('email', '=', $email); })->first(); if (!$client) { - $clientData = ['contact' => ['email' => $data['email']]]; + $validator = Validator::make(['email'=>$email], ['email' => 'email']); + if ($validator->fails()) { + $messages = $validator->messages(); + return $messages->first(); + } + + $clientData = ['contact' => ['email' => $email]]; foreach (['name', 'private_notes'] as $field) { if (isset($data[$field])) { $clientData[$field] = $data[$field]; @@ -82,58 +133,40 @@ class InvoiceApiController extends Controller $clientData[$field] = $data[$field]; } } - $error = $this->clientRepo->getErrors($clientData); - if (!$error) { - $client = $this->clientRepo->save(false, $clientData, false); - } + + $client = $this->clientRepo->save($clientData); } } else if (isset($data['client_id'])) { - $client = Client::scope($data['client_id'])->first(); + $client = Client::scope($data['client_id'])->firstOrFail(); } - if (!$error) { - if (!isset($data['client_id']) && !isset($data['email'])) { - $error = trans('validation.', ['attribute' => 'client_id or email']); - } else if (!$client) { - $error = trans('validation.not_in', ['attribute' => 'client_id']); - } + $data = self::prepareData($data, $client); + $data['client_id'] = $client->id; + $invoice = $this->invoiceRepo->save($data); + + if (!isset($data['id'])) { + $invitation = Invitation::createNew(); + $invitation->invoice_id = $invoice->id; + $invitation->contact_id = $client->contacts[0]->id; + $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); + $invitation->save(); } - if ($error) { - $response = json_encode($error, JSON_PRETTY_PRINT); - } else { - $data = self::prepareData($data); - $data['client_id'] = $client->id; - $invoice = $this->invoiceRepo->save(false, $data, false); - - if (!isset($data['id'])) { - $invitation = Invitation::createNew(); - $invitation->invoice_id = $invoice->id; - $invitation->contact_id = $client->contacts[0]->id; - $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); - $invitation->save(); - } - - if (isset($data['email_invoice']) && $data['email_invoice']) { - $this->mailer->sendInvoice($invoice); - } - - // prepare the return data - $invoice = Invoice::scope($invoice->public_id)->with('client', 'invoice_items', 'invitations')->first(); - $invoice = Utils::remapPublicIds([$invoice]); - - $response = json_encode($invoice, JSON_PRETTY_PRINT); + if (isset($data['email_invoice']) && $data['email_invoice']) { + $this->mailer->sendInvoice($invoice); } - $headers = Utils::getApiHeaders(); - - return Response::make($response, $error ? 400 : 200, $headers); + $invoice = Invoice::scope($invoice->public_id)->with('client', 'invoice_items', 'invitations')->first(); + $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($invoice, $transformer, 'invoice'); + + return $this->response($data); } - private function prepareData($data) + private function prepareData($data, $client) { $account = Auth::user()->account; - $account->loadLocalizationSettings(); + $account->loadLocalizationSettings($client); // set defaults for optional fields $fields = [ @@ -165,18 +198,13 @@ class InvoiceApiController extends Controller } } - // hardcode some fields - $fields = [ - 'is_recurring' => false - ]; - - foreach ($fields as $key => $val) { - $data[$key] = $val; - } - // initialize the line items if (isset($data['product_key']) || isset($data['cost']) || isset($data['notes']) || isset($data['qty'])) { $data['invoice_items'] = [self::prepareItem($data)]; + + // make sure the tax isn't applied twice (for the invoice and the line item) + unset($data['invoice_items'][0]['tax_name']); + unset($data['invoice_items'][0]['tax_rate']); } else { foreach ($data['invoice_items'] as $index => $item) { $data['invoice_items'][$index] = self::prepareItem($item); @@ -202,13 +230,13 @@ class InvoiceApiController extends Controller } // if only the product key is set we'll load the cost and notes - if ($item['product_key'] && (!$item['cost'] || !$item['notes'])) { + if ($item['product_key'] && (is_null($item['cost']) || is_null($item['notes']))) { $product = Product::findProductByKey($item['product_key']); if ($product) { - if (!$item['cost']) { + if (is_null($item['cost'])) { $item['cost'] = $product->cost; } - if (!$item['notes']) { + if (is_null($item['notes'])) { $item['notes'] = $product->notes; } } @@ -242,4 +270,89 @@ class InvoiceApiController extends Controller $headers = Utils::getApiHeaders(); return Response::make($response, $error ? 400 : 200, $headers); } + + /** + * @SWG\Put( + * path="/invoices", + * tags={"invoice"}, + * summary="Update an invoice", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Invoice") + * ), + * @SWG\Response( + * response=200, + * description="Update invoice", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function update(UpdateInvoiceRequest $request, $publicId) + { + if ($request->action == ACTION_ARCHIVE) { + $invoice = Invoice::scope($publicId)->firstOrFail(); + $this->invoiceRepo->archive($invoice); + /* + $response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT); + $headers = Utils::getApiHeaders(); + return Response::make($response, 200, $headers); + */ + $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($invoice, $transformer, 'invoice'); + + return $this->response($data); + } + + $data = $request->input(); + $data['public_id'] = $publicId; + $this->invoiceRepo->save($data); + + $invoice = Invoice::scope($publicId)->with('client', 'invoice_items', 'invitations')->firstOrFail(); + $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($invoice, $transformer, 'invoice'); + + return $this->response($data); + } + + /** + * @SWG\Delete( + * path="/invoices", + * tags={"invoice"}, + * summary="Delete an invoice", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Invoice") + * ), + * @SWG\Response( + * response=200, + * description="Delete invoice", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function destroy($publicId) + { + $data['public_id'] = $publicId; + $invoice = Invoice::scope($publicId)->firstOrFail(); + + $this->invoiceRepo->delete($invoice); + + $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($invoice, $transformer, 'invoice'); + + return $this->response($data); + + } + } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index b7e734ca14cd..a17453be8a1e 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -11,45 +11,39 @@ use DB; use Event; use URL; use Datatable; -use finfo; use Request; use DropdownButton; use App\Models\Invoice; -use App\Models\Invitation; use App\Models\Client; use App\Models\Account; use App\Models\Product; -use App\Models\Country; use App\Models\TaxRate; -use App\Models\Currency; -use App\Models\Size; -use App\Models\Industry; -use App\Models\PaymentTerm; use App\Models\InvoiceDesign; -use App\Models\AccountGateway; use App\Models\Activity; -use App\Models\Gateway; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; -use App\Ninja\Repositories\TaxRateRepository; -use App\Events\InvoiceViewed; +use App\Services\InvoiceService; +use App\Services\RecurringInvoiceService; +use App\Http\Requests\SaveInvoiceWithClientRequest; class InvoiceController extends BaseController { protected $mailer; protected $invoiceRepo; protected $clientRepo; - protected $taxRateRepo; + protected $invoiceService; + protected $recurringInvoiceService; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, TaxRateRepository $taxRateRepo) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, RecurringInvoiceService $recurringInvoiceService) { parent::__construct(); $this->mailer = $mailer; $this->invoiceRepo = $invoiceRepo; $this->clientRepo = $clientRepo; - $this->taxRateRepo = $taxRateRepo; + $this->invoiceService = $invoiceService; + $this->recurringInvoiceService = $recurringInvoiceService; } public function index() @@ -57,46 +51,20 @@ class InvoiceController extends BaseController $data = [ 'title' => trans('texts.invoices'), 'entityType' => ENTITY_INVOICE, - 'columns' => Utils::trans(['checkbox', 'invoice_number', 'client', 'invoice_date', 'invoice_total', 'balance_due', 'due_date', 'status', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'invoice_number', + 'client', + 'invoice_date', + 'invoice_total', + 'balance_due', + 'due_date', + 'status', + '' + ]), ]; - $recurringInvoices = Invoice::scope()->where('is_recurring', '=', true); - - if (Session::get('show_trash:invoice')) { - $recurringInvoices->withTrashed(); - } else { - $recurringInvoices->join('clients', 'clients.id', '=', 'invoices.client_id') - ->where('clients.deleted_at', '=', null); - } - - if ($recurringInvoices->count() > 0) { - $data['secEntityType'] = ENTITY_RECURRING_INVOICE; - $data['secColumns'] = Utils::trans(['checkbox', 'frequency', 'client', 'start_date', 'end_date', 'invoice_total', 'action']); - } - - return View::make('list', $data); - } - - public function clientIndex() - { - $invitationKey = Session::get('invitation_key'); - if (!$invitationKey) { - return Redirect::to('/setup'); - } - - $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $account = $invitation->account; - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - - $data = [ - 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'title' => trans('texts.invoices'), - 'entityType' => ENTITY_INVOICE, - 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']), - ]; - - return View::make('public_list', $data); + return response()->view('list', $data); } public function getDatatable($clientPublicId = null) @@ -104,166 +72,24 @@ class InvoiceController extends BaseController $accountId = Auth::user()->account_id; $search = Input::get('sSearch'); - return $this->invoiceRepo->getDatatable($accountId, $clientPublicId, ENTITY_INVOICE, $search); - } - - public function getClientDatatable() - { - //$accountId = Auth::user()->account_id; - $search = Input::get('sSearch'); - $invitationKey = Session::get('invitation_key'); - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); - - if (!$invitation || $invitation->is_deleted) { - return []; - } - - $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - return []; - } - - return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_INVOICE, $search); + return $this->invoiceService->getDatatable($accountId, $clientPublicId, ENTITY_INVOICE, $search); } public function getRecurringDatatable($clientPublicId = null) { - $query = $this->invoiceRepo->getRecurringInvoices(Auth::user()->account_id, $clientPublicId, Input::get('sSearch')); - $table = Datatable::query($query); + $accountId = Auth::user()->account_id; + $search = Input::get('sSearch'); - if (!$clientPublicId) { - $table->addColumn('checkbox', function ($model) { return ''; }); - } - - $table->addColumn('frequency', function ($model) { return link_to('invoices/'.$model->public_id, $model->frequency); }); - - if (!$clientPublicId) { - $table->addColumn('client_name', function ($model) { return link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)); }); - } - - return $table->addColumn('start_date', function ($model) { return Utils::fromSqlDate($model->start_date); }) - ->addColumn('end_date', function ($model) { return Utils::fromSqlDate($model->end_date); }) - ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); }) - ->addColumn('dropdown', function ($model) { - if ($model->is_deleted) { - return '

'; - } - - $str = ''; - - }) - ->make(); - } - - public function view($invitationKey) - { - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->firstOrFail(); - $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - return View::make('invoices.deleted'); - } - - $invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country'); - $client = $invoice->client; - $account = $client->account; - - if (!$client || $client->is_deleted) { - return View::make('invoices.deleted'); - } - - if ($account->subdomain) { - $server = explode('.', Request::server('HTTP_HOST')); - $subdomain = $server[0]; - - if (!in_array($subdomain, ['app', 'www']) && $subdomain != $account->subdomain) { - return View::make('invoices.deleted'); - } - } - - if (!Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { - Activity::viewInvoice($invitation); - Event::fire(new InvoiceViewed($invoice)); - } - - Session::set($invitationKey, true); - Session::set('invitation_key', $invitationKey); - - $account->loadLocalizationSettings(); - - $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); - $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = $account->isPro(); - - if ($invoice->invoice_design_id == CUSTOM_DESIGN) { - $invoice->invoice_design->javascript = $account->custom_design; - } else { - $invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake; - } - - $contact = $invitation->contact; - $contact->setVisible([ - 'first_name', - 'last_name', - 'email', - 'phone', ]); - - // Determine payment options - $paymentTypes = []; - if ($client->getGatewayToken()) { - $paymentTypes[] = [ - 'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file') - ]; - } - foreach(Gateway::$paymentTypes as $type) { - if ($account->getGatewayByType($type)) { - $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); - $paymentTypes[] = [ - 'url' => URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"), 'label' => trans('texts.'.strtolower($type)) - ]; - } - } - - $paymentURL = ''; - if (count($paymentTypes)) { - $paymentURL = $paymentTypes[0]['url']; - } - - $data = array( - 'isConverted' => $invoice->quote_invoice_id ? true : false, - 'showBreadcrumbs' => false, - 'hideLogo' => $account->isWhiteLabel(), - 'invoice' => $invoice->hidePrivateFields(), - 'invitation' => $invitation, - 'invoiceLabels' => $account->getInvoiceLabels(), - 'contact' => $contact, - 'paymentTypes' => $paymentTypes, - 'paymentURL' => $paymentURL, - ); - - return View::make('invoices.view', $data); + return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_INVOICE, $search); } public function edit($publicId, $clone = false) { - $invoice = Invoice::scope($publicId)->withTrashed()->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items')->firstOrFail(); + $account = Auth::user()->account; + $invoice = Invoice::scope($publicId) + ->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items') + ->withTrashed() + ->firstOrFail(); $entityType = $invoice->getEntityType(); $contactIds = DB::table('invitations') @@ -274,8 +100,8 @@ class InvoiceController extends BaseController ->select('contacts.public_id')->lists('public_id'); if ($clone) { - $invoice->id = null; - $invoice->invoice_number = Auth::user()->account->getNextInvoiceNumber($invoice->is_quote); + $invoice->id = $invoice->public_id = null; + $invoice->invoice_number = $account->getNextInvoiceNumber($invoice); $invoice->balance = $invoice->amount; $invoice->invoice_status_id = 0; $invoice->invoice_date = date_create()->format('Y-m-d'); @@ -288,9 +114,11 @@ class InvoiceController extends BaseController } $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); + $invoice->recurring_due_date = $invoice->due_date;// Keep in SQL form $invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->end_date = Utils::fromSqlDate($invoice->end_date); + $invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date); $invoice->is_pro = Auth::user()->isPro(); $actions = [ @@ -329,10 +157,10 @@ class InvoiceController extends BaseController $lastSent = ($invoice->is_recurring && $invoice->last_sent_date) ? $invoice->recurring_invoices->last() : null; $data = array( + 'clients' => Client::scope()->withTrashed()->with('contacts', 'country')->whereId($invoice->client_id)->get(), 'entityType' => $entityType, 'showBreadcrumbs' => $clone, 'invoice' => $invoice, - 'data' => false, 'method' => $method, 'invitationContactIds' => $contactIds, 'url' => $url, @@ -343,20 +171,30 @@ class InvoiceController extends BaseController 'lastSent' => $lastSent); $data = array_merge($data, self::getViewModel()); - // Set the invitation link on the client's contacts + if ($clone) { + $data['formIsChanged'] = true; + } + + // Set the invitation data on the client's contacts if (!$clone) { $clients = $data['clients']; foreach ($clients as $client) { - if ($client->id == $invoice->client->id) { - foreach ($invoice->invitations as $invitation) { - foreach ($client->contacts as $contact) { - if ($invitation->contact_id == $contact->id) { - $contact->invitation_link = $invitation->getLink(); - } + if ($client->id != $invoice->client->id) { + continue; + } + + foreach ($invoice->invitations as $invitation) { + foreach ($client->contacts as $contact) { + if ($invitation->contact_id == $contact->id) { + $contact->email_error = $invitation->email_error; + $contact->invitation_link = $invitation->getLink(); + $contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false; + $contact->invitation_status = $contact->email_error ? false : $invitation->getStatus(); } } - break; } + + break; } } @@ -365,25 +203,27 @@ class InvoiceController extends BaseController public function create($clientPublicId = 0, $isRecurring = false) { - $client = null; - $invoiceNumber = $isRecurring ? microtime(true) : Auth::user()->account->getNextInvoiceNumber(); + $account = Auth::user()->account; + $entityType = $isRecurring ? ENTITY_RECURRING_INVOICE : ENTITY_INVOICE; + $clientId = null; if ($clientPublicId) { - $client = Client::scope($clientPublicId)->firstOrFail(); + $clientId = Client::getPrivateId($clientPublicId); } - $data = array( - 'entityType' => ENTITY_INVOICE, - 'invoice' => null, - 'data' => Input::old('data'), - 'invoiceNumber' => $invoiceNumber, - 'method' => 'POST', - 'url' => 'invoices', - 'title' => trans('texts.new_invoice'), - 'isRecurring' => $isRecurring, - 'client' => $client); + $invoice = $account->createInvoice($entityType, $clientId); + $invoice->public_id = 0; + + $data = [ + 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), + 'entityType' => $invoice->getEntityType(), + 'invoice' => $invoice, + 'method' => 'POST', + 'url' => 'invoices', + 'title' => trans('texts.new_invoice'), + ]; $data = array_merge($data, self::getViewModel()); - + return View::make('invoices.edit', $data); } @@ -405,17 +245,66 @@ class InvoiceController extends BaseController } } + $recurringDueDateHelp = ''; + foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_due_date_help')) as $line) { + $parts = explode("=>", $line); + if (count($parts) > 1) { + $line = $parts[0].' => '.Utils::processVariables($parts[0]); + $recurringDueDateHelp .= '
  • '.strip_tags($line).'
  • '; + } else { + $recurringDueDateHelp .= $line; + } + } + + // Create due date options + $recurringDueDates = array( + trans('texts.use_client_terms') => array('value' => '', 'class' => 'monthly weekly'), + ); + + $ends = array('th','st','nd','rd','th','th','th','th','th','th'); + for($i = 1; $i < 31; $i++){ + if ($i >= 11 && $i <= 13) $ordinal = $i. 'th'; + else $ordinal = $i . $ends[$i % 10]; + + $dayStr = str_pad($i, 2, '0', STR_PAD_LEFT); + $str = trans('texts.day_of_month', array('ordinal'=>$ordinal)); + + $recurringDueDates[$str] = array('value' => "1998-01-$dayStr", 'data-num' => $i, 'class' => 'monthly'); + } + $recurringDueDates[trans('texts.last_day_of_month')] = array('value' => "1998-01-31", 'data-num' => 31, 'class' => 'monthly'); + + + $daysOfWeek = array( + trans('texts.sunday'), + trans('texts.monday'), + trans('texts.tuesday'), + trans('texts.wednesday'), + trans('texts.thursday'), + trans('texts.friday'), + trans('texts.saturday'), + ); + foreach(array('1st','2nd','3rd','4th') as $i=>$ordinal){ + foreach($daysOfWeek as $j=>$dayOfWeek){ + $str = trans('texts.day_of_week_after', array('ordinal' => $ordinal, 'day' => $dayOfWeek)); + + $day = $i * 7 + $j + 1; + $dayStr = str_pad($day, 2, '0', STR_PAD_LEFT); + $recurringDueDates[$str] = array('value' => "1998-02-$dayStr", 'data-num' => $day, 'class' => 'weekly'); + } + } + return [ + 'data' => Input::old('data'), 'account' => Auth::user()->account->load('country'), - 'products' => Product::scope()->orderBy('id')->get(array('product_key', 'notes', 'cost', 'qty')), - 'countries' => Cache::get('countries'), - 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), + 'products' => Product::scope()->with('default_tax_rate')->orderBy('id')->get(), 'taxRates' => TaxRate::scope()->orderBy('name')->get(), 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), 'invoiceDesigns' => InvoiceDesign::getDesigns(), + 'invoiceFonts' => Cache::get('fonts'), 'frequencies' => array( 1 => 'Weekly', 2 => 'Two weeks', @@ -425,9 +314,12 @@ class InvoiceController extends BaseController 6 => 'Six months', 7 => 'Annually', ), + 'recurringDueDates' => $recurringDueDates, 'recurringHelp' => $recurringHelp, + 'recurringDueDateHelp' => $recurringDueDateHelp, 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), 'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null, + 'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null, ]; } @@ -437,118 +329,109 @@ class InvoiceController extends BaseController * * @return Response */ - public function store() - { - return InvoiceController::save(); - } - - private function save($publicId = null) + public function store(SaveInvoiceWithClientRequest $request) { $action = Input::get('action'); $entityType = Input::get('entityType'); - if (in_array($action, ['archive', 'delete', 'mark', 'restore'])) { - return InvoiceController::bulk($entityType); + $invoice = $this->invoiceService->save($request->input()); + $entityType = $invoice->getEntityType(); + $message = trans("texts.created_{$entityType}"); + + // check if we created a new client with the invoice + // TODO: replace with HistoryListener + $input = $request->input(); + $clientPublicId = isset($input['client']['public_id']) ? $input['client']['public_id'] : false; + if ($clientPublicId == '-1') { + $message = $message.' '.trans('texts.and_created_client'); + $trackUrl = URL::to('clients/' . $invoice->client->public_id); + Utils::trackViewed($invoice->client->getDisplayName(), ENTITY_CLIENT, $trackUrl); } - $input = json_decode(Input::get('data')); - $invoice = $input->invoice; + Session::flash('message', $message); - if ($errors = $this->invoiceRepo->getErrors($invoice)) { - Session::flash('error', trans('texts.invoice_error')); + if ($action == 'email') { + return $this->emailInvoice($invoice, Input::get('pdfupload')); + } - return Redirect::to("{$entityType}s/create") - ->withInput()->withErrors($errors); + return redirect()->to($invoice->getRoute()); + } + + /** + * Update the specified resource in storage. + * + * @param int $id + * @return Response + */ + public function update(SaveInvoiceWithClientRequest $request) + { + $action = Input::get('action'); + $entityType = Input::get('entityType'); + + $invoice = $this->invoiceService->save($request->input()); + $entityType = $invoice->getEntityType(); + $message = trans("texts.updated_{$entityType}"); + Session::flash('message', $message); + + if ($action == 'clone') { + return $this->cloneInvoice($invoice->public_id); + } elseif ($action == 'convert') { + return $this->convertQuote($invoice->public_id); + } elseif ($action == 'email') { + return $this->emailInvoice($invoice, Input::get('pdfupload')); + } + + return redirect()->to($invoice->getRoute()); + } + + + private function emailInvoice($invoice, $pdfUpload) + { + $entityType = $invoice->getEntityType(); + $pdfUpload = Utils::decodePDF($pdfUpload); + + if (!Auth::user()->confirmed) { + $errorMessage = trans(Auth::user()->registered ? 'texts.confirmation_required' : 'texts.registration_required'); + Session::flash('error', $errorMessage); + return Redirect::to('invoices/'.$invoice->public_id.'/edit'); + } + + if ($invoice->is_recurring) { + $response = $this->emailRecurringInvoice($invoice); } else { - $this->taxRateRepo->save($input->tax_rates); + $response = $this->mailer->sendInvoice($invoice, false, $pdfUpload); + } - $clientData = (array) $invoice->client; - $client = $this->clientRepo->save($invoice->client->public_id, $clientData); + if ($response === true) { + $message = trans("texts.emailed_{$entityType}"); + Session::flash('message', $message); + } else { + Session::flash('error', $response); + } - $invoiceData = (array) $invoice; - $invoiceData['client_id'] = $client->id; - $invoice = $this->invoiceRepo->save($publicId, $invoiceData, $entityType); + return Redirect::to("{$entityType}s/{$invoice->public_id}/edit"); + } - $account = Auth::user()->account; - if ($account->invoice_taxes != $input->invoice_taxes - || $account->invoice_item_taxes != $input->invoice_item_taxes - || $account->invoice_design_id != $input->invoice->invoice_design_id) { - $account->invoice_taxes = $input->invoice_taxes; - $account->invoice_item_taxes = $input->invoice_item_taxes; - $account->invoice_design_id = $input->invoice->invoice_design_id; - $account->save(); - } - - $client->load('contacts'); - $sendInvoiceIds = []; - - foreach ($client->contacts as $contact) { - if ($contact->send_invoice || count($client->contacts) == 1) { - $sendInvoiceIds[] = $contact->id; - } - } - - foreach ($client->contacts as $contact) { - $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); - - if (in_array($contact->id, $sendInvoiceIds) && !$invitation) { - $invitation = Invitation::createNew(); - $invitation->invoice_id = $invoice->id; - $invitation->contact_id = $contact->id; - $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); - $invitation->save(); - } elseif (!in_array($contact->id, $sendInvoiceIds) && $invitation) { - $invitation->delete(); - } - } - - $message = trans($publicId ? "texts.updated_{$entityType}" : "texts.created_{$entityType}"); - if ($input->invoice->client->public_id == '-1') { - $message = $message.' '.trans('texts.and_created_client'); - - $url = URL::to('clients/'.$client->public_id); - Utils::trackViewed($client->getDisplayName(), ENTITY_CLIENT, $url); - } - - $pdfUpload = Input::get('pdfupload'); - if (!empty($pdfUpload) && strpos($pdfUpload, 'data:application/pdf;base64,') === 0) { - $this->storePDF(Input::get('pdfupload'), $invoice); - } - - if ($action == 'clone') { - return $this->cloneInvoice($publicId); - } elseif ($action == 'convert') { - return $this->convertQuote($publicId); - } elseif ($action == 'email') { - if (Auth::user()->confirmed && !Auth::user()->isDemo()) { - if ($invoice->is_recurring) { - if ($invoice->shouldSendToday()) { - $invoice = $this->invoiceRepo->createRecurringInvoice($invoice); - $response = $this->mailer->sendInvoice($invoice); - } else { - $response = trans('texts.recurring_too_soon'); - } - } else { - $response = $this->mailer->sendInvoice($invoice); - } - if ($response === true) { - $message = trans("texts.emailed_{$entityType}"); - Session::flash('message', $message); - } else { - Session::flash('error', $response); - } - } else { - $errorMessage = trans(Auth::user()->registered ? 'texts.confirmation_required' : 'texts.registration_required'); - Session::flash('error', $errorMessage); - Session::flash('message', $message); - } + private function emailRecurringInvoice(&$invoice) + { + if (!$invoice->shouldSendToday()) { + if ($date = $invoice->getNextSendDate()) { + $date = $invoice->account->formatDate($date); + $date .= ' ' . DEFAULT_SEND_RECURRING_HOUR . ':00 am ' . $invoice->account->getTimezone(); + return trans('texts.recurring_too_soon', ['date' => $date]); } else { - Session::flash('message', $message); + return trans('texts.no_longer_running'); } + } - $url = "{$entityType}s/".$invoice->public_id.'/edit'; + // switch from the recurring invoice to the generated invoice + $invoice = $this->invoiceRepo->createRecurringInvoice($invoice); - return Redirect::to($url); + // in case auto-bill is enabled then a receipt has been sent + if ($invoice->isPaid()) { + return true; + } else { + return $this->mailer->sendInvoice($invoice); } } @@ -562,18 +445,7 @@ class InvoiceController extends BaseController { Session::reflash(); - return Redirect::to('invoices/'.$publicId.'/edit'); - } - - /** - * Update the specified resource in storage. - * - * @param int $id - * @return Response - */ - public function update($publicId) - { - return InvoiceController::save($publicId); + return Redirect::to("invoices/{$publicId}/edit"); } /** @@ -584,13 +456,12 @@ class InvoiceController extends BaseController */ public function bulk($entityType = ENTITY_INVOICE) { - $action = Input::get('action'); - $statusId = Input::get('statusId', INVOICE_STATUS_SENT); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->invoiceRepo->bulk($ids, $action, $statusId); + $action = Input::get('bulk_action') ?: Input::get('action');; + $ids = Input::get('bulk_public_id') ?: (Input::get('public_id') ?: Input::get('ids')); + $count = $this->invoiceService->bulk($ids, $action); if ($count > 0) { - $key = $action == 'mark' ? "updated_{$entityType}" : "{$action}d_{$entityType}"; + $key = $action == 'markSent' ? "updated_{$entityType}" : "{$action}d_{$entityType}"; $message = Utils::pluralize($key, $count); Session::flash('message', $message); } @@ -605,7 +476,7 @@ class InvoiceController extends BaseController public function convertQuote($publicId) { $invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail(); - $clone = $this->invoiceRepo->cloneInvoice($invoice, $invoice->id); + $clone = $this->invoiceService->convertQuote($invoice); Session::flash('message', trans('texts.converted_to_invoice')); return Redirect::to('invoices/'.$clone->public_id); @@ -613,15 +484,6 @@ class InvoiceController extends BaseController public function cloneInvoice($publicId) { - /* - $invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail(); - $clone = $this->invoiceRepo->cloneInvoice($invoice); - $entityType = $invoice->getEntityType(); - - Session::flash('message', trans('texts.cloned_invoice')); - return Redirect::to("{$entityType}s/" . $clone->public_id); - */ - return self::edit($publicId, true); } @@ -639,7 +501,7 @@ class InvoiceController extends BaseController ->where('activity_type_id', '=', $activityTypeId) ->where('invoice_id', '=', $invoice->id) ->orderBy('id', 'desc') - ->get(['id', 'created_at', 'user_id', 'json_backup', 'message']); + ->get(['id', 'created_at', 'user_id', 'json_backup']); $versionsJson = []; $versionsSelect = []; @@ -654,7 +516,7 @@ class InvoiceController extends BaseController $backup->account = $invoice->account->toArray(); $versionsJson[$activity->id] = $backup; - $key = Utils::timestampToDateTimeString(strtotime($activity->created_at)) . ' - ' . Utils::decodeActivity($activity->message); + $key = Utils::timestampToDateTimeString(strtotime($activity->created_at)) . ' - ' . $activity->user->getDisplayName(); $versionsSelect[$lastId ? $lastId : 0] = $key; $lastId = $activity->id; } @@ -666,14 +528,9 @@ class InvoiceController extends BaseController 'versionsJson' => json_encode($versionsJson), 'versionsSelect' => $versionsSelect, 'invoiceDesigns' => InvoiceDesign::getDesigns(), + 'invoiceFonts' => Cache::get('fonts'), ]; return View::make('invoices.history', $data); } - - private function storePDF($encodedString, $invoice) - { - $encodedString = str_replace('data:application/pdf;base64,', '', $encodedString); - file_put_contents($invoice->getPDFPath(), base64_decode($encodedString)); - } } diff --git a/app/Http/Controllers/PaymentApiController.php b/app/Http/Controllers/PaymentApiController.php index 1fb81bf78283..2d4d45fa788c 100644 --- a/app/Http/Controllers/PaymentApiController.php +++ b/app/Http/Controllers/PaymentApiController.php @@ -1,36 +1,86 @@ paymentRepo = $paymentRepo; } + /** + * @SWG\Get( + * path="/payments", + * tags={"payment"}, + * summary="List of payments", + * @SWG\Response( + * response=200, + * description="A list with payments", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Payment")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { + $paginator = Payment::scope(); $payments = Payment::scope() - ->with('client', 'contact', 'invitation', 'user', 'invoice') - ->orderBy('created_at', 'desc') - ->get(); - $payments = Utils::remapPublicIds($payments); - - $response = json_encode($payments, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(count($payments)); + ->with('client.contacts', 'invitation', 'user', 'invoice'); - return Response::make($response, 200, $headers); + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $payments->whereHas('client', $filter); + $paginator->whereHas('client', $filter); + } + + $payments = $payments->orderBy('created_at', 'desc')->paginate(); + $paginator = $paginator->paginate(); + $transformer = new PaymentTransformer(Auth::user()->account, Input::get('serializer')); + + $data = $this->createCollection($payments, $transformer, 'payments', $paginator); + + return $this->response($data); } - + /** + * @SWG\Post( + * path="/payments", + * summary="Create a payment", + * tags={"payment"}, + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Payment") + * ), + * @SWG\Response( + * response=200, + * description="New payment", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function store() { $data = Input::all(); @@ -40,8 +90,8 @@ class PaymentApiController extends Controller $invoice = Invoice::scope($data['invoice_id'])->with('client')->first(); if ($invoice) { - $data['invoice'] = $invoice->public_id; - $data['client'] = $invoice->client->public_id; + $data['invoice_id'] = $invoice->id; + $data['client_id'] = $invoice->client->id; } else { $error = trans('validation.not_in', ['attribute' => 'invoice_id']); } @@ -53,15 +103,17 @@ class PaymentApiController extends Controller $data['transaction_reference'] = ''; } - if (!$error) { - $payment = $this->paymentRepo->save(false, $data); - $payment = Payment::scope($payment->public_id)->with('client', 'contact', 'user', 'invoice')->first(); - - $payment = Utils::remapPublicIds([$payment]); + if ($error) { + return $error; } - $response = json_encode($error ?: $payment, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(); - return Response::make($response, 200, $headers); + + $payment = $this->paymentRepo->save($data); + $payment = Payment::scope($payment->public_id)->with('client', 'contact', 'user', 'invoice')->first(); + + $transformer = new PaymentTransformer(Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($payment, $transformer, 'payment'); + + return $this->response($data); } } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 712f19dce6c5..8d769c126053 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -12,29 +12,25 @@ use Omnipay; use CreditCard; use URL; use Cache; -use Event; -use DateTime; -use App\Models\Account; use App\Models\Invoice; use App\Models\Invitation; use App\Models\Client; use App\Models\PaymentType; -use App\Models\Country; use App\Models\License; use App\Models\Payment; use App\Models\Affiliate; -use App\Models\AccountGatewayToken; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\AccountRepository; use App\Ninja\Mailers\ContactMailer; -use App\Events\InvoicePaid; +use App\Services\PaymentService; + +use App\Http\Requests\CreatePaymentRequest; +use App\Http\Requests\UpdatePaymentRequest; class PaymentController extends BaseController { - protected $creditRepo; - - public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer) + public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer, PaymentService $paymentService) { parent::__construct(); @@ -42,6 +38,7 @@ class PaymentController extends BaseController $this->invoiceRepo = $invoiceRepo; $this->accountRepo = $accountRepo; $this->contactMailer = $contactMailer; + $this->paymentService = $paymentService; } public function index() @@ -49,102 +46,22 @@ class PaymentController extends BaseController return View::make('list', array( 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), - 'columns' => Utils::trans(['checkbox', 'invoice', 'client', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'invoice', + 'client', + 'transaction_reference', + 'method', + 'payment_amount', + 'payment_date', + '' + ]), )); } - public function clientIndex() - { - $invitationKey = Session::get('invitation_key'); - if (!$invitationKey) { - return Redirect::to('/setup'); - } - - $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $account = $invitation->account; - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - - $data = [ - 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'entityType' => ENTITY_PAYMENT, - 'title' => trans('texts.payments'), - 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) - ]; - - return View::make('public_list', $data); - } - public function getDatatable($clientPublicId = null) { - $payments = $this->paymentRepo->find($clientPublicId, Input::get('sSearch')); - $table = Datatable::query($payments); - - if (!$clientPublicId) { - $table->addColumn('checkbox', function ($model) { return ''; }); - } - - $table->addColumn('invoice_number', function ($model) { return $model->invoice_public_id ? link_to('invoices/'.$model->invoice_public_id.'/edit', $model->invoice_number, ['class' => Utils::getEntityRowClass($model)]) : ''; }); - - if (!$clientPublicId) { - $table->addColumn('client_name', function ($model) { return link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)); }); - } - - $table->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; }) - ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : ''); }); - - return $table->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); }) - ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) - ->addColumn('dropdown', function ($model) { - if ($model->is_deleted || $model->invoice_is_deleted) { - return '
    '; - } - - $str = ''; - }) - ->make(); - } - - public function getClientDatatable() - { - $search = Input::get('sSearch'); - $invitationKey = Session::get('invitation_key'); - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->with('contact.client')->first(); - - if (!$invitation) { - return []; - } - - $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - return []; - } - - $payments = $this->paymentRepo->findForContact($invitation->contact->id, Input::get('sSearch')); - - return Datatable::query($payments) - ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number) : $model->invoice_number; }) - ->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; }) - ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); }) - ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); }) - ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) - ->make(); + return $this->paymentService->getDatatable($clientPublicId, Input::get('sSearch')); } public function create($clientPublicId = 0, $invoicePublicId = 0) @@ -166,6 +83,7 @@ class PaymentController extends BaseController 'url' => "payments", 'title' => trans('texts.new_payment'), 'paymentTypes' => Cache::get('paymentTypes'), + 'paymentTypeId' => Input::get('paymentTypeId'), 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ); return View::make('payments.edit', $data); @@ -191,36 +109,9 @@ class PaymentController extends BaseController return View::make('payments.edit', $data); } - private function createGateway($accountGateway) - { - $gateway = Omnipay::create($accountGateway->gateway->provider); - $config = json_decode($accountGateway->config); - - foreach ($config as $key => $val) { - if (!$val) { - continue; - } - - $function = "set".ucfirst($key); - $gateway->$function($val); - } - - if ($accountGateway->gateway->id == GATEWAY_DWOLLA) { - if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { - $gateway->setKey($_ENV['DWOLLA_SANDBOX_KEY']); - $gateway->setSecret($_ENV['DWOLLA_SANSBOX_SECRET']); - } elseif (isset($_ENV['DWOLLA_KEY']) && isset($_ENV['DWOLLA_SECRET'])) { - $gateway->setKey($_ENV['DWOLLA_KEY']); - $gateway->setSecret($_ENV['DWOLLA_SECRET']); - } - } - - return $gateway; - } - private function getLicensePaymentDetails($input, $affiliate) { - $data = self::convertInputForOmnipay($input); + $data = $this->paymentService->convertInputForOmnipay($input); $card = new CreditCard($data); return [ @@ -232,69 +123,9 @@ class PaymentController extends BaseController ]; } - private function convertInputForOmnipay($input) - { - $data = [ - 'firstName' => $input['first_name'], - 'lastName' => $input['last_name'], - 'number' => $input['card_number'], - 'expiryMonth' => $input['expiration_month'], - 'expiryYear' => $input['expiration_year'], - 'cvv' => $input['cvv'], - ]; - - if (isset($input['country_id'])) { - $country = Country::find($input['country_id']); - - $data = array_merge($data, [ - 'billingAddress1' => $input['address1'], - 'billingAddress2' => $input['address2'], - 'billingCity' => $input['city'], - 'billingState' => $input['state'], - 'billingPostcode' => $input['postal_code'], - 'billingCountry' => $country->iso_3166_2, - 'shippingAddress1' => $input['address1'], - 'shippingAddress2' => $input['address2'], - 'shippingCity' => $input['city'], - 'shippingState' => $input['state'], - 'shippingPostcode' => $input['postal_code'], - 'shippingCountry' => $country->iso_3166_2 - ]); - } - - return $data; - } - - private function getPaymentDetails($invitation, $input = null) - { - $invoice = $invitation->invoice; - $account = $invoice->account; - $key = $invoice->account_id.'-'.$invoice->invoice_number; - $currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD'); - - if ($input) { - $data = self::convertInputForOmnipay($input); - Session::put($key, $data); - } elseif (Session::get($key)) { - $data = Session::get($key); - } else { - $data = []; - } - - $card = new CreditCard($data); - - return [ - 'amount' => $invoice->getRequestedAmount(), - 'card' => $card, - 'currency' => $currencyCode, - 'returnUrl' => URL::to('complete'), - 'cancelUrl' => $invitation->getLink(), - 'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}", - ]; - } - public function show_payment($invitationKey, $paymentType = false) { + $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail(); $invoice = $invitation->invoice; $client = $invoice->client; @@ -304,16 +135,28 @@ class PaymentController extends BaseController if ($paymentType) { $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); } else { - $paymentType = Session::get('payment_type', $account->account_gateways[0]->getPaymentType()); + $paymentType = Session::get($invitation->id . 'payment_type') ?: + $account->account_gateways[0]->getPaymentType(); } + if ($paymentType == PAYMENT_TYPE_TOKEN) { $useToken = true; $paymentType = PAYMENT_TYPE_CREDIT_CARD; } - Session::put('payment_type', $paymentType); + Session::put($invitation->id . 'payment_type', $paymentType); + + $accountGateway = $invoice->client->account->getGatewayByType($paymentType); + $gateway = $accountGateway->gateway; + + $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); + // Handle offsite payments - if ($useToken || $paymentType != PAYMENT_TYPE_CREDIT_CARD) { + if ($useToken || $paymentType != PAYMENT_TYPE_CREDIT_CARD + || $gateway->id == GATEWAY_EWAY + || $gateway->id == GATEWAY_TWO_CHECKOUT + || $gateway->id == GATEWAY_PAYFAST + || $gateway->id == GATEWAY_MOLLIE) { if (Session::has('error')) { Session::reflash(); return Redirect::to('view/'.$invitationKey); @@ -322,10 +165,6 @@ class PaymentController extends BaseController } } - $accountGateway = $invoice->client->account->getGatewayByType($paymentType); - $gateway = $accountGateway->gateway; - $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); - $data = [ 'showBreadcrumbs' => false, 'url' => 'payment/'.$invitationKey, @@ -334,12 +173,16 @@ class PaymentController extends BaseController 'client' => $client, 'contact' => $invitation->contact, 'gateway' => $gateway, + 'accountGateway' => $accountGateway, 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, 'countries' => Cache::get('countries'), 'currencyId' => $client->getCurrencyId(), 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), 'account' => $client->account, 'hideLogo' => $account->isWhiteLabel(), + 'hideHeader' => $account->isNinjaAccount(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), 'showAddress' => $accountGateway->show_address, ]; @@ -358,7 +201,11 @@ class PaymentController extends BaseController } } - Session::set('product_id', Input::get('product_id', PRODUCT_ONE_CLICK_INSTALL)); + if (Input::has('product_id')) { + Session::set('product_id', Input::get('product_id')); + } else if (!Session::has('product_id')) { + Session::set('product_id', PRODUCT_ONE_CLICK_INSTALL); + } if (!Session::get('affiliate_id')) { return Utils::fatalError(); @@ -370,7 +217,7 @@ class PaymentController extends BaseController $account = $this->accountRepo->getNinjaAccount(); $account->load('account_gateways.gateway'); - $accountGateway = $account->getGatewayByType(Session::get('payment_type')); + $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD); $gateway = $accountGateway->gateway; $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); @@ -384,9 +231,12 @@ class PaymentController extends BaseController 'client' => false, 'contact' => false, 'gateway' => $gateway, + 'account' => $account, + 'accountGateway' => $accountGateway, 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, 'countries' => Cache::get('countries'), 'currencyId' => 1, + 'currencyCode' => 'USD', 'paymentTitle' => $affiliate->payment_title, 'paymentSubtitle' => $affiliate->payment_subtitle, 'showAddress' => true, @@ -417,7 +267,8 @@ class PaymentController extends BaseController if ($validator->fails()) { return Redirect::to('license') - ->withErrors($validator); + ->withErrors($validator) + ->withInput(); } $account = $this->accountRepo->getNinjaAccount(); @@ -430,21 +281,13 @@ class PaymentController extends BaseController if ($testMode) { $ref = 'TEST_MODE'; } else { - $gateway = self::createGateway($accountGateway); + $gateway = $this->paymentService->createGateway($accountGateway); $details = self::getLicensePaymentDetails(Input::all(), $affiliate); $response = $gateway->purchase($details)->send(); $ref = $response->getTransactionReference(); - if (!$ref) { - Session::flash('error', $response->getMessage()); - - return Redirect::to('license')->withInput(); - } - - if (!$response->isSuccessful()) { - Session::flash('error', $response->getMessage()); - Utils::logError($response->getMessage()); - + if (!$response->isSuccessful() || !$ref) { + $this->error('License', $response->getMessage(), $accountGateway); return Redirect::to('license')->withInput(); } } @@ -465,7 +308,8 @@ class PaymentController extends BaseController 'message' => $affiliate->payment_subtitle, 'license' => $licenseKey, 'hideHeader' => true, - 'productId' => $license->product_id + 'productId' => $license->product_id, + 'price' => $affiliate->price, ]; $name = "{$license->first_name} {$license->last_name}"; @@ -478,10 +322,7 @@ class PaymentController extends BaseController return View::make('public.license', $data); } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage); - Utils::logError(Utils::getErrorString($e)); - + $this->error('License-Uncaught', false, $accountGateway, $e); return Redirect::to('license')->withInput(); } } @@ -514,17 +355,26 @@ class PaymentController extends BaseController $invoice = $invitation->invoice; $client = $invoice->client; $account = $client->account; - $accountGateway = $account->getGatewayByType(Session::get('payment_type')); + $accountGateway = $account->getGatewayByType(Session::get($invitation->id . 'payment_type')); + $rules = [ 'first_name' => 'required', 'last_name' => 'required', - 'card_number' => 'required', - 'expiration_month' => 'required', - 'expiration_year' => 'required', - 'cvv' => 'required', ]; + if ( ! Input::get('stripeToken')) { + $rules = array_merge( + $rules, + [ + 'card_number' => 'required', + 'expiration_month' => 'required', + 'expiration_year' => 'required', + 'cvv' => 'required', + ] + ); + } + if ($accountGateway->show_address) { $rules = array_merge($rules, [ 'address1' => 'required', @@ -539,13 +389,11 @@ class PaymentController extends BaseController $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { - Utils::logError('Payment Error [invalid]'); return Redirect::to('payment/'.$invitationKey) ->withErrors($validator) - ->withInput(); + ->withInput(Request::except('cvv')); } - if ($accountGateway->update_address) { $client->address1 = trim(Input::get('address1')); $client->address2 = trim(Input::get('address2')); @@ -556,59 +404,67 @@ class PaymentController extends BaseController $client->save(); } } - + try { - $gateway = self::createGateway($accountGateway); - $details = self::getPaymentDetails($invitation, ($useToken || !$onSite) ? false : Input::all()); - + // For offsite payments send the client's details on file + // If we're using a token then we don't need to send any other data + if (!$onSite || $useToken) { + $data = false; + } else { + $data = Input::all(); + } + + $gateway = $this->paymentService->createGateway($accountGateway); + $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, $data); + + // check if we're creating/using a billing token if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + if ($token = Input::get('stripeToken')) { + $details['token'] = $token; + unset($details['card']); + } + if ($useToken) { - $details['cardReference'] = $client->getGatewayToken(); + $details['customerReference'] = $client->getGatewayToken(); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $tokenResponse = $gateway->createCard($details)->send(); - $cardReference = $tokenResponse->getCardReference(); - - if ($cardReference) { - $details['cardReference'] = $cardReference; - - $token = AccountGatewayToken::where('client_id', '=', $client->id) - ->where('account_gateway_id', '=', $accountGateway->id)->first(); - - if (!$token) { - $token = new AccountGatewayToken(); - $token->account_id = $account->id; - $token->contact_id = $invitation->contact_id; - $token->account_gateway_id = $accountGateway->id; - $token->client_id = $client->id; - } - - $token->token = $cardReference; - $token->save(); + $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id); + if ($token) { + $details['customerReference'] = $token; } else { - Session::flash('error', $tokenResponse->getMessage()); - Utils::logError('Payment Error [no-token-ref]: ' . $tokenResponse->getMessage()); - return Redirect::to('payment/'.$invitationKey)->withInput(); + $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); + return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } } } - + $response = $gateway->purchase($details)->send(); - $ref = $response->getTransactionReference(); + + + if ($accountGateway->gateway_id == GATEWAY_EWAY) { + $ref = $response->getData()['AccessCode']; + } elseif ($accountGateway->gateway_id == GATEWAY_TWO_CHECKOUT) { + $ref = $response->getData()['cart_order_id']; + } elseif ($accountGateway->gateway_id == GATEWAY_PAYFAST) { + $ref = $response->getData()['m_payment_id']; + } elseif ($accountGateway->gateway_id == GATEWAY_GOCARDLESS) { + $ref = $response->getData()['signature']; + } else { + $ref = $response->getTransactionReference(); + } if (!$ref) { - - Session::flash('error', $response->getMessage()); - Utils::logError('Payment Error [no-ref]: ' . $response->getMessage()); + $this->error('No-Ref', $response->getMessage(), $accountGateway); if ($onSite) { - return Redirect::to('payment/'.$invitationKey)->withInput(); + return Redirect::to('payment/'.$invitationKey) + ->withInput(Request::except('cvv')); } else { return Redirect::to('view/'.$invitationKey); } } if ($response->isSuccessful()) { - $payment = self::createPayment($invitation, $ref); + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref); Session::flash('message', trans('texts.applied_payment')); if ($account->account_key == NINJA_ACCOUNT_KEY) { @@ -618,72 +474,30 @@ class PaymentController extends BaseController return Redirect::to('view/'.$payment->invitation->invitation_key); } elseif ($response->isRedirect()) { + $invitation->transaction_reference = $ref; $invitation->save(); - Session::put('transaction_reference', $ref); Session::save(); $response->redirect(); } else { - Session::flash('error', $response->getMessage()); - Utils::logError('Payment Error [fatal]: ' . $response->getMessage()); - - return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.

    ', $response->getMessage()); + $this->error('Unknown', $response->getMessage(), $accountGateway); + if ($onSite) { + return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); + } else { + return Redirect::to('view/'.$invitationKey); + } } } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage."

    ".$e->getMessage()); - Utils::logError('Payment Error [uncaught]:' . Utils::getErrorString($e)); - + $this->error('Uncaught', false, $accountGateway, $e); if ($onSite) { - return Redirect::to('payment/'.$invitationKey)->withInput(); + return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } else { return Redirect::to('view/'.$invitationKey); } } } - private function createPayment($invitation, $ref, $payerId = null) - { - $invoice = $invitation->invoice; - $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); - - if ($invoice->account->account_key == NINJA_ACCOUNT_KEY - && $invoice->amount == PRO_PLAN_PRICE) { - $account = Account::with('users')->find($invoice->client->public_id); - if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); - $account->pro_plan_paid = $date->modify('+1 year')->format('Y-m-d'); - } else { - $account->pro_plan_paid = date_create()->format('Y-m-d'); - } - $account->save(); - - $user = $account->users()->first(); - $this->accountRepo->syncAccounts($user->id, $account->pro_plan_paid); - } - - $payment = Payment::createNew($invitation); - $payment->invitation_id = $invitation->id; - $payment->account_gateway_id = $accountGateway->id; - $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->getRequestedAmount(); - $payment->client_id = $invoice->client_id; - $payment->contact_id = $invitation->contact_id; - $payment->transaction_reference = $ref; - $payment->payment_date = date_create()->format('Y-m-d'); - - if ($payerId) { - $payment->payer_id = $payerId; - } - - $payment->save(); - - Event::fire(new InvoicePaid($payment)); - - return $payment; - } - public function offsite_payment() { $payerId = Request::query('PayerID'); @@ -692,101 +506,102 @@ class PaymentController extends BaseController if (!$token) { $token = Session::pull('transaction_reference'); } - if (!$token) { return redirect(NINJA_WEB_URL); } $invitation = Invitation::with('invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('transaction_reference', '=', $token)->firstOrFail(); $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; - $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); - $gateway = self::createGateway($accountGateway); + if ($payerId) { + $paymentType = PAYMENT_TYPE_PAYPAL; + } else { + $paymentType = Session::get($invitation->id . 'payment_type'); + } + if (!$paymentType) { + $this->error('No-Payment-Type', false, false); + return Redirect::to($invitation->getLink()); + } + $accountGateway = $account->getGatewayByType($paymentType); + $gateway = $this->paymentService->createGateway($accountGateway); // Check for Dwolla payment error if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) { - $errorMessage = trans('texts.payment_error')."\n\n".Input::get('error_description'); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage); - return Redirect::to('view/'.$invitation->invitation_key); + $this->error('Dwolla', Input::get('error_description'), $accountGateway); + return Redirect::to($invitation->getLink()); + } + + // PayFast transaction referencce + if ($accountGateway->isGateway(GATEWAY_PAYFAST) && Request::has('pt')) { + $token = Request::query('pt'); } try { - if (method_exists($gateway, 'completePurchase')) { - $details = self::getPaymentDetails($invitation); - $response = $gateway->completePurchase($details)->send(); - $ref = $response->getTransactionReference(); + if (method_exists($gateway, 'completePurchase') + && !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT) + && !$accountGateway->isGateway(GATEWAY_CHECKOUT_COM)) { + $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway); - if ($response->isSuccessful()) { - $payment = self::createPayment($invitation, $ref, $payerId); + $response = $this->paymentService->completePurchase($gateway, $accountGateway, $details, $token); + + $ref = $response->getTransactionReference() ?: $token; + + if ($response->isCancelled()) { + // do nothing + } elseif ($response->isSuccessful()) { + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId); Session::flash('message', trans('texts.applied_payment')); - - return Redirect::to('view/'.$invitation->invitation_key); } else { - $errorMessage = trans('texts.payment_error')."\n\n".$response->getMessage(); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage); - - return Redirect::to('view/'.$invitation->invitation_key); + $this->error('offsite', $response->getMessage(), $accountGateway); } + return Redirect::to($invitation->getLink()); } else { - $payment = self::createPayment($invitation, $token, $payerId); + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $token, $payerId); Session::flash('message', trans('texts.applied_payment')); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage."\n\n".$e->getMessage()); - return Redirect::to('view/'.$invitation->invitation_key); + $this->error('Offsite-uncaught', false, $accountGateway, $e); + return Redirect::to($invitation->getLink()); } } - public function store() + public function store(CreatePaymentRequest $request) { - return $this->save(); - } + $input = $request->input(); + $input['invoice_id'] = Invoice::getPrivateId($input['invoice']); + $input['client_id'] = Client::getPrivateId($input['client']); + $payment = $this->paymentRepo->save($input); - public function update($publicId) - { - return $this->save($publicId); - } - - private function save($publicId = null) - { - if (!$publicId && $errors = $this->paymentRepo->getErrors(Input::all())) { - $url = $publicId ? 'payments/'.$publicId.'/edit' : 'payments/create'; - - return Redirect::to($url) - ->withErrors($errors) - ->withInput(); + if (Input::get('email_receipt')) { + $this->contactMailer->sendPaymentConfirmation($payment); + Session::flash('message', trans('texts.created_payment_emailed_client')); } else { - $payment = $this->paymentRepo->save($publicId, Input::all()); - - if ($publicId) { - Session::flash('message', trans('texts.updated_payment')); - - return Redirect::to('payments/'); - } else { - if (Input::get('email_receipt')) { - $this->contactMailer->sendPaymentConfirmation($payment); - Session::flash('message', trans('texts.created_payment_emailed_client')); - } else { - Session::flash('message', trans('texts.created_payment')); - } - - return Redirect::to('clients/'.Input::get('client')); - } + Session::flash('message', trans('texts.created_payment')); } + + return redirect()->to($payment->client->getRoute()); + } + + public function update(UpdatePaymentRequest $request) + { + $input = $request->input(); + $payment = $this->paymentRepo->save($input); + + Session::flash('message', trans('texts.updated_payment')); + + return redirect()->to($payment->getRoute()); } public function bulk() { $action = Input::get('action'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->paymentRepo->bulk($ids, $action); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + $count = $this->paymentService->bulk($ids, $action); if ($count > 0) { $message = Utils::pluralize($action.'d_payment', $count); @@ -795,4 +610,16 @@ class PaymentController extends BaseController return Redirect::to('payments'); } + + private function error($type, $error, $accountGateway = false, $exception = false) + { + $message = ''; + if ($accountGateway && $accountGateway->gateway) { + $message = $accountGateway->gateway->name . ': '; + } + $message .= $error ?: trans('texts.payment_error'); + + Session::flash('error', $message); + Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message)); + } } diff --git a/app/Http/Controllers/PaymentTermController.php b/app/Http/Controllers/PaymentTermController.php new file mode 100644 index 000000000000..623ca1bf42da --- /dev/null +++ b/app/Http/Controllers/PaymentTermController.php @@ -0,0 +1,103 @@ +paymentTermService = $paymentTermService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS); + } + + public function getDatatable() + { + return $this->paymentTermService->getDatatable(); + } + + public function edit($publicId) + { + $data = [ + 'paymentTerm' => PaymentTerm::scope($publicId)->firstOrFail(), + 'method' => 'PUT', + 'url' => 'payment_terms/'.$publicId, + 'title' => trans('texts.edit_payment_term'), + ]; + + return View::make('accounts.payment_term', $data); + } + + public function create() + { + $data = [ + 'paymentTerm' => null, + 'method' => 'POST', + 'url' => 'payment_terms', + 'title' => trans('texts.create_payment_term'), + ]; + + return View::make('accounts.payment_term', $data); + } + + public function store() + { + return $this->save(); + } + + public function update($publicId) + { + return $this->save($publicId); + } + + private function save($publicId = false) + { + if ($publicId) { + $paymentTerm = PaymentTerm::scope($publicId)->firstOrFail(); + } else { + $paymentTerm = PaymentTerm::createNew(); + } + + $paymentTerm->name = trim(Input::get('name')); + $paymentTerm->num_days = Utils::parseInt(Input::get('num_days')); + $paymentTerm->save(); + + $message = $publicId ? trans('texts.updated_payment_term') : trans('texts.created_payment_term'); + Session::flash('message', $message); + + return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS); + } + + public function bulk() + { + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->paymentTermService->bulk($ids, $action); + + Session::flash('message', trans('texts.archived_payment_term')); + + return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS); + } + +} diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index d971115b22f1..e25f486688d5 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -12,58 +12,58 @@ use Session; use Redirect; use App\Models\Product; +use App\Models\TaxRate; +use App\Services\ProductService; class ProductController extends BaseController { + protected $productService; + + public function __construct(ProductService $productService) + { + parent::__construct(); + + $this->productService = $productService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_PRODUCTS); + } + public function getDatatable() { - $query = DB::table('products') - ->where('products.account_id', '=', Auth::user()->account_id) - ->where('products.deleted_at', '=', null) - ->select('products.public_id', 'products.product_key', 'products.notes', 'products.cost'); - - return Datatable::query($query) - ->addColumn('product_key', function ($model) { return link_to('products/'.$model->public_id.'/edit', $model->product_key); }) - ->addColumn('notes', function ($model) { return nl2br(Str::limit($model->notes, 100)); }) - ->addColumn('cost', function ($model) { return Utils::formatMoney($model->cost); }) - ->addColumn('dropdown', function ($model) { - return '

    '; - }) - ->orderColumns(['cost', 'product_key', 'cost']) - ->make(); + return $this->productService->getDatatable(Auth::user()->account_id); } public function edit($publicId) { + $account = Auth::user()->account; + $data = [ - 'showBreadcrumbs' => false, - 'product' => Product::scope($publicId)->firstOrFail(), - 'method' => 'PUT', - 'url' => 'products/'.$publicId, - 'title' => trans('texts.edit_product'), - ]; + 'account' => $account, + 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, + 'product' => Product::scope($publicId)->firstOrFail(), + 'method' => 'PUT', + 'url' => 'products/'.$publicId, + 'title' => trans('texts.edit_product'), + ]; return View::make('accounts.product', $data); } public function create() { + $account = Auth::user()->account; + $data = [ - 'showBreadcrumbs' => false, - 'product' => null, - 'method' => 'POST', - 'url' => 'products', - 'title' => trans('texts.create_product'), - ]; + 'account' => $account, + 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, + 'product' => null, + 'method' => 'POST', + 'url' => 'products', + 'title' => trans('texts.create_product'), + ]; return View::make('accounts.product', $data); } @@ -89,21 +89,24 @@ class ProductController extends BaseController $product->product_key = trim(Input::get('product_key')); $product->notes = trim(Input::get('notes')); $product->cost = trim(Input::get('cost')); + $product->default_tax_rate_id = Input::get('default_tax_rate_id'); + $product->save(); $message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product'); Session::flash('message', $message); - return Redirect::to('company/products'); + return Redirect::to('settings/' . ACCOUNT_PRODUCTS); } - public function archive($publicId) + public function bulk() { - $product = Product::scope($publicId)->firstOrFail(); - $product->delete(); + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->productService->bulk($ids, $action); Session::flash('message', trans('texts.archived_product')); - return Redirect::to('company/products'); + return Redirect::to('settings/' . ACCOUNT_PRODUCTS); } } diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php new file mode 100644 index 000000000000..834a89beac59 --- /dev/null +++ b/app/Http/Controllers/PublicClientController.php @@ -0,0 +1,347 @@ +invoiceRepo = $invoiceRepo; + $this->paymentRepo = $paymentRepo; + $this->activityRepo = $activityRepo; + $this->paymentService = $paymentService; + } + + public function view($invitationKey) + { + if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + ]); + } + + $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $invoice->account; + + if (!$account->checkSubdomain(Request::server('HTTP_HOST'))) { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + ]); + } + + if (!Input::has('phantomjs') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { + if ($invoice->is_quote) { + event(new QuoteInvitationWasViewed($invoice, $invitation)); + } else { + event(new InvoiceInvitationWasViewed($invoice, $invitation)); + } + } + + Session::put($invitationKey, true); // track this invitation has been seen + Session::put('invitation_key', $invitationKey); // track current invitation + + $account->loadLocalizationSettings($client); + + $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); + $invoice->due_date = Utils::fromSqlDate($invoice->due_date); + $invoice->is_pro = $account->isPro(); + $invoice->invoice_fonts = $account->getFontsData(); + + if ($invoice->invoice_design_id == CUSTOM_DESIGN) { + $invoice->invoice_design->javascript = $account->custom_design; + } else { + $invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake; + } + $contact = $invitation->contact; + $contact->setVisible([ + 'first_name', + 'last_name', + 'email', + 'phone', + ]); + + $paymentTypes = $this->getPaymentTypes($client, $invitation); + $paymentURL = ''; + if (count($paymentTypes)) { + $paymentURL = $paymentTypes[0]['url']; + if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { + $paymentURL = URL::to($paymentURL); + } + } + + $showApprove = $invoice->quote_invoice_id ? false : true; + if ($invoice->due_date) { + $showApprove = time() < strtotime($invoice->due_date); + } + if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) { + $showApprove = false; + } + + // Checkout.com requires first getting a payment token + $checkoutComToken = false; + $checkoutComKey = false; + if ($accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM)) { + if ($checkoutComToken = $this->paymentService->getCheckoutComToken($invitation)) { + $checkoutComKey = $accountGateway->getConfigField('publicApiKey'); + $invitation->transaction_reference = $checkoutComToken; + $invitation->save(); + } + } + + $data = array( + 'account' => $account, + 'showApprove' => $showApprove, + 'showBreadcrumbs' => false, + 'hideLogo' => $account->isWhiteLabel(), + 'hideHeader' => $account->isNinjaAccount(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'invoice' => $invoice->hidePrivateFields(), + 'invitation' => $invitation, + 'invoiceLabels' => $account->getInvoiceLabels(), + 'contact' => $contact, + 'paymentTypes' => $paymentTypes, + 'paymentURL' => $paymentURL, + 'checkoutComToken' => $checkoutComToken, + 'checkoutComKey' => $checkoutComKey, + 'phantomjs' => Input::has('phantomjs'), + ); + + return View::make('invoices.view', $data); + } + + private function getPaymentTypes($client, $invitation) + { + $paymentTypes = []; + $account = $client->account; + + if ($client->getGatewayToken()) { + $paymentTypes[] = [ + 'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file') + ]; + } + foreach(Gateway::$paymentTypes as $type) { + if ($account->getGatewayByType($type)) { + $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); + $url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"); + + // PayPal doesn't allow being run in an iframe so we need to open in new tab + if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) { + $url = 'javascript:window.open("'.$url.'", "_blank")'; + } + $paymentTypes[] = [ + 'url' => $url, 'label' => trans('texts.'.strtolower($type)) + ]; + } + } + + return $paymentTypes; + } + + public function dashboard() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $invoice = $invitation->invoice; + $client = $invoice->client; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'account' => $account, + 'client' => $client, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + ]; + + return response()->view('invited.dashboard', $data); + } + + public function activityDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + $invoice = $invitation->invoice; + + $query = $this->activityRepo->findByClientId($invoice->client_id); + $query->where('activities.adjustment', '!=', 0); + + return Datatable::query($query) + ->addColumn('activities.id', function ($model) { return Utils::timestampToDateTimeString(strtotime($model->created_at)); }) + ->addColumn('activity_type_id', function ($model) { + $data = [ + 'client' => Utils::getClientDisplayName($model), + 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name), + 'invoice' => trans('texts.invoice') . ' ' . $model->invoice, + 'contact' => Utils::getClientDisplayName($model), + 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), + ]; + + return trans("texts.activity_{$model->activity_type_id}", $data); + }) + ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); }) + ->addColumn('adjustment', function ($model) { return $model->adjustment != 0 ? Utils::wrapAdjustment($model->adjustment, $model->currency_id, $model->country_id) : ''; }) + ->make(); + } + + public function invoiceIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.invoices'), + 'entityType' => ENTITY_INVOICE, + 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']), + ]; + + return response()->view('public_list', $data); + } + + public function invoiceDatatable() + { + if (!$invitation = $this->getInvitation()) { + return ''; + } + + return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_INVOICE, Input::get('sSearch')); + } + + + public function paymentIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'entityType' => ENTITY_PAYMENT, + 'title' => trans('texts.payments'), + 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) + ]; + + return response()->view('public_list', $data); + } + + public function paymentDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + $payments = $this->paymentRepo->findForContact($invitation->contact->id, Input::get('sSearch')); + + return Datatable::query($payments) + ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number) : $model->invoice_number; }) + ->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; }) + ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); }) + ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }) + ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) + ->make(); + } + + public function quoteIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.quotes'), + 'entityType' => ENTITY_QUOTE, + 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), + ]; + + return response()->view('public_list', $data); + } + + + public function quoteDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + + return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, Input::get('sSearch')); + } + + private function returnError() + { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + ]); + } + + private function getInvitation() + { + $invitationKey = session('invitation_key'); + + if (!$invitationKey) { + return false; + } + + $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); + + if (!$invitation || $invitation->is_deleted) { + return false; + } + + $invoice = $invitation->invoice; + + if (!$invoice || $invoice->is_deleted) { + return false; + } + + return $invitation; + } + +} diff --git a/app/Http/Controllers/QuoteApiController.php b/app/Http/Controllers/QuoteApiController.php index 83e5e8781179..3e3cfa580c23 100644 --- a/app/Http/Controllers/QuoteApiController.php +++ b/app/Http/Controllers/QuoteApiController.php @@ -1,32 +1,64 @@ invoiceRepo = $invoiceRepo; } + /** + * @SWG\Get( + * path="/quotes", + * tags={"quote"}, + * summary="List of quotes", + * @SWG\Response( + * response=200, + * description="A list with quotes", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { + $paginator = Invoice::scope(); $invoices = Invoice::scope() - ->with('client', 'user') - ->where('invoices.is_quote', '=', true) - ->orderBy('created_at', 'desc') - ->get(); - $invoices = Utils::remapPublicIds($invoices); + ->with('client', 'invitations', 'user', 'invoice_items') + ->where('invoices.is_quote', '=', true); - $response = json_encode($invoices, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(count($invoices)); + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $invoices->whereHas('client', $filter); + $paginator->whereHas('client', $filter); + } - return Response::make($response, 200, $headers); + $invoices = $invoices->orderBy('created_at', 'desc')->paginate(); + + $transformer = new QuoteTransformer(\Auth::user()->account, Input::get('serializer')); + $paginator = $paginator->paginate(); + + $data = $this->createCollection($invoices, $transformer, 'quotes', $paginator); + + return $this->response($data); } /* diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index d84ce2fece65..47b5c1a338e4 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -24,24 +24,24 @@ use App\Models\Invoice; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; -use App\Ninja\Repositories\TaxRateRepository; -use App\Events\QuoteApproved; +use App\Events\QuoteInvitationWasApproved; +use App\Services\InvoiceService; class QuoteController extends BaseController { protected $mailer; protected $invoiceRepo; protected $clientRepo; - protected $taxRateRepo; + protected $invoiceService; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, TaxRateRepository $taxRateRepo) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService) { parent::__construct(); $this->mailer = $mailer; $this->invoiceRepo = $invoiceRepo; $this->clientRepo = $clientRepo; - $this->taxRateRepo = $taxRateRepo; + $this->invoiceService = $invoiceService; } public function index() @@ -53,40 +53,19 @@ class QuoteController extends BaseController $data = [ 'title' => trans('texts.quotes'), 'entityType' => ENTITY_QUOTE, - 'columns' => Utils::trans(['checkbox', 'quote_number', 'client', 'quote_date', 'quote_total', 'due_date', 'status', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'quote_number', + 'client', + 'quote_date', + 'quote_total', + 'valid_until', + 'status', + 'action' + ]), ]; - /* - if (Invoice::scope()->where('is_recurring', '=', true)->count() > 0) - { - $data['secEntityType'] = ENTITY_RECURRING_INVOICE; - $data['secColumns'] = Utils::trans(['checkbox', 'frequency', 'client', 'start_date', 'end_date', 'quote_total', 'action']); - } - */ - - return View::make('list', $data); - } - - public function clientIndex() - { - $invitationKey = Session::get('invitation_key'); - if (!$invitationKey) { - return Redirect::to('/setup'); - } - - $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $account = $invitation->account; - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - - $data = [ - 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'title' => trans('texts.quotes'), - 'entityType' => ENTITY_QUOTE, - 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), - ]; - - return View::make('public_list', $data); + return response()->view('list', $data); } public function getDatatable($clientPublicId = null) @@ -94,26 +73,7 @@ class QuoteController extends BaseController $accountId = Auth::user()->account_id; $search = Input::get('sSearch'); - return $this->invoiceRepo->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search); - } - - public function getClientDatatable() - { - $search = Input::get('sSearch'); - $invitationKey = Session::get('invitation_key'); - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); - - if (!$invitation || $invitation->is_deleted) { - return []; - } - - $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - return []; - } - - return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, $search); + return $this->invoiceService->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search); } public function create($clientPublicId = 0) @@ -122,25 +82,24 @@ class QuoteController extends BaseController return Redirect::to('/invoices/create'); } - $client = null; - $invoiceNumber = Auth::user()->account->getNextInvoiceNumber(true); - $account = Account::with('country')->findOrFail(Auth::user()->account_id); - + $account = Auth::user()->account; + $clientId = null; if ($clientPublicId) { - $client = Client::scope($clientPublicId)->firstOrFail(); + $clientId = Client::getPrivateId($clientPublicId); } + $invoice = $account->createInvoice(ENTITY_QUOTE, $clientId); + $invoice->public_id = 0; - $data = array( - 'account' => $account, - 'invoice' => null, - 'data' => Input::old('data'), - 'invoiceNumber' => $invoiceNumber, - 'method' => 'POST', - 'url' => 'invoices', - 'title' => trans('texts.new_quote'), - 'client' => $client, ); + $data = [ + 'entityType' => $invoice->getEntityType(), + 'invoice' => $invoice, + 'data' => Input::old('data'), + 'method' => 'POST', + 'url' => 'invoices', + 'title' => trans('texts.new_quote'), + ]; $data = array_merge($data, self::getViewModel()); - + return View::make('invoices.edit', $data); } @@ -156,8 +115,10 @@ class QuoteController extends BaseController 'currencies' => Cache::get('currencies'), 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), + 'languages' => Cache::get('languages'), 'industries' => Cache::get('industries'), 'invoiceDesigns' => InvoiceDesign::getDesigns(), + 'invoiceFonts' => Cache::get('fonts'), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), 'isRecurring' => false, ]; @@ -165,22 +126,21 @@ class QuoteController extends BaseController public function bulk() { - $action = Input::get('action'); + $action = Input::get('bulk_action') ?: Input::get('action');; + $ids = Input::get('bulk_public_id') ?: (Input::get('public_id') ?: Input::get('ids')); if ($action == 'convert') { - $invoice = Invoice::with('invoice_items')->scope(Input::get('id'))->firstOrFail(); - $clone = $this->invoiceRepo->cloneInvoice($invoice, $invoice->id); + $invoice = Invoice::with('invoice_items')->scope($ids)->firstOrFail(); + $clone = $this->invoiceService->convertQuote($invoice); Session::flash('message', trans('texts.converted_to_invoice')); return Redirect::to('invoices/'.$clone->public_id); } - - $statusId = Input::get('statusId'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->invoiceRepo->bulk($ids, $action, $statusId); + + $count = $this->invoiceService->bulk($ids, $action); if ($count > 0) { - $key = $action == 'mark' ? "updated_quote" : "{$action}d_quote"; + $key = $action == 'markSent' ? "updated_quote" : "{$action}d_quote"; $message = Utils::pluralize($key, $count); Session::flash('message', $message); } @@ -197,19 +157,8 @@ class QuoteController extends BaseController $invitation = Invitation::with('invoice.invoice_items', 'invoice.invitations')->where('invitation_key', '=', $invitationKey)->firstOrFail(); $invoice = $invitation->invoice; - if ($invoice->is_quote && !$invoice->quote_invoice_id) { - Event::fire(new QuoteApproved($invoice)); - Activity::approveQuote($invitation); - - $invoice = $this->invoiceRepo->cloneInvoice($invoice, $invoice->id); - Session::flash('message', trans('texts.converted_to_invoice')); - - foreach ($invoice->invitations as $invitationClone) { - if ($invitation->contact_id == $invitationClone->contact_id) { - $invitationKey = $invitationClone->invitation_key; - } - } - } + $invitationKey = $this->invoiceService->approveQuote($invoice, $invitation); + Session::flash('message', trans('texts.quote_is_approved')); return Redirect::to("view/{$invitationKey}"); } diff --git a/app/Http/Controllers/RecurringInvoiceController.php b/app/Http/Controllers/RecurringInvoiceController.php new file mode 100644 index 000000000000..c59370647bb0 --- /dev/null +++ b/app/Http/Controllers/RecurringInvoiceController.php @@ -0,0 +1,36 @@ +invoiceRepo = $invoiceRepo; + } + + public function index() + { + $data = [ + 'title' => trans('texts.recurring_invoices'), + 'entityType' => ENTITY_RECURRING_INVOICE, + 'columns' => Utils::trans([ + 'checkbox', + 'frequency', + 'client', + 'start_date', + 'end_date', + 'invoice_total', + 'action' + ]) + ]; + + return response()->view('list', $data); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 25114f8d36aa..1757cb58501c 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -9,7 +9,6 @@ use DateInterval; use DatePeriod; use Session; use View; - use App\Models\Account; class ReportController extends BaseController @@ -17,7 +16,7 @@ class ReportController extends BaseController public function d3() { $message = ''; - $fileName = storage_path() . '/dataviz_sample.txt'; + $fileName = storage_path().'/dataviz_sample.txt'; if (Auth::user()->account->isPro()) { $account = Account::where('id', '=', Auth::user()->account->id) @@ -33,7 +32,6 @@ class ReportController extends BaseController } $data = [ - 'feature' => ACCOUNT_DATA_VISUALIZATIONS, 'clients' => $clients, 'message' => $message, ]; @@ -56,200 +54,13 @@ class ReportController extends BaseController } else { $groupBy = 'MONTH'; $chartType = 'Bar'; - $reportType = ''; + $reportType = ENTITY_INVOICE; $startDate = Utils::today(false)->modify('-3 month'); $endDate = Utils::today(false); $enableReport = true; $enableChart = true; } - $datasets = []; - $labels = []; - $maxTotals = 0; - $width = 10; - - $displayData = []; - $exportData = []; - $reportTotals = [ - 'amount' => [], - 'balance' => [], - 'paid' => [] - ]; - - if ($reportType) { - $columns = ['client', 'amount', 'paid', 'balance']; - } else { - $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; - } - - - if (Auth::user()->account->isPro()) { - - if ($enableReport) { - $query = DB::table('invoices') - ->join('clients', 'clients.id', '=', 'invoices.client_id') - ->join('contacts', 'contacts.client_id', '=', 'clients.id') - ->where('invoices.account_id', '=', Auth::user()->account_id) - ->where('invoices.is_deleted', '=', false) - ->where('clients.is_deleted', '=', false) - ->where('contacts.deleted_at', '=', null) - ->where('invoices.invoice_date', '>=', $startDate->format('Y-m-d')) - ->where('invoices.invoice_date', '<=', $endDate->format('Y-m-d')) - ->where('invoices.is_quote', '=', false) - ->where('invoices.is_recurring', '=', false) - ->where('contacts.is_primary', '=', true); - - $select = ['clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'clients.name as client_name', 'clients.public_id as client_public_id', 'invoices.public_id as invoice_public_id']; - - if ($reportType) { - $query->groupBy('clients.id'); - array_push($select, DB::raw('sum(invoices.amount) amount'), DB::raw('sum(invoices.balance) balance'), DB::raw('sum(invoices.amount - invoices.balance) paid')); - } else { - array_push($select, 'invoices.invoice_number', 'invoices.amount', 'invoices.balance', 'invoices.invoice_date', DB::raw('(invoices.amount - invoices.balance) paid')); - $query->orderBy('invoices.id'); - } - - $query->select($select); - $data = $query->get(); - - foreach ($data as $record) { - // web display data - $displayRow = [link_to('/clients/'.$record->client_public_id, Utils::getClientDisplayName($record))]; - if (!$reportType) { - array_push($displayRow, - link_to('/invoices/'.$record->invoice_public_id, $record->invoice_number), - Utils::fromSqlDate($record->invoice_date, true) - ); - } - array_push($displayRow, - Utils::formatMoney($record->amount, $record->currency_id), - Utils::formatMoney($record->paid, $record->currency_id), - Utils::formatMoney($record->balance, $record->currency_id) - ); - - // export data - $exportRow = [trans('texts.client') => Utils::getClientDisplayName($record)]; - if (!$reportType) { - $exportRow[trans('texts.invoice_number')] = $record->invoice_number; - $exportRow[trans('texts.invoice_date')] = Utils::fromSqlDate($record->invoice_date, true); - } - $exportRow[trans('texts.amount')] = Utils::formatMoney($record->amount, $record->currency_id); - $exportRow[trans('texts.paid')] = Utils::formatMoney($record->paid, $record->currency_id); - $exportRow[trans('texts.balance')] = Utils::formatMoney($record->balance, $record->currency_id); - - $displayData[] = $displayRow; - $exportData[] = $exportRow; - - $accountCurrencyId = Auth::user()->account->currency_id; - $currencyId = $record->currency_id ? $record->currency_id : ($accountCurrencyId ? $accountCurrencyId : DEFAULT_CURRENCY); - if (!isset($reportTotals['amount'][$currencyId])) { - $reportTotals['amount'][$currencyId] = 0; - $reportTotals['balance'][$currencyId] = 0; - $reportTotals['paid'][$currencyId] = 0; - } - $reportTotals['amount'][$currencyId] += $record->amount; - $reportTotals['paid'][$currencyId] += $record->paid; - $reportTotals['balance'][$currencyId] += $record->balance; - } - - if ($action == 'export') - { - self::export($exportData, $reportTotals); - } - } - - if ($enableChart) - { - foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) - { - // SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions. - // Let's see if SQLite is being used. - if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') - { - // Replace the unsupported function with it's date format counterpart - switch ($groupBy) - { - case 'MONTH': - $dateFormat = '%m'; // returns 01-12 - break; - case 'WEEK': - $dateFormat = '%W'; // returns 00-53 - break; - case 'DAYOFYEAR': - $dateFormat = '%j'; // returns 001-366 - break; - default: - $dateFormat = '%m'; // MONTH by default - break; - } - - // Concatenate the year and the chosen timeframe (Month, Week or Day) - $timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)'; - } - else - { - // Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL) - $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; - } - - $records = DB::table($entityType.'s') - ->select(DB::raw('sum(amount) as total, '.$timeframe.' as '.$groupBy)) - ->where('account_id', '=', Auth::user()->account_id) - ->where($entityType.'s.is_deleted', '=', false) - ->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d')) - ->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d')) - ->groupBy($groupBy); - - if ($entityType == ENTITY_INVOICE) - { - $records->where('is_quote', '=', false) - ->where('is_recurring', '=', false); - } - - $totals = $records->lists('total'); - $dates = $records->lists($groupBy); - $data = array_combine($dates, $totals); - - $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); - $endDate->modify('+1 '.$padding); - $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); - $period = new DatePeriod($startDate, $interval, $endDate); - $endDate->modify('-1 '.$padding); - - $totals = []; - - foreach ($period as $d) - { - $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); - // MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365 - $date = $groupBy == 'DAYOFYEAR' ? $d->format('Y') . ($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat); - $totals[] = isset($data[$date]) ? $data[$date] : 0; - - if ($entityType == ENTITY_INVOICE) - { - $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); - $label = $d->format($labelFormat); - $labels[] = $label; - } - } - - $max = max($totals); - - if ($max > 0) - { - $datasets[] = [ - 'totals' => $totals, - 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), - ]; - $maxTotals = max($max, $maxTotals); - } - } - - $width = (ceil($maxTotals / 100) * 100) / 10; - $width = max($width, 10); - } - } - $dateTypes = [ 'DAYOFYEAR' => 'Daily', 'WEEK' => 'Weekly', @@ -262,24 +73,18 @@ class ReportController extends BaseController ]; $reportTypes = [ - '' => '', - 'Client' => trans('texts.client') + ENTITY_CLIENT => trans('texts.client'), + ENTITY_INVOICE => trans('texts.invoice'), + ENTITY_PAYMENT => trans('texts.payment'), ]; $params = [ - 'labels' => $labels, - 'datasets' => $datasets, - 'scaleStepWidth' => $width, 'dateTypes' => $dateTypes, 'chartTypes' => $chartTypes, 'chartType' => $chartType, 'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), 'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), 'groupBy' => $groupBy, - 'feature' => ACCOUNT_CHART_BUILDER, - 'displayData' => $displayData, - 'columns' => $columns, - 'reportTotals' => $reportTotals, 'reportTypes' => $reportTypes, 'reportType' => $reportType, 'enableChart' => $enableChart, @@ -287,9 +92,273 @@ class ReportController extends BaseController 'title' => trans('texts.charts_and_reports'), ]; + if (Auth::user()->account->isPro()) { + if ($enableReport) { + $params = array_merge($params, self::generateReport($reportType, $groupBy, $startDate, $endDate)); + + if ($action == 'export') { + self::export($params['exportData'], $params['reportTotals']); + } + } + if ($enableChart) { + $params = array_merge($params, self::generateChart($groupBy, $startDate, $endDate)); + } + } else { + $params['columns'] = []; + $params['displayData'] = []; + $params['reportTotals'] = [ + 'amount' => [], + 'balance' => [], + 'paid' => [], + ]; + $params['labels'] = []; + $params['datasets'] = []; + $params['scaleStepWidth'] = 100; + } + return View::make('reports.chart_builder', $params); } + private function generateChart($groupBy, $startDate, $endDate) + { + $width = 10; + $datasets = []; + $labels = []; + $maxTotals = 0; + + foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) { + // SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions. + // Let's see if SQLite is being used. + if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') { + // Replace the unsupported function with it's date format counterpart + switch ($groupBy) { + case 'MONTH': + $dateFormat = '%m'; // returns 01-12 + break; + case 'WEEK': + $dateFormat = '%W'; // returns 00-53 + break; + case 'DAYOFYEAR': + $dateFormat = '%j'; // returns 001-366 + break; + default: + $dateFormat = '%m'; // MONTH by default + break; + } + + // Concatenate the year and the chosen timeframe (Month, Week or Day) + $timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)'; + } else { + // Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL) + $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; + } + + $records = DB::table($entityType.'s') + ->select(DB::raw('sum(amount) as total, '.$timeframe.' as '.$groupBy)) + ->where('account_id', '=', Auth::user()->account_id) + ->where($entityType.'s.is_deleted', '=', false) + ->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d')) + ->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d')) + ->groupBy($groupBy); + + if ($entityType == ENTITY_INVOICE) { + $records->where('is_quote', '=', false) + ->where('is_recurring', '=', false); + } + + $totals = $records->lists('total'); + $dates = $records->lists($groupBy); + $data = array_combine($dates, $totals); + + $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); + $endDate->modify('+1 '.$padding); + $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); + $period = new DatePeriod($startDate, $interval, $endDate); + $endDate->modify('-1 '.$padding); + + $totals = []; + + foreach ($period as $d) { + $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); + // MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365 + $date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat); + $totals[] = isset($data[$date]) ? $data[$date] : 0; + + if ($entityType == ENTITY_INVOICE) { + $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); + $label = $d->format($labelFormat); + $labels[] = $label; + } + } + + $max = max($totals); + + if ($max > 0) { + $datasets[] = [ + 'totals' => $totals, + 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), + ]; + $maxTotals = max($max, $maxTotals); + } + } + + $width = (ceil($maxTotals / 100) * 100) / 10; + $width = max($width, 10); + + return [ + 'datasets' => $datasets, + 'scaleStepWidth' => $width, + 'labels' => $labels, + ]; + } + + private function generateReport($reportType, $groupBy, $startDate, $endDate) + { + if ($reportType == ENTITY_CLIENT) { + $columns = ['client', 'amount', 'paid', 'balance']; + } elseif ($reportType == ENTITY_INVOICE) { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; + } else { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'payment_date', 'paid', 'method']; + } + + $query = DB::table('invoices') + ->join('accounts', 'accounts.id', '=', 'invoices.account_id') + ->join('clients', 'clients.id', '=', 'invoices.client_id') + ->join('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('invoices.account_id', '=', Auth::user()->account_id) + ->where('invoices.is_deleted', '=', false) + ->where('clients.is_deleted', '=', false) + ->where('contacts.deleted_at', '=', null) + ->where('invoices.invoice_date', '>=', $startDate->format('Y-m-d')) + ->where('invoices.invoice_date', '<=', $endDate->format('Y-m-d')) + ->where('invoices.is_quote', '=', false) + ->where('invoices.is_recurring', '=', false) + ->where('contacts.is_primary', '=', true); + + $select = [ + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + 'accounts.country_id', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'invoices.public_id as invoice_public_id' + ]; + + if ($reportType == ENTITY_CLIENT) { + $query->groupBy('clients.id'); + array_push($select, DB::raw('sum(invoices.amount) amount'), DB::raw('sum(invoices.balance) balance'), DB::raw('sum(invoices.amount - invoices.balance) paid')); + } else { + $query->orderBy('invoices.id'); + array_push($select, 'invoices.invoice_number', 'invoices.amount', 'invoices.balance', 'invoices.invoice_date'); + if ($reportType == ENTITY_INVOICE) { + array_push($select, DB::raw('(invoices.amount - invoices.balance) paid')); + } else { + $query->join('payments', 'payments.invoice_id', '=', 'invoices.id') + ->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id') + ->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id') + ->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id'); + array_push($select, 'payments.payment_date', 'payments.amount as paid', 'payment_types.name as payment_type', 'gateways.name as gateway'); + } + } + + $query->select($select); + $data = $query->get(); + + $lastInvoiceId = null; + $sameAsLast = false; + $displayData = []; + + $exportData = []; + $reportTotals = [ + 'amount' => [], + 'balance' => [], + 'paid' => [], + ]; + + foreach ($data as $record) { + $sameAsLast = ($lastInvoiceId == $record->invoice_public_id); + $lastInvoiceId = $record->invoice_public_id; + + $displayRow = []; + if ($sameAsLast) { + array_push($displayRow, '', '', '', ''); + } else { + array_push($displayRow, link_to('/clients/'.$record->client_public_id, Utils::getClientDisplayName($record))); + if ($reportType != ENTITY_CLIENT) { + array_push($displayRow, + link_to('/invoices/'.$record->invoice_public_id, $record->invoice_number), + Utils::fromSqlDate($record->invoice_date, true) + ); + } + array_push($displayRow, Utils::formatMoney($record->amount, $record->currency_id, $record->country_id)); + } + if ($reportType != ENTITY_PAYMENT) { + array_push($displayRow, Utils::formatMoney($record->paid, $record->currency_id, $record->country_id)); + } + if ($reportType == ENTITY_PAYMENT) { + array_push($displayRow, + Utils::fromSqlDate($record->payment_date, true), + Utils::formatMoney($record->paid, $record->currency_id, $record->country_id), + $record->gateway ?: $record->payment_type + ); + } else { + array_push($displayRow, Utils::formatMoney($record->balance, $record->currency_id, $record->country_id)); + } + + // export data + $exportRow = []; + if ($sameAsLast) { + $exportRow[trans('texts.client')] = ' '; + $exportRow[trans('texts.invoice_number')] = ' '; + $exportRow[trans('texts.invoice_date')] = ' '; + $exportRow[trans('texts.amount')] = ' '; + } else { + $exportRow[trans('texts.client')] = Utils::getClientDisplayName($record); + if ($reportType != ENTITY_CLIENT) { + $exportRow[trans('texts.invoice_number')] = $record->invoice_number; + $exportRow[trans('texts.invoice_date')] = Utils::fromSqlDate($record->invoice_date, true); + } + $exportRow[trans('texts.amount')] = Utils::formatMoney($record->amount, $record->currency_id, $record->country_id); + } + if ($reportType != ENTITY_PAYMENT) { + $exportRow[trans('texts.paid')] = Utils::formatMoney($record->paid, $record->currency_id, $record->country_id); + } + if ($reportType == ENTITY_PAYMENT) { + $exportRow[trans('texts.payment_date')] = Utils::fromSqlDate($record->payment_date, true); + $exportRow[trans('texts.payment_amount')] = Utils::formatMoney($record->paid, $record->currency_id, $record->country_id); + $exportRow[trans('texts.method')] = $record->gateway ?: $record->payment_type; + } else { + $exportRow[trans('texts.balance')] = Utils::formatMoney($record->balance, $record->currency_id, $record->country_id); + } + + $displayData[] = $displayRow; + $exportData[] = $exportRow; + + $accountCurrencyId = Auth::user()->account->currency_id; + $currencyId = $record->currency_id ? $record->currency_id : ($accountCurrencyId ? $accountCurrencyId : DEFAULT_CURRENCY); + if (!isset($reportTotals['amount'][$currencyId])) { + $reportTotals['amount'][$currencyId] = 0; + $reportTotals['balance'][$currencyId] = 0; + $reportTotals['paid'][$currencyId] = 0; + } + if (!$sameAsLast) { + $reportTotals['amount'][$currencyId] += $record->amount; + $reportTotals['balance'][$currencyId] += $record->balance; + } + $reportTotals['paid'][$currencyId] += $record->paid; + } + + return [ + 'columns' => $columns, + 'displayData' => $displayData, + 'reportTotals' => $reportTotals, + 'exportData' => $exportData + ]; + } + private function export($data, $totals) { $output = fopen('php://output', 'w') or Utils::fatalError(); @@ -299,11 +368,11 @@ class ReportController extends BaseController Utils::exportData($output, $data); foreach (['amount', 'paid', 'balance'] as $type) { - $csv = trans("texts.{$type}") . ','; + $csv = trans("texts.{$type}").','; foreach ($totals[$type] as $currencyId => $amount) { - $csv .= Utils::formatMoney($amount, $currencyId) . ','; + $csv .= Utils::formatMoney($amount, $currencyId).','; } - fwrite($output, $csv . "\n"); + fwrite($output, $csv."\n"); } fclose($output); diff --git a/app/Http/Controllers/TaskApiController.php b/app/Http/Controllers/TaskApiController.php new file mode 100644 index 000000000000..a302944d2b62 --- /dev/null +++ b/app/Http/Controllers/TaskApiController.php @@ -0,0 +1,101 @@ +taskRepo = $taskRepo; + } + + /** + * @SWG\Get( + * path="/tasks", + * tags={"task"}, + * summary="List of tasks", + * @SWG\Response( + * response=200, + * description="A list with tasks", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Task")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function index() + { + $paginator = Task::scope(); + $tasks = Task::scope() + ->with($this->getIncluded()); + + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $tasks->whereHas('client', $filter); + $paginator->whereHas('client', $filter); + } + + $tasks = $tasks->orderBy('created_at', 'desc')->paginate(); + $paginator = $paginator->paginate(); + $transformer = new TaskTransformer(\Auth::user()->account, Input::get('serializer')); + + $data = $this->createCollection($tasks, $transformer, 'tasks', $paginator); + + return $this->response($data); + } + + /** + * @SWG\Post( + * path="/tasks", + * tags={"task"}, + * summary="Create a task", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Task") + * ), + * @SWG\Response( + * response=200, + * description="New task", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Task")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store() + { + $data = Input::all(); + $taskId = isset($data['id']) ? $data['id'] : false; + + if (isset($data['client_id']) && $data['client_id']) { + $data['client'] = $data['client_id']; + } + + $task = $this->taskRepo->save($taskId, $data); + $task = Task::scope($task->public_id)->with('client')->first(); + + $transformer = new TaskTransformer(Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($task, $transformer, 'task'); + + return $this->response($data); + } + +} diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index c9e65148a024..fc822df5d124 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -16,17 +16,20 @@ use App\Models\Client; use App\Models\Task; use App\Ninja\Repositories\TaskRepository; use App\Ninja\Repositories\InvoiceRepository; +use App\Services\TaskService; class TaskController extends BaseController { protected $taskRepo; + protected $taskService; - public function __construct(TaskRepository $taskRepo, InvoiceRepository $invoiceRepo) + public function __construct(TaskRepository $taskRepo, InvoiceRepository $invoiceRepo, TaskService $taskService) { parent::__construct(); $this->taskRepo = $taskRepo; $this->invoiceRepo = $invoiceRepo; + $this->taskService = $taskService; } /** @@ -36,81 +39,27 @@ class TaskController extends BaseController */ public function index() { - self::checkTimezone(); - return View::make('list', array( 'entityType' => ENTITY_TASK, 'title' => trans('texts.tasks'), 'sortCol' => '2', - 'columns' => Utils::trans(['checkbox', 'client', 'date', 'duration', 'description', 'status', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'client', + 'date', + 'duration', + 'description', + 'status', + '' + ]), )); } public function getDatatable($clientPublicId = null) { - $tasks = $this->taskRepo->find($clientPublicId, Input::get('sSearch')); - - $table = Datatable::query($tasks); - - if (!$clientPublicId) { - $table->addColumn('checkbox', function ($model) { return ''; }) - ->addColumn('client_name', function ($model) { return $model->client_public_id ? link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)) : ''; }); - } - - return $table->addColumn('created_at', function($model) { return Task::calcStartTime($model); }) - ->addColumn('time_log', function($model) { return gmdate('H:i:s', Task::calcDuration($model)); }) - ->addColumn('description', function($model) { return $model->description; }) - ->addColumn('invoice_number', function($model) { return self::getStatusLabel($model); }) - ->addColumn('dropdown', function ($model) { - $str = ''; - }) - ->make(); + return $this->taskService->getDatatable($clientPublicId, Input::get('sSearch')); } - private function getStatusLabel($model) { - if ($model->invoice_number) { - $class = 'success'; - $label = trans('texts.invoiced'); - } elseif ($model->is_running) { - $class = 'primary'; - $label = trans('texts.running'); - } else { - $class = 'default'; - $label = trans('texts.logged'); - } - return "

    $label

    "; - } - - /** * Store a newly created resource in storage. * @@ -121,6 +70,13 @@ class TaskController extends BaseController return $this->save(); } + public function show($publicId) + { + Session::reflash(); + + return Redirect::to("tasks/{$publicId}/edit"); + } + /** * Show the form for creating a new resource. * @@ -128,7 +84,7 @@ class TaskController extends BaseController */ public function create($clientPublicId = 0) { - self::checkTimezone(); + $this->checkTimezone(); $data = [ 'task' => null, @@ -136,7 +92,8 @@ class TaskController extends BaseController 'method' => 'POST', 'url' => 'tasks', 'title' => trans('texts.new_task'), - 'minuteOffset' => Utils::getTiemstampOffset(), + 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, + 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), ]; $data = array_merge($data, self::getViewModel()); @@ -152,15 +109,15 @@ class TaskController extends BaseController */ public function edit($publicId) { - self::checkTimezone(); + $this->checkTimezone(); - $task = Task::scope($publicId)->with('client', 'invoice')->firstOrFail(); + $task = Task::scope($publicId)->with('client', 'invoice')->withTrashed()->firstOrFail(); $actions = []; if ($task->invoice) { - $actions[] = ['url' => URL::to("inovices/{$task->invoice->public_id}/edit"), 'label' => trans("texts.view_invoice")]; + $actions[] = ['url' => URL::to("invoices/{$task->invoice->public_id}/edit"), 'label' => trans("texts.view_invoice")]; } else { - $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.create_invoice")]; + $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.invoice_task")]; // check for any open invoices $invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : []; @@ -186,7 +143,8 @@ class TaskController extends BaseController 'title' => trans('texts.edit_task'), 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), 'actions' => $actions, - 'minuteOffset' => Utils::getTiemstampOffset(), + 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, + 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), ]; $data = array_merge($data, self::getViewModel()); @@ -208,7 +166,8 @@ class TaskController extends BaseController private static function getViewModel() { return [ - 'clients' => Client::scope()->with('contacts')->orderBy('name')->get() + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + 'account' => Auth::user()->account, ]; } @@ -216,20 +175,32 @@ class TaskController extends BaseController { $action = Input::get('action'); - if (in_array($action, ['archive', 'delete', 'invoice', 'restore', 'add_to_invoice'])) { + if (in_array($action, ['archive', 'delete', 'restore'])) { return self::bulk(); } + if ($validator = $this->taskRepo->getErrors(Input::all())) { + $url = $publicId ? 'tasks/'.$publicId.'/edit' : 'tasks/create'; + Session::flash('error', trans('texts.task_errors')); + return Redirect::to($url) + ->withErrors($validator) + ->withInput(); + } + $task = $this->taskRepo->save($publicId, Input::all()); Session::flash('message', trans($publicId ? 'texts.updated_task' : 'texts.created_task')); + if (in_array($action, ['invoice', 'add_to_invoice'])) { + return self::bulk(); + } + return Redirect::to("tasks/{$task->public_id}/edit"); } public function bulk() { $action = Input::get('action'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); + $ids = Input::get('public_id') ?: (Input::get('id') ?: Input::get('ids')); if ($action == 'stop') { $this->taskRepo->save($ids, ['action' => $action]); @@ -258,10 +229,10 @@ class TaskController extends BaseController return Redirect::to('tasks'); } + $account = Auth::user()->account; $data[] = [ 'publicId' => $task->public_id, - 'description' => $task->description, - 'startTime' => $task->getStartTime(), + 'description' => $task->description . "\n\n" . $task->present()->times($account), 'duration' => $task->getHours(), ]; } @@ -289,7 +260,7 @@ class TaskController extends BaseController private function checkTimezone() { if (!Auth::user()->account->timezone) { - $link = link_to('/company/details?focus=timezone_id', trans('texts.click_here'), ['target' => '_blank']); + $link = link_to('/settings/localization?focus=timezone_id', trans('texts.click_here'), ['target' => '_blank']); Session::flash('warning', trans('texts.timezone_unset', ['link' => $link])); } } diff --git a/app/Http/Controllers/TaxRateController.php b/app/Http/Controllers/TaxRateController.php new file mode 100644 index 000000000000..85d3c7903383 --- /dev/null +++ b/app/Http/Controllers/TaxRateController.php @@ -0,0 +1,100 @@ +taxRateService = $taxRateService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_TAX_RATES); + } + + public function getDatatable() + { + return $this->taxRateService->getDatatable(Auth::user()->account_id); + } + + public function edit($publicId) + { + $data = [ + 'taxRate' => TaxRate::scope($publicId)->firstOrFail(), + 'method' => 'PUT', + 'url' => 'tax_rates/'.$publicId, + 'title' => trans('texts.edit_tax_rate'), + ]; + + return View::make('accounts.tax_rate', $data); + } + + public function create() + { + $data = [ + 'taxRate' => null, + 'method' => 'POST', + 'url' => 'tax_rates', + 'title' => trans('texts.create_tax_rate'), + ]; + + return View::make('accounts.tax_rate', $data); + } + + public function store() + { + return $this->save(); + } + + public function update($publicId) + { + return $this->save($publicId); + } + + private function save($publicId = false) + { + if ($publicId) { + $taxRate = TaxRate::scope($publicId)->firstOrFail(); + } else { + $taxRate = TaxRate::createNew(); + } + + $taxRate->name = trim(Input::get('name')); + $taxRate->rate = Utils::parseFloat(Input::get('rate')); + $taxRate->save(); + + $message = $publicId ? trans('texts.updated_tax_rate') : trans('texts.created_tax_rate'); + Session::flash('message', $message); + + return Redirect::to('settings/' . ACCOUNT_TAX_RATES); + } + + public function bulk() + { + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->taxRateService->bulk($ids, $action); + + Session::flash('message', trans('texts.archived_tax_rate')); + + return Redirect::to('settings/' . ACCOUNT_TAX_RATES); + } +} diff --git a/app/Http/Controllers/TokenController.php b/app/Http/Controllers/TokenController.php index 3b9d546849ab..604b94d576b3 100644 --- a/app/Http/Controllers/TokenController.php +++ b/app/Http/Controllers/TokenController.php @@ -1,13 +1,4 @@ tokenService = $tokenService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_API_TOKENS); + } + public function getDatatable() { - $query = DB::table('account_tokens') - ->where('account_tokens.account_id', '=', Auth::user()->account_id); - - if (!Session::get('show_trash:token')) { - $query->where('account_tokens.deleted_at', '=', null); - } - - $query->select('account_tokens.public_id', 'account_tokens.name', 'account_tokens.token', 'account_tokens.public_id', 'account_tokens.deleted_at'); - - return Datatable::query($query) - ->addColumn('name', function ($model) { return link_to('tokens/'.$model->public_id.'/edit', $model->name); }) - ->addColumn('token', function ($model) { return $model->token; }) - ->addColumn('dropdown', function ($model) { - $actions = ''; - - return $actions; - }) - ->orderColumns(['name', 'token']) - ->make(); + return $this->tokenService->getDatatable(Auth::user()->account_id); } public function edit($publicId) @@ -67,7 +41,6 @@ class TokenController extends BaseController ->where('public_id', '=', $publicId)->firstOrFail(); $data = [ - 'showBreadcrumbs' => false, 'token' => $token, 'method' => 'PUT', 'url' => 'tokens/'.$publicId, @@ -94,7 +67,6 @@ class TokenController extends BaseController public function create() { $data = [ - 'showBreadcrumbs' => false, 'token' => null, 'method' => 'POST', 'url' => 'tokens', @@ -104,17 +76,15 @@ class TokenController extends BaseController return View::make('accounts.token', $data); } - public function delete() + public function bulk() { - $tokenPublicId = Input::get('tokenPublicId'); - $token = AccountToken::where('account_id', '=', Auth::user()->account_id) - ->where('public_id', '=', $tokenPublicId)->firstOrFail(); + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->tokenService->bulk($ids, $action); - $token->delete(); + Session::flash('message', trans('texts.archived_token')); - Session::flash('message', trans('texts.deleted_token')); - - return Redirect::to('company/advanced_settings/token_management'); + return Redirect::to('settings/' . ACCOUNT_API_TOKENS); } /** @@ -142,13 +112,9 @@ class TokenController extends BaseController if ($tokenPublicId) { $token->name = trim(Input::get('name')); } else { - $lastToken = AccountToken::withTrashed()->where('account_id', '=', Auth::user()->account_id) - ->orderBy('public_id', 'DESC')->first(); - $token = AccountToken::createNew(); $token->name = trim(Input::get('name')); $token->token = str_random(RANDOM_KEY_LENGTH); - $token->public_id = $lastToken ? $lastToken->public_id + 1 : 1; } $token->save(); @@ -162,7 +128,7 @@ class TokenController extends BaseController Session::flash('message', $message); } - return Redirect::to('company/advanced_settings/token_management'); + return Redirect::to('settings/' . ACCOUNT_API_TOKENS); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 85b4cede7d9a..afad67b0d3ec 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -19,65 +19,33 @@ use App\Http\Requests; use App\Ninja\Repositories\AccountRepository; use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\UserMailer; +use App\Services\UserService; class UserController extends BaseController { protected $accountRepo; protected $contactMailer; protected $userMailer; + protected $userService; - public function __construct(AccountRepository $accountRepo, ContactMailer $contactMailer, UserMailer $userMailer) + public function __construct(AccountRepository $accountRepo, ContactMailer $contactMailer, UserMailer $userMailer, UserService $userService) { parent::__construct(); $this->accountRepo = $accountRepo; $this->contactMailer = $contactMailer; $this->userMailer = $userMailer; + $this->userService = $userService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } public function getDatatable() { - $query = DB::table('users') - ->where('users.account_id', '=', Auth::user()->account_id); - - if (!Session::get('show_trash:user')) { - $query->where('users.deleted_at', '=', null); - } - - $query->where('users.public_id', '>', 0) - ->select('users.public_id', 'users.first_name', 'users.last_name', 'users.email', 'users.confirmed', 'users.public_id', 'users.deleted_at'); - - return Datatable::query($query) - ->addColumn('first_name', function ($model) { return link_to('users/'.$model->public_id.'/edit', $model->first_name.' '.$model->last_name); }) - ->addColumn('email', function ($model) { return $model->email; }) - ->addColumn('confirmed', function ($model) { return $model->deleted_at ? trans('texts.deleted') : ($model->confirmed ? trans('texts.active') : trans('texts.pending')); }) - ->addColumn('dropdown', function ($model) { - $actions = ''; - - return $actions; - }) - ->orderColumns(['first_name', 'email', 'confirmed']) - ->make(); + return $this->userService->getDatatable(Auth::user()->account_id); } public function setTheme() @@ -106,7 +74,6 @@ class UserController extends BaseController ->where('public_id', '=', $publicId)->firstOrFail(); $data = [ - 'showBreadcrumbs' => false, 'user' => $user, 'method' => 'PUT', 'url' => 'users/'.$publicId, @@ -134,24 +101,22 @@ class UserController extends BaseController { if (!Auth::user()->registered) { Session::flash('error', trans('texts.register_to_add_user')); - return Redirect::to('company/advanced_settings/user_management'); - } + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); + } if (!Auth::user()->confirmed) { Session::flash('error', trans('texts.confirmation_required')); - return Redirect::to('company/advanced_settings/user_management'); + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } if (Utils::isNinja()) { $count = User::where('account_id', '=', Auth::user()->account_id)->count(); if ($count >= MAX_NUM_USERS) { Session::flash('error', trans('texts.limit_users')); - - return Redirect::to('company/advanced_settings/user_management'); + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } } $data = [ - 'showBreadcrumbs' => false, 'user' => null, 'method' => 'POST', 'url' => 'users', @@ -161,17 +126,25 @@ class UserController extends BaseController return View::make('users.edit', $data); } - public function delete() + public function bulk() { - $userPublicId = Input::get('userPublicId'); + $action = Input::get('bulk_action'); + $id = Input::get('bulk_public_id'); + $user = User::where('account_id', '=', Auth::user()->account_id) - ->where('public_id', '=', $userPublicId)->firstOrFail(); + ->where('public_id', '=', $id) + ->withTrashed() + ->firstOrFail(); - $user->delete(); + if ($action === 'archive') { + $user->delete(); + } else { + $user->restore(); + } - Session::flash('message', trans('texts.deleted_user')); + Session::flash('message', trans("texts.{$action}d_user")); - return Redirect::to('company/advanced_settings/user_management'); + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } public function restoreUser($userPublicId) @@ -184,7 +157,7 @@ class UserController extends BaseController Session::flash('message', trans('texts.restored_user')); - return Redirect::to('company/advanced_settings/user_management'); + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } /** @@ -247,7 +220,7 @@ class UserController extends BaseController Session::flash('message', $message); } - return Redirect::to('company/advanced_settings/user_management'); + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } public function sendConfirmation($userPublicId) @@ -258,7 +231,7 @@ class UserController extends BaseController $this->userMailer->sendConfirmation($user, Auth::user()); Session::flash('message', trans('texts.sent_invite')); - return Redirect::to('company/advanced_settings/user_management'); + return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); } @@ -348,7 +321,7 @@ class UserController extends BaseController return RESULT_SUCCESS; } - public function switchAccount($newUserId) + public function switchAccount($newUserId) { $oldUserId = Auth::user()->id; $referer = Request::header('referer'); @@ -384,4 +357,5 @@ class UserController extends BaseController { return View::make('users.account_management'); } + } diff --git a/app/Http/Controllers/VendorApiController.php b/app/Http/Controllers/VendorApiController.php new file mode 100644 index 000000000000..80236226dda0 --- /dev/null +++ b/app/Http/Controllers/VendorApiController.php @@ -0,0 +1,94 @@ +vendorRepo = $vendorRepo; + } + + public function ping() + { + $headers = Utils::getApiHeaders(); + + return Response::make('', 200, $headers); + } + + /** + * @SWG\Get( + * path="/vendors", + * summary="List of vendors", + * tags={"vendor"}, + * @SWG\Response( + * response=200, + * description="A list with vendors", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Vendor")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function index() + { + $vendors = Vendor::scope() + ->with($this->getIncluded()) + ->orderBy('created_at', 'desc') + ->paginate(); + + $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer')); + $paginator = Vendor::scope()->paginate(); + $data = $this->createCollection($vendors, $transformer, ENTITY_VENDOR, $paginator); + + return $this->response($data); + } + + /** + * @SWG\Post( + * path="/vendors", + * tags={"vendor"}, + * summary="Create a vendor", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Vendor") + * ), + * @SWG\Response( + * response=200, + * description="New vendor", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store(CreateVendorRequest $request) + { + $vendor = $this->vendorRepo->save($request->input()); + + $vendor = Vendor::scope($vendor->public_id) + ->with('country', 'vendorcontacts', 'industry', 'size', 'currency') + ->first(); + + $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($vendor, $transformer, ENTITY_VENDOR); + return $this->response($data); + } +} diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php new file mode 100644 index 000000000000..bbd69ed23300 --- /dev/null +++ b/app/Http/Controllers/VendorController.php @@ -0,0 +1,204 @@ +vendorRepo = $vendorRepo; + $this->vendorService = $vendorService; + + + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + return View::make('list', array( + 'entityType' => 'vendor', + 'title' => trans('texts.vendors'), + 'sortCol' => '4', + 'columns' => Utils::trans([ + 'checkbox', + 'vendor', + 'contact', + 'email', + 'date_created', + '' + ]), + )); + } + + public function getDatatable() + { + return $this->vendorService->getDatatable(Input::get('sSearch')); + } + + /** + * Store a newly created resource in storage. + * + * @return Response + */ + public function store(CreateVendorRequest $request) + { + $vendor = $this->vendorService->save($request->input()); + + Session::flash('message', trans('texts.created_vendor')); + + return redirect()->to($vendor->getRoute()); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return Response + */ + public function show($publicId) + { + $vendor = Vendor::withTrashed()->scope($publicId)->with('vendorcontacts', 'size', 'industry')->firstOrFail(); + Utils::trackViewed($vendor->getDisplayName(), 'vendor'); + + $actionLinks = [ + ['label' => trans('texts.new_vendor'), 'url' => '/vendors/create/' . $vendor->public_id] + ]; + + $data = array( + 'actionLinks' => $actionLinks, + 'showBreadcrumbs' => false, + 'vendor' => $vendor, + 'totalexpense' => $vendor->getTotalExpense(), + 'title' => trans('texts.view_vendor'), + 'hasRecurringInvoices' => false, + 'hasQuotes' => false, + 'hasTasks' => false, + ); + + return View::make('vendors.show', $data); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + if (Vendor::scope()->count() > Auth::user()->getMaxNumVendors()) { + return View::make('error', ['hideHeader' => true, 'error' => "Sorry, you've exceeded the limit of ".Auth::user()->getMaxNumVendors()." vendors"]); + } + + $data = [ + 'vendor' => null, + 'method' => 'POST', + 'url' => 'vendors', + 'title' => trans('texts.new_vendor'), + ]; + + $data = array_merge($data, self::getViewModel()); + + return View::make('vendors.edit', $data); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return Response + */ + public function edit($publicId) + { + $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail(); + $data = [ + 'vendor' => $vendor, + 'method' => 'PUT', + 'url' => 'vendors/'.$publicId, + 'title' => trans('texts.edit_vendor'), + ]; + + $data = array_merge($data, self::getViewModel()); + + if (Auth::user()->account->isNinjaAccount()) { + if ($account = Account::whereId($vendor->public_id)->first()) { + $data['proPlanPaid'] = $account['pro_plan_paid']; + } + } + + return View::make('vendors.edit', $data); + } + + private static function getViewModel() + { + return [ + 'data' => Input::old('data'), + 'account' => Auth::user()->account, + 'currencies' => Cache::get('currencies'), + 'countries' => Cache::get('countries'), + ]; + } + + /** + * Update the specified resource in storage. + * + * @param int $id + * @return Response + */ + public function update(UpdateVendorRequest $request) + { + $vendor = $this->vendorService->save($request->input()); + + Session::flash('message', trans('texts.updated_vendor')); + + return redirect()->to($vendor->getRoute()); + } + + public function bulk() + { + $action = Input::get('action'); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + $count = $this->vendorService->bulk($ids, $action); + + $message = Utils::pluralize($action.'d_vendor', $count); + Session::flash('message', $message); + + if ($action == 'restore' && $count == 1) { + return Redirect::to('vendors/' . Utils::getFirst($ids)); + } else { + return Redirect::to('vendors'); + } + } +} diff --git a/app/Http/Controllers/old/HomeController.php b/app/Http/Controllers/old/HomeController.php deleted file mode 100644 index 2050070ab5ef..000000000000 --- a/app/Http/Controllers/old/HomeController.php +++ /dev/null @@ -1,36 +0,0 @@ -middleware('auth'); - } - - /** - * Show the application dashboard to the user. - * - * @return Response - */ - public function index() - { - return view('home'); - } - -} diff --git a/app/Http/Controllers/old/WelcomeController.php b/app/Http/Controllers/old/WelcomeController.php deleted file mode 100644 index c7da91c94522..000000000000 --- a/app/Http/Controllers/old/WelcomeController.php +++ /dev/null @@ -1,36 +0,0 @@ -middleware('guest'); - } - - /** - * Show the application welcome screen to the user. - * - * @return Response - */ - public function index() - { - return view('welcome'); - } - -} diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 517dc905eb16..5632e7de4e90 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -21,33 +21,38 @@ class ApiCheck { */ public function handle($request, Closure $next) { + $loggingIn = $request->is('api/v1/login'); $headers = Utils::getApiHeaders(); - // check for a valid token - $token = AccountToken::where('token', '=', Request::header('X-Ninja-Token'))->first(['id', 'user_id']); - - if ($token) { - Auth::loginUsingId($token->user_id); - Session::set('token_id', $token->id); + if ($loggingIn) { + // do nothing } else { - sleep(3); - return Response::make('Invalid token', 403, $headers); + // check for a valid token + $token = AccountToken::where('token', '=', Request::header('X-Ninja-Token'))->first(['id', 'user_id']); + + if ($token) { + Auth::loginUsingId($token->user_id); + Session::set('token_id', $token->id); + } else { + sleep(3); + return Response::json('Invalid token', 403, $headers); + } } - if (!Utils::isNinja()) { + if (!Utils::isNinja() && !$loggingIn) { return $next($request); } - if (!Utils::isPro()) { - return Response::make('API requires pro plan', 403, $headers); + if (!Utils::isPro() && !$loggingIn) { + return Response::json('API requires pro plan', 403, $headers); } else { - $accountId = Auth::user()->account->id; + $key = Auth::check() ? Auth::user()->account->id : $request->getClientIp(); // http://stackoverflow.com/questions/1375501/how-do-i-throttle-my-sites-api-users $hour = 60 * 60; $hour_limit = 100; # users are limited to 100 requests/hour - $hour_throttle = Cache::get("hour_throttle:{$accountId}", null); - $last_api_request = Cache::get("last_api_request:{$accountId}", 0); + $hour_throttle = Cache::get("hour_throttle:{$key}", null); + $last_api_request = Cache::get("last_api_request:{$key}", 0); $last_api_diff = time() - $last_api_request; if (is_null($hour_throttle)) { @@ -63,14 +68,13 @@ class ApiCheck { if ($new_hour_throttle > $hour) { $wait = ceil($new_hour_throttle - $hour); sleep(1); - return Response::make("Please wait {$wait} second(s)", 403, $headers); + return Response::json("Please wait {$wait} second(s)", 403, $headers); } - Cache::put("hour_throttle:{$accountId}", $new_hour_throttle, 10); - Cache::put("last_api_request:{$accountId}", time(), 10); + Cache::put("hour_throttle:{$key}", $new_hour_throttle, 10); + Cache::put("last_api_request:{$key}", time(), 10); } - return $next($request); } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 68ddfc7df095..bfda7bcb4916 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -10,6 +10,7 @@ use Redirect; use Cache; use Session; use Event; +use Schema; use App\Models\Language; use App\Models\InvoiceDesign; use App\Events\UserSettingsChanged; @@ -25,11 +26,16 @@ class StartupCheck */ public function handle($request, Closure $next) { + // Set up trusted X-Forwarded-Proto proxies + // TRUSTED_PROXIES accepts a comma delimited list of subnets + // ie, TRUSTED_PROXIES='10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' + if (isset($_ENV['TRUSTED_PROXIES'])) { + Request::setTrustedProxies(array_map('trim', explode(',', env('TRUSTED_PROXIES')))); + } + // Ensure all request are over HTTPS in production - if (App::environment() == ENV_PRODUCTION) { - if (!Request::secure()) { - return Redirect::secure(Request::getRequestUri()); - } + if (Utils::requireHTTPS() && !Request::secure()) { + return Redirect::secure(Request::path()); } // If the database doens't yet exist we'll skip the rest @@ -37,40 +43,19 @@ class StartupCheck return $next($request); } - // Check data has been cached - $cachedTables = [ - 'currencies' => 'App\Models\Currency', - 'sizes' => 'App\Models\Size', - 'industries' => 'App\Models\Industry', - 'timezones' => 'App\Models\Timezone', - 'dateFormats' => 'App\Models\DateFormat', - 'datetimeFormats' => 'App\Models\DatetimeFormat', - 'languages' => 'App\Models\Language', - 'paymentTerms' => 'App\Models\PaymentTerm', - 'paymentTypes' => 'App\Models\PaymentType', - 'countries' => 'App\Models\Country', - 'invoiceDesigns' => 'App\Models\InvoiceDesign', - ]; - foreach ($cachedTables as $name => $class) { - if (Input::has('clear_cache')) { - Session::flash('message', 'Cache cleared'); - } - if (Input::has('clear_cache') || !Cache::has($name)) { - if ($name == 'paymentTerms') { - $orderBy = 'num_days'; - } elseif (in_array($name, ['currencies', 'sizes', 'industries', 'languages', 'countries'])) { - $orderBy = 'name'; - } else { - $orderBy = 'id'; - } - $tableData = $class::orderBy($orderBy)->get(); - if (count($tableData)) { - Cache::forever($name, $tableData); - } + // Check if a new version was installed + if (!Utils::isNinja()) { + $file = storage_path() . '/version.txt'; + $version = @file_get_contents($file); + if ($version != NINJA_VERSION) { + $handle = fopen($file, 'w'); + fwrite($handle, NINJA_VERSION); + fclose($handle); + return Redirect::to('/update'); } } - // check the application is up to date and for any news feed messages + // Check the application is up to date and for any news feed messages if (Auth::check()) { $count = Session::get(SESSION_COUNTER, 0); Session::put(SESSION_COUNTER, ++$count); @@ -91,11 +76,11 @@ class StartupCheck 'releases_link' => link_to(RELEASES_URL, 'Invoice Ninja', ['target' => '_blank']), ]; Session::put('news_feed_id', NEW_VERSION_AVAILABLE); - Session::put('news_feed_message', trans('texts.new_version_available', $params)); + Session::flash('news_feed_message', trans('texts.new_version_available', $params)); } else { Session::put('news_feed_id', $data->id); if ($data->message && $data->id > Auth::user()->news_feed_id) { - Session::put('news_feed_message', $data->message); + Session::flash('news_feed_message', $data->message); } } } else { @@ -118,8 +103,10 @@ class StartupCheck } } } elseif (Auth::check()) { - $locale = Session::get(SESSION_LOCALE, DEFAULT_LOCALE); + $locale = Auth::user()->account->language ? Auth::user()->account->language->locale : DEFAULT_LOCALE; App::setLocale($locale); + } elseif (session(SESSION_LOCALE)) { + App::setLocale(session(SESSION_LOCALE)); } // Make sure the account/user localization settings are in the session @@ -142,10 +129,11 @@ class StartupCheck $design = new InvoiceDesign(); $design->id = $item->id; $design->name = $item->name; - $design->javascript = $item->javascript; + $design->pdfmake = $item->pdfmake; $design->save(); } + Cache::forget('invoiceDesigns'); Session::flash('message', trans('texts.bought_designs')); } } elseif ($productId == PRODUCT_WHITE_LABEL) { @@ -159,14 +147,41 @@ class StartupCheck } } } + + // Check data has been cached + $cachedTables = unserialize(CACHED_TABLES); + if (Input::has('clear_cache')) { + Session::flash('message', 'Cache cleared'); + } + foreach ($cachedTables as $name => $class) { + if (Input::has('clear_cache') || !Cache::has($name)) { + // check that the table exists in case the migration is pending + if ( ! Schema::hasTable((new $class)->getTable())) { + continue; + } + if ($name == 'paymentTerms') { + $orderBy = 'num_days'; + } elseif ($name == 'fonts') { + $orderBy = 'sort_order'; + } elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) { + $orderBy = 'name'; + } else { + $orderBy = 'id'; + } + $tableData = $class::orderBy($orderBy)->get(); + if (count($tableData)) { + Cache::forever($name, $tableData); + } + } + } + // Show message to IE 8 and before users if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT'])) { Session::flash('error', trans('texts.old_browser')); } - // for security prevent displaying within an iframe $response = $next($request); - $response->headers->set('X-Frame-Options', 'DENY'); + //$response->headers->set('X-Frame-Options', 'DENY'); return $response; } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index bc70cdf6a810..e1cd17f5dd37 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -7,12 +7,17 @@ class VerifyCsrfToken extends BaseVerifier { private $openRoutes = [ 'signup/register', + 'api/v1/login', 'api/v1/clients', + 'api/v1/invoices/*', 'api/v1/invoices', 'api/v1/quotes', 'api/v1/payments', + 'api/v1/tasks', 'api/v1/email_invoice', 'api/v1/hooks', + 'hook/email_opened', + 'hook/email_bounced', ]; /** diff --git a/app/Http/Requests/CreateClientRequest.php b/app/Http/Requests/CreateClientRequest.php new file mode 100644 index 000000000000..6fb7060e422f --- /dev/null +++ b/app/Http/Requests/CreateClientRequest.php @@ -0,0 +1,46 @@ + 'valid_contacts', + ]; + } + + public function validator($factory) + { + // support submiting the form with a single contact record + $input = $this->input(); + if (isset($input['contact'])) { + $input['contacts'] = [$input['contact']]; + unset($input['contact']); + $this->replace($input); + } + + return $factory->make( + $this->input(), + $this->container->call([$this, 'rules']), + $this->messages() + ); + } +} diff --git a/app/Http/Requests/CreateCreditRequest.php b/app/Http/Requests/CreateCreditRequest.php new file mode 100644 index 000000000000..f2dc44d31aa0 --- /dev/null +++ b/app/Http/Requests/CreateCreditRequest.php @@ -0,0 +1,30 @@ + 'required', + 'amount' => 'required|positive', + ]; + } +} diff --git a/app/Http/Requests/CreateExpenseRequest.php b/app/Http/Requests/CreateExpenseRequest.php new file mode 100644 index 000000000000..78f6eeee77ed --- /dev/null +++ b/app/Http/Requests/CreateExpenseRequest.php @@ -0,0 +1,30 @@ + 'positive', + ]; + } +} diff --git a/app/Http/Requests/CreateInvoiceRequest.php b/app/Http/Requests/CreateInvoiceRequest.php new file mode 100644 index 000000000000..4a11ea56044a --- /dev/null +++ b/app/Http/Requests/CreateInvoiceRequest.php @@ -0,0 +1,37 @@ + 'required_without:client_id', + 'client_id' => 'required_without:email', + 'invoice_items' => 'valid_invoice_items', + 'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,'.Auth::user()->account_id, + 'discount' => 'positive', + ]; + + return $rules; + } +} diff --git a/app/Http/Requests/CreatePaymentRequest.php b/app/Http/Requests/CreatePaymentRequest.php new file mode 100644 index 000000000000..52e9d313bf9f --- /dev/null +++ b/app/Http/Requests/CreatePaymentRequest.php @@ -0,0 +1,41 @@ +input(); + $invoice = Invoice::scope($input['invoice'])->firstOrFail(); + + $rules = array( + 'client' => 'required', + 'invoice' => 'required', + 'amount' => "required|less_than:{$invoice->balance}|positive", + ); + + if ($input['payment_type_id'] == PAYMENT_TYPE_CREDIT) { + $rules['payment_type_id'] = 'has_credit:'.$input['client'].','.$input['amount']; + } + + return $rules; + } +} diff --git a/app/Http/Requests/CreatePaymentTermRequest.php b/app/Http/Requests/CreatePaymentTermRequest.php new file mode 100644 index 000000000000..d8581793160e --- /dev/null +++ b/app/Http/Requests/CreatePaymentTermRequest.php @@ -0,0 +1,30 @@ + 'required', + 'name' => 'required', + ]; + } +} diff --git a/app/Http/Requests/CreateVendorRequest.php b/app/Http/Requests/CreateVendorRequest.php new file mode 100644 index 000000000000..7186077fc666 --- /dev/null +++ b/app/Http/Requests/CreateVendorRequest.php @@ -0,0 +1,44 @@ + 'valid_contacts', + ]; + } + + public function validator($factory) + { + // support submiting the form with a single contact record + $input = $this->input(); + if (isset($input['vendor_contact'])) { + $input['vendor_contacts'] = [$input['vendor_contact']]; + unset($input['vendor_contact']); + $this->replace($input); + } + + return $factory->make( + $this->input(), $this->container->call([$this, 'rules']), $this->messages() + ); + } +} diff --git a/app/Http/Requests/SaveInvoiceWithClientRequest.php b/app/Http/Requests/SaveInvoiceWithClientRequest.php new file mode 100644 index 000000000000..be925c03207f --- /dev/null +++ b/app/Http/Requests/SaveInvoiceWithClientRequest.php @@ -0,0 +1,45 @@ + 'valid_contacts', + 'invoice_items' => 'valid_invoice_items', + 'invoice_number' => 'required|unique:invoices,invoice_number,'.$invoiceId.',id,account_id,'.Auth::user()->account_id, + 'discount' => 'positive', + ]; + + /* There's a problem parsing the dates + if (Request::get('is_recurring') && Request::get('start_date') && Request::get('end_date')) { + $rules['end_date'] = 'after' . Request::get('start_date'); + } + */ + + return $rules; + } +} diff --git a/app/Http/Requests/UpdateClientRequest.php b/app/Http/Requests/UpdateClientRequest.php new file mode 100644 index 000000000000..b73e019c4964 --- /dev/null +++ b/app/Http/Requests/UpdateClientRequest.php @@ -0,0 +1,29 @@ + 'valid_contacts', + ]; + } +} diff --git a/app/Http/Requests/UpdateExpenseRequest.php b/app/Http/Requests/UpdateExpenseRequest.php new file mode 100644 index 000000000000..7b67ca89230e --- /dev/null +++ b/app/Http/Requests/UpdateExpenseRequest.php @@ -0,0 +1,31 @@ + 'positive', + 'expense_date' => 'required', + ]; + } +} diff --git a/app/Http/Requests/UpdateInvoiceRequest.php b/app/Http/Requests/UpdateInvoiceRequest.php new file mode 100644 index 000000000000..68fa2acaa708 --- /dev/null +++ b/app/Http/Requests/UpdateInvoiceRequest.php @@ -0,0 +1,42 @@ +action == ACTION_ARCHIVE) { + return []; + } + + $publicId = $this->route('invoices'); + $invoiceId = Invoice::getPrivateId($publicId); + + $rules = [ + 'invoice_items' => 'required|valid_invoice_items', + 'invoice_number' => 'unique:invoices,invoice_number,'.$invoiceId.',id,account_id,'.Auth::user()->account_id, + 'discount' => 'positive', + ]; + + return $rules; + } +} diff --git a/app/Http/Requests/UpdatePaymentRequest.php b/app/Http/Requests/UpdatePaymentRequest.php new file mode 100644 index 000000000000..29ac70e85e74 --- /dev/null +++ b/app/Http/Requests/UpdatePaymentRequest.php @@ -0,0 +1,28 @@ + 'required|positive', + ]; + + } +} diff --git a/app/Http/Requests/UpdateVendorRequest.php b/app/Http/Requests/UpdateVendorRequest.php new file mode 100644 index 000000000000..568166735d8c --- /dev/null +++ b/app/Http/Requests/UpdateVendorRequest.php @@ -0,0 +1,29 @@ + 'valid_contacts', + ]; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 63a678f357bc..4bd84629f0c6 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -11,7 +11,7 @@ | */ -//Cache::flush(); +//Crypt::decrypt(); //apc_clear_cache(); //dd(DB::getQueryLog()); //dd(Client::getPrivateId(1)); @@ -21,38 +21,34 @@ //Log::error('test'); // Application setup -Route::get('setup', 'AppController@showSetup'); -Route::post('setup', 'AppController@doSetup'); -Route::get('install', 'AppController@install'); -Route::get('update', 'AppController@update'); - -/* -// Codeception code coverage -Route::get('/c3.php', function () { - include '../c3.php'; -}); -*/ +Route::get('/setup', 'AppController@showSetup'); +Route::post('/setup', 'AppController@doSetup'); +Route::get('/install', 'AppController@install'); +Route::get('/update', 'AppController@update'); // Public pages Route::get('/', 'HomeController@showIndex'); -Route::get('terms', 'HomeController@showTerms'); -Route::get('log_error', 'HomeController@logError'); -Route::get('invoice_now', 'HomeController@invoiceNow'); -Route::get('keep_alive', 'HomeController@keepAlive'); -Route::post('get_started', 'AccountController@getStarted'); +Route::get('/terms', 'HomeController@showTerms'); +Route::get('/log_error', 'HomeController@logError'); +Route::get('/invoice_now', 'HomeController@invoiceNow'); +Route::get('/keep_alive', 'HomeController@keepAlive'); +Route::post('/get_started', 'AccountController@getStarted'); // Client visible pages -Route::get('view/{invitation_key}', 'InvoiceController@view'); +Route::get('view/{invitation_key}', 'PublicClientController@view'); +Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); Route::post('payment/{invitation_key}', 'PaymentController@do_payment'); Route::get('complete', 'PaymentController@offsite_payment'); -Route::get('client/quotes', 'QuoteController@clientIndex'); -Route::get('client/invoices', 'InvoiceController@clientIndex'); -Route::get('client/payments', 'PaymentController@clientIndex'); -Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'QuoteController@getClientDatatable')); -Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'InvoiceController@getClientDatatable')); -Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PaymentController@getClientDatatable')); +Route::get('client/quotes', 'PublicClientController@quoteIndex'); +Route::get('client/invoices', 'PublicClientController@invoiceIndex'); +Route::get('client/payments', 'PublicClientController@paymentIndex'); +Route::get('client/dashboard', 'PublicClientController@dashboard'); +Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); +Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); +Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable')); +Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable')); Route::get('license', 'PaymentController@show_license_payment'); Route::post('license', 'PaymentController@do_license_payment'); @@ -61,15 +57,13 @@ Route::get('claim_license', 'PaymentController@claim_license'); Route::post('signup/validate', 'AccountController@checkEmail'); Route::post('signup/submit', 'AccountController@submitSignup'); +Route::get('/auth/{provider}', 'Auth\AuthController@authLogin'); +Route::get('/auth_unlink', 'Auth\AuthController@authUnlink'); + +Route::post('/hook/email_bounced', 'AppController@emailBounced'); +Route::post('/hook/email_opened', 'AppController@emailOpened'); // Laravel auth routes -/* -Route::controllers([ - 'auth' => 'Auth\AuthController', - 'password' => 'Auth\PasswordController', -]); -*/ - get('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@getRegister')); post('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@postRegister')); get('/login', array('as' => 'login', 'uses' => 'Auth\AuthController@getLoginWrapper')); @@ -93,10 +87,10 @@ Route::group(['middleware' => 'auth'], function() { Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible'); Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); - + Route::get('api/users', array('as'=>'api.users', 'uses'=>'UserController@getDatatable')); Route::resource('users', 'UserController'); - Route::post('users/delete', 'UserController@delete'); + Route::post('users/bulk', 'UserController@bulk'); Route::get('send_confirmation/{user_id}', 'UserController@sendConfirmation'); Route::get('restore_user/{user_id}', 'UserController@restoreUser'); Route::post('users/change_password', 'UserController@changePassword'); @@ -106,27 +100,47 @@ Route::group(['middleware' => 'auth'], function() { Route::get('api/tokens', array('as'=>'api.tokens', 'uses'=>'TokenController@getDatatable')); Route::resource('tokens', 'TokenController'); - Route::post('tokens/delete', 'TokenController@delete'); + Route::post('tokens/bulk', 'TokenController@bulk'); Route::get('api/products', array('as'=>'api.products', 'uses'=>'ProductController@getDatatable')); Route::resource('products', 'ProductController'); - Route::get('products/{product_id}/archive', 'ProductController@archive'); + Route::post('products/bulk', 'ProductController@bulk'); - Route::get('company/advanced_settings/data_visualizations', 'ReportController@d3'); - Route::get('company/advanced_settings/charts_and_reports', 'ReportController@showReports'); - Route::post('company/advanced_settings/charts_and_reports', 'ReportController@showReports'); + Route::get('api/tax_rates', array('as'=>'api.tax_rates', 'uses'=>'TaxRateController@getDatatable')); + Route::resource('tax_rates', 'TaxRateController'); + Route::post('tax_rates/bulk', 'TaxRateController@bulk'); + + Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy'); + Route::get('settings/data_visualizations', 'ReportController@d3'); + Route::get('settings/charts_and_reports', 'ReportController@showReports'); + Route::post('settings/charts_and_reports', 'ReportController@showReports'); + + Route::post('settings/cancel_account', 'AccountController@cancelAccount'); + Route::get('settings/{section?}', 'AccountController@showSection'); + Route::post('settings/{section?}', 'AccountController@doSection'); + + // Payment term + Route::get('api/payment_terms', array('as'=>'api.payment_terms', 'uses'=>'PaymentTermController@getDatatable')); + Route::resource('payment_terms', 'PaymentTermController'); + Route::post('payment_terms/bulk', 'PaymentTermController@bulk'); - Route::post('company/cancel_account', 'AccountController@cancelAccount'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); - Route::get('company/{section?}/{sub_section?}', 'AccountController@showSection'); - Route::post('company/{section?}/{sub_section?}', 'AccountController@doSection'); Route::post('user/setTheme', 'UserController@setTheme'); Route::post('remove_logo', 'AccountController@removeLogo'); Route::post('account/go_pro', 'AccountController@enableProPlan'); + Route::post('/export', 'ExportController@doExport'); + Route::post('/import', 'ImportController@doImport'); + Route::post('/import_csv', 'ImportController@doImportCSV'); + Route::resource('gateways', 'AccountGatewayController'); Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); - Route::post('gateways/delete', 'AccountGatewayController@delete'); + Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); + + Route::resource('bank_accounts', 'BankAccountController'); + Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable')); + Route::post('bank_accounts/bulk', 'BankAccountController@bulk'); + Route::post('bank_accounts/test', 'BankAccountController@test'); Route::resource('clients', 'ClientController'); Route::get('api/clients', array('as'=>'api.clients', 'uses'=>'ClientController@getDatatable')); @@ -142,13 +156,15 @@ Route::group(['middleware' => 'auth'], function() { Route::get('invoices/invoice_history/{invoice_id}', 'InvoiceController@invoiceHistory'); Route::get('quotes/quote_history/{invoice_id}', 'InvoiceController@invoiceHistory'); - + Route::resource('invoices', 'InvoiceController'); Route::get('api/invoices/{client_id?}', array('as'=>'api.invoices', 'uses'=>'InvoiceController@getDatatable')); Route::get('invoices/create/{client_id?}', 'InvoiceController@create'); Route::get('recurring_invoices/create/{client_id?}', 'InvoiceController@createRecurring'); + Route::get('recurring_invoices', 'RecurringInvoiceController@index'); Route::get('invoices/{public_id}/clone', 'InvoiceController@cloneInvoice'); Route::post('invoices/bulk', 'InvoiceController@bulk'); + Route::post('recurring_invoices/bulk', 'InvoiceController@bulk'); Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{public_id}/clone', 'InvoiceController@cloneInvoice'); @@ -174,19 +190,47 @@ Route::group(['middleware' => 'auth'], function() { Route::post('credits/bulk', 'CreditController@bulk'); get('/resend_confirmation', 'AccountController@resendConfirmation'); - //Route::resource('timesheets', 'TimesheetController'); + post('/update_setup', 'AppController@updateSetup'); + + + // vendor + Route::resource('vendors', 'VendorController'); + Route::get('api/vendor', array('as'=>'api.vendors', 'uses'=>'VendorController@getDatatable')); + Route::post('vendors/bulk', 'VendorController@bulk'); + + // Expense + Route::resource('expenses', 'ExpenseController'); + Route::get('expenses/create/{vendor_id?}/{client_id?}', 'ExpenseController@create'); + Route::get('api/expense', array('as'=>'api.expenses', 'uses'=>'ExpenseController@getDatatable')); + Route::get('api/expenseVendor/{id}', array('as'=>'api.expense', 'uses'=>'ExpenseController@getDatatableVendor')); + Route::post('expenses/bulk', 'ExpenseController@bulk'); }); -// Route group for API +// Route groups for API Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() { - Route::resource('ping', 'ClientApiController@ping'); + Route::get('ping', 'ClientApiController@ping'); + Route::post('login', 'AccountApiController@login'); + Route::get('static', 'AccountApiController@getStaticData'); + Route::get('accounts', 'AccountApiController@show'); Route::resource('clients', 'ClientApiController'); - Route::resource('invoices', 'InvoiceApiController'); + Route::get('quotes', 'QuoteApiController@index'); Route::resource('quotes', 'QuoteApiController'); + Route::get('invoices', 'InvoiceApiController@index'); + Route::resource('invoices', 'InvoiceApiController'); + Route::get('payments', 'PaymentApiController@index'); Route::resource('payments', 'PaymentApiController'); + Route::get('tasks', 'TaskApiController@index'); + Route::resource('tasks', 'TaskApiController'); Route::post('hooks', 'IntegrationController@subscribe'); Route::post('email_invoice', 'InvoiceApiController@emailInvoice'); + Route::get('user_accounts','AccountApiController@getUserAccounts'); + + // Vendor + Route::resource('vendors', 'VendorApiController'); + + //Expense + Route::resource('expenses', 'ExpenseApiController'); }); // Redirects for legacy links @@ -218,7 +262,6 @@ Route::get('/forgot_password', function() { return Redirect::to(NINJA_APP_URL.'/forgot', 301); }); - if (!defined('CONTACT_EMAIL')) { define('CONTACT_EMAIL', Config::get('mail.from.address')); define('CONTACT_NAME', Config::get('mail.from.name')); @@ -226,37 +269,67 @@ if (!defined('CONTACT_EMAIL')) { define('ENV_DEVELOPMENT', 'local'); define('ENV_STAGING', 'staging'); - define('ENV_PRODUCTION', 'fortrabbit'); define('RECENTLY_VIEWED', 'RECENTLY_VIEWED'); + define('ENTITY_CLIENT', 'client'); + define('ENTITY_CONTACT', 'contact'); define('ENTITY_INVOICE', 'invoice'); + define('ENTITY_INVOICE_ITEMS', 'invoice_items'); + define('ENTITY_INVITATION', 'invitation'); define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); define('ENTITY_PAYMENT', 'payment'); define('ENTITY_CREDIT', 'credit'); define('ENTITY_QUOTE', 'quote'); define('ENTITY_TASK', 'task'); + define('ENTITY_ACCOUNT_GATEWAY', 'account_gateway'); + define('ENTITY_BANK_ACCOUNT', 'bank_account'); + define('ENTITY_USER', 'user'); + define('ENTITY_TOKEN', 'token'); + define('ENTITY_TAX_RATE', 'tax_rate'); + define('ENTITY_PRODUCT', 'product'); + define('ENTITY_ACTIVITY', 'activity'); + define('ENTITY_VENDOR','vendor'); + define('ENTITY_VENDOR_ACTIVITY','vendor_activity'); + define('ENTITY_EXPENSE', 'expense'); + define('ENTITY_PAYMENT_TERM','payment_term'); + define('ENTITY_EXPENSE_ACTIVITY','expense_activity'); define('PERSON_CONTACT', 'contact'); define('PERSON_USER', 'user'); + define('PERSON_VENDOR_CONTACT','vendorcontact'); - define('ACCOUNT_DETAILS', 'details'); + define('BASIC_SETTINGS', 'basic_settings'); + define('ADVANCED_SETTINGS', 'advanced_settings'); + + define('ACCOUNT_COMPANY_DETAILS', 'company_details'); + define('ACCOUNT_USER_DETAILS', 'user_details'); + define('ACCOUNT_LOCALIZATION', 'localization'); define('ACCOUNT_NOTIFICATIONS', 'notifications'); define('ACCOUNT_IMPORT_EXPORT', 'import_export'); - define('ACCOUNT_PAYMENTS', 'payments'); + define('ACCOUNT_PAYMENTS', 'online_payments'); + define('ACCOUNT_BANKS', 'bank_accounts'); define('ACCOUNT_MAP', 'import_map'); define('ACCOUNT_EXPORT', 'export'); + define('ACCOUNT_TAX_RATES', 'tax_rates'); define('ACCOUNT_PRODUCTS', 'products'); define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); define('ACCOUNT_INVOICE_DESIGN', 'invoice_design'); - define('ACCOUNT_CHART_BUILDER', 'chart_builder'); + define('ACCOUNT_CLIENT_PORTAL', 'client_portal'); + define('ACCOUNT_EMAIL_SETTINGS', 'email_settings'); + define('ACCOUNT_CHARTS_AND_REPORTS', 'charts_and_reports'); define('ACCOUNT_USER_MANAGEMENT', 'user_management'); define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations'); - define('ACCOUNT_EMAIL_TEMPLATES', 'email_templates'); - define('ACCOUNT_TOKEN_MANAGEMENT', 'token_management'); + define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders'); + define('ACCOUNT_API_TOKENS', 'api_tokens'); define('ACCOUNT_CUSTOMIZE_DESIGN', 'customize_design'); + define('ACCOUNT_SYSTEM_SETTINGS', 'system_settings'); + define('ACCOUNT_PAYMENT_TERMS','payment_terms'); + define('ACTION_RESTORE', 'restore'); + define('ACTION_ARCHIVE', 'archive'); + define('ACTION_DELETE', 'delete'); define('ACTIVITY_TYPE_CREATE_CLIENT', 1); define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); @@ -270,12 +343,12 @@ if (!defined('CONTACT_EMAIL')) { define('ACTIVITY_TYPE_DELETE_INVOICE', 9); define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); - define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); + //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); define('ACTIVITY_TYPE_CREATE_CREDIT', 14); - define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); + //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); define('ACTIVITY_TYPE_DELETE_CREDIT', 17); @@ -293,21 +366,59 @@ if (!defined('CONTACT_EMAIL')) { define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); + // Vendors + define('ACTIVITY_TYPE_CREATE_VENDOR', 30); + define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31); + define('ACTIVITY_TYPE_DELETE_VENDOR', 32); + define('ACTIVITY_TYPE_RESTORE_VENDOR', 33); + + // expenses + define('ACTIVITY_TYPE_CREATE_EXPENSE', 34); + define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35); + define('ACTIVITY_TYPE_DELETE_EXPENSE', 36); + define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37); + define('DEFAULT_INVOICE_NUMBER', '0001'); define('RECENTLY_VIEWED_LIMIT', 8); define('LOGGED_ERROR_LIMIT', 100); define('RANDOM_KEY_LENGTH', 32); - define('MAX_NUM_CLIENTS', 500); - define('MAX_NUM_CLIENTS_PRO', 20000); define('MAX_NUM_USERS', 20); define('MAX_SUBDOMAIN_LENGTH', 30); + define('MAX_IFRAME_URL_LENGTH', 250); + define('MAX_LOGO_FILE_SIZE', 200); // KB + define('MAX_FAILED_LOGINS', 10); define('DEFAULT_FONT_SIZE', 9); + define('DEFAULT_HEADER_FONT', 1);// Roboto + define('DEFAULT_BODY_FONT', 1);// Roboto + define('DEFAULT_SEND_RECURRING_HOUR', 8); + + define('IMPORT_CSV', 'CSV'); + define('IMPORT_FRESHBOOKS', 'FreshBooks'); + define('IMPORT_WAVE', 'Wave'); + define('IMPORT_RONIN', 'Ronin'); + define('IMPORT_HIVEAGE', 'Hiveage'); + define('IMPORT_ZOHO', 'Zoho'); + define('IMPORT_NUTCACHE', 'Nutcache'); + define('IMPORT_INVOICEABLE', 'Invoiceable'); + define('IMPORT_HARVEST', 'Harvest'); + + define('MAX_NUM_CLIENTS', 100); + define('MAX_NUM_CLIENTS_PRO', 20000); + define('MAX_NUM_CLIENTS_LEGACY', 500); + define('MAX_INVOICE_AMOUNT', 1000000000); + define('LEGACY_CUTOFF', 57800); + define('ERROR_DELAY', 3); + + define('MAX_NUM_VENDORS', 100); + define('MAX_NUM_VENDORS_PRO', 20000); + define('MAX_NUM_VENDORS_LEGACY', 500); define('INVOICE_STATUS_DRAFT', 1); define('INVOICE_STATUS_SENT', 2); define('INVOICE_STATUS_VIEWED', 3); - define('INVOICE_STATUS_PARTIAL', 4); - define('INVOICE_STATUS_PAID', 5); + define('INVOICE_STATUS_APPROVED', 4); + define('INVOICE_STATUS_PARTIAL', 5); + define('INVOICE_STATUS_PAID', 6); define('PAYMENT_TYPE_CREDIT', 1); define('CUSTOM_DESIGN', 11); @@ -328,18 +439,24 @@ if (!defined('CONTACT_EMAIL')) { define('SESSION_COUNTER', 'sessionCounter'); define('SESSION_LOCALE', 'sessionLocale'); define('SESSION_USER_ACCOUNTS', 'userAccounts'); + define('SESSION_REFERRAL_CODE', 'referralCode'); define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); + define('CURRENCY_DOLLAR', 1); + define('CURRENCY_EURO', 3); + define('DEFAULT_TIMEZONE', 'US/Eastern'); - define('DEFAULT_CURRENCY', 1); // US Dollar + define('DEFAULT_COUNTRY', 840); // United Stated + define('DEFAULT_CURRENCY', CURRENCY_DOLLAR); define('DEFAULT_LANGUAGE', 1); // English define('DEFAULT_DATE_FORMAT', 'M j, Y'); define('DEFAULT_DATE_PICKER_FORMAT', 'M d, yyyy'); - define('DEFAULT_DATETIME_FORMAT', 'F j, Y, g:i a'); - define('DEFAULT_QUERY_CACHE', 120); // minutes + define('DEFAULT_DATETIME_FORMAT', 'F j, Y g:i a'); + define('DEFAULT_DATETIME_MOMENT_FORMAT', 'MMM D, YYYY h:mm:ss a'); define('DEFAULT_LOCALE', 'en'); + define('DEFAULT_MAP_ZOOM', 10); define('RESULT_SUCCESS', 'success'); define('RESULT_FAILURE', 'failure'); @@ -350,37 +467,51 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_AUTHORIZE_NET', 1); define('GATEWAY_AUTHORIZE_NET_SIM', 2); + define('GATEWAY_EWAY', 4); + define('GATEWAY_MOLLIE', 9); + define('GATEWAY_PAYFAST', 13); define('GATEWAY_PAYPAL_EXPRESS', 17); define('GATEWAY_PAYPAL_PRO', 18); define('GATEWAY_STRIPE', 23); + define('GATEWAY_GOCARDLESS', 6); define('GATEWAY_TWO_CHECKOUT', 27); define('GATEWAY_BEANSTREAM', 29); define('GATEWAY_PSIGATE', 30); define('GATEWAY_MOOLAH', 31); define('GATEWAY_BITPAY', 42); define('GATEWAY_DWOLLA', 43); + define('GATEWAY_CHECKOUT_COM', 47); define('EVENT_CREATE_CLIENT', 1); define('EVENT_CREATE_INVOICE', 2); define('EVENT_CREATE_QUOTE', 3); define('EVENT_CREATE_PAYMENT', 4); + define('EVENT_CREATE_VENDOR',5); define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); define('PREV_USER_ID', 'PREV_USER_ID'); define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); - define('NINJA_GATEWAY_CONFIG', ''); + define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG'); define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); define('NINJA_APP_URL', 'https://app.invoiceninja.com'); - define('NINJA_VERSION', '2.3.4'); + define('NINJA_VERSION', '2.4.9.6'); define('NINJA_DATE', '2000-01-01'); + define('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'); + define('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'); + define('SOCIAL_LINK_GITHUB', 'https://github.com/invoiceninja/invoiceninja/'); + define('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'); - define('RELEASES_URL', 'https://github.com/hillelcoren/invoice-ninja/releases/'); - define('ZAPIER_URL', 'https://zapier.com/developer/invite/11276/85cf0ee4beae8e802c6c579eb4e351f1/'); + define('RELEASES_URL', 'https://trello.com/b/63BbiVVe/invoice-ninja'); + define('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja'); define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/'); define('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html'); + define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/api/browser/v2/'); + define('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php'); + define('REFERRAL_PROGRAM_URL', 'https://www.invoiceninja.com/referral-program/'); + define('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup'); define('COUNT_FREE_DESIGNS', 4); define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design @@ -402,6 +533,7 @@ if (!defined('CONTACT_EMAIL')) { define('TEST_USERNAME', 'user@example.com'); define('TEST_PASSWORD', 'password'); + define('API_SECRET', 'API_SECRET'); define('TOKEN_BILLING_DISABLED', 1); define('TOKEN_BILLING_OPT_IN', 2); @@ -410,11 +542,41 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_TYPE_PAYPAL', 'PAYMENT_TYPE_PAYPAL'); define('PAYMENT_TYPE_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD'); + define('PAYMENT_TYPE_DIRECT_DEBIT', 'PAYMENT_TYPE_DIRECT_DEBIT'); define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN'); define('PAYMENT_TYPE_DWOLLA', 'PAYMENT_TYPE_DWOLLA'); define('PAYMENT_TYPE_TOKEN', 'PAYMENT_TYPE_TOKEN'); define('PAYMENT_TYPE_ANY', 'PAYMENT_TYPE_ANY'); + define('REMINDER1', 'reminder1'); + define('REMINDER2', 'reminder2'); + define('REMINDER3', 'reminder3'); + + define('REMINDER_DIRECTION_AFTER', 1); + define('REMINDER_DIRECTION_BEFORE', 2); + + define('REMINDER_FIELD_DUE_DATE', 1); + define('REMINDER_FIELD_INVOICE_DATE', 2); + + define('SOCIAL_GOOGLE', 'Google'); + define('SOCIAL_FACEBOOK', 'Facebook'); + define('SOCIAL_GITHUB', 'GitHub'); + define('SOCIAL_LINKEDIN', 'LinkedIn'); + + define('USER_STATE_ACTIVE', 'active'); + define('USER_STATE_PENDING', 'pending'); + define('USER_STATE_DISABLED', 'disabled'); + define('USER_STATE_ADMIN', 'admin'); + + define('API_SERIALIZER_ARRAY', 'array'); + define('API_SERIALIZER_JSON', 'json'); + + define('EMAIL_DESIGN_PLAIN', 1); + define('EMAIL_DESIGN_LIGHT', 2); + define('EMAIL_DESIGN_DARK', 3); + + define('BANK_LIBRARY_OFX', 1); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -422,9 +584,28 @@ if (!defined('CONTACT_EMAIL')) { 8 => ['card' => 'images/credit_cards/Test-Diners-Icon.png', 'text' => 'Diners'], 16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'] ]; - define('CREDIT_CARDS', serialize($creditCards)); + $cachedTables = [ + 'currencies' => 'App\Models\Currency', + 'sizes' => 'App\Models\Size', + 'industries' => 'App\Models\Industry', + 'timezones' => 'App\Models\Timezone', + 'dateFormats' => 'App\Models\DateFormat', + 'datetimeFormats' => 'App\Models\DatetimeFormat', + 'languages' => 'App\Models\Language', + 'paymentTerms' => 'App\Models\PaymentTerm', + 'paymentTypes' => 'App\Models\PaymentType', + 'countries' => 'App\Models\Country', + 'invoiceDesigns' => 'App\Models\InvoiceDesign', + 'invoiceStatus' => 'App\Models\InvoiceStatus', + 'frequencies' => 'App\Models\Frequency', + 'gateways' => 'App\Models\Gateway', + 'fonts' => 'App\Models\Font', + 'banks' => 'App\Models\Bank', + ]; + define('CACHED_TABLES', serialize($cachedTables)); + function uctrans($text) { return ucwords(trans($text)); @@ -447,29 +628,26 @@ if (!defined('CONTACT_EMAIL')) { /* // Log all SQL queries to laravel.log -Event::listen('illuminate.query', function($query, $bindings, $time, $name) -{ - $data = compact('bindings', 'time', 'name'); +if (Utils::isNinjaDev()) { + Event::listen('illuminate.query', function($query, $bindings, $time, $name) { + $data = compact('bindings', 'time', 'name'); - // Format binding data for sql insertion - foreach ($bindings as $i => $binding) - { - if ($binding instanceof \DateTime) - { - $bindings[$i] = $binding->format('\'Y-m-d H:i:s\''); + // Format binding data for sql insertion + foreach ($bindings as $i => $binding) { + if ($binding instanceof \DateTime) { + $bindings[$i] = $binding->format('\'Y-m-d H:i:s\''); + } elseif (is_string($binding)) { + $bindings[$i] = "'$binding'"; + } } - else if (is_string($binding)) - { - $bindings[$i] = "'$binding'"; - } - } - // Insert bindings into query - $query = str_replace(array('%', '?'), array('%%', '%s'), $query); - $query = vsprintf($query, $bindings); + // Insert bindings into query + $query = str_replace(array('%', '?'), array('%%', '%s'), $query); + $query = vsprintf($query, $bindings); - Log::info($query, $data); -}); + Log::info($query, $data); + }); +} */ /* @@ -477,4 +655,5 @@ if (Auth::check() && Auth::user()->id === 1) { Auth::loginUsingId(1); } -*/ \ No newline at end of file +*/ + diff --git a/app/Libraries/OFX.php b/app/Libraries/OFX.php new file mode 100644 index 000000000000..734a27be30b5 --- /dev/null +++ b/app/Libraries/OFX.php @@ -0,0 +1,225 @@ +bank = $bank; + $this->request = $request; + } + public function go() + { + $c = curl_init(); + curl_setopt($c, CURLOPT_URL, $this->bank->url); + curl_setopt($c, CURLOPT_POST, 1); + curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx')); + curl_setopt($c, CURLOPT_POSTFIELDS, $this->request); + curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); + //curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false); + $this->response = curl_exec($c); + curl_close($c); + $tmp = explode('', $this->response); + $this->responseHeader = $tmp[0]; + $this->responseBody = ''.$tmp[1]; + } + public function xml() + { + $xml = $this->responseBody; + self::closeTags($xml); + $x = new SimpleXMLElement($xml); + + return $x; + } + public static function closeTags(&$x) + { + $x = preg_replace('/(<([^<\/]+)>)(?!.*?<\/\2>)([^<]+)/', '\1\3', $x); + } +} + +class Finance +{ + public $banks; +} + +class Bank +{ + public $logins; // array of class User + public $finance; // the Finance object that hold this Bank object + public $fid; + public $org; + public $url; + public function __construct($finance, $fid, $url, $org) + { + $this->finance = $finance; + $this->fid = $fid; + $this->url = $url; + $this->org = $org; + } +} + +class Login +{ + public $accounts; + public $bank; + public $id; + public $pass; + public function __construct($bank, $id, $pass) + { + $this->bank = $bank; + $this->id = $id; + $this->pass = $pass; + } + public function setup() + { + $ofxRequest = + "OFXHEADER:100\n". + "DATA:OFXSGML\n". + "VERSION:102\n". + "SECURITY:NONE\n". + "ENCODING:USASCII\n". + "CHARSET:1252\n". + "COMPRESSION:NONE\n". + "OLDFILEUID:NONE\n". + "NEWFILEUID:NONE\n". + "\n". + "\n". + "\n". + "\n". + "20110412162900.000[-7:MST]\n". + "".$this->id."\n". + "".$this->pass."\n". + "N\n". + "ENG\n". + "\n". + "".$this->bank->org."\n". + "".$this->bank->fid."\n". + "\n". + "QMOFX\n". + "1900\n". + "\n". + "\n". + "\n". + "\n". + "".md5(time().$this->bank->url.$this->id)."\n". + "\n". + "19900101\n". + "\n". + " \n". + "\n". + "\n"; + $o = new OFX($this->bank, $ofxRequest); + $o->go(); + $x = $o->xml(); + foreach ($x->xpath('/OFX/SIGNUPMSGSRSV1/ACCTINFOTRNRS/ACCTINFORS/ACCTINFO/BANKACCTINFO/BANKACCTFROM') as $a) { + $this->accounts[] = new Account($this, (string) $a->ACCTID, 'BANK', (string) $a->ACCTTYPE, (string) $a->BANKID); + } + foreach ($x->xpath('/OFX/SIGNUPMSGSRSV1/ACCTINFOTRNRS/ACCTINFORS/ACCTINFO/CCACCTINFO/CCACCTFROM') as $a) { + $this->accounts[] = new Account($this, (string) $a->ACCTID, 'CC'); + } + } +} + +class Account +{ + public $login; + public $id; + public $type; + public $subType; + public $bankId; + public $ledgerBalance; + public $availableBalance; + public $response; + public function __construct($login, $id, $type, $subType = null, $bankId = null) + { + $this->login = $login; + $this->id = $id; + $this->type = $type; + $this->subType = $subType; + $this->bankId = $bankId; + } + public function setup($includeTransactions = true) + { + $ofxRequest = + "OFXHEADER:100\n". + "DATA:OFXSGML\n". + "VERSION:102\n". + "SECURITY:NONE\n". + "ENCODING:USASCII\n". + "CHARSET:1252\n". + "COMPRESSION:NONE\n". + "OLDFILEUID:NONE\n". + "NEWFILEUID:NONE\n". + "\n". + "\n". + "\n". + "\n". + "20110412162900.000[-7:MST]\n". + "".$this->login->id."\n". + "".$this->login->pass."\n". + "ENG\n". + "\n". + "".$this->login->bank->org."\n". + "".$this->login->bank->fid."\n". + "\n". + "QMOFX\n". + "1900\n". + "\n". + "\n"; + if ($this->type == 'BANK') { + $ofxRequest .= + " \n". + " \n". + " ".md5(time().$this->login->bank->url.$this->id)."\n". + " \n". + " \n". + " ".$this->bankId."\n". + " ".$this->id."\n". + " ".$this->subType."\n". + " \n". + " \n". + " 20110301\n". + " ".($includeTransactions ? 'Y' : 'N')."\n". + " \n". + " \n". + " \n". + " \n"; + } elseif ($this->type == 'CC') { + $ofxRequest .= + " \n". + " \n". + " ".md5(time().$this->login->bank->url.$this->id)."\n". + " \n". + " \n". + " ".$this->id."\n". + " \n". + " \n". + " 20110320\n". + " ".($includeTransactions ? 'Y' : 'N')."\n". + " \n". + " \n". + " \n". + " \n"; + } + $ofxRequest .= + ""; + $o = new OFX($this->login->bank, $ofxRequest); + $o->go(); + $this->response = $o->response; + $x = $o->xml(); + $a = $x->xpath('/OFX/*/*/*/LEDGERBAL/BALAMT'); + $this->ledgerBalance = (double) $a[0]; + $a = $x->xpath('/OFX/*/*/*/AVAILBAL/BALAMT'); + if (isset($a[0])) { + $this->availableBalance = (double) $a[0]; + } + } +} diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index a5eb3b9a4a4c..113a78671071 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -7,6 +7,7 @@ use App; use Schema; use Session; use Request; +use Exception; use View; use DateTimeZone; use Input; @@ -35,14 +36,19 @@ class Utils if (Schema::hasTable('accounts')) { return true; } - } catch (\Exception $e) { + } catch (Exception $e) { return false; } } - public static function isProd() + public static function isDownForMaintenance() { - return App::environment() == ENV_PRODUCTION; + return file_exists(storage_path() . '/framework/down'); + } + + public static function isCron() + { + return php_sapi_name() == 'cli'; } public static function isNinja() @@ -60,6 +66,30 @@ class Utils return isset($_ENV['NINJA_DEV']) && $_ENV['NINJA_DEV'] == 'true'; } + public static function requireHTTPS() + { + return Utils::isNinjaProd() || (isset($_ENV['REQUIRE_HTTPS']) && $_ENV['REQUIRE_HTTPS'] == 'true'); + } + + public static function isOAuthEnabled() + { + $providers = [ + SOCIAL_GOOGLE, + SOCIAL_FACEBOOK, + SOCIAL_GITHUB, + SOCIAL_LINKEDIN + ]; + + foreach ($providers as $provider) { + $key = strtoupper($provider) . '_CLIENT_ID'; + if (isset($_ENV[$key]) && $_ENV[$key]) { + return true; + } + } + + return false; + } + public static function allowNewAccounts() { return Utils::isNinja() || Auth::check(); @@ -89,11 +119,6 @@ class Utils return isset($_ENV[DEMO_ACCOUNT_ID]) ? $_ENV[DEMO_ACCOUNT_ID] : false; } - public static function isDemo() - { - return Auth::check() && Auth::user()->isDemo(); - } - public static function getNewsFeedResponse($userType = false) { if (!$userType) { @@ -108,6 +133,19 @@ class Utils return $response; } + public static function getLastURL() + { + if (!count(Session::get(RECENTLY_VIEWED))) { + return '#'; + } + + $history = Session::get(RECENTLY_VIEWED); + $last = $history[0]; + $penultimate = count($history) > 1 ? $history[1] : $last; + + return Request::url() == $last->url ? $penultimate->url : $last->url; + } + public static function getProLabel($feature) { if (Auth::check() @@ -131,8 +169,10 @@ class Utils foreach ($input as $field) { if ($field == "checkbox") { $data[] = $field; - } else { + } elseif ($field) { $data[] = trans("texts.$field"); + } else { + $data[] = ''; } } @@ -164,6 +204,10 @@ class Utils public static function logError($error, $context = 'PHP') { + if ($error instanceof Exception) { + $error = self::getErrorString($error); + } + $count = Session::get('error_count', 0); Session::put('error_count', ++$count); if ($count > 100) { @@ -173,12 +217,13 @@ class Utils $data = [ 'context' => $context, 'user_id' => Auth::check() ? Auth::user()->id : 0, + 'account_id' => Auth::check() ? Auth::user()->account_id : 0, 'user_name' => Auth::check() ? Auth::user()->getDisplayName() : '', + 'method' => Request::method(), 'url' => Input::get('url', Request::url()), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'ip' => Request::getClientIp(), 'count' => Session::get('error_count', 0), - //'input' => Input::all() ]; Log::error($error."\n", $data); @@ -198,72 +243,62 @@ class Utils return floatval($value); } - public static function formatPhoneNumber($phoneNumber) + public static function parseInt($value) { - $phoneNumber = preg_replace('/[^0-9a-zA-Z]/', '', $phoneNumber); + $value = preg_replace('/[^0-9]/', '', $value); - if (!$phoneNumber) { - return ''; - } - - if (strlen($phoneNumber) > 10) { - $countryCode = substr($phoneNumber, 0, strlen($phoneNumber)-10); - $areaCode = substr($phoneNumber, -10, 3); - $nextThree = substr($phoneNumber, -7, 3); - $lastFour = substr($phoneNumber, -4, 4); - - $phoneNumber = '+'.$countryCode.' ('.$areaCode.') '.$nextThree.'-'.$lastFour; - } elseif (strlen($phoneNumber) == 10 && in_array(substr($phoneNumber, 0, 3), array(653, 656, 658, 659))) { - /** - * SG country code are 653, 656, 658, 659 - * US area code consist of 650, 651 and 657 - * @see http://en.wikipedia.org/wiki/Telephone_numbers_in_Singapore#Numbering_plan - * @see http://www.bennetyee.org/ucsd-pages/area.html - */ - $countryCode = substr($phoneNumber, 0, 2); - $nextFour = substr($phoneNumber, 2, 4); - $lastFour = substr($phoneNumber, 6, 4); - - $phoneNumber = '+'.$countryCode.' '.$nextFour.' '.$lastFour; - } elseif (strlen($phoneNumber) == 10) { - $areaCode = substr($phoneNumber, 0, 3); - $nextThree = substr($phoneNumber, 3, 3); - $lastFour = substr($phoneNumber, 6, 4); - - $phoneNumber = '('.$areaCode.') '.$nextThree.'-'.$lastFour; - } elseif (strlen($phoneNumber) == 7) { - $nextThree = substr($phoneNumber, 0, 3); - $lastFour = substr($phoneNumber, 3, 4); - - $phoneNumber = $nextThree.'-'.$lastFour; - } - - return $phoneNumber; + return intval($value); } - public static function formatMoney($value, $currencyId = false) + public static function getFromCache($id, $type) { + $data = Cache::get($type)->filter(function($item) use ($id) { + return $item->id == $id; + }); + + return $data->first(); + } + + public static function formatMoney($value, $currencyId = false, $countryId = false, $showCode = false) { - if (!$currencyId) { - $currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY); - } - - foreach (Cache::get('currencies') as $currency) { - if ($currency->id == $currencyId) { - break; - } - } - - if (!$currency) { - $currency = Currency::find(1); - } - if (!$value) { $value = 0; } - Cache::add('currency', $currency, DEFAULT_QUERY_CACHE); + if (!$currencyId) { + $currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY); + } - return $currency->symbol.number_format($value, $currency->precision, $currency->decimal_separator, $currency->thousand_separator); + if (!$countryId && Auth::check()) { + $countryId = Auth::user()->account->country_id; + } + + $currency = self::getFromCache($currencyId, 'currencies'); + $thousand = $currency->thousand_separator; + $decimal = $currency->decimal_separator; + $code = $currency->code; + $swapSymbol = false; + + if ($countryId && $currencyId == CURRENCY_EURO) { + $country = self::getFromCache($countryId, 'countries'); + $swapSymbol = $country->swap_currency_symbol; + if ($country->thousand_separator) { + $thousand = $country->thousand_separator; + } + if ($country->decimal_separator) { + $decimal = $country->decimal_separator; + } + } + + $value = number_format($value, $currency->precision, $decimal, $thousand); + $symbol = $currency->symbol; + + if ($showCode || !$symbol) { + return "{$value} {$code}"; + } elseif ($swapSymbol) { + return "{$value} " . trim($symbol); + } else { + return "{$symbol}{$value}"; + } } public static function pluralize($string, $count) @@ -274,14 +309,57 @@ class Utils return $string; } + public static function maskAccountNumber($value) + { + $length = strlen($value); + if ($length < 4) { + str_repeat('*', 16); + } + + $lastDigits = substr($value, -4); + return str_repeat('*', $length - 4) . $lastDigits; + } + + // http://wephp.co/detect-credit-card-type-php/ + public static function getCardType($number) + { + $number = preg_replace('/[^\d]/', '', $number); + + if (preg_match('/^3[47][0-9]{13}$/', $number)) { + return 'American Express'; + } elseif (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/', $number)) { + return 'Diners Club'; + } elseif (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/', $number)) { + return 'Discover'; + } elseif (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/', $number)) { + return 'JCB'; + } elseif (preg_match('/^5[1-5][0-9]{14}$/', $number)) { + return 'MasterCard'; + } elseif (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/', $number)) { + return 'Visa'; + } else { + return 'Unknown'; + } + } + public static function toArray($data) { return json_decode(json_encode((array) $data), true); } - public static function toSpaceCase($camelStr) + public static function toSpaceCase($string) { - return preg_replace('/([a-z])([A-Z])/s', '$1 $2', $camelStr); + return preg_replace('/([a-z])([A-Z])/s', '$1 $2', $string); + } + + public static function toSnakeCase($string) + { + return preg_replace('/([a-z])([A-Z])/s', '$1_$2', $string); + } + + public static function toCamelCase($string) + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $string)))); } public static function timestampToDateTimeString($timestamp) @@ -302,6 +380,10 @@ class Utils public static function dateToString($date) { + if (!$date) { + return false; + } + $dateTime = new DateTime($date); $timestamp = $dateTime->getTimestamp(); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); @@ -325,29 +407,19 @@ class Utils return $date->format($format); } - public static function getTiemstampOffset() - { - $timezone = new DateTimeZone(Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE)); - $datetime = new DateTime('now', $timezone); - $offset = $timezone->getOffset($datetime); - $minutes = $offset / 60; - - return $minutes; - } - public static function toSqlDate($date, $formatResult = true) { if (!$date) { return; } - //$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); - - //$dateTime = DateTime::createFromFormat($format, $date, new DateTimeZone($timezone)); $dateTime = DateTime::createFromFormat($format, $date); - return $formatResult ? $dateTime->format('Y-m-d') : $dateTime; + if(!$dateTime) + return $date; + else + return $formatResult ? $dateTime->format('Y-m-d') : $dateTime; } public static function fromSqlDate($date, $formatResult = true) @@ -356,13 +428,13 @@ class Utils return ''; } - //$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); - $dateTime = DateTime::createFromFormat('Y-m-d', $date); - //$dateTime->setTimeZone(new DateTimeZone($timezone)); - return $formatResult ? $dateTime->format($format) : $dateTime; + if(!$dateTime) + return $date; + else + return $formatResult ? $dateTime->format($format) : $dateTime; } public static function fromSqlDateTime($date, $formatResult = true) @@ -380,6 +452,13 @@ class Utils return $formatResult ? $dateTime->format($format) : $dateTime; } + public static function formatTime($t) + { + // http://stackoverflow.com/a/3172665 + $f = ':'; + return sprintf("%02d%s%02d%s%02d", floor($t/3600), $f, ($t/60)%60, $f, $t%60); + } + public static function today($formatResult = true) { $timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); @@ -421,12 +500,8 @@ class Utils continue; } - // temporary fix to check for new property in session - if (!property_exists($item, 'accountId')) { - continue; - } + array_push($data, $item); - array_unshift($data, $item); if (isset($counts[$item->accountId])) { $counts[$item->accountId]++; } else { @@ -435,7 +510,7 @@ class Utils } array_unshift($data, $object); - + if (isset($counts[Auth::user()->account_id]) && $counts[Auth::user()->account_id] > RECENTLY_VIEWED_LIMIT) { array_pop($data); } @@ -544,32 +619,26 @@ class Utils } } - public static function encodeActivity($person = null, $action, $entity = null, $otherPerson = null) + public static function getVendorDisplayName($model) { - $person = $person ? $person->getDisplayName() : 'System'; - $entity = $entity ? $entity->getActivityKey() : ''; - $otherPerson = $otherPerson ? 'to '.$otherPerson->getDisplayName() : ''; - $token = Session::get('token_id') ? ' ('.trans('texts.token').')' : ''; + if(is_null($model)) + return ''; - return trim("$person $token $action $entity $otherPerson"); + if($model->vendor_name) + return $model->vendor_name; + + return 'No vendor name'; } - public static function decodeActivity($message) + public static function getPersonDisplayName($firstName, $lastName, $email) { - $pattern = '/\[([\w]*):([\d]*):(.*)\]/i'; - preg_match($pattern, $message, $matches); - - if (count($matches) > 0) { - $match = $matches[0]; - $type = $matches[1]; - $publicId = $matches[2]; - $name = $matches[3]; - - $link = link_to($type.'s/'.$publicId, $name); - $message = str_replace($match, "$type $link", $message); + if ($firstName || $lastName) { + return $firstName.' '.$lastName; + } elseif ($email) { + return $email; + } else { + return trans('texts.guest'); } - - return $message; } public static function generateLicense() @@ -592,7 +661,9 @@ class Utils return EVENT_CREATE_QUOTE; } elseif ($eventName == 'create_payment') { return EVENT_CREATE_PAYMENT; - } else { + } elseif ($eventName == 'create_vendor') { + return EVENT_CREATE_VENDOR; + }else { return false; } } @@ -600,9 +671,8 @@ class Utils public static function notifyZapier($subscription, $data) { $curl = curl_init(); - $jsonEncodedData = json_encode($data->toPublicArray()); - + $opts = [ CURLOPT_URL => $subscription->target_url, CURLOPT_RETURNTRANSFER => true, @@ -624,26 +694,23 @@ class Utils } } - - public static function remapPublicIds($items) - { - $return = []; - - foreach ($items as $item) { - $return[] = $item->toPublicArray(); - } - - return $return; - } - - public static function hideIds($data) + public static function hideIds($data, $mapped = false) { $publicId = null; + if (!$mapped) { + $mapped = []; + } + foreach ($data as $key => $val) { if (is_array($val)) { - $data[$key] = Utils::hideIds($val); - } else if ($key == 'id' || strpos($key, '_id')) { + if ($key == 'account' || isset($mapped[$key])) { + // do nothing + } else { + $mapped[$key] = true; + $data[$key] = Utils::hideIds($val, $mapped); + } + } elseif ($key == 'id' || strpos($key, '_id')) { if ($key == 'public_id') { $publicId = $val; } @@ -654,7 +721,7 @@ class Utils if ($publicId) { $data['id'] = $publicId; } - + return $data; } @@ -667,12 +734,18 @@ class Utils //'Access-Control-Allow-Headers' => 'Origin, Content-Type, Accept, Authorization, X-Requested-With', //'Access-Control-Allow-Credentials' => 'true', 'X-Total-Count' => $count, + 'X-Ninja-Version' => NINJA_VERSION, //'X-Rate-Limit-Limit' - The number of allowed requests in the current period //'X-Rate-Limit-Remaining' - The number of remaining requests in the current period //'X-Rate-Limit-Reset' - The number of seconds left in the current period, ]; } + public static function isEmpty($value) + { + return !$value || $value == '0' || $value == '0.00' || $value == '0,00'; + } + public static function startsWith($haystack, $needle) { return $needle === "" || strpos($haystack, $needle) === 0; @@ -685,10 +758,14 @@ class Utils public static function getEntityRowClass($model) { - $str = $model->is_deleted || ($model->deleted_at && $model->deleted_at != '0000-00-00') ? 'DISABLED ' : ''; + $str = ''; - if ($model->is_deleted) { - $str .= 'ENTITY_DELETED '; + if (property_exists($model, 'is_deleted')) { + $str = $model->is_deleted || ($model->deleted_at && $model->deleted_at != '0000-00-00') ? 'DISABLED ' : ''; + + if ($model->is_deleted) { + $str .= 'ENTITY_DELETED '; + } } if ($model->deleted_at && $model->deleted_at != '0000-00-00') { @@ -710,37 +787,172 @@ class Utils fwrite($output, "\n"); } - - public static function stringToObjectResolution($baseObject, $rawPath) - { - $val = ''; - - if (!is_object($baseObject)) { - return $val; - } - - $path = preg_split('/->/', $rawPath); - $node = $baseObject; - - while (($prop = array_shift($path)) !== null) { - if (property_exists($node, $prop)) { - $val = $node->$prop; - $node = $node->$prop; - } else if (is_object($node) && isset($node->$prop)) { - $node = $node->{$prop}; - } else if ( method_exists($node, $prop)) { - $val = call_user_func(array($node, $prop)); - } - } - - return $val; - } - public static function getFirst($values) { + public static function getFirst($values) + { if (is_array($values)) { return count($values) ? $values[0] : false; } else { return $values; } } + + // nouns in German and French should be uppercase + public static function transFlowText($key) + { + $str = trans("texts.$key"); + if (!in_array(App::getLocale(), ['de', 'fr'])) { + $str = strtolower($str); + } + return $str; + } + + public static function getSubdomainPlaceholder() + { + $parts = parse_url(SITE_URL); + $subdomain = ''; + if (isset($parts['host'])) { + $host = explode('.', $parts['host']); + if (count($host) > 2) { + $subdomain = $host[0]; + } + } + return $subdomain; + } + + public static function getDomainPlaceholder() + { + $parts = parse_url(SITE_URL); + $domain = ''; + if (isset($parts['host'])) { + $host = explode('.', $parts['host']); + if (count($host) > 2) { + array_shift($host); + $domain .= implode('.', $host); + } else { + $domain .= $parts['host']; + } + } + if (isset($parts['path'])) { + $domain .= $parts['path']; + } + return $domain; + } + + public static function replaceSubdomain($domain, $subdomain) + { + $parsedUrl = parse_url($domain); + $host = explode('.', $parsedUrl['host']); + if (count($host) > 0) { + $oldSubdomain = $host[0]; + $domain = str_replace("://{$oldSubdomain}.", "://{$subdomain}.", $domain); + } + return $domain; + } + + public static function splitName($name) + { + $name = trim($name); + $lastName = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name); + $firstName = trim(preg_replace('#'.$lastName.'#', '', $name)); + return array($firstName, $lastName); + } + + public static function decodePDF($string) + { + $string = str_replace('data:application/pdf;base64,', '', $string); + return base64_decode($string); + } + + public static function cityStateZip($city, $state, $postalCode, $swap) + { + $str = $city; + + if ($state) { + if ($str) { + $str .= ', '; + } + $str .= $state; + } + + if ($swap) { + return $postalCode . ' ' . $str; + } else { + return $str . ' ' . $postalCode; + } + } + + public static function formatWebsite($website) + { + if (!$website) { + return ''; + } + + $link = $website; + $title = $website; + $prefix = 'http://'; + + if (strlen($link) > 7 && substr($link, 0, 7) === $prefix) { + $title = substr($title, 7); + } else { + $link = $prefix.$link; + } + + return link_to($link, $title, array('target' => '_blank')); + } + + public static function wrapAdjustment($adjustment, $currencyId, $countryId) + { + $class = $adjustment <= 0 ? 'success' : 'default'; + $adjustment = Utils::formatMoney($adjustment, $currencyId, $countryId); + return "

    $adjustment

    "; + } + + public static function copyContext($entity1, $entity2) + { + if (!$entity2) { + return $entity1; + } + + $fields = [ + 'contact_id', + 'payment_id', + 'invoice_id', + 'credit_id', + 'invitation_id' + ]; + + $fields1 = $entity1->getAttributes(); + $fields2 = $entity2->getAttributes(); + + foreach ($fields as $field) { + if (isset($fields2[$field]) && $fields2[$field]) { + $entity1->$field = $entity2->$field; + } + } + + return $entity1; + } + + public static function withinPastYear($date) + { + if (!$date || $date == '0000-00-00') { + return false; + } + + $today = new DateTime('now'); + $datePaid = DateTime::createFromFormat('Y-m-d', $date); + $interval = $today->diff($datePaid); + + return $interval->y == 0; + } + + public static function addHttp($url) + { + if (!preg_match("~^(?:f|ht)tps?://~i", $url)) { + $url = "http://" . $url; + } + + return $url; + } } diff --git a/app/Libraries/lib_autolink.php b/app/Libraries/lib_autolink.php new file mode 100644 index 000000000000..3bc86b843e29 --- /dev/null +++ b/app/Libraries/lib_autolink.php @@ -0,0 +1,335 @@ + + # This code is licensed under the MIT license + # + + #################################################################### + + # + # These are global options. You can set them before calling the autolinking + # functions to change the output. + # + + $GLOBALS['autolink_options'] = array( + + # Should http:// be visibly stripped from the front + # of URLs? + 'strip_protocols' => false, + + ); + + #################################################################### + + function autolink($text, $limit=30, $tagfill='', $auto_title = true){ + + $text = autolink_do($text, '![a-z][a-z-]+://!i', $limit, $tagfill, $auto_title); + $text = autolink_do($text, '!(mailto|skype):!i', $limit, $tagfill, $auto_title); + $text = autolink_do($text, '!www\\.!i', $limit, $tagfill, $auto_title, 'http://'); + return $text; + } + + #################################################################### + + function autolink_do($text, $sub, $limit, $tagfill, $auto_title, $force_prefix=null){ + + $text_l = StrToLower($text); + $cursor = 0; + $loop = 1; + $buffer = ''; + + while (($cursor < strlen($text)) && $loop){ + + $ok = 1; + $matched = preg_match($sub, $text_l, $m, PREG_OFFSET_CAPTURE, $cursor); + + if (!$matched){ + + $loop = 0; + $ok = 0; + + }else{ + + $pos = $m[0][1]; + $sub_len = strlen($m[0][0]); + + $pre_hit = substr($text, $cursor, $pos-$cursor); + $hit = substr($text, $pos, $sub_len); + $pre = substr($text, 0, $pos); + $post = substr($text, $pos + $sub_len); + + $fail_text = $pre_hit.$hit; + $fail_len = strlen($fail_text); + + # + # substring found - first check to see if we're inside a link tag already... + # + + $bits = preg_split("!!i", $pre); + $last_bit = array_pop($bits); + if (preg_match("!\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # looks like a nice spot to autolink from - check the pre + # to see if there was whitespace before this match + # + + if ($ok){ + + if ($pre){ + if (!preg_match('![\s\(\[\{>]$!s', $pre)){ + + #echo "fail 2 at $cursor ($pre)
    \n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + } + + # + # we want to autolink here - find the extent of the url + # + + if ($ok){ + if (preg_match('/^([a-z0-9\-\.\/\-_%~!?=,:;&+*#@\(\)\$]+)/i', $post, $matches)){ + + $url = $hit.$matches[1]; + + $cursor += strlen($url) + strlen($pre_hit); + $buffer .= $pre_hit; + + $url = html_entity_decode($url); + + + # + # remove trailing punctuation from url + # + + while (preg_match('|[.,!;:?]$|', $url)){ + $url = substr($url, 0, strlen($url)-1); + $cursor--; + } + foreach (array('()', '[]', '{}') as $pair){ + $o = substr($pair, 0, 1); + $c = substr($pair, 1, 1); + if (preg_match("!^(\\$c|^)[^\\$o]+\\$c$!", $url)){ + $url = substr($url, 0, strlen($url)-1); + $cursor--; + } + } + + + # + # nice-i-fy url here + # + + $link_url = $url; + $display_url = $url; + + if ($force_prefix) $link_url = $force_prefix.$link_url; + + if ($GLOBALS['autolink_options']['strip_protocols']){ + if (preg_match('!^(http|https)://!i', $display_url, $m)){ + + $display_url = substr($display_url, strlen($m[1])+3); + } + } + + $display_url = autolink_label($display_url, $limit); + + + # + # add the url + # + + $currentTagfill = $tagfill; + if ($display_url != $link_url && !preg_match('@title=@msi',$currentTagfill) && $auto_title) { + + $display_quoted = preg_quote($display_url, '!'); + + if (!preg_match("!^(http|https)://{$display_quoted}$!i", $link_url)){ + + $currentTagfill .= ' title="'.$link_url.'"'; + } + } + + $link_url_enc = HtmlSpecialChars($link_url); + $display_url_enc = HtmlSpecialChars($display_url); + + $buffer .= "{$display_url_enc}"; + + }else{ + #echo "fail 3 at $cursor
    \n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + } + + # + # add everything from the cursor to the end onto the buffer. + # + + $buffer .= substr($text, $cursor); + + return $buffer; + } + + #################################################################### + + function autolink_label($text, $limit){ + + if (!$limit){ return $text; } + + if (strlen($text) > $limit){ + return substr($text, 0, $limit-3).'...'; + } + + return $text; + } + + #################################################################### + + function autolink_email($text, $tagfill=''){ + + $atom = '[^()<>@,;:\\\\".\\[\\]\\x00-\\x20\\x7f]+'; # from RFC822 + + #die($atom); + + $text_l = StrToLower($text); + $cursor = 0; + $loop = 1; + $buffer = ''; + + while(($cursor < strlen($text)) && $loop){ + + # + # find an '@' symbol + # + + $ok = 1; + $pos = strpos($text_l, '@', $cursor); + + if ($pos === false){ + + $loop = 0; + $ok = 0; + + }else{ + + $pre = substr($text, $cursor, $pos-$cursor); + $hit = substr($text, $pos, 1); + $post = substr($text, $pos + 1); + + $fail_text = $pre.$hit; + $fail_len = strlen($fail_text); + + #die("$pre::$hit::$post::$fail_text"); + + # + # substring found - first check to see if we're inside a link tag already... + # + + $bits = preg_split("!!i", $pre); + $last_bit = array_pop($bits); + if (preg_match("!\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # check backwards + # + + if ($ok){ + if (preg_match("!($atom(\.$atom)*)\$!", $pre, $matches)){ + + # move matched part of address into $hit + + $len = strlen($matches[1]); + $plen = strlen($pre); + + $hit = substr($pre, $plen-$len).$hit; + $pre = substr($pre, 0, $plen-$len); + + }else{ + + #echo "fail 2 at $cursor ($pre)
    \n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # check forwards + # + + if ($ok){ + if (preg_match("!^($atom(\.$atom)*)!", $post, $matches)){ + + # move matched part of address into $hit + + $len = strlen($matches[1]); + + $hit .= substr($post, 0, $len); + $post = substr($post, $len); + + }else{ + #echo "fail 3 at $cursor ($post)
    \n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # commit + # + + if ($ok) { + + $cursor += strlen($pre) + strlen($hit); + $buffer .= $pre; + $buffer .= "$hit"; + + } + + } + + # + # add everything from the cursor to the end onto the buffer. + # + + $buffer .= substr($text, $cursor); + + return $buffer; + } + + #################################################################### + +?> diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php new file mode 100644 index 000000000000..52c2e26f9027 --- /dev/null +++ b/app/Listeners/ActivityListener.php @@ -0,0 +1,335 @@ +activityRepo = $activityRepo; + } + + // Clients + public function createdClient(ClientWasCreated $event) + { + $this->activityRepo->create( + $event->client, + ACTIVITY_TYPE_CREATE_CLIENT + ); + } + + public function deletedClient(ClientWasDeleted $event) + { + $this->activityRepo->create( + $event->client, + ACTIVITY_TYPE_DELETE_CLIENT + ); + } + + public function archivedClient(ClientWasArchived $event) + { + if ($event->client->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->client, + ACTIVITY_TYPE_ARCHIVE_CLIENT + ); + } + + public function restoredClient(ClientWasRestored $event) + { + $this->activityRepo->create( + $event->client, + ACTIVITY_TYPE_RESTORE_CLIENT + ); + } + + // Invoices + public function createdInvoice(InvoiceWasCreated $event) + { + $this->activityRepo->create( + $event->invoice, + ACTIVITY_TYPE_CREATE_INVOICE, + $event->invoice->getAdjustment() + ); + } + + public function updatedInvoice(InvoiceWasUpdated $event) + { + if (! $event->invoice->isChanged()) { + return; + } + + $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($event->invoice->id); + + $activity = $this->activityRepo->create( + $event->invoice, + ACTIVITY_TYPE_UPDATE_INVOICE, + $event->invoice->getAdjustment() + ); + + $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON(); + $activity->save(); + } + + public function deletedInvoice(InvoiceWasDeleted $event) + { + $invoice = $event->invoice; + + $this->activityRepo->create( + $invoice, + ACTIVITY_TYPE_DELETE_INVOICE, + $invoice->affectsBalance() ? $invoice->balance * -1 : 0, + $invoice->affectsBalance() ? $invoice->getAmountPaid() * -1 : 0 + ); + } + + public function archivedInvoice(InvoiceWasArchived $event) + { + if ($event->invoice->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->invoice, + ACTIVITY_TYPE_ARCHIVE_INVOICE + ); + } + + public function restoredInvoice(InvoiceWasRestored $event) + { + $invoice = $event->invoice; + + $this->activityRepo->create( + $invoice, + ACTIVITY_TYPE_RESTORE_INVOICE, + $invoice->affectsBalance() && $event->fromDeleted ? $invoice->balance : 0, + $invoice->affectsBalance() && $event->fromDeleted ? $invoice->getAmountPaid() : 0 + ); + } + + public function emailedInvoice(InvoiceInvitationWasEmailed $event) + { + $this->activityRepo->create( + $event->invitation->invoice, + ACTIVITY_TYPE_EMAIL_INVOICE, + false, + false, + $event->invitation + ); + } + + public function viewedInvoice(InvoiceInvitationWasViewed $event) + { + $this->activityRepo->create( + $event->invoice, + ACTIVITY_TYPE_VIEW_INVOICE, + false, + false, + $event->invitation + ); + } + + // Quotes + public function createdQuote(QuoteWasCreated $event) + { + $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_CREATE_QUOTE + ); + } + + public function updatedQuote(QuoteWasUpdated $event) + { + if (! $event->quote->isChanged()) { + return; + } + + $backupQuote = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($event->quote->id); + + $activity = $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_UPDATE_QUOTE + ); + + $activity->json_backup = $backupQuote->hidePrivateFields()->toJSON(); + $activity->save(); + } + + public function deletedQuote(QuoteWasDeleted $event) + { + $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_DELETE_QUOTE + ); + } + + public function archivedQuote(QuoteWasArchived $event) + { + if ($event->quote->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_ARCHIVE_QUOTE + ); + } + + public function restoredQuote(QuoteWasRestored $event) + { + $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_RESTORE_QUOTE + ); + } + + public function emailedQuote(QuoteInvitationWasEmailed $event) + { + $this->activityRepo->create( + $event->invitation->invoice, + ACTIVITY_TYPE_EMAIL_QUOTE, + false, + false, + $event->invitation + ); + } + + public function viewedQuote(QuoteInvitationWasViewed $event) + { + $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_VIEW_QUOTE, + false, + false, + $event->invitation + ); + } + + public function approvedQuote(QuoteInvitationWasApproved $event) + { + $this->activityRepo->create( + $event->quote, + ACTIVITY_TYPE_APPROVE_QUOTE, + false, + false, + $event->invitation + ); + } + + // Credits + public function createdCredit(CreditWasCreated $event) + { + $this->activityRepo->create( + $event->credit, + ACTIVITY_TYPE_CREATE_CREDIT + ); + } + + public function deletedCredit(CreditWasDeleted $event) + { + $this->activityRepo->create( + $event->credit, + ACTIVITY_TYPE_DELETE_CREDIT + ); + } + + public function archivedCredit(CreditWasArchived $event) + { + if ($event->credit->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->credit, + ACTIVITY_TYPE_ARCHIVE_CREDIT + ); + } + + public function restoredCredit(CreditWasRestored $event) + { + $this->activityRepo->create( + $event->credit, + ACTIVITY_TYPE_RESTORE_CREDIT + ); + } + + // Payments + public function createdPayment(PaymentWasCreated $event) + { + $this->activityRepo->create( + $event->payment, + ACTIVITY_TYPE_CREATE_PAYMENT, + $event->payment->amount * -1, + $event->payment->amount + ); + } + + public function deletedPayment(PaymentWasDeleted $event) + { + $payment = $event->payment; + + $this->activityRepo->create( + $payment, + ACTIVITY_TYPE_DELETE_PAYMENT, + $payment->amount, + $payment->amount * -1 + ); + } + + public function archivedPayment(PaymentWasArchived $event) + { + if ($event->payment->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->payment, + ACTIVITY_TYPE_ARCHIVE_PAYMENT + ); + } + + public function restoredPayment(PaymentWasRestored $event) + { + $payment = $event->payment; + + $this->activityRepo->create( + $payment, + ACTIVITY_TYPE_RESTORE_PAYMENT, + $event->fromDeleted ? $payment->amount * -1 : 0, + $event->fromDeleted ? $payment->amount : 0 + ); + } +} diff --git a/app/Listeners/CreditListener.php b/app/Listeners/CreditListener.php new file mode 100644 index 000000000000..bed71a47f59d --- /dev/null +++ b/app/Listeners/CreditListener.php @@ -0,0 +1,33 @@ +creditRepo = $creditRepo; + } + + public function deletedPayment(PaymentWasDeleted $event) + { + $payment = $event->payment; + + // if the payment was from a credit we need to refund the credit + if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { + return; + } + + $credit = Credit::createNew(); + $credit->client_id = $payment->client_id; + $credit->credit_date = Carbon::now()->toDateTimeString(); + $credit->balance = $credit->amount = $payment->amount; + $credit->private_notes = $payment->transaction_reference; + $credit->save(); + } +} diff --git a/app/Listeners/ExpenseListener.php b/app/Listeners/ExpenseListener.php new file mode 100644 index 000000000000..c8b0e7db5966 --- /dev/null +++ b/app/Listeners/ExpenseListener.php @@ -0,0 +1,25 @@ +expenseRepo = $expenseRepo; + } + + public function deletedInvoice(InvoiceWasDeleted $event) + { + // Release any tasks associated with the deleted invoice + Expense::where('invoice_id', '=', $event->invoice->id) + ->update(['invoice_id' => null]); + } +} diff --git a/app/Listeners/HandleInvoicePaid.php b/app/Listeners/HandleInvoicePaid.php deleted file mode 100644 index d072abd00357..000000000000 --- a/app/Listeners/HandleInvoicePaid.php +++ /dev/null @@ -1,48 +0,0 @@ -userMailer = $userMailer; - $this->contactMailer = $contactMailer; - } - - /** - * Handle the event. - * - * @param InvoicePaid $event - * @return void - */ - public function handle(InvoicePaid $event) - { - $payment = $event->payment; - $invoice = $payment->invoice; - - $this->contactMailer->sendPaymentConfirmation($payment); - - foreach ($invoice->account->users as $user) - { - if ($user->{'notify_paid'}) - { - $this->userMailer->sendNotification($user, $invoice, 'paid', $payment); - } - } - } - -} diff --git a/app/Listeners/HandleInvoiceSent.php b/app/Listeners/HandleInvoiceSent.php deleted file mode 100644 index 119936e9500d..000000000000 --- a/app/Listeners/HandleInvoiceSent.php +++ /dev/null @@ -1,42 +0,0 @@ -userMailer = $userMailer; - } - - /** - * Handle the event. - * - * @param InvoiceSent $event - * @return void - */ - public function handle(InvoiceSent $event) - { - $invoice = $event->invoice; - - foreach ($invoice->account->users as $user) - { - if ($user->{'notify_sent'}) - { - $this->userMailer->sendNotification($user, $invoice, 'sent'); - } - } - } - -} diff --git a/app/Listeners/HandleInvoiceViewed.php b/app/Listeners/HandleInvoiceViewed.php deleted file mode 100644 index 47ee62a8585a..000000000000 --- a/app/Listeners/HandleInvoiceViewed.php +++ /dev/null @@ -1,42 +0,0 @@ -userMailer = $userMailer; - } - - /** - * Handle the event. - * - * @param InvoiceViewed $event - * @return void - */ - public function handle(InvoiceViewed $event) - { - $invoice = $event->invoice; - - foreach ($invoice->account->users as $user) - { - if ($user->{'notify_viewed'}) - { - $this->userMailer->sendNotification($user, $invoice, 'viewed'); - } - } - } - -} diff --git a/app/Listeners/HandleQuoteApproved.php b/app/Listeners/HandleQuoteApproved.php deleted file mode 100644 index 3a49aa9b5af5..000000000000 --- a/app/Listeners/HandleQuoteApproved.php +++ /dev/null @@ -1,42 +0,0 @@ -userMailer = $userMailer; - } - - /** - * Handle the event. - * - * @param QuoteApproved $event - * @return void - */ - public function handle(QuoteApproved $event) - { - $invoice = $event->invoice; - - foreach ($invoice->account->users as $user) - { - if ($user->{'notify_approved'}) - { - $this->userMailer->sendNotification($user, $invoice, 'approved'); - } - } - } - -} diff --git a/app/Listeners/HandleUserLoggedIn.php b/app/Listeners/HandleUserLoggedIn.php index 26f7cc455fc7..bf39d7e34e11 100644 --- a/app/Listeners/HandleUserLoggedIn.php +++ b/app/Listeners/HandleUserLoggedIn.php @@ -5,6 +5,7 @@ use Auth; use Carbon; use Session; use App\Events\UserLoggedIn; +use App\Events\UserSignedUp; use App\Ninja\Repositories\AccountRepository; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldBeQueued; @@ -33,8 +34,8 @@ class HandleUserLoggedIn { { $account = Auth::user()->account; - if (!Utils::isNinja() && Auth::user()->id == 1 && empty($account->last_login)) { - $this->accountRepo->registerUser(Auth::user()); + if (empty($account->last_login)) { + event(new UserSignedUp()); } $account->last_login = Carbon::now()->toDateTimeString(); @@ -44,6 +45,14 @@ class HandleUserLoggedIn { Session::put(SESSION_USER_ACCOUNTS, $users); $account->loadLocalizationSettings(); + + // if they're using Stripe make sure they're using Stripe.js + $accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE); + if ($accountGateway && ! $accountGateway->getPublishableStripeKey()) { + Session::flash('warning', trans('texts.missing_publishable_key')); + } elseif ($account->isLogoTooLarge()) { + Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize() . 'KB'])); + } } } diff --git a/app/Listeners/HandleUserSettingsChanged.php b/app/Listeners/HandleUserSettingsChanged.php index 993e30141db0..42598334990f 100644 --- a/app/Listeners/HandleUserSettingsChanged.php +++ b/app/Listeners/HandleUserSettingsChanged.php @@ -6,6 +6,7 @@ use App\Events\UserSettingsChanged; use App\Ninja\Repositories\AccountRepository; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldBeQueued; +use App\Ninja\Mailers\UserMailer; class HandleUserSettingsChanged { @@ -14,9 +15,10 @@ class HandleUserSettingsChanged { * * @return void */ - public function __construct(AccountRepository $accountRepo) + public function __construct(AccountRepository $accountRepo, UserMailer $userMailer) { $this->accountRepo = $accountRepo; + $this->userMailer = $userMailer; } /** @@ -27,12 +29,19 @@ class HandleUserSettingsChanged { */ public function handle(UserSettingsChanged $event) { - if (Auth::check()) { - $account = Auth::user()->account; - $account->loadLocalizationSettings(); + if (!Auth::check()) { + return; + } - $users = $this->accountRepo->loadAccounts(Auth::user()->id); - Session::put(SESSION_USER_ACCOUNTS, $users); + $account = Auth::user()->account; + $account->loadLocalizationSettings(); + + $users = $this->accountRepo->loadAccounts(Auth::user()->id); + Session::put(SESSION_USER_ACCOUNTS, $users); + + if ($event->user && $event->user->isEmailBeingChanged()) { + $this->userMailer->sendConfirmation($event->user); + Session::flash('warning', trans('texts.verify_email')); } } diff --git a/app/Listeners/HandleUserSignedUp.php b/app/Listeners/HandleUserSignedUp.php new file mode 100644 index 000000000000..08961e161752 --- /dev/null +++ b/app/Listeners/HandleUserSignedUp.php @@ -0,0 +1,46 @@ +accountRepo = $accountRepo; + $this->userMailer = $userMailer; + } + + /** + * Handle the event. + * + * @param UserSignedUp $event + * @return void + */ + public function handle(UserSignedUp $event) + { + $user = Auth::user(); + + if (Utils::isNinjaProd()) { + $this->userMailer->sendConfirmation($user); + } elseif (Utils::isNinjaDev()) { + // do nothing + } else { + $this->accountRepo->registerNinjaUser($user); + } + + session([SESSION_COUNTER => -1]); + } +} diff --git a/app/Listeners/InvoiceListener.php b/app/Listeners/InvoiceListener.php new file mode 100644 index 000000000000..9e40bbbe6d6b --- /dev/null +++ b/app/Listeners/InvoiceListener.php @@ -0,0 +1,79 @@ +invoice; + $account = Auth::user()->account; + + if ($invoice->invoice_design_id + && $account->invoice_design_id != $invoice->invoice_design_id) { + $account->invoice_design_id = $invoice->invoice_design_id; + $account->save(); + } + } + } + + public function updatedInvoice(InvoiceWasUpdated $event) + { + $invoice = $event->invoice; + $invoice->updatePaidStatus(false); + } + + public function viewedInvoice(InvoiceInvitationWasViewed $event) + { + $invitation = $event->invitation; + $invitation->markViewed(); + } + + public function createdPayment(PaymentWasCreated $event) + { + $payment = $event->payment; + $invoice = $payment->invoice; + $adjustment = $payment->amount * -1; + $partial = max(0, $invoice->partial - $payment->amount); + + $invoice->updateBalances($adjustment, $partial); + $invoice->updatePaidStatus(); + } + + public function deletedPayment(PaymentWasDeleted $event) + { + $payment = $event->payment; + $invoice = $payment->invoice; + $adjustment = $payment->amount; + + $invoice->updateBalances($adjustment); + $invoice->updatePaidStatus(); + } + + public function restoredPayment(PaymentWasRestored $event) + { + if ( ! $event->fromDeleted) { + return; + } + + $payment = $event->payment; + $invoice = $payment->invoice; + $adjustment = $payment->amount * -1; + + $invoice->updateBalances($adjustment); + $invoice->updatePaidStatus(); + } +} diff --git a/app/Listeners/NotificationListener.php b/app/Listeners/NotificationListener.php new file mode 100644 index 000000000000..aba304457528 --- /dev/null +++ b/app/Listeners/NotificationListener.php @@ -0,0 +1,71 @@ +userMailer = $userMailer; + $this->contactMailer = $contactMailer; + } + + private function sendEmails($invoice, $type, $payment = null) + { + foreach ($invoice->account->users as $user) + { + if ($user->{"notify_{$type}"}) + { + $this->userMailer->sendNotification($user, $invoice, $type, $payment); + } + } + } + + public function emailedInvoice(InvoiceWasEmailed $event) + { + $this->sendEmails($event->invoice, 'sent'); + } + + public function emailedQuote(QuoteWasEmailed $event) + { + $this->sendEmails($event->quote, 'sent'); + } + + public function viewedInvoice(InvoiceInvitationWasViewed $event) + { + $this->sendEmails($event->invoice, 'viewed'); + } + + public function viewedQuote(QuoteInvitationWasViewed $event) + { + $this->sendEmails($event->quote, 'viewed'); + } + + public function approvedQuote(QuoteInvitationWasApproved $event) + { + $this->sendEmails($event->quote, 'approved'); + } + + public function createdPayment(PaymentWasCreated $event) + { + // only send emails for online payments + if ( ! $event->payment->account_gateway_id) { + return; + } + + $this->contactMailer->sendPaymentConfirmation($event->payment); + $this->sendEmails($event->payment->invoice, 'paid', $event->payment); + } + +} \ No newline at end of file diff --git a/app/Listeners/QuoteListener.php b/app/Listeners/QuoteListener.php new file mode 100644 index 000000000000..5dfa0e45aeb3 --- /dev/null +++ b/app/Listeners/QuoteListener.php @@ -0,0 +1,15 @@ +invitation; + $invitation->markViewed(); + } +} diff --git a/app/Listeners/SubscriptionListener.php b/app/Listeners/SubscriptionListener.php new file mode 100644 index 000000000000..fab5a2c57493 --- /dev/null +++ b/app/Listeners/SubscriptionListener.php @@ -0,0 +1,61 @@ +checkSubscriptions(ACTIVITY_TYPE_CREATE_CLIENT, $event->client); + } + + public function createdQuote(QuoteWasCreated $event) + { + $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_QUOTE, $event->quote); + } + + public function createdPayment(PaymentWasCreated $event) + { + $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_PAYMENT, $event->payment); + } + + public function createdCredit(CreditWasCreated $event) + { + $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_CREDIT, $event->credit); + } + + public function createdInvoice(InvoiceWasCreated $event) + { + $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_INVOICE, $event->invoice); + } + + private function checkSubscriptions($activityTypeId, $entity) + { + $subscription = $entity->account->getSubscription($activityTypeId); + + if ($subscription) { + Utils::notifyZapier($subscription, $entity); + } + } + + public function createdVendor(VendorWasCreated $event) + { + $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_VENDOR, $event->vendor); + } + + public function createdExpense(ExpenseWasCreated $event) + { + $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_EXPENSE, $event->expense); + } + +} diff --git a/app/Listeners/TaskListener.php b/app/Listeners/TaskListener.php new file mode 100644 index 000000000000..b52c2fd5f31c --- /dev/null +++ b/app/Listeners/TaskListener.php @@ -0,0 +1,14 @@ +invoice->id) + ->update(['invoice_id' => null]); + } +} diff --git a/app/Models/Account.php b/app/Models/Account.php index fbd10efcd9b6..9af3350dd094 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -5,21 +5,56 @@ use Utils; use Session; use DateTime; use Event; +use Cache; use App; +use File; use App\Events\UserSettingsChanged; use Illuminate\Database\Eloquent\SoftDeletes; +use Laracasts\Presenter\PresentableTrait; class Account extends Eloquent { + use PresentableTrait; use SoftDeletes; + + protected $presenter = 'App\Ninja\Presenters\AccountPresenter'; protected $dates = ['deleted_at']; + protected $hidden = ['ip']; + + public static $basicSettings = [ + ACCOUNT_COMPANY_DETAILS, + ACCOUNT_USER_DETAILS, + ACCOUNT_LOCALIZATION, + ACCOUNT_PAYMENTS, + //ACCOUNT_BANKS, + ACCOUNT_TAX_RATES, + ACCOUNT_PRODUCTS, + ACCOUNT_NOTIFICATIONS, + ACCOUNT_IMPORT_EXPORT, + ]; + + public static $advancedSettings = [ + ACCOUNT_INVOICE_SETTINGS, + ACCOUNT_INVOICE_DESIGN, + ACCOUNT_EMAIL_SETTINGS, + ACCOUNT_TEMPLATES_AND_REMINDERS, + ACCOUNT_CLIENT_PORTAL, + ACCOUNT_CHARTS_AND_REPORTS, + ACCOUNT_DATA_VISUALIZATIONS, + ACCOUNT_USER_MANAGEMENT, + ACCOUNT_API_TOKENS, + ]; /* protected $casts = [ - 'hide_quantity' => 'boolean', + 'invoice_settings' => 'object', ]; */ - + public function account_tokens() + { + return $this->hasMany('App\Models\AccountToken'); + } + public function users() { return $this->hasMany('App\Models\User'); @@ -30,6 +65,11 @@ class Account extends Eloquent return $this->hasMany('App\Models\Client'); } + public function contacts() + { + return $this->hasMany('App\Models\Contact'); + } + public function invoices() { return $this->hasMany('App\Models\Invoice'); @@ -40,11 +80,21 @@ class Account extends Eloquent return $this->hasMany('App\Models\AccountGateway'); } + public function bank_accounts() + { + return $this->hasMany('App\Models\BankAccount'); + } + public function tax_rates() { return $this->hasMany('App\Models\TaxRate'); } + public function products() + { + return $this->hasMany('App\Models\Product'); + } + public function country() { return $this->belongsTo('App\Models\Country'); @@ -85,6 +135,11 @@ class Account extends Eloquent return $this->belongsTo('App\Models\Industry'); } + public function default_tax_rate() + { + return $this->belongsTo('App\Models\TaxRate'); + } + public function isGatewayConfigured($gatewayId = 0) { $this->load('account_gateways'); @@ -101,6 +156,15 @@ class Account extends Eloquent return !$this->language_id || $this->language_id == DEFAULT_LANGUAGE; } + public function hasInvoicePrefix() + { + if ( ! $this->invoice_number_prefix && ! $this->quote_number_prefix) { + return false; + } + + return $this->invoice_number_prefix != $this->quote_number_prefix; + } + public function getDisplayName() { if ($this->name) { @@ -113,6 +177,32 @@ class Account extends Eloquent return $user->getDisplayName(); } + public function getCityState() + { + $swap = $this->country && $this->country->swap_postal_code; + return Utils::cityStateZip($this->city, $this->state, $this->postal_code, $swap); + } + + public function getMomentDateTimeFormat() + { + $format = $this->datetime_format ? $this->datetime_format->format_moment : DEFAULT_DATETIME_MOMENT_FORMAT; + + if ($this->military_time) { + $format = str_replace('h:mm:ss a', 'H:mm:ss', $format); + } + + return $format; + } + + public function getMomentDateFormat() + { + $format = $this->getMomentDateTimeFormat(); + $format = str_replace('h:mm:ss a', '', $format); + $format = str_replace('H:mm:ss', '', $format); + + return trim($format); + } + public function getTimezone() { if ($this->timezone) { @@ -122,6 +212,99 @@ class Account extends Eloquent } } + public function getDateTime($date = 'now') + { + if ( ! $date) { + return null; + } elseif ( ! $date instanceof \DateTime) { + $date = new \DateTime($date); + } + + $date->setTimeZone(new \DateTimeZone($this->getTimezone())); + + return $date; + } + + public function getCustomDateFormat() + { + return $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT; + } + + public function formatMoney($amount, $client = null, $hideSymbol = false) + { + if ($client && $client->currency_id) { + $currencyId = $client->currency_id; + } elseif ($this->currency_id) { + $currencyId = $this->currency_id; + } else { + $currencyId = DEFAULT_CURRENCY; + } + + if ($client && $client->country_id) { + $countryId = $client->country_id; + } elseif ($this->country_id) { + $countryId = $this->country_id; + } else { + $countryId = false; + } + + return Utils::formatMoney($amount, $currencyId, $countryId, $hideSymbol); + } + + public function getCurrencyId() + { + return $this->currency_id ?: DEFAULT_CURRENCY; + } + + public function formatDate($date) + { + $date = $this->getDateTime($date); + + if ( ! $date) { + return null; + } + + return $date->format($this->getCustomDateFormat()); + } + + public function formatDateTime($date) + { + $date = $this->getDateTime($date); + + if ( ! $date) { + return null; + } + + return $date->format($this->getCustomDateTimeFormat()); + } + + public function formatTime($date) + { + $date = $this->getDateTime($date); + + if ( ! $date) { + return null; + } + + return $date->format($this->getCustomTimeFormat()); + } + + public function getCustomTimeFormat() + { + return $this->military_time ? 'H:i' : 'g:i a'; + } + + public function getCustomDateTimeFormat() + { + $format = $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT; + + if ($this->military_time) { + $format = str_replace('g:i a', 'H:i', $format); + } + + return $format; + } + public function getGatewayByType($type = PAYMENT_TYPE_ANY) { foreach ($this->account_gateways as $gateway) { @@ -146,12 +329,10 @@ class Account extends Eloquent return false; } - /* public function hasLogo() { - file_exists($this->getLogoPath()); + return file_exists($this->getLogoFullPath()); } - */ public function getLogoPath() { @@ -160,9 +341,32 @@ class Account extends Eloquent return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg'; } + public function getLogoFullPath() + { + $fileName = public_path() . '/logo/' . $this->account_key; + + return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg'; + } + + public function getLogoURL() + { + return SITE_URL . '/' . $this->getLogoPath(); + } + + public function getToken($name) + { + foreach ($this->account_tokens as $token) { + if ($token->name === $name) { + return $token->token; + } + } + + return null; + } + public function getLogoWidth() { - $path = $this->getLogoPath(); + $path = $this->getLogoFullPath(); if (!file_exists($path)) { return 0; } @@ -173,7 +377,7 @@ class Account extends Eloquent public function getLogoHeight() { - $path = $this->getLogoPath(); + $path = $this->getLogoFullPath(); if (!file_exists($path)) { return 0; } @@ -182,15 +386,131 @@ class Account extends Eloquent return $height; } - public function getNextInvoiceNumber($isQuote = false, $prefix = '') + public function createInvoice($entityType, $clientId = null) { - $counter = $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; - $prefix .= $isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix; + $invoice = Invoice::createNew(); + + $invoice->is_recurring = false; + $invoice->is_quote = false; + $invoice->invoice_date = Utils::today(); + $invoice->start_date = Utils::today(); + $invoice->invoice_design_id = $this->invoice_design_id; + $invoice->client_id = $clientId; + + if ($entityType === ENTITY_RECURRING_INVOICE) { + $invoice->invoice_number = microtime(true); + $invoice->is_recurring = true; + } else { + if ($entityType == ENTITY_QUOTE) { + $invoice->is_quote = true; + } + + if ($this->hasClientNumberPattern($invoice) && !$clientId) { + // do nothing, we don't yet know the value + } else { + $invoice->invoice_number = $this->getNextInvoiceNumber($invoice); + } + } + + if (!$clientId) { + $invoice->client = Client::createNew(); + $invoice->client->public_id = 0; + } + + return $invoice; + } + + public function hasNumberPattern($isQuote) + { + return $isQuote ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false); + } + + public function hasClientNumberPattern($invoice) + { + $pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; + + return strstr($pattern, '$custom'); + } + + public function getNumberPattern($invoice) + { + $pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; + + if (!$pattern) { + return false; + } + + $search = ['{$year}']; + $replace = [date('Y')]; + + $search[] = '{$counter}'; + $replace[] = str_pad($this->getCounter($invoice->is_quote), 4, '0', STR_PAD_LEFT); + + if (strstr($pattern, '{$userId}')) { + $search[] = '{$userId}'; + $replace[] = str_pad(($invoice->user->public_id + 1), 2, '0', STR_PAD_LEFT); + } + + $matches = false; + preg_match('/{\$date:(.*?)}/', $pattern, $matches); + if (count($matches) > 1) { + $format = $matches[1]; + $search[] = $matches[0]; + $replace[] = str_replace($format, date($format), $matches[1]); + } + + $pattern = str_replace($search, $replace, $pattern); + + if ($invoice->client_id) { + $pattern = $this->getClientInvoiceNumber($pattern, $invoice); + } + + return $pattern; + } + + private function getClientInvoiceNumber($pattern, $invoice) + { + if (!$invoice->client) { + return $pattern; + } + + $search = [ + '{$custom1}', + '{$custom2}', + ]; + + $replace = [ + $invoice->client->custom_value1, + $invoice->client->custom_value2, + ]; + + return str_replace($search, $replace, $pattern); + } + + public function getCounter($isQuote) + { + return $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; + } + + public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE) + { + $invoice = $this->createInvoice($entityType); + return $this->getNextInvoiceNumber($invoice); + } + + public function getNextInvoiceNumber($invoice) + { + if ($this->hasNumberPattern($invoice->is_quote)) { + return $this->getNumberPattern($invoice); + } + + $counter = $this->getCounter($invoice->is_quote); + $prefix = $invoice->is_quote ? $this->quote_number_prefix : $this->invoice_number_prefix; $counterOffset = 0; // confirm the invoice number isn't already taken do { - $number = $prefix.str_pad($counter, 4, "0", STR_PAD_LEFT); + $number = $prefix . str_pad($counter, 4, '0', STR_PAD_LEFT); $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); $counter++; $counterOffset++; @@ -198,7 +518,7 @@ class Account extends Eloquent // update the invoice counter to be caught up if ($counterOffset > 1) { - if ($isQuote && !$this->share_counter) { + if ($invoice->is_quote && !$this->share_counter) { $this->quote_number_counter += $counterOffset - 1; } else { $this->invoice_number_counter += $counterOffset - 1; @@ -210,36 +530,47 @@ class Account extends Eloquent return $number; } - public function incrementCounter($isQuote = false) + public function incrementCounter($invoice) { - if ($isQuote && !$this->share_counter) { + if ($invoice->is_quote && !$this->share_counter) { $this->quote_number_counter += 1; } else { - $this->invoice_number_counter += 1; - } + $default = $this->invoice_number_counter; + $actual = Utils::parseInt($invoice->invoice_number); + if ( ! $this->isPro() && $default != $actual) { + $this->invoice_number_counter = $actual + 1; + } else { + $this->invoice_number_counter += 1; + } + } + $this->save(); } - public function getLocale() - { - $language = Language::where('id', '=', $this->account->language_id)->first(); - - return $language->locale; - } - - public function loadLocalizationSettings() + public function loadLocalizationSettings($client = false) { $this->load('timezone', 'date_format', 'datetime_format', 'language'); - Session::put(SESSION_TIMEZONE, $this->timezone ? $this->timezone->name : DEFAULT_TIMEZONE); + $timezone = $this->timezone ? $this->timezone->name : DEFAULT_TIMEZONE; + Session::put(SESSION_TIMEZONE, $timezone); + Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT); Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT); - Session::put(SESSION_DATETIME_FORMAT, $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT); - Session::put(SESSION_CURRENCY, $this->currency_id ? $this->currency_id : DEFAULT_CURRENCY); - Session::put(SESSION_LOCALE, $this->language_id ? $this->language->locale : DEFAULT_LOCALE); - App::setLocale(session(SESSION_LOCALE)); + $currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY; + $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); + + Session::put(SESSION_CURRENCY, $currencyId); + Session::put(SESSION_LOCALE, $locale); + + App::setLocale($locale); + + $format = $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT; + if ($this->military_time) { + $format = str_replace('g:i a', 'H:i', $format); + } + Session::put(SESSION_DATETIME_FORMAT, $format); } public function getInvoiceLabels() @@ -273,7 +604,7 @@ class Account extends Eloquent 'quote_number', 'total', 'invoice_issued_to', - 'date', + //'date', 'rate', 'hours', 'balance', @@ -282,6 +613,7 @@ class Account extends Eloquent 'invoice_to', 'details', 'invoice_no', + 'valid_until', ]; foreach ($fields as $field) { @@ -299,33 +631,36 @@ class Account extends Eloquent return $data; } + public function isNinjaAccount() + { + return $this->account_key === NINJA_ACCOUNT_KEY; + } + public function isPro() { if (!Utils::isNinjaProd()) { return true; } - if ($this->account_key == NINJA_ACCOUNT_KEY) { + if ($this->isNinjaAccount()) { return true; } $datePaid = $this->pro_plan_paid; - if (!$datePaid || $datePaid == '0000-00-00') { - return false; - } elseif ($datePaid == NINJA_DATE) { + if ($datePaid == NINJA_DATE) { return true; } - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $datePaid); - $interval = $today->diff($datePaid); - - return $interval->y == 0; + return Utils::withinPastYear($datePaid); } public function isWhiteLabel() { + if ($this->isNinjaAccount()) { + return false; + } + if (Utils::isNinjaProd()) { return self::isPro() && $this->pro_plan_paid != NINJA_DATE; } else { @@ -333,6 +668,21 @@ class Account extends Eloquent } } + public function getLogoSize() + { + if (!$this->hasLogo()) { + return 0; + } + + $filename = $this->getLogoFullPath(); + return round(File::size($filename) / 1000); + } + + public function isLogoTooLarge() + { + return $this->getLogoSize() > MAX_LOGO_FILE_SIZE; + } + public function getSubscription($eventId) { return Subscription::where('account_id', '=', $this->id)->where('event_id', '=', $eventId)->first(); @@ -384,20 +734,43 @@ class Account extends Eloquent return $this; } - public function getEmailTemplate($entityType, $message = false) + public function getDefaultEmailSubject($entityType) { - $field = "email_template_$entityType"; - $template = $this->$field; - - if ($template) { - return $template; + if (strpos($entityType, 'reminder') !== false) { + $entityType = 'reminder'; } - $template = "\$client,

    \r\n\r\n" . - trans("texts.{$entityType}_message", ['amount' => '$amount']) . "

    \r\n\r\n"; + return trans("texts.{$entityType}_subject", ['invoice' => '$invoice', 'account' => '$account']); + } - if ($entityType != ENTITY_PAYMENT) { - $template .= "\$link

    \r\n\r\n"; + public function getEmailSubject($entityType) + { + if ($this->isPro()) { + $field = "email_subject_{$entityType}"; + $value = $this->$field; + + if ($value) { + return $value; + } + } + + return $this->getDefaultEmailSubject($entityType); + } + + public function getDefaultEmailTemplate($entityType, $message = false) + { + if (strpos($entityType, 'reminder') !== false) { + $entityType = ENTITY_INVOICE; + } + + $template = "

    \$client,

    "; + + if ($this->isPro() && $this->email_design_id != EMAIL_DESIGN_PLAIN) { + $template .= "
    " . trans("texts.{$entityType}_message_button", ['amount' => '$amount']) . "

    " . + "
    \$viewButton

    "; + } else { + $template .= "
    " . trans("texts.{$entityType}_message", ['amount' => '$amount']) . "

    " . + "
    \$viewLink

    "; } if ($message) { @@ -407,16 +780,59 @@ class Account extends Eloquent return $template . "\$footer"; } + public function getEmailTemplate($entityType, $message = false) + { + $template = false; + + if ($this->isPro()) { + $field = "email_template_{$entityType}"; + $template = $this->$field; + } + + if (!$template) { + $template = $this->getDefaultEmailTemplate($entityType, $message); + } + + //
    is causing page breaks with the email designs + return str_replace('/>', ' />', $template); + } + public function getEmailFooter() { if ($this->email_footer) { // Add line breaks if HTML isn't already being used - return strip_tags($this->email_footer) == $this->email_footer ? nl2br($this->email_footer) : $this->email_footer; + return strip_tags($this->email_footer) == $this->email_footer ? nl2br($this->email_footer) : $this->email_footer; } else { - return "

    " . trans('texts.email_signature') . "
    \$account

    "; + return "

    " . trans('texts.email_signature') . "\n
    \$account"; } } + public function getReminderDate($reminder) + { + if ( ! $this->{"enable_reminder{$reminder}"}) { + return false; + } + + $numDays = $this->{"num_days_reminder{$reminder}"}; + $plusMinus = $this->{"direction_reminder{$reminder}"} == REMINDER_DIRECTION_AFTER ? '-' : '+'; + + return date('Y-m-d', strtotime("$plusMinus $numDays days")); + } + + public function getInvoiceReminder($invoice) + { + for ($i=1; $i<=3; $i++) { + if ($date = $this->getReminderDate($i)) { + $field = $this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date'; + if ($invoice->$field == $date) { + return "reminder{$i}"; + } + } + } + + return false; + } + public function showTokenCheckbox() { if (!$this->isGatewayConfigured(GATEWAY_STRIPE)) { @@ -431,6 +847,154 @@ class Account extends Eloquent { return $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT; } + + public function getSiteUrl() + { + $url = SITE_URL; + $iframe_url = $this->iframe_url; + + if ($iframe_url) { + return "{$iframe_url}/?"; + } else if ($this->subdomain) { + $url = Utils::replaceSubdomain($url, $this->subdomain); + } + + return $url; + } + + public function checkSubdomain($host) + { + if (!$this->subdomain) { + return true; + } + + $server = explode('.', $host); + $subdomain = $server[0]; + + if (!in_array($subdomain, ['app', 'www']) && $subdomain != $this->subdomain) { + return false; + } + + return true; + } + + public function showCustomField($field, $entity) + { + if ($this->isPro()) { + return $this->$field ? true : false; + } + + if (!$entity) { + return false; + } + + // convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1' + $field = str_replace(['invoice_', 'label'], ['', 'value'], $field); + + return Utils::isEmpty($entity->$field) ? false : true; + } + + public function attatchPDF() + { + return $this->isPro() && $this->pdf_email_attachment; + } + + public function clientViewCSS(){ + $css = null; + + if ($this->isPro()) { + $bodyFont = $this->getBodyFontCss(); + $headerFont = $this->getHeaderFontCss(); + + $css = 'body{'.$bodyFont.'}'; + if ($headerFont != $bodyFont) { + $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}'; + } + + if ((Utils::isNinja() && $this->isPro()) || $this->isWhiteLabel()) { + // For self-hosted users, a white-label license is required for custom CSS + $css .= $this->client_view_css; + } + } + + return $css; + } + + public function hasLargeFont() + { + return stripos($this->getBodyFontName(), 'chinese') || stripos($this->getHeaderFontName(), 'chinese'); + } + + public function getFontsUrl($protocol = ''){ + $bodyFont = $this->getHeaderFontId(); + $headerFont = $this->getBodyFontId(); + + $bodyFontSettings = Utils::getFromCache($bodyFont, 'fonts'); + $google_fonts = array($bodyFontSettings['google_font']); + + if($headerFont != $bodyFont){ + $headerFontSettings = Utils::getFromCache($headerFont, 'fonts'); + $google_fonts[] = $headerFontSettings['google_font']; + } + + return ($protocol?$protocol.':':'').'//fonts.googleapis.com/css?family='.implode('|',$google_fonts); + } + + public function getHeaderFontId() { + return ($this->isPro() && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; + } + + public function getBodyFontId() { + return ($this->isPro() && $this->body_font_id) ? $this->body_font_id : DEFAULT_BODY_FONT; + } + + public function getHeaderFontName(){ + return Utils::getFromCache($this->getHeaderFontId(), 'fonts')['name']; + } + + public function getBodyFontName(){ + return Utils::getFromCache($this->getBodyFontId(), 'fonts')['name']; + } + + public function getHeaderFontCss($include_weight = true){ + $font_data = Utils::getFromCache($this->getHeaderFontId(), 'fonts'); + $css = 'font-family:'.$font_data['css_stack'].';'; + + if($include_weight){ + $css .= 'font-weight:'.$font_data['css_weight'].';'; + } + + return $css; + } + + public function getBodyFontCss($include_weight = true){ + $font_data = Utils::getFromCache($this->getBodyFontId(), 'fonts'); + $css = 'font-family:'.$font_data['css_stack'].';'; + + if($include_weight){ + $css .= 'font-weight:'.$font_data['css_weight'].';'; + } + + return $css; + } + + public function getFonts(){ + return array_unique(array($this->getHeaderFontId(), $this->getBodyFontId())); + } + + public function getFontsData(){ + $data = array(); + + foreach($this->getFonts() as $font){ + $data[] = Utils::getFromCache($font, 'fonts'); + } + + return $data; + } + + public function getFontFolders(){ + return array_map(function($item){return $item['folder'];}, $this->getFontsData()); + } } Account::updated(function ($account) { diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 16d72076c394..a4027db6ef36 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -1,5 +1,6 @@ belongsTo('App\Models\Gateway'); @@ -27,16 +33,49 @@ class AccountGateway extends EntityModel return $arrayOfImages; } - public function getPaymentType() { + public function getPaymentType() + { return Gateway::getPaymentType($this->gateway_id); } - public function isPaymentType($type) { + public function isPaymentType($type) + { return $this->getPaymentType() == $type; } - public function isGateway($gatewayId) { + public function isGateway($gatewayId) + { return $this->gateway_id == $gatewayId; } + + public function setConfig($config) + { + $this->config = Crypt::encrypt(json_encode($config)); + } + + public function getConfig() + { + return json_decode(Crypt::decrypt($this->config)); + } + + public function getConfigField($field) + { + $config = $this->getConfig(); + + if (!$field || !property_exists($config, $field)) { + return false; + } + + return $config->$field; + } + + public function getPublishableStripeKey() + { + if ( ! $this->isGateway(GATEWAY_STRIPE)) { + return false; + } + + return $this->getConfigField('publishableKey'); + } } diff --git a/app/Models/AccountToken.php b/app/Models/AccountToken.php index 909cfbfe1195..dd9a98800535 100644 --- a/app/Models/AccountToken.php +++ b/app/Models/AccountToken.php @@ -7,6 +7,11 @@ class AccountToken extends EntityModel use SoftDeletes; protected $dates = ['deleted_at']; + public function getEntityType() + { + return ENTITY_TOKEN; + } + public function account() { return $this->belongsTo('App\Models\Account'); diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 0dd5ec54bf37..921c037d5f69 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -23,471 +23,56 @@ class Activity extends Eloquent public function user() { - return $this->belongsTo('App\Models\User'); + return $this->belongsTo('App\Models\User')->withTrashed(); } - private static function getBlank($entity = false) + public function contact() { - $activity = new Activity(); - - if ($entity) { - $activity->user_id = $entity instanceof User ? $entity->id : $entity->user_id; - $activity->account_id = $entity->account_id; - } elseif (Auth::check()) { - $activity->user_id = Auth::user()->id; - $activity->account_id = Auth::user()->account_id; - } else { - Utils::fatalError(); - } - - $activity->token_id = Session::get('token_id', null); - $activity->ip = Request::getClientIp(); - - return $activity; + return $this->belongsTo('App\Models\Contact')->withTrashed(); } - public static function createClient($client, $notify = true) + public function client() { - $activity = Activity::getBlank(); - $activity->client_id = $client->id; - $activity->activity_type_id = ACTIVITY_TYPE_CREATE_CLIENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'created', $client); - $activity->save(); - - if ($notify) { - Activity::checkSubscriptions(EVENT_CREATE_CLIENT, $client); - } + return $this->belongsTo('App\Models\Client')->withTrashed(); } - public static function updateClient($client) + public function invoice() { - if ($client->is_deleted && !$client->getOriginal('is_deleted')) { - $activity = Activity::getBlank(); - $activity->client_id = $client->id; - $activity->activity_type_id = ACTIVITY_TYPE_DELETE_CLIENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'deleted', $client); - $activity->balance = $client->balance; - $activity->save(); - } + return $this->belongsTo('App\Models\Invoice')->withTrashed(); } - public static function archiveClient($client) + public function credit() { - if (!$client->is_deleted) { - $activity = Activity::getBlank(); - $activity->client_id = $client->id; - $activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_CLIENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'archived', $client); - $activity->balance = $client->balance; - $activity->save(); - } + return $this->belongsTo('App\Models\Credit')->withTrashed(); } - public static function restoreClient($client) + public function payment() { - $activity = Activity::getBlank(); - $activity->client_id = $client->id; - $activity->activity_type_id = ACTIVITY_TYPE_RESTORE_CLIENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'restored', $client); - $activity->balance = $client->balance; - $activity->save(); + return $this->belongsTo('App\Models\Payment')->withTrashed(); } - public static function createInvoice($invoice) + public function getMessage() { - if (Auth::check()) { - $message = Utils::encodeActivity(Auth::user(), 'created', $invoice); - } else { - $message = Utils::encodeActivity(null, 'created', $invoice); - } + $activityTypeId = $this->activity_type_id; + $account = $this->account; + $client = $this->client; + $user = $this->user; + $invoice = $this->invoice; + $contactId = $this->contact_id; + $payment = $this->payment; + $credit = $this->credit; + $isSystem = $this->is_system; - $adjustment = 0; - $client = $invoice->client; - if (!$invoice->is_quote && !$invoice->is_recurring) { - $adjustment = $invoice->amount; - $client->balance = $client->balance + $adjustment; - $client->save(); - } + $data = [ + 'client' => link_to($client->getRoute(), $client->getDisplayName()), + 'user' => $isSystem ? '' . trans('texts.system') . '' : $user->getDisplayName(), + 'invoice' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null, + 'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null, + 'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(), + 'payment' => $payment ? $payment->transaction_reference : null, + 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null, + ]; - $activity = Activity::getBlank($invoice); - $activity->invoice_id = $invoice->id; - $activity->client_id = $invoice->client_id; - $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_CREATE_QUOTE : ACTIVITY_TYPE_CREATE_INVOICE; - $activity->message = $message; - $activity->balance = $client->balance; - $activity->adjustment = $adjustment; - $activity->save(); - - Activity::checkSubscriptions($invoice->is_quote ? EVENT_CREATE_QUOTE : EVENT_CREATE_INVOICE, $invoice); - } - - public static function archiveInvoice($invoice) - { - if (!$invoice->is_deleted) { - $activity = Activity::getBlank(); - $activity->invoice_id = $invoice->id; - $activity->client_id = $invoice->client_id; - $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_ARCHIVE_QUOTE : ACTIVITY_TYPE_ARCHIVE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'archived', $invoice); - $activity->balance = $invoice->client->balance; - - $activity->save(); - } - } - - public static function restoreInvoice($invoice) - { - $activity = Activity::getBlank(); - $activity->invoice_id = $invoice->id; - $activity->client_id = $invoice->client_id; - $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_RESTORE_QUOTE : ACTIVITY_TYPE_RESTORE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'restored', $invoice); - $activity->balance = $invoice->client->balance; - - $activity->save(); - } - - public static function emailInvoice($invitation) - { - $adjustment = 0; - $client = $invitation->invoice->client; - - $activity = Activity::getBlank($invitation); - $activity->client_id = $invitation->invoice->client_id; - $activity->invoice_id = $invitation->invoice_id; - $activity->contact_id = $invitation->contact_id; - $activity->activity_type_id = $invitation->invoice ? ACTIVITY_TYPE_EMAIL_QUOTE : ACTIVITY_TYPE_EMAIL_INVOICE; - $activity->message = Utils::encodeActivity(Auth::check() ? Auth::user() : null, 'emailed', $invitation->invoice, $invitation->contact); - $activity->balance = $client->balance; - $activity->save(); - } - - public static function updateInvoice($invoice) - { - $client = $invoice->client; - - if ($invoice->is_deleted && !$invoice->getOriginal('is_deleted')) { - $adjustment = 0; - if (!$invoice->is_quote && !$invoice->is_recurring) { - $adjustment = $invoice->balance * -1; - $client->balance = $client->balance - $invoice->balance; - $client->paid_to_date = $client->paid_to_date - ($invoice->amount - $invoice->balance); - $client->save(); - } - - $activity = Activity::getBlank(); - $activity->client_id = $invoice->client_id; - $activity->invoice_id = $invoice->id; - $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_DELETE_QUOTE : ACTIVITY_TYPE_DELETE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'deleted', $invoice); - $activity->balance = $invoice->client->balance; - $activity->adjustment = $adjustment; - $activity->save(); - } else { - $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); - - $fieldChanged = false; - foreach (['invoice_number', 'po_number', 'invoice_date', 'due_date', 'terms', 'public_notes', 'invoice_footer', 'partial'] as $field) { - if ($invoice->$field != $invoice->getOriginal($field)) { - $fieldChanged = true; - break; - } - } - - if ($diff != 0 || $fieldChanged) { - $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($invoice->id); - - if ($diff != 0 && !$invoice->is_quote && !$invoice->is_recurring) { - $client->balance = $client->balance + $diff; - $client->save(); - } - - $activity = Activity::getBlank($invoice); - $activity->client_id = $invoice->client_id; - $activity->invoice_id = $invoice->id; - $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice); - $activity->balance = $client->balance; - $activity->adjustment = $invoice->is_quote || $invoice->is_recurring ? 0 : $diff; - $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON(); - $activity->save(); - - if ($invoice->isPaid() && $invoice->balance > 0) { - $invoice->invoice_status_id = INVOICE_STATUS_PARTIAL; - } elseif ($invoice->invoice_status_id && $invoice->balance == 0) { - $invoice->invoice_status_id = INVOICE_STATUS_PAID; - } - } - } - } - - public static function viewInvoice($invitation) - { - if (Session::get($invitation->invitation_key)) { - return; - } - - Session::put($invitation->invitation_key, true); - $invoice = $invitation->invoice; - - if (!$invoice->isViewed()) { - $invoice->invoice_status_id = INVOICE_STATUS_VIEWED; - $invoice->save(); - } - - $now = Carbon::now()->toDateTimeString(); - - $invitation->viewed_date = $now; - $invitation->save(); - - $client = $invoice->client; - $client->last_login = $now; - $client->save(); - - $activity = Activity::getBlank($invitation); - $activity->client_id = $invitation->invoice->client_id; - $activity->invitation_id = $invitation->id; - $activity->contact_id = $invitation->contact_id; - $activity->invoice_id = $invitation->invoice_id; - $activity->activity_type_id = $invitation->invoice->is_quote ? ACTIVITY_TYPE_VIEW_QUOTE : ACTIVITY_TYPE_VIEW_INVOICE; - $activity->message = Utils::encodeActivity($invitation->contact, 'viewed', $invitation->invoice); - $activity->balance = $invitation->invoice->client->balance; - $activity->save(); - } - - public static function approveQuote($invitation) { - - $activity = Activity::getBlank($invitation); - $activity->client_id = $invitation->invoice->client_id; - $activity->invitation_id = $invitation->id; - $activity->contact_id = $invitation->contact_id; - $activity->invoice_id = $invitation->invoice_id; - $activity->activity_type_id = ACTIVITY_TYPE_APPROVE_QUOTE; - $activity->message = Utils::encodeActivity($invitation->contact, 'approved', $invitation->invoice); - $activity->balance = $invitation->invoice->client->balance; - $activity->save(); - } - - public static function createPayment($payment) - { - $client = $payment->client; - $client->balance = $client->balance - $payment->amount; - $client->paid_to_date = $client->paid_to_date + $payment->amount; - $client->save(); - - if ($payment->contact_id) { - $activity = Activity::getBlank($client); - $activity->contact_id = $payment->contact_id; - $activity->message = Utils::encodeActivity($payment->invitation->contact, 'entered '.$payment->getName().' for ', $payment->invoice); - } else { - $activity = Activity::getBlank($client); - $message = $payment->payment_type_id == PAYMENT_TYPE_CREDIT ? 'applied credit for ' : 'entered '.$payment->getName().' for '; - $activity->message = Utils::encodeActivity(Auth::user(), $message, $payment->invoice); - } - - $activity->payment_id = $payment->id; - - if ($payment->invoice_id) { - $activity->invoice_id = $payment->invoice_id; - - $invoice = $payment->invoice; - $invoice->balance = $invoice->balance - $payment->amount; - $invoice->invoice_status_id = ($invoice->balance > 0) ? INVOICE_STATUS_PARTIAL : INVOICE_STATUS_PAID; - if ($invoice->partial > 0) { - $invoice->partial = max(0, $invoice->partial - $payment->amount); - } - $invoice->save(); - } - - $activity->payment_id = $payment->id; - $activity->client_id = $payment->client_id; - $activity->activity_type_id = ACTIVITY_TYPE_CREATE_PAYMENT; - $activity->balance = $client->balance; - $activity->adjustment = $payment->amount * -1; - $activity->save(); - - Activity::checkSubscriptions(EVENT_CREATE_PAYMENT, $payment); - } - - public static function updatePayment($payment) - { - if ($payment->is_deleted && !$payment->getOriginal('is_deleted')) { - $client = $payment->client; - $client->balance = $client->balance + $payment->amount; - $client->paid_to_date = $client->paid_to_date - $payment->amount; - $client->save(); - - $invoice = $payment->invoice; - $invoice->balance = $invoice->balance + $payment->amount; - if ($invoice->isPaid() && $invoice->balance > 0) { - $invoice->invoice_status_id = ($invoice->balance == $invoice->amount ? INVOICE_STATUS_DRAFT : INVOICE_STATUS_PARTIAL); - } - $invoice->save(); - - // deleting a payment from credit creates a new credit - if ($payment->payment_type_id == PAYMENT_TYPE_CREDIT) { - $credit = Credit::createNew(); - $credit->client_id = $client->id; - $credit->credit_date = Carbon::now()->toDateTimeString(); - $credit->balance = $credit->amount = $payment->amount; - $credit->private_notes = $payment->transaction_reference; - $credit->save(); - } - - $activity = Activity::getBlank(); - $activity->payment_id = $payment->id; - $activity->client_id = $invoice->client_id; - $activity->invoice_id = $invoice->id; - $activity->activity_type_id = ACTIVITY_TYPE_DELETE_PAYMENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'deleted '.$payment->getName()); - $activity->balance = $client->balance; - $activity->adjustment = $payment->amount; - $activity->save(); - } else { - /* - $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); - - if ($diff == 0) - { - return; - } - - $client = $invoice->client; - $client->balance = $client->balance + $diff; - $client->save(); - - $activity = Activity::getBlank($invoice); - $activity->client_id = $invoice->client_id; - $activity->invoice_id = $invoice->id; - $activity->activity_type_id = ACTIVITY_TYPE_UPDATE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice); - $activity->balance = $client->balance; - $activity->adjustment = $diff; - $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON(); - $activity->save(); - */ - } - } - - public static function archivePayment($payment) - { - if ($payment->is_deleted) { - return; - } - - $client = $payment->client; - $invoice = $payment->invoice; - - $activity = Activity::getBlank(); - $activity->payment_id = $payment->id; - $activity->invoice_id = $invoice->id; - $activity->client_id = $client->id; - $activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_PAYMENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'archived '.$payment->getName()); - $activity->balance = $client->balance; - $activity->adjustment = 0; - $activity->save(); - } - - public static function restorePayment($payment) - { - $client = $payment->client; - $invoice = $payment->invoice; - - $activity = Activity::getBlank(); - $activity->payment_id = $payment->id; - $activity->invoice_id = $invoice->id; - $activity->client_id = $client->id; - $activity->activity_type_id = ACTIVITY_TYPE_RESTORE_PAYMENT; - $activity->message = Utils::encodeActivity(Auth::user(), 'restored '.$payment->getName()); - $activity->balance = $client->balance; - $activity->adjustment = 0; - $activity->save(); - } - - public static function createCredit($credit) - { - $activity = Activity::getBlank(); - $activity->message = Utils::encodeActivity(Auth::user(), 'entered '.Utils::formatMoney($credit->amount, $credit->client->getCurrencyId()).' credit'); - $activity->credit_id = $credit->id; - $activity->client_id = $credit->client_id; - $activity->activity_type_id = ACTIVITY_TYPE_CREATE_CREDIT; - $activity->balance = $credit->client->balance; - $activity->save(); - } - - public static function updateCredit($credit) - { - if ($credit->is_deleted && !$credit->getOriginal('is_deleted')) { - $activity = Activity::getBlank(); - $activity->credit_id = $credit->id; - $activity->client_id = $credit->client_id; - $activity->activity_type_id = ACTIVITY_TYPE_DELETE_CREDIT; - $activity->message = Utils::encodeActivity(Auth::user(), 'deleted '.Utils::formatMoney($credit->balance, $credit->client->getCurrencyId()).' credit'); - $activity->balance = $credit->client->balance; - $activity->save(); - } else { - /* - $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); - - if ($diff == 0) - { - return; - } - - $client = $invoice->client; - $client->balance = $client->balance + $diff; - $client->save(); - - $activity = Activity::getBlank($invoice); - $activity->client_id = $invoice->client_id; - $activity->invoice_id = $invoice->id; - $activity->activity_type_id = ACTIVITY_TYPE_UPDATE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice); - $activity->balance = $client->balance; - $activity->adjustment = $diff; - $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON(); - $activity->save(); - */ - } - } - - public static function archiveCredit($credit) - { - if ($credit->is_deleted) { - return; - } - - $activity = Activity::getBlank(); - $activity->client_id = $credit->client_id; - $activity->credit_id = $credit->id; - $activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_CREDIT; - $activity->message = Utils::encodeActivity(Auth::user(), 'archived '.Utils::formatMoney($credit->balance, $credit->client->getCurrencyId()).' credit'); - $activity->balance = $credit->client->balance; - $activity->save(); - } - - public static function restoreCredit($credit) - { - $activity = Activity::getBlank(); - $activity->client_id = $credit->client_id; - $activity->credit_id = $credit->id; - $activity->activity_type_id = ACTIVITY_TYPE_RESTORE_CREDIT; - $activity->message = Utils::encodeActivity(Auth::user(), 'restored '.Utils::formatMoney($credit->balance, $credit->client->getCurrencyId()).' credit'); - $activity->balance = $credit->client->balance; - $activity->save(); - } - - private static function checkSubscriptions($event, $data) - { - if (!Auth::check()) { - return; - } - - $subscription = Auth::user()->account->getSubscription($event); - - if ($subscription) { - Utils::notifyZapier($subscription, $data); - } + return trans("texts.activity_{$activityTypeId}", $data); } } diff --git a/app/Models/BalanceAffecting.php b/app/Models/BalanceAffecting.php new file mode 100644 index 000000000000..0ba99f73a284 --- /dev/null +++ b/app/Models/BalanceAffecting.php @@ -0,0 +1,6 @@ +config); + + return new \App\Libraries\Bank($finance, $config->fid, $config->url, $config->org); + } +} diff --git a/app/Models/BankAccount.php b/app/Models/BankAccount.php new file mode 100644 index 000000000000..01ae612dc839 --- /dev/null +++ b/app/Models/BankAccount.php @@ -0,0 +1,23 @@ +belongsTo('App\Models\Bank'); + } + +} + diff --git a/app/Models/Client.php b/app/Models/Client.php index 554f74556910..6655a9ef2e67 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -1,29 +1,101 @@ 'first_name', + 'last' => 'last_name', + 'email' => 'email', + 'mobile|phone' => 'phone', + 'name|organization' => 'name', + 'street2|address2' => 'address2', + 'street|address|address1' => 'address1', + 'city' => 'city', + 'state|province' => 'state', + 'zip|postal|code' => 'postal_code', + 'country' => 'country', + 'note' => 'notes', + ]; + } public function account() { return $this->belongsTo('App\Models\Account'); } + public function user() + { + return $this->belongsTo('App\Models\User')->withTrashed(); + } + public function invoices() { return $this->hasMany('App\Models\Invoice'); @@ -39,11 +111,6 @@ class Client extends EntityModel return $this->hasMany('App\Models\Contact'); } - public function projects() - { - return $this->hasMany('App\Models\Project'); - } - public function country() { return $this->belongsTo('App\Models\Country'); @@ -54,6 +121,11 @@ class Client extends EntityModel return $this->belongsTo('App\Models\Currency'); } + public function language() + { + return $this->belongsTo('App\Models\Language'); + } + public function size() { return $this->belongsTo('App\Models\Size'); @@ -64,6 +136,40 @@ class Client extends EntityModel return $this->belongsTo('App\Models\Industry'); } + public function addContact($data, $isPrimary = false) + { + $publicId = isset($data['public_id']) ? $data['public_id'] : false; + + if ($publicId && $publicId != '-1') { + $contact = Contact::scope($publicId)->firstOrFail(); + } else { + $contact = Contact::createNew(); + $contact->send_invoice = true; + } + + $contact->fill($data); + $contact->is_primary = $isPrimary; + + return $this->contacts()->save($contact); + } + + public function updateBalances($balanceAdjustment, $paidToDateAdjustment) + { + if ($balanceAdjustment === 0 && $paidToDateAdjustment === 0) { + return; + } + + $this->balance = $this->balance + $balanceAdjustment; + $this->paid_to_date = $this->paid_to_date + $paidToDateAdjustment; + + $this->save(); + } + + public function getRoute() + { + return "/clients/{$this->public_id}"; + } + public function getTotalCredit() { return DB::table('credits') @@ -83,33 +189,43 @@ class Client extends EntityModel return $this->name; } - $contact = $this->contacts()->first(); + if ( ! count($this->contacts)) { + return ''; + } + $contact = $this->contacts[0]; return $contact->getDisplayName(); } + public function getCityState() + { + $swap = $this->country && $this->country->swap_postal_code; + return Utils::cityStateZip($this->city, $this->state, $this->postal_code, $swap); + } + public function getEntityType() { return ENTITY_CLIENT; } - public function getWebsite() + public function hasAddress() { - if (!$this->website) { - return ''; + $fields = [ + 'address1', + 'address2', + 'city', + 'state', + 'postal_code', + 'country_id', + ]; + + foreach ($fields as $field) { + if ($this->$field) { + return true; + } } - $link = $this->website; - $title = $this->website; - $prefix = 'http://'; - - if (strlen($link) > 7 && substr($link, 0, 7) === $prefix) { - $title = substr($title, 7); - } else { - $link = $prefix.$link; - } - - return link_to($link, $title, array('target' => '_blank')); + return false; } public function getDateCreated() @@ -160,24 +276,31 @@ class Client extends EntityModel return $this->account->currency_id ?: DEFAULT_CURRENCY; } + + public function getCounter($isQuote) + { + return $isQuote ? $this->quote_number_counter : $this->invoice_number_counter; + } + + public function markLoggedIn() + { + $this->last_login = Carbon::now()->toDateTimeString(); + $this->save(); + } } -/* -Client::created(function($client) -{ - Activity::createClient($client); +Client::creating(function ($client) { + $client->setNullValues(); +}); + +Client::created(function ($client) { + event(new ClientWasCreated($client)); }); -*/ Client::updating(function ($client) { - Activity::updateClient($client); + $client->setNullValues(); }); -Client::deleting(function ($client) { - Activity::archiveClient($client); +Client::updated(function ($client) { + event(new ClientWasUpdated($client)); }); - -/*Client::restoring(function ($client) { - Activity::restoreClient($client); -}); -*/ \ No newline at end of file diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 0856a6d431d4..a95f40bab059 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -9,14 +9,32 @@ class Contact extends EntityModel use SoftDeletes; protected $dates = ['deleted_at']; - public static $fieldFirstName = 'Contact - First Name'; - public static $fieldLastName = 'Contact - Last Name'; - public static $fieldEmail = 'Contact - Email'; - public static $fieldPhone = 'Contact - Phone'; + protected $fillable = [ + 'first_name', + 'last_name', + 'email', + 'phone', + 'send_invoice', + ]; + + public static $fieldFirstName = 'first_name'; + public static $fieldLastName = 'last_name'; + public static $fieldEmail = 'email'; + public static $fieldPhone = 'phone'; + + public function account() + { + return $this->belongsTo('App\Models\Account'); + } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } public function client() { - return $this->belongsTo('App\Models\Client'); + return $this->belongsTo('App\Models\Client')->withTrashed(); } public function getPersonType() @@ -24,20 +42,6 @@ class Contact extends EntityModel return PERSON_CONTACT; } - /* - public function getLastLogin() - { - if ($this->last_login == '0000-00-00 00:00:00') - { - return '---'; - } - else - { - return $this->last_login->format('m/d/y h:i a'); - } - } - */ - public function getName() { return $this->getDisplayName(); diff --git a/app/Models/Country.php b/app/Models/Country.php index 219251a44cc4..8a87500e3299 100644 --- a/app/Models/Country.php +++ b/app/Models/Country.php @@ -6,7 +6,19 @@ class Country extends Eloquent { public $timestamps = false; - protected $visible = ['id', 'name']; + protected $visible = [ + 'id', + 'name', + 'swap_postal_code', + 'swap_currency_symbol', + 'thousand_separator', + 'decimal_separator' + ]; + + protected $casts = [ + 'swap_postal_code' => 'boolean', + 'swap_currency_symbol' => 'boolean', + ]; public function getName() { diff --git a/app/Models/Credit.php b/app/Models/Credit.php index 9a507bc6bb27..c46095e80ee3 100644 --- a/app/Models/Credit.php +++ b/app/Models/Credit.php @@ -1,11 +1,26 @@ belongsTo('App\Models\Account'); + } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } public function invoice() { @@ -43,18 +58,10 @@ class Credit extends EntityModel } } +Credit::creating(function ($credit) { + +}); + Credit::created(function ($credit) { - Activity::createCredit($credit); -}); - -Credit::updating(function ($credit) { - Activity::updateCredit($credit); -}); - -Credit::deleting(function ($credit) { - Activity::archiveCredit($credit); -}); - -Credit::restoring(function ($credit) { - Activity::restoreCredit($credit); -}); + event(new CreditWasCreated($credit)); +}); \ No newline at end of file diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index 550de1d3cef0..b8e7d651ada4 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -9,14 +9,14 @@ class EntityModel extends Eloquent public $timestamps = true; protected $hidden = ['id']; - public static function createNew($parent = false) + public static function createNew($context = null) { $className = get_called_class(); $entity = new $className(); - if ($parent) { - $entity->user_id = $parent instanceof User ? $parent->id : $parent->user_id; - $entity->account_id = $parent->account_id; + if ($context) { + $entity->user_id = $context instanceof User ? $context->id : $context->user_id; + $entity->account_id = $context->account_id; } elseif (Auth::check()) { $entity->user_id = Auth::user()->id; $entity->account_id = Auth::user()->account_id; @@ -24,7 +24,10 @@ class EntityModel extends Eloquent Utils::fatalError(); } - $lastEntity = $className::withTrashed()->scope(false, $entity->account_id)->orderBy('public_id', 'DESC')->first(); + $lastEntity = $className::withTrashed() + ->scope(false, $entity->account_id) + ->orderBy('public_id', 'DESC') + ->first(); if ($lastEntity) { $entity->public_id = $lastEntity->public_id + 1; @@ -39,7 +42,7 @@ class EntityModel extends Eloquent { $className = get_called_class(); - return $className::scope($publicId)->pluck('id'); + return $className::scope($publicId)->withTrashed()->pluck('id'); } public function getActivityKey() @@ -112,4 +115,21 @@ class EntityModel extends Eloquent return $data; } + public function setNullValues() + { + foreach ($this->fillable as $field) { + if (strstr($field, '_id') && !$this->$field) { + $this->$field = null; + } + } + } + + // converts "App\Models\Client" to "client_id" + public function getKeyField() + { + $class = get_class($this); + $parts = explode('\\', $class); + $name = $parts[count($parts)-1]; + return strtolower($name) . '_id'; + } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php new file mode 100644 index 000000000000..ce1241b11b2a --- /dev/null +++ b/app/Models/Expense.php @@ -0,0 +1,114 @@ +belongsTo('App\Models\Account'); + } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } + + public function vendor() + { + return $this->belongsTo('App\Models\Vendor')->withTrashed(); + } + + public function client() + { + return $this->belongsTo('App\Models\Client')->withTrashed(); + } + + public function invoice() + { + return $this->belongsTo('App\Models\Invoice')->withTrashed(); + } + + public function getName() + { + if($this->expense_number) + return $this->expense_number; + + return $this->public_id; + } + + public function getDisplayName() + { + return $this->getName(); + } + + public function getRoute() + { + return "/expenses/{$this->public_id}"; + } + + public function getEntityType() + { + return ENTITY_EXPENSE; + } + + public function apply($amount) + { + if ($amount > $this->balance) { + $applied = $this->balance; + $this->balance = 0; + } else { + $applied = $amount; + $this->balance = $this->balance - $amount; + } + + $this->save(); + + return $applied; + } +} + +Expense::creating(function ($expense) { + $expense->setNullValues(); +}); + +Expense::created(function ($expense) { + event(new ExpenseWasCreated($expense)); +}); + +Expense::updating(function ($expense) { + $expense->setNullValues(); +}); + +Expense::updated(function ($expense) { + event(new ExpenseWasUpdated($expense)); +}); + +Expense::deleting(function ($expense) { + $expense->setNullValues(); +}); + +Expense::deleted(function ($expense) { + event(new ExpenseWasDeleted($expense)); +}); diff --git a/app/Models/Font.php b/app/Models/Font.php new file mode 100644 index 000000000000..b9518a91ba4d --- /dev/null +++ b/app/Models/Font.php @@ -0,0 +1,8 @@ +provider.'.png'; } + public function isGateway($gatewayId) + { + return $this->id == $gatewayId; + } + + public static function getPaymentTypeName($type) + { + return Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type))); + } + + /* public static function getPaymentTypeLinks() { $data = []; foreach (self::$paymentTypes as $type) { - $data[] = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); + $data[] = Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type))); } return $data; } + */ public function getHelp() { @@ -81,6 +95,8 @@ class Gateway extends Eloquent return PAYMENT_TYPE_BITCOIN; } else if ($gatewayId == GATEWAY_DWOLLA) { return PAYMENT_TYPE_DWOLLA; + }else if ($gatewayId == GATEWAY_GOCARDLESS) { + return PAYMENT_TYPE_DIRECT_DEBIT; } else { return PAYMENT_TYPE_CREDIT_CARD; } diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 7941c81f4788..2cc71029578d 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -1,5 +1,7 @@ belongsTo('App\Models\Account'); } - public function getLink() + public function getLink($type = 'view') { if (!$this->account) { $this->load('account'); } - + $url = SITE_URL; - - if ($this->account->subdomain) { - $parsedUrl = parse_url($url); - $host = explode('.', $parsedUrl['host']); - $subdomain = $host[0]; - $url = str_replace("://{$subdomain}.", "://{$this->account->subdomain}.", $url); + $iframe_url = $this->account->iframe_url; + + if ($this->account->isPro()) { + if ($iframe_url) { + return "{$iframe_url}/?{$this->invitation_key}"; + } elseif ($this->account->subdomain) { + $url = Utils::replaceSubdomain($url, $this->account->subdomain); + } + } + + return "{$url}/{$type}/{$this->invitation_key}"; + } + + public function getStatus() + { + $hasValue = false; + $parts = []; + $statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed']; + + foreach ($statuses as $status) { + $field = "{$status}_date"; + $date = ''; + if ($this->$field && $this->field != '0000-00-00 00:00:00') { + $date = Utils::dateToString($this->$field); + $hasValue = true; + } + $parts[] = trans('texts.invitation_status.' . $status) . ': ' . $date; } - return "{$url}/view/{$this->invitation_key}"; + return $hasValue ? implode($parts, '
    ') : false; } public function getName() { return $this->invitation_key; } + + public function markSent($messageId = null) + { + $this->message_id = $messageId; + $this->email_error = null; + $this->sent_date = Carbon::now()->toDateTimeString(); + $this->save(); + } + + public function markViewed() + { + $invoice = $this->invoice; + $client = $invoice->client; + + $this->viewed_date = Carbon::now()->toDateTimeString(); + $this->save(); + + $invoice->markViewed(); + $client->markLoggedIn(); + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index aeebfaed6ab0..39fedd489731 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -1,18 +1,152 @@ 'boolean', 'has_tasks' => 'boolean', + 'auto_bill' => 'boolean', + 'has_expenses' => 'boolean', ]; + // used for custom invoice numbers + public static $patternFields = [ + 'counter', + 'custom1', + 'custom2', + 'userId', + 'year', + 'date:', + ]; + + public static $fieldInvoiceNumber = 'invoice_number'; + public static $fieldInvoiceDate = 'invoice_date'; + public static $fieldDueDate = 'due_date'; + public static $fieldAmount = 'amount'; + public static $fieldPaid = 'paid'; + public static $fieldNotes = 'notes'; + public static $fieldTerms = 'terms'; + + public static function getImportColumns() + { + return [ + Client::$fieldName, + Invoice::$fieldInvoiceNumber, + Invoice::$fieldInvoiceDate, + Invoice::$fieldDueDate, + Invoice::$fieldAmount, + Invoice::$fieldPaid, + Invoice::$fieldNotes, + Invoice::$fieldTerms, + ]; + } + + public static function getImportMap() + { + return [ + 'number^po' => 'invoice_number', + 'amount' => 'amount', + 'organization' => 'name', + 'paid^date' => 'paid', + 'invoice_date|create_date' => 'invoice_date', + 'terms' => 'terms', + 'notes' => 'notes', + ]; + } + public function getRoute() + { + $entityType = $this->getEntityType(); + return "/{$entityType}s/{$this->public_id}/edit"; + } + + public function getDisplayName() + { + return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number; + } + + public function affectsBalance() + { + return !$this->is_quote && !$this->is_recurring; + } + + public function getAdjustment() + { + if (!$this->affectsBalance()) { + return 0; + } + + return $this->getRawAdjustment(); + } + + private function getRawAdjustment() + { + return floatval($this->amount) - floatval($this->getOriginal('amount')); + } + + public function isChanged() + { + if ($this->getRawAdjustment() != 0) { + return true; + } + + foreach ([ + 'invoice_number', + 'po_number', + 'invoice_date', + 'due_date', + 'terms', + 'public_notes', + 'invoice_footer', + 'partial', + ] as $field) { + if ($this->$field != $this->getOriginal($field)) { + return true; + } + } + + return false; + } + + public function getAmountPaid() + { + if ($this->is_quote || $this->is_recurring) { + return 0; + } + + return ($this->amount - $this->balance); + } + + public function trashed() + { + if ($this->client && $this->client->trashed()) { + return true; + } + + return self::parentTrashed(); + } + public function account() { return $this->belongsTo('App\Models\Account'); @@ -20,7 +154,7 @@ class Invoice extends EntityModel public function user() { - return $this->belongsTo('App\Models\User'); + return $this->belongsTo('App\Models\User')->withTrashed(); } public function client() @@ -43,6 +177,11 @@ class Invoice extends EntityModel return $this->belongsTo('App\Models\InvoiceDesign'); } + public function payments() + { + return $this->hasMany('App\Models\Payment', 'invoice_id', 'id'); + } + public function recurring_invoice() { return $this->belongsTo('App\Models\Invoice'); @@ -58,6 +197,85 @@ class Invoice extends EntityModel return $this->hasMany('App\Models\Invitation')->orderBy('invitations.contact_id'); } + public function markInvitationsSent($notify = false) + { + foreach ($this->invitations as $invitation) { + $this->markInvitationSent($invitation, false, $notify); + } + } + + public function markInvitationSent($invitation, $messageId = false, $notify = true) + { + if (!$this->isSent()) { + $this->invoice_status_id = INVOICE_STATUS_SENT; + $this->save(); + } + + $invitation->markSent($messageId); + + // if the user marks it as sent rather than acually sending it + // then we won't track it in the activity log + if (!$notify) { + return; + } + + if ($this->is_quote) { + event(new QuoteInvitationWasEmailed($invitation)); + } else { + event(new InvoiceInvitationWasEmailed($invitation)); + } + } + + public function markViewed() + { + if (!$this->isViewed()) { + $this->invoice_status_id = INVOICE_STATUS_VIEWED; + $this->save(); + } + } + + public function updatePaidStatus($save = true) + { + $statusId = false; + if ($this->amount > 0 && $this->balance == 0) { + $statusId = INVOICE_STATUS_PAID; + } elseif ($this->balance > 0 && $this->balance < $this->amount) { + $statusId = INVOICE_STATUS_PARTIAL; + } elseif ($this->isPartial() && $this->balance > 0) { + $statusId = ($this->balance == $this->amount ? INVOICE_STATUS_SENT : INVOICE_STATUS_PARTIAL); + } + + if ($statusId && $statusId != $this->invoice_status_id) { + $this->invoice_status_id = $statusId; + if ($save) { + $this->save(); + } + } + } + + public function markApproved() + { + if ($this->is_quote) { + $this->invoice_status_id = INVOICE_STATUS_APPROVED; + $this->save(); + } + } + + public function updateBalances($balanceAdjustment, $partial = 0) + { + if ($this->is_deleted) { + return; + } + + $this->balance = $this->balance + $balanceAdjustment; + + if ($this->partial > 0) { + $this->partial = $partial; + } + + $this->save(); + } + public function getName() { return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number; @@ -99,16 +317,41 @@ class Invoice extends EntityModel return $this->invoice_status_id >= INVOICE_STATUS_VIEWED; } + public function isPartial() + { + return $this->invoice_status_id >= INVOICE_STATUS_PARTIAL; + } + public function isPaid() { return $this->invoice_status_id >= INVOICE_STATUS_PAID; } + public function isOverdue() + { + if ( ! $this->due_date) { + return false; + } + + return time() > strtotime($this->due_date); + } + public function getRequestedAmount() { return $this->partial > 0 ? $this->partial : $this->balance; } + public function getCurrencyCode() + { + if ($this->client->currency) { + return $this->client->currency->code; + } elseif ($this->account->currency) { + return $this->account->currency->code; + } else { + return 'USD'; + } + } + public function hidePrivateFields() { $this->setVisible([ @@ -130,6 +373,7 @@ class Invoice extends EntityModel 'account', 'invoice_design', 'invoice_design_id', + 'invoice_fonts', 'is_pro', 'is_quote', 'custom_value1', @@ -138,6 +382,9 @@ class Invoice extends EntityModel 'custom_taxes2', 'partial', 'has_tasks', + 'custom_text_value1', + 'custom_text_value2', + 'has_expenses', ]); $this->client->setVisible([ @@ -160,6 +407,7 @@ class Invoice extends EntityModel $this->account->setVisible([ 'name', + 'website', 'id_number', 'vat_number', 'address1', @@ -184,6 +432,9 @@ class Invoice extends EntityModel 'custom_invoice_label1', 'custom_invoice_label2', 'pdf_email_attachment', + 'show_item_taxes', + 'custom_invoice_text_label1', + 'custom_invoice_text_label2', ]); foreach ($this->invoice_items as $invoiceItem) { @@ -209,6 +460,212 @@ class Invoice extends EntityModel return $this; } + public function getSchedule() + { + if (!$this->start_date || !$this->is_recurring || !$this->frequency_id) { + return false; + } + + $startDate = $this->getOriginal('last_sent_date') ?: $this->getOriginal('start_date'); + $startDate .= ' ' . $this->account->recurring_hour . ':00:00'; + $startDate = $this->account->getDateTime($startDate); + $endDate = $this->end_date ? $this->account->getDateTime($this->getOriginal('end_date')) : null; + $timezone = $this->account->getTimezone(); + + $rule = $this->getRecurrenceRule(); + $rule = new \Recurr\Rule("{$rule}", $startDate, $endDate, $timezone); + + // Fix for months with less than 31 days + $transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig(); + $transformerConfig->enableLastDayOfMonthFix(); + + $transformer = new \Recurr\Transformer\ArrayTransformer(); + $transformer->setConfig($transformerConfig); + $dates = $transformer->transform($rule); + + if (count($dates) < 2) { + return false; + } + + return $dates; + } + + public function getNextSendDate() + { + if ($this->start_date && !$this->last_sent_date) { + $startDate = $this->getOriginal('start_date') . ' ' . $this->account->recurring_hour . ':00:00'; + return $this->account->getDateTime($startDate); + } + + if (!$schedule = $this->getSchedule()) { + return null; + } + + if (count($schedule) < 2) { + return null; + } + + return $schedule[1]->getStart(); + } + + public function getDueDate($invoice_date = null){ + if(!$this->is_recurring) { + return $this->due_date ? $this->due_date : null; + } + else{ + $now = time(); + if($invoice_date) { + // If $invoice_date is specified, all calculations are based on that date + if(is_numeric($invoice_date)) { + $now = $invoice_date; + } + else if(is_string($invoice_date)) { + $now = strtotime($invoice_date); + } + elseif ($invoice_date instanceof \DateTime) { + $now = $invoice_date->getTimestamp(); + } + } + + if($this->due_date && $this->due_date != '0000-00-00'){ + // This is a recurring invoice; we're using a custom format here. + // The year is always 1998; January is 1st, 2nd, last day of the month. + // February is 1st Sunday after, 1st Monday after, ..., through 4th Saturday after. + $dueDateVal = strtotime($this->due_date); + $monthVal = (int)date('n', $dueDateVal); + $dayVal = (int)date('j', $dueDateVal); + $dueDate = false; + + if($monthVal == 1) {// January; day of month + $currentDay = (int)date('j', $now); + $lastDayOfMonth = (int)date('t', $now); + + $dueYear = (int)date('Y', $now);// This year + $dueMonth = (int)date('n', $now);// This month + $dueDay = $dayVal;// The day specified for the invoice + + if($dueDay > $lastDayOfMonth) { + // No later than the end of the month + $dueDay = $lastDayOfMonth; + } + + if($currentDay >= $dueDay) { + // Wait until next month + // We don't need to handle the December->January wraparaound, since PHP handles month 13 as January of next year + $dueMonth++; + + // Reset the due day + $dueDay = $dayVal; + $lastDayOfMonth = (int)date('t', mktime(0, 0, 0, $dueMonth, 1, $dueYear));// The number of days in next month + + // Check against the last day again + if($dueDay > $lastDayOfMonth){ + // No later than the end of the month + $dueDay = $lastDayOfMonth; + } + } + + $dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear); + } + else if($monthVal == 2) {// February; day of week + $ordinals = array('first', 'second', 'third', 'fourth'); + $daysOfWeek = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'); + + $ordinalIndex = ceil($dayVal / 7) - 1;// 1-7 are "first"; 8-14 are "second", etc. + $dayOfWeekIndex = ($dayVal - 1) % 7;// 1,8,15,22 are Sunday, 2,9,16,23 are Monday, etc. + $dayStr = $ordinals[$ordinalIndex] . ' ' . $daysOfWeek[$dayOfWeekIndex];// "first sunday", "first monday", etc. + + $dueDate = strtotime($dayStr, $now); + } + + if($dueDate) { + return date('Y-m-d', $dueDate);// SQL format + } + } + else if ($this->client->payment_terms != 0) { + // No custom due date set for this invoice; use the client's payment terms + $days = $this->client->payment_terms; + if ($days == -1) { + $days = 0; + } + return date('Y-m-d', strtotime('+'.$days.' day', $now)); + } + } + + // Couldn't calculate one + return null; + } + + public function getPrettySchedule($min = 1, $max = 10) + { + if (!$schedule = $this->getSchedule($max)) { + return null; + } + + $dates = []; + + for ($i=$min; $igetStart(); + $date = $this->account->formatDate($dateStart); + $dueDate = $this->getDueDate($dateStart); + + if($dueDate) { + $date .= ' (' . trans('texts.due') . ' ' . $this->account->formatDate($dueDate) . ')'; + } + + $dates[] = $date; + } + + return implode('
    ', $dates); + } + + private function getRecurrenceRule() + { + $rule = ''; + + switch ($this->frequency_id) { + case FREQUENCY_WEEKLY: + $rule = 'FREQ=WEEKLY;'; + break; + case FREQUENCY_TWO_WEEKS: + $rule = 'FREQ=WEEKLY;INTERVAL=2;'; + break; + case FREQUENCY_FOUR_WEEKS: + $rule = 'FREQ=WEEKLY;INTERVAL=4;'; + break; + case FREQUENCY_MONTHLY: + $rule = 'FREQ=MONTHLY;'; + break; + case FREQUENCY_THREE_MONTHS: + $rule = 'FREQ=MONTHLY;INTERVAL=3;'; + break; + case FREQUENCY_SIX_MONTHS: + $rule = 'FREQ=MONTHLY;INTERVAL=6;'; + break; + case FREQUENCY_ANNUALLY: + $rule = 'FREQ=YEARLY;'; + break; + } + + if ($this->end_date) { + $rule .= 'UNTIL=' . $this->getOriginal('end_date'); + } + + return $rule; + } + + /* + public function shouldSendToday() + { + if (!$nextSendDate = $this->getNextSendDate()) { + return false; + } + + return $this->account->getDateTime() >= $nextSendDate; + } + */ + public function shouldSendToday() { if (!$this->start_date || strtotime($this->start_date) > strtotime('now')) { @@ -260,26 +717,72 @@ class Invoice extends EntityModel return false; } + + public function getPDFString() + { + if (!env('PHANTOMJS_CLOUD_KEY')) { + return false; + } + + $invitation = $this->invitations[0]; + $link = $invitation->getLink(); + $curl = curl_init(); + + $jsonEncodedData = json_encode([ + 'url' => "{$link}?phantomjs=true", + 'renderType' => 'html', + 'outputAsJson' => false, + 'renderSettings' => [ + 'passThroughHeaders' => true, + ], + // 'delayTime' => 1000, + ]); + + $opts = [ + CURLOPT_URL => PHANTOMJS_CLOUD . env('PHANTOMJS_CLOUD_KEY') . '/', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $jsonEncodedData, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Content-Length: '.strlen($jsonEncodedData) + ], + ]; + + curl_setopt_array($curl, $opts); + $response = curl_exec($curl); + curl_close($curl); + + $encodedString = strip_tags($response); + $pdfString = Utils::decodePDF($encodedString); + + if ( ! $pdfString || strlen($pdfString) < 200) { + Utils::logError("PhantomJSCloud - failed to create pdf: {$encodedString}"); + } + + return $pdfString; + } } Invoice::creating(function ($invoice) { if (!$invoice->is_recurring) { - $invoice->account->incrementCounter($invoice->is_quote); + $invoice->account->incrementCounter($invoice); } }); Invoice::created(function ($invoice) { - Activity::createInvoice($invoice); + if ($invoice->is_quote) { + event(new QuoteWasCreated($invoice)); + } else { + event(new InvoiceWasCreated($invoice)); + } }); Invoice::updating(function ($invoice) { - Activity::updateInvoice($invoice); + if ($invoice->is_quote) { + event(new QuoteWasUpdated($invoice)); + } else { + event(new InvoiceWasUpdated($invoice)); + } }); - -Invoice::deleting(function ($invoice) { - Activity::archiveInvoice($invoice); -}); - -Invoice::restoring(function ($invoice) { - Activity::restoreInvoice($invoice); -}); \ No newline at end of file diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php index 396a1c713413..b7b3c8ffc8e9 100644 --- a/app/Models/InvoiceItem.php +++ b/app/Models/InvoiceItem.php @@ -16,4 +16,10 @@ class InvoiceItem extends EntityModel { return $this->belongsTo('App\Models\Product'); } + + public function account() + { + return $this->belongsTo('App\Models\Account'); + } + } diff --git a/app/Models/Language.php b/app/Models/Language.php index d1e757936808..084c2fe86da7 100644 --- a/app/Models/Language.php +++ b/app/Models/Language.php @@ -5,4 +5,9 @@ use Eloquent; class Language extends Eloquent { public $timestamps = false; + + public function getName() + { + return $this->name; + } } diff --git a/app/Models/OwnedByClientTrait.php b/app/Models/OwnedByClientTrait.php new file mode 100644 index 000000000000..7f11448324b9 --- /dev/null +++ b/app/Models/OwnedByClientTrait.php @@ -0,0 +1,13 @@ +client) { + return false; + } + + return $this->client->trashed(); + } +} \ No newline at end of file diff --git a/app/Models/Payment.php b/app/Models/Payment.php index ee382dece1b6..a2e8b2591fe3 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -1,11 +1,16 @@ belongsTo('App\Models\Contact'); } + public function account_gateway() + { + return $this->belongsTo('App\Models\AccountGateway'); + } + + public function payment_type() + { + return $this->belongsTo('App\Models\PaymentType'); + } + + public function getRoute() + { + return "/payments/{$this->public_id}/edit"; + } + + /* public function getAmount() { return Utils::formatMoney($this->amount, $this->client->getCurrencyId()); } + */ public function getName() { @@ -53,18 +75,10 @@ class Payment extends EntityModel } } +Payment::creating(function ($payment) { + +}); + Payment::created(function ($payment) { - Activity::createPayment($payment); -}); - -Payment::updating(function ($payment) { - Activity::updatePayment($payment); -}); - -Payment::deleting(function ($payment) { - Activity::archivePayment($payment); -}); - -Payment::restoring(function ($payment) { - Activity::restorePayment($payment); -}); + event(new PaymentWasCreated($payment)); +}); \ No newline at end of file diff --git a/app/Models/PaymentTerm.php b/app/Models/PaymentTerm.php index de8cced5db72..dbb788aef1c2 100644 --- a/app/Models/PaymentTerm.php +++ b/app/Models/PaymentTerm.php @@ -1,8 +1,17 @@ where('product_key', '=', $key)->first(); } + + public function default_tax_rate() + { + return $this->belongsTo('App\Models\TaxRate'); + } } diff --git a/app/Models/Task.php b/app/Models/Task.php index 4ccbf9688ab3..17b667558c49 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -3,10 +3,14 @@ use DB; use Utils; use Illuminate\Database\Eloquent\SoftDeletes; +use Laracasts\Presenter\PresentableTrait; class Task extends EntityModel { use SoftDeletes; + use PresentableTrait; + + protected $presenter = 'App\Ninja\Presenters\TaskPresenter'; public function account() { @@ -18,6 +22,11 @@ class Task extends EntityModel return $this->belongsTo('App\Models\Invoice'); } + public function user() + { + return $this->belongsTo('App\Models\User'); + } + public function client() { return $this->belongsTo('App\Models\Client')->withTrashed(); @@ -82,20 +91,4 @@ class Task extends EntityModel { return round($this->getDuration() / (60 * 60), 2); } -} - -Task::created(function ($task) { - //Activity::createTask($task); -}); - -Task::updating(function ($task) { - //Activity::updateTask($task); -}); - -Task::deleting(function ($task) { - //Activity::archiveTask($task); -}); - -Task::restoring(function ($task) { - //Activity::restoreTask($task); -}); +} \ No newline at end of file diff --git a/app/Models/TaxRate.php b/app/Models/TaxRate.php index bb74c89f541c..751cdb3205b2 100644 --- a/app/Models/TaxRate.php +++ b/app/Models/TaxRate.php @@ -6,4 +6,9 @@ class TaxRate extends EntityModel { use SoftDeletes; protected $dates = ['deleted_at']; + + public function getEntityType() + { + return ENTITY_TAX_RATE; + } } diff --git a/app/Models/User.php b/app/Models/User.php index 93b263a55c63..129ddb0abecd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ use Auth; use Event; use App\Libraries\Utils; use App\Events\UserSettingsChanged; +use App\Events\UserSignedUp; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Auth\Passwords\CanResetPassword; @@ -28,14 +29,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon * * @var array */ - protected $fillable = ['name', 'email', 'password']; + protected $fillable = ['first_name', 'last_name', 'email', 'password']; /** * The attributes excluded from the model's JSON form. * * @var array */ - protected $hidden = ['password', 'remember_token']; + protected $hidden = ['password', 'remember_token', 'confirmation_code']; use SoftDeletes; protected $dates = ['deleted_at']; @@ -95,11 +96,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isPro(); } - public function isDemo() - { - return $this->account->id == Utils::getDemoAccountId(); - } - public function maxInvoiceDesignId() { return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); @@ -134,27 +130,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon { return Session::get(SESSION_COUNTER, 0); } - - /* - public function getPopOverText() - { - if (!Utils::isNinja() || !Auth::check() || Session::has('error')) { - return false; - } - - $count = self::getRequestsCount(); - - if ($count == 1 || $count % 5 == 0) { - if (!Utils::isRegistered()) { - return trans('texts.sign_up_to_save'); - } elseif (!Auth::user()->account->name) { - return trans('texts.set_name'); - } - } - - return false; - } - */ public function afterSave($success = true, $forced = false) { @@ -167,9 +142,31 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumClients() { - return $this->isPro() ? MAX_NUM_CLIENTS_PRO : MAX_NUM_CLIENTS; + if ($this->isPro()) { + return MAX_NUM_CLIENTS_PRO; + } + + if ($this->id < LEGACY_CUTOFF) { + return MAX_NUM_CLIENTS_LEGACY; + } + + return MAX_NUM_CLIENTS; } + public function getMaxNumVendors() + { + if ($this->isPro()) { + return MAX_NUM_VENDORS_PRO; + } + + if ($this->id < LEGACY_CUTOFF) { + return MAX_NUM_VENDORS_LEGACY; + } + + return MAX_NUM_VENDORS; + } + + public function getRememberToken() { return $this->remember_token; @@ -203,20 +200,44 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } } - public static function updateUser($user) + public static function onUpdatingUser($user) { - if ($user->password != !$user->getOriginal('password')) { + if ($user->password != $user->getOriginal('password')) { $user->failed_logins = 0; } + + // if the user changes their email then they need to reconfirm it + if ($user->isEmailBeingChanged()) { + $user->confirmed = 0; + $user->confirmation_code = str_random(RANDOM_KEY_LENGTH); + } + } + + public static function onUpdatedUser($user) + { + if (!$user->getOriginal('email') + || $user->getOriginal('email') == TEST_USERNAME + || $user->getOriginal('username') == TEST_USERNAME + || $user->getOriginal('email') == 'tests@bitrock.com') { + event(new UserSignedUp()); + } + + event(new UserSettingsChanged($user)); + } + + public function isEmailBeingChanged() + { + return Utils::isNinjaProd() + && $this->email != $this->getOriginal('email') + && $this->getOriginal('confirmed'); } } User::updating(function ($user) { - User::updateUser($user); + User::onUpdatingUser($user); }); User::updated(function ($user) { - Event::fire(new UserSettingsChanged()); + User::onUpdatedUser($user); }); - diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php new file mode 100644 index 000000000000..bc34fbd2b1dc --- /dev/null +++ b/app/Models/Vendor.php @@ -0,0 +1,242 @@ + 'first_name', + 'last' => 'last_name', + 'email' => 'email', + 'mobile|phone' => 'phone', + 'name|organization' => 'name', + 'street2|address2' => 'address2', + 'street|address|address1' => 'address1', + 'city' => 'city', + 'state|province' => 'state', + 'zip|postal|code' => 'postal_code', + 'country' => 'country', + 'note' => 'notes', + ]; + } + + public function account() + { + return $this->belongsTo('App\Models\Account'); + } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } + + public function payments() + { + return $this->hasMany('App\Models\Payment'); + } + + public function vendorContacts() + { + return $this->hasMany('App\Models\VendorContact'); + } + + public function country() + { + return $this->belongsTo('App\Models\Country'); + } + + public function currency() + { + return $this->belongsTo('App\Models\Currency'); + } + + public function language() + { + return $this->belongsTo('App\Models\Language'); + } + + public function size() + { + return $this->belongsTo('App\Models\Size'); + } + + public function industry() + { + return $this->belongsTo('App\Models\Industry'); + } + + public function addVendorContact($data, $isPrimary = false) + { + $publicId = isset($data['public_id']) ? $data['public_id'] : false; + + if ($publicId && $publicId != '-1') { + $contact = VendorContact::scope($publicId)->firstOrFail(); + } else { + $contact = VendorContact::createNew(); + } + + $contact->fill($data); + $contact->is_primary = $isPrimary; + + return $this->vendorContacts()->save($contact); + } + + public function getRoute() + { + return "/vendors/{$this->public_id}"; + } + + public function getName() + { + return $this->name; + } + + public function getDisplayName() + { + return $this->getName(); + } + + public function getCityState() + { + $swap = $this->country && $this->country->swap_postal_code; + return Utils::cityStateZip($this->city, $this->state, $this->postal_code, $swap); + } + + public function getEntityType() + { + return 'vendor'; + } + + public function hasAddress() + { + $fields = [ + 'address1', + 'address2', + 'city', + 'state', + 'postal_code', + 'country_id', + ]; + + foreach ($fields as $field) { + if ($this->$field) { + return true; + } + } + + return false; + } + + public function getDateCreated() + { + if ($this->created_at == '0000-00-00 00:00:00') { + return '---'; + } else { + return $this->created_at->format('m/d/y h:i a'); + } + } + + public function getCurrencyId() + { + if ($this->currency_id) { + return $this->currency_id; + } + + if (!$this->account) { + $this->load('account'); + } + + return $this->account->currency_id ?: DEFAULT_CURRENCY; + } + + public function getTotalExpense() + { + return DB::table('expenses') + ->where('vendor_id', '=', $this->id) + ->whereNull('deleted_at') + ->sum('amount'); + } +} + +Vendor::creating(function ($vendor) { + $vendor->setNullValues(); +}); + +Vendor::created(function ($vendor) { + event(new VendorWasCreated($vendor)); +}); + +Vendor::updating(function ($vendor) { + $vendor->setNullValues(); +}); + +Vendor::updated(function ($vendor) { + event(new VendorWasUpdated($vendor)); +}); + + +Vendor::deleting(function ($vendor) { + $vendor->setNullValues(); +}); + +Vendor::deleted(function ($vendor) { + event(new VendorWasDeleted($vendor)); +}); diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php new file mode 100644 index 000000000000..5546b27d2adb --- /dev/null +++ b/app/Models/VendorContact.php @@ -0,0 +1,68 @@ +belongsTo('App\Models\Account'); + } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } + + public function vendor() + { + return $this->belongsTo('App\Models\Vendor')->withTrashed(); + } + + public function getPersonType() + { + return PERSON_VENDOR_CONTACT; + } + + public function getName() + { + return $this->getDisplayName(); + } + + public function getDisplayName() + { + if ($this->getFullName()) { + return $this->getFullName(); + } else { + return $this->email; + } + } + + public function getFullName() + { + if ($this->first_name || $this->last_name) { + return $this->first_name.' '.$this->last_name; + } else { + return ''; + } + } +} diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php new file mode 100644 index 000000000000..8e17bfeec36f --- /dev/null +++ b/app/Ninja/Import/BaseTransformer.php @@ -0,0 +1,97 @@ +maps = $maps; + } + + protected function hasClient($name) + { + $name = strtolower($name); + return isset($this->maps[ENTITY_CLIENT][$name]); + } + + protected function getString($data, $field) + { + return (isset($data->$field) && $data->$field) ? $data->$field : ''; + } + + protected function getClientId($name) + { + $name = strtolower($name); + return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null; + } + + protected function getCountryId($name) + { + $name = strtolower($name); + return isset($this->maps['countries'][$name]) ? $this->maps['countries'][$name] : null; + } + + protected function getCountryIdBy2($name) + { + $name = strtolower($name); + return isset($this->maps['countries2'][$name]) ? $this->maps['countries2'][$name] : null; + } + + protected function getFirstName($name) + { + $name = Utils::splitName($name); + return $name[0]; + } + + protected function getDate($date, $format = 'Y-m-d') + { + if ( ! $date instanceof DateTime) { + $date = DateTime::createFromFormat($format, $date); + } + + return $date ? $date->format('Y-m-d') : null; + } + + protected function getLastName($name) + { + $name = Utils::splitName($name); + return $name[1]; + } + + protected function getInvoiceNumber($number) + { + $number = strtolower($number); + return str_pad($number, 4, '0', STR_PAD_LEFT); + } + + protected function getInvoiceId($invoiceNumber) + { + $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); + return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE][$invoiceNumber] : null; + } + + protected function hasInvoice($invoiceNumber) + { + $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); + return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]); + } + + protected function getInvoiceClientId($invoiceNumber) + { + $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); + return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null; + } + + + protected function getVendorId($name) + { + $name = strtolower($name); + return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; + } + +} \ No newline at end of file diff --git a/app/Ninja/Import/CSV/ClientTransformer.php b/app/Ninja/Import/CSV/ClientTransformer.php new file mode 100644 index 000000000000..b480d5e88f4e --- /dev/null +++ b/app/Ninja/Import/CSV/ClientTransformer.php @@ -0,0 +1,35 @@ +name) && $this->hasClient($data->name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'name'), + 'work_phone' => $this->getString($data, 'work_phone'), + 'address1' => $this->getString($data, 'address1'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'state'), + 'postal_code' => $this->getString($data, 'postal_code'), + 'private_notes' => $this->getString($data, 'notes'), + 'contacts' => [ + [ + 'first_name' => $this->getString($data, 'first_name'), + 'last_name' => $this->getString($data, 'last_name'), + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'phone'), + ], + ], + 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null, + ]; + }); + } +} diff --git a/app/Ninja/Import/CSV/InvoiceTransformer.php b/app/Ninja/Import/CSV/InvoiceTransformer.php new file mode 100644 index 000000000000..e58bfe335ed2 --- /dev/null +++ b/app/Ninja/Import/CSV/InvoiceTransformer.php @@ -0,0 +1,38 @@ +getClientId($data->name)) { + return false; + } + + if (isset($data->invoice_number) && $this->hasInvoice($data->invoice_number)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->name), + 'invoice_number' => isset($data->invoice_number) ? $this->getInvoiceNumber($data->invoice_number) : null, + 'paid' => isset($data->paid) ? (float) $data->paid : null, + 'po_number' => $this->getString($data, 'po_number'), + 'terms' => $this->getString($data, 'terms'), + 'public_notes' => $this->getString($data, 'public_notes'), + 'invoice_date_sql' => isset($data->invoice_date) ? $data->invoice_date : null, + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'notes'), + 'cost' => isset($data->amount) ? (float) $data->amount : null, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/CSV/PaymentTransformer.php b/app/Ninja/Import/CSV/PaymentTransformer.php new file mode 100644 index 000000000000..7acd3d88f839 --- /dev/null +++ b/app/Ninja/Import/CSV/PaymentTransformer.php @@ -0,0 +1,19 @@ + $data->paid, + 'payment_date_sql' => isset($data->invoice_date) ? $data->invoice_date : null, + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/CSV/VendorTransformer.php b/app/Ninja/Import/CSV/VendorTransformer.php new file mode 100644 index 000000000000..464274e5a4fa --- /dev/null +++ b/app/Ninja/Import/CSV/VendorTransformer.php @@ -0,0 +1,35 @@ +name) && $this->hasVendor($data->name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'name'), + 'work_phone' => $this->getString($data, 'work_phone'), + 'address1' => $this->getString($data, 'address1'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'state'), + 'postal_code' => $this->getString($data, 'postal_code'), + 'private_notes' => $this->getString($data, 'notes'), + 'contacts' => [ + [ + 'first_name' => $this->getString($data, 'first_name'), + 'last_name' => $this->getString($data, 'last_name'), + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'phone'), + ], + ], + 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null, + ]; + }); + } +} diff --git a/app/Ninja/Import/FreshBooks/ClientTransformer.php b/app/Ninja/Import/FreshBooks/ClientTransformer.php new file mode 100644 index 000000000000..d71be4befdd9 --- /dev/null +++ b/app/Ninja/Import/FreshBooks/ClientTransformer.php @@ -0,0 +1,36 @@ +hasClient($data->organization)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'organization'), + 'work_phone' => $this->getString($data, 'busphone'), + 'address1' => $this->getString($data, 'street'), + 'address2' => $this->getString($data, 'street2'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'province'), + 'postal_code' => $this->getString($data, 'postalcode'), + 'private_notes' => $this->getString($data, 'notes'), + 'contacts' => [ + [ + 'first_name' => $this->getString($data, 'firstname'), + 'last_name' => $this->getString($data, 'lastname'), + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'mobphone') ?: $this->getString($data, 'homephone'), + ], + ], + 'country_id' => $this->getCountryId($data->country), + ]; + }); + } +} diff --git a/app/Ninja/Import/FreshBooks/InvoiceTransformer.php b/app/Ninja/Import/FreshBooks/InvoiceTransformer.php new file mode 100644 index 000000000000..06c4af967491 --- /dev/null +++ b/app/Ninja/Import/FreshBooks/InvoiceTransformer.php @@ -0,0 +1,37 @@ +getClientId($data->organization)) { + return false; + } + + if ($this->hasInvoice($data->invoice_number)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->organization), + 'invoice_number' => $this->getInvoiceNumber($data->invoice_number), + 'paid' => (float) $data->paid, + 'po_number' => $this->getString($data, 'po_number'), + 'terms' => $this->getString($data, 'terms'), + 'invoice_date_sql' => $data->create_date, + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'notes'), + 'cost' => (float) $data->amount, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/PaymentTransformer.php b/app/Ninja/Import/FreshBooks/PaymentTransformer.php new file mode 100644 index 000000000000..1f69fdbacf41 --- /dev/null +++ b/app/Ninja/Import/FreshBooks/PaymentTransformer.php @@ -0,0 +1,19 @@ + $data->paid, + 'payment_date_sql' => $data->create_date, + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/TaskTransformer.php b/app/Ninja/Import/FreshBooks/TaskTransformer.php new file mode 100644 index 000000000000..8c1363edcfec --- /dev/null +++ b/app/Ninja/Import/FreshBooks/TaskTransformer.php @@ -0,0 +1,29 @@ +hours * 3600); + $timeLogFinish = strtotime($data->date); + $timeLogStart = intval($timeLogFinish - $seconds); + $timeLog[] = []; + $timelog[] = $timeLogStart; + $timelog[] = $timeLogFinish; + $timeLog = json_encode(array($timelog)); + + return [ + 'action' => 'stop', + 'time_log' => $timeLog, + 'description' => $data->task, + ]; + } + +} +*/ \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/VendorTransformer.php b/app/Ninja/Import/FreshBooks/VendorTransformer.php new file mode 100644 index 000000000000..c083360aa305 --- /dev/null +++ b/app/Ninja/Import/FreshBooks/VendorTransformer.php @@ -0,0 +1,36 @@ +hasVendor($data->organization)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->organization, + 'work_phone' => $data->busphone, + 'address1' => $data->street, + 'address2' => $data->street2, + 'city' => $data->city, + 'state' => $data->province, + 'postal_code' => $data->postalcode, + 'private_notes' => $data->notes, + 'contacts' => [ + [ + 'first_name' => $data->firstname, + 'last_name' => $data->lastname, + 'email' => $data->email, + 'phone' => $data->mobphone ?: $data->homephone, + ], + ], + 'country_id' => $this->getCountryId($data->country), + ]; + }); + } +} diff --git a/app/Ninja/Import/Harvest/ClientTransformer.php b/app/Ninja/Import/Harvest/ClientTransformer.php new file mode 100644 index 000000000000..fb8200ec3ae0 --- /dev/null +++ b/app/Ninja/Import/Harvest/ClientTransformer.php @@ -0,0 +1,20 @@ +hasClient($data->client_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'client_name'), + ]; + }); + } +} diff --git a/app/Ninja/Import/Harvest/ContactTransformer.php b/app/Ninja/Import/Harvest/ContactTransformer.php new file mode 100644 index 000000000000..6baf883c95c2 --- /dev/null +++ b/app/Ninja/Import/Harvest/ContactTransformer.php @@ -0,0 +1,24 @@ +hasClient($data->client)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->client), + 'first_name' => $this->getString($data, 'first_name'), + 'last_name' => $this->getString($data, 'last_name'), + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'office_phone') ?: $this->getString($data, 'mobile_phone'), + ]; + }); + } +} diff --git a/app/Ninja/Import/Harvest/InvoiceTransformer.php b/app/Ninja/Import/Harvest/InvoiceTransformer.php new file mode 100644 index 000000000000..850eeede594c --- /dev/null +++ b/app/Ninja/Import/Harvest/InvoiceTransformer.php @@ -0,0 +1,36 @@ +getClientId($data->client)) { + return false; + } + + if ($this->hasInvoice($data->id)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->client), + 'invoice_number' => $this->getInvoiceNumber($data->id), + 'paid' => (float) $data->paid_amount, + 'po_number' => $this->getString($data, 'po_number'), + 'invoice_date_sql' => $this->getDate($data->issue_date, 'm/d/Y'), + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'subject'), + 'cost' => (float) $data->invoice_amount, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Harvest/PaymentTransformer.php b/app/Ninja/Import/Harvest/PaymentTransformer.php new file mode 100644 index 000000000000..0efd442886cc --- /dev/null +++ b/app/Ninja/Import/Harvest/PaymentTransformer.php @@ -0,0 +1,19 @@ + $data->paid_amount, + 'payment_date_sql' => $this->getDate($data->last_payment_date, 'm/d/Y'), + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Harvest/VendorContactTransformer.php b/app/Ninja/Import/Harvest/VendorContactTransformer.php new file mode 100644 index 000000000000..3aa0b0b36aa2 --- /dev/null +++ b/app/Ninja/Import/Harvest/VendorContactTransformer.php @@ -0,0 +1,24 @@ +hasVendor($data->vendor)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'vendor_id' => $this->getVendorId($data->vendor), + 'first_name' => $data->first_name, + 'last_name' => $data->last_name, + 'email' => $data->email, + 'phone' => $data->office_phone ?: $data->mobile_phone, + ]; + }); + } +} diff --git a/app/Ninja/Import/Harvest/VendorTransformer.php b/app/Ninja/Import/Harvest/VendorTransformer.php new file mode 100644 index 000000000000..efab1e6b66ad --- /dev/null +++ b/app/Ninja/Import/Harvest/VendorTransformer.php @@ -0,0 +1,20 @@ +hasVendor($data->vendor_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->vendor_name, + ]; + }); + } +} diff --git a/app/Ninja/Import/Hiveage/ClientTransformer.php b/app/Ninja/Import/Hiveage/ClientTransformer.php new file mode 100644 index 000000000000..515eb8353562 --- /dev/null +++ b/app/Ninja/Import/Hiveage/ClientTransformer.php @@ -0,0 +1,35 @@ +hasClient($data->name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'name'), + 'contacts' => [ + [ + 'first_name' => $this->getFirstName($data->primary_contact), + 'last_name' => $this->getLastName($data->primary_contactk), + 'email' => $this->getString($data, 'business_email'), + ], + ], + 'address1' => $this->getString($data, 'address_1'), + 'address2' => $this->getString($data, 'address_2'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'state_name'), + 'postal_code' => $this->getString($data, 'zip_code'), + 'work_phone' => $this->getString($data, 'phone'), + 'website' => $this->getString($data, 'website'), + 'country_id' => $this->getCountryId($data->country), + ]; + }); + } +} diff --git a/app/Ninja/Import/Hiveage/InvoiceTransformer.php b/app/Ninja/Import/Hiveage/InvoiceTransformer.php new file mode 100644 index 000000000000..e9054f1b8791 --- /dev/null +++ b/app/Ninja/Import/Hiveage/InvoiceTransformer.php @@ -0,0 +1,36 @@ +getClientId($data->client)) { + return false; + } + + if ($this->hasInvoice($data->statement_no)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->client), + 'invoice_number' => $this->getInvoiceNumber($data->statement_no), + 'paid' => (float) $data->paid_total, + 'invoice_date_sql' => $this->getDate($data->date), + 'due_date_sql' => $this->getDate($data->due_date), + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'summary'), + 'cost' => (float) $data->billed_total, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Hiveage/PaymentTransformer.php b/app/Ninja/Import/Hiveage/PaymentTransformer.php new file mode 100644 index 000000000000..d6232d05bcc9 --- /dev/null +++ b/app/Ninja/Import/Hiveage/PaymentTransformer.php @@ -0,0 +1,19 @@ + $data->paid_total, + 'payment_date_sql' => $this->getDate($data->last_paid_on), + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Hiveage/VendorTransformer.php b/app/Ninja/Import/Hiveage/VendorTransformer.php new file mode 100644 index 000000000000..dec1b62d1ccb --- /dev/null +++ b/app/Ninja/Import/Hiveage/VendorTransformer.php @@ -0,0 +1,35 @@ +hasVendor($data->name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->name, + 'contacts' => [ + [ + 'first_name' => $this->getFirstName($data->primary_contact), + 'last_name' => $this->getLastName($data->primary_contactk), + 'email' => $data->business_email, + ], + ], + 'address1' => $data->address_1, + 'address2' => $data->address_2, + 'city' => $data->city, + 'state' => $data->state_name, + 'postal_code' => $data->zip_code, + 'work_phone' => $data->phone, + 'website' => $data->website, + 'country_id' => $this->getCountryId($data->country), + ]; + }); + } +} diff --git a/app/Ninja/Import/Invoiceable/ClientTransformer.php b/app/Ninja/Import/Invoiceable/ClientTransformer.php new file mode 100644 index 000000000000..7e462ceef9b0 --- /dev/null +++ b/app/Ninja/Import/Invoiceable/ClientTransformer.php @@ -0,0 +1,34 @@ +hasClient($data->client_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'client_name'), + 'work_phone' => $this->getString($data, 'tel'), + 'website' => $this->getString($data, 'website'), + 'address1' => $this->getString($data, 'address'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'state'), + 'postal_code' => $this->getString($data, 'postcode'), + 'country_id' => $this->getCountryIdBy2($data->country), + 'private_notes' => $this->getString($data, 'notes'), + 'contacts' => [ + [ + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'mobile'), + ], + ], + ]; + }); + } +} diff --git a/app/Ninja/Import/Invoiceable/InvoiceTransformer.php b/app/Ninja/Import/Invoiceable/InvoiceTransformer.php new file mode 100644 index 000000000000..f6697a7e90c2 --- /dev/null +++ b/app/Ninja/Import/Invoiceable/InvoiceTransformer.php @@ -0,0 +1,38 @@ +getClientId($data->client_name)) { + return false; + } + + if ($this->hasInvoice($data->ref)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->client_name), + 'invoice_number' => $this->getInvoiceNumber($data->ref), + 'po_number' => $this->getString($data, 'po_number'), + 'invoice_date_sql' => $data->date, + 'due_date_sql' => $data->due_date, + 'invoice_footer' => $this->getString($data, 'footer'), + 'paid' => (float) $data->paid, + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'description'), + 'cost' => (float) $data->total, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Invoiceable/PaymentTransformer.php b/app/Ninja/Import/Invoiceable/PaymentTransformer.php new file mode 100644 index 000000000000..c52494cdc689 --- /dev/null +++ b/app/Ninja/Import/Invoiceable/PaymentTransformer.php @@ -0,0 +1,19 @@ + $data->paid, + 'payment_date_sql' => $data->date_paid, + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Invoiceable/VendorTransformer.php b/app/Ninja/Import/Invoiceable/VendorTransformer.php new file mode 100644 index 000000000000..1ec4a2876884 --- /dev/null +++ b/app/Ninja/Import/Invoiceable/VendorTransformer.php @@ -0,0 +1,34 @@ +hasVendor($data->vendor_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->vendor_name, + 'work_phone' => $data->tel, + 'website' => $data->website, + 'address1' => $data->address, + 'city' => $data->city, + 'state' => $data->state, + 'postal_code' => $data->postcode, + 'country_id' => $this->getCountryIdBy2($data->country), + 'private_notes' => $data->notes, + 'contacts' => [ + [ + 'email' => $data->email, + 'phone' => $data->mobile, + ], + ], + ]; + }); + } +} diff --git a/app/Ninja/Import/Nutcache/ClientTransformer.php b/app/Ninja/Import/Nutcache/ClientTransformer.php new file mode 100644 index 000000000000..74705a597a24 --- /dev/null +++ b/app/Ninja/Import/Nutcache/ClientTransformer.php @@ -0,0 +1,35 @@ +hasClient($data->name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'name'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'stateprovince'), + 'id_number' => $this->getString($data, 'registration_number'), + 'postal_code' => $this->getString($data, 'postalzip_code'), + 'private_notes' => $this->getString($data, 'notes'), + 'work_phone' => $this->getString($data, 'phone'), + 'contacts' => [ + [ + 'first_name' => isset($data->contact_name) ? $this->getFirstName($data->contact_name) : '', + 'last_name' => isset($data->contact_name) ? $this->getLastName($data->contact_name) : '', + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'mobile'), + ], + ], + 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null, + ]; + }); + } +} diff --git a/app/Ninja/Import/Nutcache/InvoiceTransformer.php b/app/Ninja/Import/Nutcache/InvoiceTransformer.php new file mode 100644 index 000000000000..a3e3bc91372d --- /dev/null +++ b/app/Ninja/Import/Nutcache/InvoiceTransformer.php @@ -0,0 +1,39 @@ +getClientId($data->client)) { + return false; + } + + if ($this->hasInvoice($data->document_no)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->client), + 'invoice_number' => $this->getInvoiceNumber($data->document_no), + 'paid' => (float) $data->paid_to_date, + 'po_number' => $this->getString($data, 'purchase_order'), + 'terms' => $this->getString($data, 'terms'), + 'public_notes' => $this->getString($data, 'notes'), + 'invoice_date_sql' => $this->getDate($data->date), + 'due_date_sql' => $this->getDate($data->due_date), + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'description'), + 'cost' => (float) $data->total, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Nutcache/PaymentTransformer.php b/app/Ninja/Import/Nutcache/PaymentTransformer.php new file mode 100644 index 000000000000..04e783361f80 --- /dev/null +++ b/app/Ninja/Import/Nutcache/PaymentTransformer.php @@ -0,0 +1,19 @@ + (float) $data->paid_to_date, + 'payment_date_sql' => $this->getDate($data->date), + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Nutcache/TaskTransformer.php b/app/Ninja/Import/Nutcache/TaskTransformer.php new file mode 100644 index 000000000000..8c1363edcfec --- /dev/null +++ b/app/Ninja/Import/Nutcache/TaskTransformer.php @@ -0,0 +1,29 @@ +hours * 3600); + $timeLogFinish = strtotime($data->date); + $timeLogStart = intval($timeLogFinish - $seconds); + $timeLog[] = []; + $timelog[] = $timeLogStart; + $timelog[] = $timeLogFinish; + $timeLog = json_encode(array($timelog)); + + return [ + 'action' => 'stop', + 'time_log' => $timeLog, + 'description' => $data->task, + ]; + } + +} +*/ \ No newline at end of file diff --git a/app/Ninja/Import/Nutcache/VendorTransformer.php b/app/Ninja/Import/Nutcache/VendorTransformer.php new file mode 100644 index 000000000000..b97f0811906e --- /dev/null +++ b/app/Ninja/Import/Nutcache/VendorTransformer.php @@ -0,0 +1,35 @@ +hasVendor($data->name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->name, + 'city' => isset($data->city) ? $data->city : '', + 'state' => isset($data->city) ? $data->stateprovince : '', + 'id_number' => isset($data->registration_number) ? $data->registration_number : '', + 'postal_code' => isset($data->postalzip_code) ? $data->postalzip_code : '', + 'private_notes' => isset($data->notes) ? $data->notes : '', + 'work_phone' => isset($data->phone) ? $data->phone : '', + 'contacts' => [ + [ + 'first_name' => isset($data->contact_name) ? $this->getFirstName($data->contact_name) : '', + 'last_name' => isset($data->contact_name) ? $this->getLastName($data->contact_name) : '', + 'email' => $data->email, + 'phone' => isset($data->mobile) ? $data->mobile : '', + ], + ], + 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null, + ]; + }); + } +} diff --git a/app/Ninja/Import/Ronin/ClientTransformer.php b/app/Ninja/Import/Ronin/ClientTransformer.php new file mode 100644 index 000000000000..f79523830e99 --- /dev/null +++ b/app/Ninja/Import/Ronin/ClientTransformer.php @@ -0,0 +1,28 @@ +hasClient($data->company)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'company'), + 'work_phone' => $this->getString($data, 'phone'), + 'contacts' => [ + [ + 'first_name' => $this->getFirstName($data->name), + 'last_name' => $this->getLastName($data->name), + 'email' => $this->getString($data, 'email'), + ], + ], + ]; + }); + } +} diff --git a/app/Ninja/Import/Ronin/InvoiceTransformer.php b/app/Ninja/Import/Ronin/InvoiceTransformer.php new file mode 100644 index 000000000000..5a4ff6ce2aba --- /dev/null +++ b/app/Ninja/Import/Ronin/InvoiceTransformer.php @@ -0,0 +1,37 @@ +getClientId($data->client)) { + return false; + } + + if ($this->hasInvoice($data->number)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->client), + 'invoice_number' => $this->getInvoiceNumber($data->number), + 'paid' => (float) $data->total - (float) $data->balance, + 'public_notes' => $this->getString($data, 'subject'), + 'invoice_date_sql' => $data->date_sent, + 'due_date_sql' => $data->date_due, + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'line_item'), + 'cost' => (float) $data->total, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Ronin/PaymentTransformer.php b/app/Ninja/Import/Ronin/PaymentTransformer.php new file mode 100644 index 000000000000..c04101456200 --- /dev/null +++ b/app/Ninja/Import/Ronin/PaymentTransformer.php @@ -0,0 +1,19 @@ + (float) $data->total - (float) $data->balance, + 'payment_date_sql' => $data->date_paid, + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Ronin/VendorTransformer.php b/app/Ninja/Import/Ronin/VendorTransformer.php new file mode 100644 index 000000000000..817de03d6647 --- /dev/null +++ b/app/Ninja/Import/Ronin/VendorTransformer.php @@ -0,0 +1,28 @@ +hasVendor($data->company)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->company, + 'work_phone' => $data->phone, + 'contacts' => [ + [ + 'first_name' => $this->getFirstName($data->name), + 'last_name' => $this->getLastName($data->name), + 'email' => $data->email, + ], + ], + ]; + }); + } +} diff --git a/app/Ninja/Import/Wave/ClientTransformer.php b/app/Ninja/Import/Wave/ClientTransformer.php new file mode 100644 index 000000000000..f76ba9c48a26 --- /dev/null +++ b/app/Ninja/Import/Wave/ClientTransformer.php @@ -0,0 +1,38 @@ +hasClient($data->customer_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'customer_name'), + 'id_number' => $this->getString($data, 'account_number'), + 'work_phone' => $this->getString($data, 'phone'), + 'website' => $this->getString($data, 'website'), + 'address1' => $this->getString($data, 'address_line_1'), + 'address2' => $this->getString($data, 'address_line_2'), + 'city' => $this->getString($data, 'city'), + 'state' => $this->getString($data, 'provincestate'), + 'postal_code' => $this->getString($data, 'postal_codezip_code'), + 'private_notes' => $this->getString($data, 'delivery_instructions'), + 'contacts' => [ + [ + 'first_name' => $this->getString($data, 'contact_first_name'), + 'last_name' => $this->getString($data, 'contact_last_name'), + 'email' => $this->getString($data, 'email'), + 'phone' => $this->getString($data, 'mobile'), + ], + ], + 'country_id' => $this->getCountryId($data->country), + ]; + }); + } +} diff --git a/app/Ninja/Import/Wave/InvoiceTransformer.php b/app/Ninja/Import/Wave/InvoiceTransformer.php new file mode 100644 index 000000000000..b10585aa72bd --- /dev/null +++ b/app/Ninja/Import/Wave/InvoiceTransformer.php @@ -0,0 +1,37 @@ +getClientId($data->customer)) { + return false; + } + + if ($this->hasInvoice($data->invoice_num)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->customer), + 'invoice_number' => $this->getInvoiceNumber($data->invoice_num), + 'po_number' => $this->getString($data, 'po_so'), + 'invoice_date_sql' => $this->getDate($data->invoice_date), + 'due_date_sql' => $this->getDate($data->due_date), + 'paid' => 0, + 'invoice_items' => [ + [ + 'product_key' => $this->getString($data, 'product'), + 'notes' => $this->getString($data, 'description'), + 'cost' => (float) $data->amount, + 'qty' => (float) $data->quantity, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Wave/PaymentTransformer.php b/app/Ninja/Import/Wave/PaymentTransformer.php new file mode 100644 index 000000000000..522fe8ff9238 --- /dev/null +++ b/app/Ninja/Import/Wave/PaymentTransformer.php @@ -0,0 +1,23 @@ +getInvoiceClientId($data->invoice_num)) { + return false; + } + + return new Item($data, function ($data) use ($maps) { + return [ + 'amount' => (float) $data->amount, + 'payment_date_sql' => $this->getDate($data->payment_date), + 'client_id' => $this->getInvoiceClientId($data->invoice_num), + 'invoice_id' => $this->getInvoiceId($data->invoice_num), + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Wave/VendorTransformer.php b/app/Ninja/Import/Wave/VendorTransformer.php new file mode 100644 index 000000000000..f2fe2f43e375 --- /dev/null +++ b/app/Ninja/Import/Wave/VendorTransformer.php @@ -0,0 +1,38 @@ +hasVendor($data->customer_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->customer_name, + 'id_number' => $data->account_number, + 'work_phone' => $data->phone, + 'website' => $data->website, + 'address1' => $data->address_line_1, + 'address2' => $data->address_line_2, + 'city' => $data->city, + 'state' => $data->provincestate, + 'postal_code' => $data->postal_codezip_code, + 'private_notes' => $data->delivery_instructions, + 'contacts' => [ + [ + 'first_name' => $data->contact_first_name, + 'last_name' => $data->contact_last_name, + 'email' => $data->email, + 'phone' => $data->mobile, + ], + ], + 'country_id' => $this->getCountryId($data->country), + ]; + }); + } +} diff --git a/app/Ninja/Import/Zoho/ClientTransformer.php b/app/Ninja/Import/Zoho/ClientTransformer.php new file mode 100644 index 000000000000..689bd1cf1a26 --- /dev/null +++ b/app/Ninja/Import/Zoho/ClientTransformer.php @@ -0,0 +1,37 @@ +hasClient($data->customer_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $this->getString($data, 'customer_name'), + 'id_number' => $this->getString($data, 'customer_id'), + 'work_phone' => $this->getString($data, 'phone'), + 'address1' => $this->getString($data, 'billing_address'), + 'city' => $this->getString($data, 'billing_city'), + 'state' => $this->getString($data, 'billing_state'), + 'postal_code' => $this->getString($data, 'billing_code'), + 'private_notes' => $this->getString($data, 'notes'), + 'website' => $this->getString($data, 'website'), + 'contacts' => [ + [ + 'first_name' => $this->getString($data, 'first_name'), + 'last_name' => $this->getString($data, 'last_name'), + 'email' => $this->getString($data, 'emailid'), + 'phone' => $this->getString($data, 'mobilephone'), + ], + ], + 'country_id' => $this->getCountryId($data->billing_country), + ]; + }); + } +} diff --git a/app/Ninja/Import/Zoho/InvoiceTransformer.php b/app/Ninja/Import/Zoho/InvoiceTransformer.php new file mode 100644 index 000000000000..f6fc3c44a7a1 --- /dev/null +++ b/app/Ninja/Import/Zoho/InvoiceTransformer.php @@ -0,0 +1,37 @@ +getClientId($data->customer_name)) { + return false; + } + + if ($this->hasInvoice($data->invoice_number)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'client_id' => $this->getClientId($data->customer_name), + 'invoice_number' => $this->getInvoiceNumber($data->invoice_number), + 'paid' => (float) $data->total - (float) $data->balance, + 'po_number' => $this->getString($data, 'purchaseorder'), + 'due_date_sql' => $data->due_date, + 'invoice_date_sql' => $data->invoice_date, + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $this->getString($data, 'item_desc'), + 'cost' => (float) $data->total, + 'qty' => 1, + ] + ], + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Zoho/PaymentTransformer.php b/app/Ninja/Import/Zoho/PaymentTransformer.php new file mode 100644 index 000000000000..a8fc74962321 --- /dev/null +++ b/app/Ninja/Import/Zoho/PaymentTransformer.php @@ -0,0 +1,19 @@ + (float) $data->total - (float) $data->balance, + 'payment_date_sql' => $data->last_payment_date, + 'client_id' => $data->client_id, + 'invoice_id' => $data->invoice_id, + ]; + }); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/Zoho/VendorTransformer.php b/app/Ninja/Import/Zoho/VendorTransformer.php new file mode 100644 index 000000000000..811a9f7ff2d9 --- /dev/null +++ b/app/Ninja/Import/Zoho/VendorTransformer.php @@ -0,0 +1,37 @@ +hasVendor($data->customer_name)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'name' => $data->customer_name, + 'id_number' => $data->customer_id, + 'work_phone' => $data->phonek, + 'address1' => $data->billing_address, + 'city' => $data->billing_city, + 'state' => $data->billing_state, + 'postal_code' => $data->billing_code, + 'private_notes' => $data->notes, + 'website' => $data->website, + 'contacts' => [ + [ + 'first_name' => $data->first_name, + 'last_name' => $data->last_name, + 'email' => $data->emailid, + 'phone' => $data->mobilephone, + ], + ], + 'country_id' => $this->getCountryId($data->billing_country), + ]; + }); + } +} diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index f68633e83414..5e75a81120ad 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -1,110 +1,204 @@ load('invitations', 'client', 'account'); + $invoice->load('invitations', 'client.language', 'account'); $entityType = $invoice->getEntityType(); - $view = 'invoice'; - $subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]); - $accountName = $invoice->account->getDisplayName(); - $emailTemplate = $invoice->account->getEmailTemplate($entityType); - $invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()); + $client = $invoice->client; + $account = $invoice->account; - $this->initClosure($invoice); + if ($client->trashed()) { + return trans('texts.email_errors.inactive_client'); + } elseif ($invoice->trashed()) { + return trans('texts.email_errors.inactive_invoice'); + } + + $account->loadLocalizationSettings($client); + $emailTemplate = $account->getEmailTemplate($reminder ?: $entityType); + $emailSubject = $account->getEmailSubject($reminder ?: $entityType); + + $sent = false; + + if ($account->attatchPDF() && !$pdfString) { + $pdfString = $invoice->getPDFString(); + } foreach ($invoice->invitations as $invitation) { - if (!$invitation->user || !$invitation->user->email || $invitation->user->trashed()) { - return false; + $response = $this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString); + if ($response === true) { + $sent = true; } - if (!$invitation->contact || !$invitation->contact->email || $invitation->contact->trashed()) { - return false; - } - - $invitation->sent_date = \Carbon::now()->toDateTimeString(); - $invitation->save(); - - $variables = [ - '$footer' => $invoice->account->getEmailFooter(), - '$link' => $invitation->getLink(), - '$client' => $invoice->client->getDisplayName(), - '$account' => $accountName, - '$contact' => $invitation->contact->getDisplayName(), - '$amount' => $invoiceAmount, - '$advancedRawInvoice->' => '$' - ]; - - // Add variables for available payment types - foreach (Gateway::getPaymentTypeLinks() as $type) { - $variables["\${$type}_link"] = URL::to("/payment/{$invitation->invitation_key}/{$type}"); - } - - $data['body'] = str_replace(array_keys($variables), array_values($variables), $emailTemplate); - $data['body'] = preg_replace_callback('/\{\{\$?(.*)\}\}/', $this->advancedTemplateHandler, $data['body']); - $data['link'] = $invitation->getLink(); - $data['entityType'] = $entityType; - $data['invoice_id'] = $invoice->id; - - $fromEmail = $invitation->user->email; - $response = $this->sendTo($invitation->contact->email, $fromEmail, $accountName, $subject, $view, $data); - - if ($response !== true) { - return $response; - } - - Activity::emailInvoice($invitation); } + + $account->loadLocalizationSettings(); - if (!$invoice->isSent()) { - $invoice->invoice_status_id = INVOICE_STATUS_SENT; - $invoice->save(); + if ($sent === true) { + if ($invoice->is_quote) { + event(new QuoteWasEmailed($invoice)); + } else { + event(new InvoiceWasEmailed($invoice)); + } } - Event::fire(new InvoiceSent($invoice)); - return $response; } - public function sendPaymentConfirmation(Payment $payment) + private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString) { - $invoice = $payment->invoice; - $view = 'payment_confirmation'; - $subject = trans('texts.payment_subject', ['invoice' => $invoice->invoice_number]); - $accountName = $payment->account->getDisplayName(); - $emailTemplate = $invoice->account->getEmailTemplate(ENTITY_PAYMENT); + $client = $invoice->client; + $account = $invoice->account; + + if (Auth::check()) { + $user = Auth::user(); + } else { + $user = $invitation->user; + if ($invitation->user->trashed()) { + $user = $account->users()->orderBy('id')->first(); + } + } + + if (!$user->email || !$user->registered) { + return trans('texts.email_errors.user_unregistered'); + } elseif (!$user->confirmed) { + return trans('texts.email_errors.user_unconfirmed'); + } elseif (!$invitation->contact->email) { + return trans('texts.email_errors.invalid_contact_email'); + } elseif ($invitation->contact->trashed()) { + return trans('texts.email_errors.inactive_contact'); + } $variables = [ - '$footer' => $payment->account->getEmailFooter(), - '$client' => $payment->client->getDisplayName(), - '$account' => $accountName, - '$amount' => Utils::formatMoney($payment->amount, $payment->client->getCurrencyId()) + 'account' => $account, + 'client' => $client, + 'invitation' => $invitation, + 'amount' => $invoice->getRequestedAmount() ]; - $data = ['body' => str_replace(array_keys($variables), array_values($variables), $emailTemplate)]; + $data = [ + 'body' => $this->processVariables($body, $variables), + 'link' => $invitation->getLink(), + 'entityType' => $invoice->getEntityType(), + 'invoiceId' => $invoice->id, + 'invitation' => $invitation, + 'account' => $account, + 'client' => $client, + 'invoice' => $invoice, + ]; + + if ($account->attatchPDF()) { + $data['pdfString'] = $pdfString; + $data['pdfFileName'] = $invoice->getFileName(); + } + + $subject = $this->processVariables($subject, $variables); + $fromEmail = $user->email; + + if ($account->email_design_id == EMAIL_DESIGN_PLAIN) { + $view = ENTITY_INVOICE; + } else { + $view = 'design' . ($account->email_design_id - 1); + } + + $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data); + + if ($response === true) { + return true; + } else { + return $response; + } + } + + public function sendPaymentConfirmation(Payment $payment) + { + $account = $payment->account; + $client = $payment->client; + + $account->loadLocalizationSettings($client); + + $invoice = $payment->invoice; + $accountName = $account->getDisplayName(); + $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT); + $emailSubject = $invoice->account->getEmailSubject(ENTITY_PAYMENT); if ($payment->invitation) { $user = $payment->invitation->user; $contact = $payment->contact; + $invitation = $payment->invitation; } else { $user = $payment->user; - $contact = $payment->client->contacts[0]; + $contact = $client->contacts[0]; + $invitation = $payment->invoice->invitations[0]; + } + + $variables = [ + 'account' => $account, + 'client' => $client, + 'invitation' => $invitation, + 'amount' => $payment->amount, + ]; + + $data = [ + 'body' => $this->processVariables($emailTemplate, $variables), + 'link' => $invitation->getLink(), + 'invoice' => $invoice, + 'client' => $client, + 'account' => $account, + 'payment' => $payment, + 'entityType' => ENTITY_INVOICE, + ]; + + if ($account->attatchPDF()) { + $data['pdfString'] = $invoice->getPDFString(); + $data['pdfFileName'] = $invoice->getFileName(); + } + + $subject = $this->processVariables($emailSubject, $variables); + $data['invoice_id'] = $payment->invoice->id; + + if ($account->email_design_id == EMAIL_DESIGN_PLAIN) { + $view = 'payment_confirmation'; + } else { + $view = 'design' . ($account->email_design_id - 1); } if ($user->email && $contact->email) { $this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data); } + + $account->loadLocalizationSettings(); } public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId) @@ -121,30 +215,53 @@ class ContactMailer extends Mailer } $data = [ - 'account' => trans('texts.email_from'), 'client' => $name, - 'amount' => Utils::formatMoney($amount, 1), + 'amount' => Utils::formatMoney($amount, DEFAULT_CURRENCY, DEFAULT_COUNTRY), 'license' => $license ]; $this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } - private function initClosure($object) + private function processVariables($template, $data) { - $this->advancedTemplateHandler = function($match) use ($object) { - for ($i = 1; $i < count($match); $i++) { - $blobConversion = $match[$i]; + $account = $data['account']; + $client = $data['client']; + $invitation = $data['invitation']; + $invoice = $invitation->invoice; - if (isset($$blobConversion)) { - return $$blobConversion; - } else if (preg_match('/trans\(([\w\.]+)\)/', $blobConversion, $regexTranslation)) { - return trans($regexTranslation[1]); - } else if (strpos($blobConversion, '->') !== false) { - return Utils::stringToObjectResolution($object, $blobConversion); - } + $variables = [ + '$footer' => $account->getEmailFooter(), + '$client' => $client->getDisplayName(), + '$account' => $account->getDisplayName(), + '$contact' => $invitation->contact->getDisplayName(), + '$firstName' => $invitation->contact->first_name, + '$amount' => $account->formatMoney($data['amount'], $client), + '$invoice' => $invoice->invoice_number, + '$quote' => $invoice->invoice_number, + '$link' => $invitation->getLink(), + '$dueDate' => $account->formatDate($invoice->due_date), + '$viewLink' => $invitation->getLink(), + '$viewButton' => HTML::emailViewButton($invitation->getLink(), $invoice->getEntityType()), + '$paymentLink' => $invitation->getLink('payment'), + '$paymentButton' => HTML::emailPaymentButton($invitation->getLink('payment')), + '$customClient1' => $account->custom_client_label1, + '$customClient2' => $account->custom_client_label2, + '$customInvoice1' => $account->custom_invoice_text_label1, + '$customInvoice2' => $account->custom_invoice_text_label2, + ]; - } - }; + // Add variables for available payment types + foreach (Gateway::$paymentTypes as $type) { + $camelType = Gateway::getPaymentTypeName($type); + $type = Utils::toSnakeCase($camelType); + $variables["\${$camelType}Link"] = $invitation->getLink() . "/{$type}"; + $variables["\${$camelType}Button"] = HTML::emailPaymentButton($invitation->getLink('payment') . "/{$type}"); + } + + $str = str_replace(array_keys($variables), array_values($variables), $template); + $str = autolink($str, 100); + + return $str; } } diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index 9995a27c4df5..c30c9a10d74c 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -9,44 +9,86 @@ class Mailer { public function sendTo($toEmail, $fromEmail, $fromName, $subject, $view, $data = []) { - $views = [ - 'emails.'.$view.'_html', - 'emails.'.$view.'_text', - ]; + // check the username is set + if ( ! env('POSTMARK_API_TOKEN') && ! env('MAIL_USERNAME')) { + return trans('texts.invalid_mail_config'); + } + + // don't send emails to dummy addresses + if (stristr($toEmail, '@example.com')) { + return true; + } + + if (isset($_ENV['POSTMARK_API_TOKEN'])) { + $views = 'emails.'.$view.'_html'; + } else { + $views = [ + 'emails.'.$view.'_html', + 'emails.'.$view.'_text', + ]; + } try { - Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) { + $response = Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) { $toEmail = strtolower($toEmail); $replyEmail = $fromEmail; $fromEmail = CONTACT_EMAIL; - if (isset($data['invoice_id'])) { - $invoice = Invoice::with('account')->where('id', '=', $data['invoice_id'])->get()->first(); - if($invoice->account->pdf_email_attachment && file_exists($invoice->getPDFPath())) { - $message->attach( - $invoice->getPDFPath(), - array('as' => $invoice->getFileName(), 'mime' => 'application/pdf') - ); - } - } - $message->to($toEmail) ->from($fromEmail, $fromName) ->replyTo($replyEmail, $fromName) ->subject($subject); + // Attach the PDF to the email + if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) { + $message->attachData($data['pdfString'], $data['pdfFileName']); + } }); - - return true; + + return $this->handleSuccess($response, $data); } catch (Exception $exception) { - if (isset($_ENV['POSTMARK_API_TOKEN'])) { - $response = $exception->getResponse()->getBody()->getContents(); - $response = json_decode($response); - return nl2br($response->Message); - } else { - return $exception->getMessage(); - } + return $this->handleFailure($exception); } } + + private function handleSuccess($response, $data) + { + if (isset($data['invitation'])) { + $invitation = $data['invitation']; + $invoice = $invitation->invoice; + $messageId = false; + + // Track the Postmark message id + if (isset($_ENV['POSTMARK_API_TOKEN']) && $response) { + $json = json_decode((string) $response->getBody()); + $messageId = $json->MessageID; + } + + $invoice->markInvitationSent($invitation, $messageId); + } + + return true; + } + + private function handleFailure($exception) + { + if (isset($_ENV['POSTMARK_API_TOKEN']) && method_exists($exception, 'getResponse')) { + $response = $exception->getResponse()->getBody()->getContents(); + $response = json_decode($response); + $emailError = nl2br($response->Message); + } else { + $emailError = $exception->getMessage(); + } + + Utils::logError("Email Error: $emailError"); + + if (isset($data['invitation'])) { + $invitation = $data['invitation']; + $invitation->email_error = $emailError; + $invitation->save(); + } + + return $emailError; + } } diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php index 3f2b4290a7eb..ce0c5383410d 100644 --- a/app/Ninja/Mailers/UserMailer.php +++ b/app/Ninja/Mailers/UserMailer.php @@ -2,6 +2,7 @@ use Utils; +use App\Models\Invitation; use App\Models\Invoice; use App\Models\Payment; use App\Models\User; @@ -41,22 +42,52 @@ class UserMailer extends Mailer $entityType = $notificationType == 'approved' ? ENTITY_QUOTE : ENTITY_INVOICE; $view = "{$entityType}_{$notificationType}"; + $account = $user->account; + $client = $invoice->client; $data = [ 'entityType' => $entityType, - 'clientName' => $invoice->client->getDisplayName(), - 'accountName' => $invoice->account->getDisplayName(), + 'clientName' => $client->getDisplayName(), + 'accountName' => $account->getDisplayName(), 'userName' => $user->getDisplayName(), - 'invoiceAmount' => Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()), + 'invoiceAmount' => $account->formatMoney($invoice->getRequestedAmount(), $client), 'invoiceNumber' => $invoice->invoice_number, 'invoiceLink' => SITE_URL."/{$entityType}s/{$invoice->public_id}", + 'account' => $account, ]; if ($payment) { - $data['paymentAmount'] = Utils::formatMoney($payment->amount, $invoice->client->getCurrencyId()); + $data['paymentAmount'] = $account->formatMoney($payment->amount, $client); } - $subject = trans("texts.notification_{$entityType}_{$notificationType}_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->getDisplayName()]); + $subject = trans("texts.notification_{$entityType}_{$notificationType}_subject", [ + 'invoice' => $invoice->invoice_number, + 'client' => $client->getDisplayName() + ]); + + $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); + } + + public function sendEmailBounced(Invitation $invitation) + { + $user = $invitation->user; + $account = $user->account; + $invoice = $invitation->invoice; + $entityType = $invoice->getEntityType(); + + if (!$user->email) { + return; + } + + $subject = trans("texts.notification_{$entityType}_bounced_subject", ['invoice' => $invoice->invoice_number]); + $view = 'email_bounced'; + $data = [ + 'userName' => $user->getDisplayName(), + 'emailError' => $invitation->email_error, + 'entityType' => $entityType, + 'contactName' => $invitation->contact->getDisplayName(), + 'invoiceNumber' => $invoice->invoice_number, + ]; $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } diff --git a/app/Ninja/Presenters/AccountPresenter.php b/app/Ninja/Presenters/AccountPresenter.php new file mode 100644 index 000000000000..dc9cacbb8aa7 --- /dev/null +++ b/app/Ninja/Presenters/AccountPresenter.php @@ -0,0 +1,24 @@ +entity->name ?: trans('texts.untitled_account'); + } + + public function website() + { + return Utils::addHttp($this->entity->website); + } + + public function currencyCode() + { + $currencyId = $this->entity->getCurrencyId(); + $currency = Utils::getFromCache($currencyId, 'currencies'); + return $currency->code; + } +} \ No newline at end of file diff --git a/app/Ninja/Presenters/ClientPresenter.php b/app/Ninja/Presenters/ClientPresenter.php new file mode 100644 index 000000000000..bb6e7db0657b --- /dev/null +++ b/app/Ninja/Presenters/ClientPresenter.php @@ -0,0 +1,12 @@ +entity->country ? $this->entity->country->name : ''; + } +} \ No newline at end of file diff --git a/app/Ninja/Presenters/CreditPresenter.php b/app/Ninja/Presenters/CreditPresenter.php new file mode 100644 index 000000000000..7e38205b1067 --- /dev/null +++ b/app/Ninja/Presenters/CreditPresenter.php @@ -0,0 +1,17 @@ +entity->client ? $this->entity->client->getDisplayName() : ''; + } + + public function credit_date() + { + return Utils::fromSqlDate($this->entity->credit_date); + } +} \ No newline at end of file diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php new file mode 100644 index 000000000000..9cede24d039e --- /dev/null +++ b/app/Ninja/Presenters/ExpensePresenter.php @@ -0,0 +1,23 @@ +entity->vendor ? $this->entity->vendor->getDisplayName() : ''; + } + + public function expense_date() + { + return Utils::fromSqlDate($this->entity->expense_date); + } + + public function converted_amount() + { + return round($this->entity->amount * $this->entity->exchange_rate, 2); + } +} \ No newline at end of file diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php new file mode 100644 index 000000000000..fb292a8859b9 --- /dev/null +++ b/app/Ninja/Presenters/InvoicePresenter.php @@ -0,0 +1,58 @@ +entity->client ? $this->entity->client->getDisplayName() : ''; + } + + public function user() + { + return $this->entity->user->getDisplayName(); + } + + public function balanceDueLabel() + { + if ($this->entity->partial) { + return 'amount_due'; + } elseif ($this->entity->is_quote) { + return 'total'; + } else { + return 'balance_due'; + } + } + + // https://schema.org/PaymentStatusType + public function paymentStatus() + { + if ( ! $this->entity->balance) { + return 'PaymentComplete'; + } elseif ($this->entity->isOverdue()) { + return 'PaymentPastDue'; + } else { + return 'PaymentDue'; + } + } + + public function status() + { + $status = $this->entity->invoice_status ? $this->entity->invoice_status->name : 'draft'; + $status = strtolower($status); + return trans("texts.status_{$status}"); + } + + public function invoice_date() + { + return Utils::fromSqlDate($this->entity->invoice_date); + } + + public function due_date() + { + return Utils::fromSqlDate($this->entity->due_date); + } + +} \ No newline at end of file diff --git a/app/Ninja/Presenters/PaymentPresenter.php b/app/Ninja/Presenters/PaymentPresenter.php new file mode 100644 index 000000000000..a0a58663e5a7 --- /dev/null +++ b/app/Ninja/Presenters/PaymentPresenter.php @@ -0,0 +1,27 @@ +entity->client ? $this->entity->client->getDisplayName() : ''; + } + + public function payment_date() + { + return Utils::fromSqlDate($this->entity->payment_date); + } + + public function method() + { + if ($this->entity->account_gateway) { + return $this->entity->account_gateway->gateway->name; + } elseif ($this->entity->payment_type) { + return $this->entity->payment_type->name; + } + } + +} \ No newline at end of file diff --git a/app/Ninja/Presenters/TaskPresenter.php b/app/Ninja/Presenters/TaskPresenter.php new file mode 100644 index 000000000000..09b860a1a2bc --- /dev/null +++ b/app/Ninja/Presenters/TaskPresenter.php @@ -0,0 +1,39 @@ +entity->client ? $this->entity->client->getDisplayName() : ''; + } + + public function user() + { + return $this->entity->user->getDisplayName(); + } + + public function times($account) + { + $parts = json_decode($this->entity->time_log) ?: []; + $times = []; + + foreach ($parts as $part) { + $start = $part[0]; + if (count($part) == 1 || !$part[1]) { + $end = time(); + } else { + $end = $part[1]; + } + + $start = $account->formatDateTime("@{$start}"); + $end = $account->formatTime("@{$end}"); + + $times[] = "### {$start} - {$end}"; + } + + return implode("\n", $times); + } +} \ No newline at end of file diff --git a/app/Ninja/Presenters/VendorPresenter.php b/app/Ninja/Presenters/VendorPresenter.php new file mode 100644 index 000000000000..b3da402bec40 --- /dev/null +++ b/app/Ninja/Presenters/VendorPresenter.php @@ -0,0 +1,12 @@ +entity->country ? $this->entity->country->name : ''; + } +} \ No newline at end of file diff --git a/app/Ninja/Repositories/AccountGatewayRepository.php b/app/Ninja/Repositories/AccountGatewayRepository.php new file mode 100644 index 000000000000..b61f854f2dc5 --- /dev/null +++ b/app/Ninja/Repositories/AccountGatewayRepository.php @@ -0,0 +1,24 @@ +join('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') + ->where('account_gateways.deleted_at', '=', null) + ->where('account_gateways.account_id', '=', $accountId) + ->select('account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); + } +} diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 593d113072c7..1fd6c7117dcc 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -6,6 +6,7 @@ use Session; use Utils; use DB; use stdClass; +use Validator; use Schema; use App\Models\AccountGateway; use App\Models\Invitation; @@ -17,6 +18,7 @@ use App\Models\Contact; use App\Models\Account; use App\Models\User; use App\Models\UserAccount; +use App\Models\AccountToken; class AccountRepository { @@ -26,8 +28,14 @@ class AccountRepository $account->ip = Request::getClientIp(); $account->account_key = str_random(RANDOM_KEY_LENGTH); - if (Session::has(SESSION_LOCALE)) { - $locale = Session::get(SESSION_LOCALE); + // Track referal code + if ($referralCode = Session::get(SESSION_REFERRAL_CODE)) { + if ($user = User::whereReferralCode($referralCode)->first()) { + $account->referral_user_id = $user->id; + } + } + + if ($locale = Session::get(SESSION_LOCALE)) { if ($language = Language::whereLocale($locale)->first()) { $account->language_id = $language->id; } @@ -43,6 +51,9 @@ class AccountRepository $user->first_name = $firstName; $user->last_name = $lastName; $user->email = $user->username = $email; + if (!$password) { + $password = str_random(RANDOM_KEY_LENGTH); + } $user->password = bcrypt($password); } @@ -129,7 +140,7 @@ class AccountRepository $invoice->user_id = $account->users()->first()->id; $invoice->public_id = $publicId; $invoice->client_id = $client->id; - $invoice->invoice_number = $account->getNextInvoiceNumber(); + $invoice->invoice_number = $account->getNextInvoiceNumber($invoice); $invoice->invoice_date = date_create()->format('Y-m-d'); $invoice->amount = PRO_PLAN_PRICE; $invoice->balance = PRO_PLAN_PRICE; @@ -188,7 +199,7 @@ class AccountRepository $accountGateway->user_id = $user->id; $accountGateway->gateway_id = NINJA_GATEWAY_ID; $accountGateway->public_id = 1; - $accountGateway->config = NINJA_GATEWAY_CONFIG; + $accountGateway->setConfig(json_decode(env(NINJA_GATEWAY_CONFIG))); $account->account_gateways()->save($accountGateway); } @@ -206,7 +217,7 @@ class AccountRepository $client->public_id = $account->id; $client->user_id = $ninjaAccount->users()->first()->id; $client->currency_id = 1; - foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone'] as $field) { + foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id'] as $field) { $client->$field = $account->$field; } $ninjaAccount->clients()->save($client); @@ -225,9 +236,52 @@ class AccountRepository return $client; } - public function registerUser($user) + public function findByKey($key) { - $url = (Utils::isNinjaDev() ? '' : NINJA_APP_URL) . '/signup/register'; + $account = Account::whereAccountKey($key) + ->with('clients.invoices.invoice_items', 'clients.contacts') + ->firstOrFail(); + + return $account; + } + + public function unlinkUserFromOauth($user) + { + $user->oauth_provider_id = null; + $user->oauth_user_id = null; + $user->save(); + } + + public function updateUserFromOauth($user, $firstName, $lastName, $email, $providerId, $oauthUserId) + { + if (!$user->registered) { + $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id']; + $validator = Validator::make(['email' => $email], $rules); + if ($validator->fails()) { + $messages = $validator->messages(); + return $messages->first('email'); + } + + $user->email = $email; + $user->first_name = $firstName; + $user->last_name = $lastName; + $user->registered = true; + } + + $user->oauth_provider_id = $providerId; + $user->oauth_user_id = $oauthUserId; + $user->save(); + + return true; + } + + public function registerNinjaUser($user) + { + if ($user->email == TEST_USERNAME) { + return false; + } + + $url = (Utils::isNinjaDev() ? SITE_URL : NINJA_APP_URL) . '/signup/register'; $data = ''; $fields = [ 'first_name' => urlencode($user->first_name), @@ -244,10 +298,29 @@ class AccountRepository curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, count($fields)); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch); } + public function findUserByOauth($providerId, $oauthUserId) + { + return User::where('oauth_user_id', $oauthUserId) + ->where('oauth_provider_id', $providerId) + ->first(); + } + + public function findUsers($user, $with = null) + { + $accounts = $this->findUserAccounts($user->id); + + if ($accounts) { + return $this->getUserAccounts($accounts, $with); + } else { + return [$user]; + } + } + public function findUserAccounts($userId1, $userId2 = false) { if (!Schema::hasTable('user_accounts')) { @@ -271,7 +344,8 @@ class AccountRepository return $query->first(['id', 'user_id1', 'user_id2', 'user_id3', 'user_id4', 'user_id5']); } - public function prepareUsersData($record) { + public function getUserAccounts($record, $with = null) + { if (!$record) { return false; } @@ -285,8 +359,22 @@ class AccountRepository } $users = User::with('account') - ->whereIn('id', $userIds) - ->get(); + ->whereIn('id', $userIds); + + if ($with) { + $users->with($with); + } + + return $users->get(); + } + + public function prepareUsersData($record) + { + if (!$record) { + return false; + } + + $users = $this->getUserAccounts($record); $data = []; foreach ($users as $user) { @@ -297,7 +385,7 @@ class AccountRepository $item->account_id = $user->account->id; $item->account_name = $user->account->getDisplayName(); $item->pro_plan_paid = $user->account->pro_plan_paid; - $item->logo_path = file_exists($user->account->getLogoPath()) ? $user->account->getLogoPath() : null; + $item->logo_path = $user->account->hasLogo() ? $user->account->getLogoPath() : null; $data[] = $item; } @@ -386,4 +474,46 @@ class AccountRepository $userAccount->save(); } } + + public function findWithReminders() + { + return Account::whereRaw('enable_reminder1 = 1 OR enable_reminder2 = 1 OR enable_reminder3 = 1')->get(); + } + + public function getReferralCode() + { + do { + $code = strtoupper(str_random(8)); + $match = User::whereReferralCode($code) + ->withTrashed() + ->first(); + } while ($match); + + return $code; + } + + public function createTokens($user, $name) + { + $name = trim($name) ?: 'TOKEN'; + $users = $this->findUsers($user); + + foreach ($users as $user) { + if ($token = AccountToken::whereUserId($user->id)->whereName($name)->first()) { + continue; + } + + $token = AccountToken::createNew($user); + $token->name = $name; + $token->token = str_random(RANDOM_KEY_LENGTH); + $token->save(); + } + } + + public function getUserAccountId($account) + { + $user = $account->users()->first(); + $userAccount = $this->findUserAccounts($user->id); + + return $userAccount ? $userAccount->id : false; + } } diff --git a/app/Ninja/Repositories/ActivityRepository.php b/app/Ninja/Repositories/ActivityRepository.php new file mode 100644 index 000000000000..b51c8bbf2997 --- /dev/null +++ b/app/Ninja/Repositories/ActivityRepository.php @@ -0,0 +1,104 @@ +invoice->client; + } else { + $client = $entity->client; + } + + // init activity and copy over context + $activity = self::getBlank($altEntity ?: $client); + $activity = Utils::copyContext($activity, $entity); + $activity = Utils::copyContext($activity, $altEntity); + + $activity->client_id = $client->id; + $activity->activity_type_id = $activityTypeId; + $activity->adjustment = $balanceChange; + $activity->balance = $client->balance + $balanceChange; + + $keyField = $entity->getKeyField(); + $activity->$keyField = $entity->id; + + $activity->ip = Request::getClientIp(); + $activity->save(); + + $client->updateBalances($balanceChange, $paidToDateChange); + + return $activity; + } + + private function getBlank($entity) + { + $activity = new Activity(); + + if (Auth::check() && Auth::user()->account_id == $entity->account_id) { + $activity->user_id = Auth::user()->id; + $activity->account_id = Auth::user()->account_id; + } else { + $activity->user_id = $entity->user_id; + $activity->account_id = $entity->account_id; + + if ( ! $entity instanceof Invitation) { + $activity->is_system = true; + } + } + + $activity->token_id = session('token_id'); + + return $activity; + } + + public function findByClientId($clientId) + { + return DB::table('activities') + ->join('accounts', 'accounts.id', '=', 'activities.account_id') + ->join('users', 'users.id', '=', 'activities.user_id') + ->join('clients', 'clients.id', '=', 'activities.client_id') + ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') + ->leftJoin('invoices', 'invoices.id', '=', 'activities.invoice_id') + ->leftJoin('payments', 'payments.id', '=', 'activities.payment_id') + ->leftJoin('credits', 'credits.id', '=', 'activities.credit_id') + ->where('clients.id', '=', $clientId) + ->where('contacts.is_primary', '=', 1) + ->whereNull('contacts.deleted_at') + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'activities.id', + 'activities.created_at', + 'activities.contact_id', + 'activities.activity_type_id', + 'activities.is_system', + 'activities.balance', + 'activities.adjustment', + 'users.first_name as user_first_name', + 'users.last_name as user_last_name', + 'users.email as user_email', + 'invoices.invoice_number as invoice', + 'invoices.public_id as invoice_public_id', + 'invoices.is_recurring', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'contacts.id as contact', + 'contacts.first_name as first_name', + 'contacts.last_name as last_name', + 'contacts.email as email', + 'payments.transaction_reference as payment', + 'credits.amount as credit' + ); + } + +} \ No newline at end of file diff --git a/app/Ninja/Repositories/BankAccountRepository.php b/app/Ninja/Repositories/BankAccountRepository.php new file mode 100644 index 000000000000..5ab3148e381b --- /dev/null +++ b/app/Ninja/Repositories/BankAccountRepository.php @@ -0,0 +1,24 @@ +join('banks', 'banks.id', '=', 'bank_accounts.bank_id') + ->where('bank_accounts.deleted_at', '=', null) + ->where('bank_accounts.account_id', '=', $accountId) + ->select('bank_accounts.public_id', 'banks.name as bank_name', 'bank_accounts.deleted_at', 'banks.bank_library_id'); + } +} diff --git a/app/Ninja/Repositories/BaseRepository.php b/app/Ninja/Repositories/BaseRepository.php new file mode 100644 index 000000000000..bec95fb96921 --- /dev/null +++ b/app/Ninja/Repositories/BaseRepository.php @@ -0,0 +1,73 @@ +getClassName(); + return new $className(); + } + + private function getEventClass($entity, $type) + { + return 'App\Events\\' . ucfirst($entity->getEntityType()) . 'Was' . $type; + } + + public function archive($entity) + { + $entity->delete(); + + $className = $this->getEventClass($entity, 'Archived'); + + if (class_exists($className)) { + event(new $className($entity)); + } + } + + public function restore($entity) + { + $fromDeleted = false; + $entity->restore(); + + if ($entity->is_deleted) { + $fromDeleted = true; + $entity->is_deleted = false; + $entity->save(); + } + + $className = $this->getEventClass($entity, 'Restored'); + + if (class_exists($className)) { + event(new $className($entity, $fromDeleted)); + } + } + + public function delete($entity) + { + $entity->is_deleted = true; + $entity->save(); + + $entity->delete(); + + $className = $this->getEventClass($entity, 'Deleted'); + + if (class_exists($className)) { + event(new $className($entity)); + } + } + + public function findByPublicIds($ids) + { + return $this->getInstance()->scope($ids)->get(); + } + + public function findByPublicIdsWithTrashed($ids) + { + return $this->getInstance()->scope($ids)->withTrashed()->get(); + } +} diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index ab0baacda59f..3e43d8f34f5f 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -1,19 +1,50 @@ with('user', 'contacts', 'country') + ->withTrashed() + ->where('is_deleted', '=', false) + ->get(); + } + public function find($filter = null) { - $query = \DB::table('clients') + $query = DB::table('clients') + ->join('accounts', 'accounts.id', '=', 'clients.account_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') ->where('clients.account_id', '=', \Auth::user()->account_id) ->where('contacts.is_primary', '=', true) ->where('contacts.deleted_at', '=', null) - ->select('clients.public_id', 'clients.name', 'contacts.first_name', 'contacts.last_name', 'clients.balance', 'clients.last_login', 'clients.created_at', 'clients.work_phone', 'contacts.email', 'clients.currency_id', 'clients.deleted_at', 'clients.is_deleted'); + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'clients.public_id', + 'clients.name', + 'contacts.first_name', + 'contacts.last_name', + 'clients.balance', + 'clients.last_login', + 'clients.created_at', + 'clients.work_phone', + 'contacts.email', + 'clients.deleted_at', + 'clients.is_deleted' + ); if (!\Session::get('show_trash:client')) { $query->where('clients.deleted_at', '=', null); @@ -30,176 +61,42 @@ class ClientRepository return $query; } - - public function getErrors($data) + + public function save($data) { - $contact = isset($data['contacts']) ? (array) $data['contacts'][0] : (isset($data['contact']) ? $data['contact'] : []); - $validator = \Validator::make($contact, [ - 'email' => 'email|required_without:first_name', - 'first_name' => 'required_without:email', - ]); - if ($validator->fails()) { - return $validator->messages(); - } + $publicId = isset($data['public_id']) ? $data['public_id'] : false; - return false; - } - - public function save($publicId, $data, $notify = true) - { - if (!$publicId || $publicId == "-1") { + if (!$publicId || $publicId == '-1') { $client = Client::createNew(); - $contact = Contact::createNew(); - $contact->is_primary = true; - $contact->send_invoice = true; } else { $client = Client::scope($publicId)->with('contacts')->firstOrFail(); - $contact = $client->contacts()->where('is_primary', '=', true)->firstOrFail(); - } - - if (isset($data['name'])) { - $client->name = trim($data['name']); - } - if (isset($data['id_number'])) { - $client->id_number = trim($data['id_number']); - } - if (isset($data['vat_number'])) { - $client->vat_number = trim($data['vat_number']); - } - if (isset($data['work_phone'])) { - $client->work_phone = trim($data['work_phone']); - } - if (isset($data['custom_value1'])) { - $client->custom_value1 = trim($data['custom_value1']); - } - if (isset($data['custom_value2'])) { - $client->custom_value2 = trim($data['custom_value2']); - } - if (isset($data['address1'])) { - $client->address1 = trim($data['address1']); - } - if (isset($data['address2'])) { - $client->address2 = trim($data['address2']); - } - if (isset($data['city'])) { - $client->city = trim($data['city']); - } - if (isset($data['state'])) { - $client->state = trim($data['state']); - } - if (isset($data['postal_code'])) { - $client->postal_code = trim($data['postal_code']); - } - if (isset($data['country_id'])) { - $client->country_id = $data['country_id'] ? $data['country_id'] : null; - } - if (isset($data['private_notes'])) { - $client->private_notes = trim($data['private_notes']); - } - if (isset($data['size_id'])) { - $client->size_id = $data['size_id'] ? $data['size_id'] : null; - } - if (isset($data['industry_id'])) { - $client->industry_id = $data['industry_id'] ? $data['industry_id'] : null; - } - if (isset($data['currency_id'])) { - $client->currency_id = $data['currency_id'] ? $data['currency_id'] : null; - } - if (isset($data['payment_terms'])) { - $client->payment_terms = $data['payment_terms']; - } - if (isset($data['website'])) { - $client->website = trim($data['website']); } + $client->fill($data); $client->save(); - $isPrimary = true; + /* + if ( ! isset($data['contact']) && ! isset($data['contacts'])) { + return $client; + } + */ + + $first = true; + $contacts = isset($data['contact']) ? [$data['contact']] : $data['contacts']; $contactIds = []; - if (isset($data['contact'])) { - $info = $data['contact']; - if (isset($info['email'])) { - $contact->email = trim($info['email']); - } - if (isset($info['first_name'])) { - $contact->first_name = trim($info['first_name']); - } - if (isset($info['last_name'])) { - $contact->last_name = trim($info['last_name']); - } - if (isset($info['phone'])) { - $contact->phone = trim($info['phone']); - } - $contact->is_primary = true; - $contact->send_invoice = true; - $client->contacts()->save($contact); - } else { - foreach ($data['contacts'] as $record) { - $record = (array) $record; - - if ($publicId != "-1" && isset($record['public_id']) && $record['public_id']) { - $contact = Contact::scope($record['public_id'])->firstOrFail(); - } else { - $contact = Contact::createNew(); - } - - if (isset($record['email'])) { - $contact->email = trim($record['email']); - } - if (isset($record['first_name'])) { - $contact->first_name = trim($record['first_name']); - } - if (isset($record['last_name'])) { - $contact->last_name = trim($record['last_name']); - } - if (isset($record['phone'])) { - $contact->phone = trim($record['phone']); - } - $contact->is_primary = $isPrimary; - $contact->send_invoice = isset($record['send_invoice']) ? $record['send_invoice'] : true; - $isPrimary = false; - - $client->contacts()->save($contact); - $contactIds[] = $contact->public_id; - } - - foreach ($client->contacts as $contact) { - if (!in_array($contact->public_id, $contactIds)) { - $contact->delete(); - } - } + foreach ($contacts as $contact) { + $contact = $client->addContact($contact, $first); + $contactIds[] = $contact->public_id; + $first = false; } - $client->save(); - - if (!$publicId || $publicId == "-1") { - Activity::createClient($client, $notify); + foreach ($client->contacts as $contact) { + if (!in_array($contact->public_id, $contactIds)) { + $contact->delete(); + } } return $client; } - - public function bulk($ids, $action) - { - $clients = Client::withTrashed()->scope($ids)->get(); - - foreach ($clients as $client) { - if ($action == 'restore') { - $client->restore(); - - $client->is_deleted = false; - $client->save(); - } else { - if ($action == 'delete') { - $client->is_deleted = true; - $client->save(); - } - - $client->delete(); - } - } - - return count($clients); - } } diff --git a/app/Ninja/Repositories/ContactRepository.php b/app/Ninja/Repositories/ContactRepository.php new file mode 100644 index 000000000000..49b73e91a664 --- /dev/null +++ b/app/Ninja/Repositories/ContactRepository.php @@ -0,0 +1,26 @@ +send_invoice = true; + $contact->client_id = $data['client_id']; + $contact->is_primary = Contact::scope()->where('client_id', '=', $contact->client_id)->count() == 0; + } else { + $contact = Contact::scope($publicId)->firstOrFail(); + } + + $contact->fill($data); + $contact->save(); + + return $contact; + } +} \ No newline at end of file diff --git a/app/Ninja/Repositories/CreditRepository.php b/app/Ninja/Repositories/CreditRepository.php index 700467620294..1c33cb19e41d 100644 --- a/app/Ninja/Repositories/CreditRepository.php +++ b/app/Ninja/Repositories/CreditRepository.php @@ -1,20 +1,44 @@ join('accounts', 'accounts.id', '=', 'credits.account_id') ->join('clients', 'clients.id', '=', 'credits.client_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') ->where('clients.account_id', '=', \Auth::user()->account_id) ->where('clients.deleted_at', '=', null) + ->where('contacts.deleted_at', '=', null) ->where('contacts.is_primary', '=', true) - ->select('credits.public_id', 'clients.name as client_name', 'clients.public_id as client_public_id', 'credits.amount', 'credits.balance', 'credits.credit_date', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'credits.private_notes', 'credits.deleted_at', 'credits.is_deleted'); + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'credits.public_id', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'credits.amount', + 'credits.balance', + 'credits.credit_date', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'credits.private_notes', + 'credits.deleted_at', + 'credits.is_deleted' + ); if ($clientPublicId) { $query->where('clients.public_id', '=', $clientPublicId); @@ -33,8 +57,10 @@ class CreditRepository return $query; } - public function save($publicId = null, $input) + public function save($input) { + $publicId = isset($data['public_id']) ? $data['public_id'] : false; + if ($publicId) { $credit = Credit::scope($publicId)->firstOrFail(); } else { @@ -50,28 +76,4 @@ class CreditRepository return $credit; } - - public function bulk($ids, $action) - { - if (!$ids) { - return 0; - } - - $credits = Credit::withTrashed()->scope($ids)->get(); - - foreach ($credits as $credit) { - if ($action == 'restore') { - $credit->restore(); - } else { - if ($action == 'delete') { - $credit->is_deleted = true; - $credit->save(); - } - - $credit->delete(); - } - } - - return count($credits); - } } diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php new file mode 100644 index 000000000000..3c65f1c2520b --- /dev/null +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -0,0 +1,161 @@ +with('user') + ->withTrashed() + ->where('is_deleted', '=', false) + ->get(); + } + + public function findVendor($vendorPublicId) + { + $accountid = \Auth::user()->account_id; + $query = DB::table('expenses') + ->join('accounts', 'accounts.id', '=', 'expenses.account_id') + ->where('expenses.account_id', '=', $accountid) + ->where('expenses.vendor_id', '=', $vendorPublicId) + ->select( + 'expenses.id', + 'expenses.expense_date', + 'expenses.amount', + 'expenses.public_notes', + 'expenses.public_id', + 'expenses.deleted_at', + 'expenses.should_be_invoiced', + 'expenses.created_at' + ); + + return $query; + } + + public function find($filter = null) + { + $accountid = \Auth::user()->account_id; + $query = DB::table('expenses') + ->join('accounts', 'accounts.id', '=', 'expenses.account_id') + ->leftjoin('clients', 'clients.id', '=', 'expenses.client_id') + ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') + ->leftjoin('vendors', 'vendors.id', '=', 'expenses.vendor_id') + ->leftJoin('invoices', 'invoices.id', '=', 'expenses.invoice_id') + ->where('expenses.account_id', '=', $accountid) + ->where('contacts.deleted_at', '=', null) + ->where('vendors.deleted_at', '=', null) + ->where('clients.deleted_at', '=', null) + ->where(function ($query) { + $query->where('contacts.is_primary', '=', true) + ->orWhere('contacts.is_primary', '=', null); + }) + ->select( + 'expenses.account_id', + 'expenses.amount', + 'expenses.currency_id', + 'expenses.deleted_at', + 'expenses.exchange_rate', + 'expenses.expense_date', + 'expenses.id', + 'expenses.is_deleted', + 'expenses.private_notes', + 'expenses.public_id', + 'expenses.invoice_id', + 'expenses.public_notes', + 'expenses.should_be_invoiced', + 'expenses.vendor_id', + 'invoices.public_id as invoice_public_id', + 'accounts.country_id as account_country_id', + 'accounts.currency_id as account_currency_id', + 'vendors.name as vendor_name', + 'vendors.public_id as vendor_public_id', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'contacts.first_name', + 'contacts.email', + 'contacts.last_name', + 'clients.country_id as client_country_id' + ); + + $showTrashed = \Session::get('show_trash:expense'); + + if (!$showTrashed) { + $query->where('expenses.deleted_at', '=', null); + } + + if ($filter) { + $query->where(function ($query) use ($filter) { + $query->where('expenses.public_notes', 'like', '%'.$filter.'%'); + }); + } + + return $query; + } + + public function save($input) + { + $publicId = isset($input['public_id']) ? $input['public_id'] : false; + + if ($publicId) { + $expense = Expense::scope($publicId)->firstOrFail(); + } else { + $expense = Expense::createNew(); + } + + // First auto fill + $expense->fill($input); + + $expense->expense_date = Utils::toSqlDate($input['expense_date']); + $expense->private_notes = trim($input['private_notes']); + $expense->public_notes = trim($input['public_notes']); + $expense->should_be_invoiced = isset($input['should_be_invoiced']) || $expense->client_id ? true : false; + + if (! $expense->currency_id) { + $expense->currency_id = \Auth::user()->account->getCurrencyId(); + } + + $rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1; + $expense->exchange_rate = round($rate, 4); + $expense->amount = round(Utils::parseFloat($input['amount']), 2); + + $expense->save(); + + return $expense; + } + + public function bulk($ids, $action) + { + $expenses = Expense::withTrashed()->scope($ids)->get(); + + foreach ($expenses as $expense) { + if ($action == 'restore') { + $expense->restore(); + + $expense->is_deleted = false; + $expense->save(); + } else { + if ($action == 'delete') { + $expense->is_deleted = true; + $expense->save(); + } + + $expense->delete(); + } + } + + return count($tasks); + } +} diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index df73c6f6a1e6..9c1a6dcc0356 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -1,18 +1,42 @@ -paymentService = $paymentService; + } + + public function all() + { + return Invoice::scope() + ->with('user', 'client.contacts', 'invoice_status') + ->withTrashed() + ->where('is_quote', '=', false) + ->where('is_recurring', '=', false) + ->get(); + } + public function getInvoices($accountId, $clientPublicId = false, $entityType = ENTITY_INVOICE, $filter = false) { - $query = \DB::table('invoices') + $query = DB::table('invoices') + ->join('accounts', 'accounts.id', '=', 'invoices.account_id') ->join('clients', 'clients.id', '=', 'invoices.client_id') ->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') @@ -21,7 +45,28 @@ class InvoiceRepository ->where('contacts.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) ->where('contacts.is_primary', '=', true) - ->select('clients.public_id as client_public_id', 'invoice_number', 'invoice_status_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'quote_id', 'quote_invoice_id', 'invoices.deleted_at', 'invoices.is_deleted', 'invoices.partial'); + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'clients.public_id as client_public_id', + 'invoice_number', + 'invoice_status_id', + 'clients.name as client_name', + 'invoices.public_id', + 'invoices.amount', + 'invoices.balance', + 'invoices.invoice_date', + 'invoices.due_date', + 'invoice_statuses.name as invoice_status_name', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'invoices.quote_id', + 'invoices.quote_invoice_id', + 'invoices.deleted_at', + 'invoices.is_deleted', + 'invoices.partial' + ); if (!\Session::get('show_trash:'.$entityType)) { $query->where('invoices.deleted_at', '=', null); @@ -47,7 +92,8 @@ class InvoiceRepository public function getRecurringInvoices($accountId, $clientPublicId = false, $filter = false) { - $query = \DB::table('invoices') + $query = DB::table('invoices') + ->join('accounts', 'accounts.id', '=', 'invoices.account_id') ->join('clients', 'clients.id', '=', 'invoices.client_id') ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') @@ -57,13 +103,28 @@ class InvoiceRepository ->where('invoices.is_recurring', '=', true) ->where('contacts.is_primary', '=', true) ->where('clients.deleted_at', '=', null) - ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'frequencies.name as frequency', 'start_date', 'end_date', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'invoices.deleted_at', 'invoices.is_deleted'); + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'clients.public_id as client_public_id', + 'clients.name as client_name', + 'invoices.public_id', + 'invoices.amount', + 'frequencies.name as frequency', + 'invoices.start_date', + 'invoices.end_date', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'invoices.deleted_at', + 'invoices.is_deleted' + ); if ($clientPublicId) { $query->where('clients.public_id', '=', $clientPublicId); } - if (!\Session::get('show_trash:invoice')) { + if (!\Session::get('show_trash:recurring_invoice')) { $query->where('invoices.deleted_at', '=', null); } @@ -79,7 +140,8 @@ class InvoiceRepository public function getClientDatatable($contactId, $entityType, $search) { - $query = \DB::table('invitations') + $query = DB::table('invitations') + ->join('accounts', 'accounts.id', '=', 'invitations.account_id') ->join('invoices', 'invoices.id', '=', 'invitations.invoice_id') ->join('clients', 'clients.id', '=', 'invoices.client_id') ->where('invitations.contact_id', '=', $contactId) @@ -88,18 +150,38 @@ class InvoiceRepository ->where('invoices.is_deleted', '=', false) ->where('clients.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) - ->select('invitation_key', 'invoice_number', 'invoice_date', 'invoices.balance as balance', 'due_date', 'clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'start_date', 'end_date', 'clients.currency_id', 'invoices.partial'); + // This needs to be a setting to also hide the activity on the dashboard page + //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'invitations.invitation_key', + 'invoices.invoice_number', + 'invoices.invoice_date', + 'invoices.balance as balance', + 'invoices.due_date', + 'clients.public_id as client_public_id', + 'clients.name as client_name', + 'invoices.public_id', + 'invoices.amount', + 'invoices.start_date', + 'invoices.end_date', + 'invoices.partial' + ); $table = \Datatable::query($query) ->addColumn('invoice_number', function ($model) use ($entityType) { return link_to('/view/'.$model->invitation_key, $model->invoice_number); }) ->addColumn('invoice_date', function ($model) { return Utils::fromSqlDate($model->invoice_date); }) - ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); }); + ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }); if ($entityType == ENTITY_INVOICE) { $table->addColumn('balance', function ($model) { return $model->partial > 0 ? - trans('texts.partial_remaining', ['partial' => Utils::formatMoney($model->partial, $model->currency_id), 'balance' => Utils::formatMoney($model->balance, $model->currency_id)]) : - Utils::formatMoney($model->balance, $model->currency_id); + trans('texts.partial_remaining', [ + 'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id), + 'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id) + ]) : + Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); }); } @@ -107,204 +189,99 @@ class InvoiceRepository ->make(); } - public function getDatatable($accountId, $clientPublicId = null, $entityType, $search) + public function save($data) { - $query = $this->getInvoices($accountId, $clientPublicId, $entityType, $search) - ->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE ? true : false); - - $table = \Datatable::query($query); - - if (!$clientPublicId) { - $table->addColumn('checkbox', function ($model) { return ''; }); - } - - $table->addColumn("invoice_number", function ($model) use ($entityType) { return link_to("{$entityType}s/".$model->public_id.'/edit', $model->invoice_number, ['class' => Utils::getEntityRowClass($model)]); }); - - if (!$clientPublicId) { - $table->addColumn('client_name', function ($model) { return link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)); }); - } - - $table->addColumn("invoice_date", function ($model) { return Utils::fromSqlDate($model->invoice_date); }) - ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); }); - - if ($entityType == ENTITY_INVOICE) { - $table->addColumn('balance', function ($model) { - return $model->partial > 0 ? - trans('texts.partial_remaining', ['partial' => Utils::formatMoney($model->partial, $model->currency_id), 'balance' => Utils::formatMoney($model->balance, $model->currency_id)]) : - Utils::formatMoney($model->balance, $model->currency_id); - }); - } - - return $table->addColumn('due_date', function ($model) { return Utils::fromSqlDate($model->due_date); }) - ->addColumn('invoice_status_name', function ($model) { return $model->quote_invoice_id ? link_to("invoices/{$model->quote_invoice_id}/edit", trans('texts.converted')) : self::getStatusLabel($model->invoice_status_id, $model->invoice_status_name); }) - ->addColumn('dropdown', function ($model) use ($entityType) { - - if ($model->is_deleted) { - return '

    '; - } - - $str = ''; - }) - ->make(); - } - - private function getStatusLabel($statusId, $statusName) { - $label = trans("texts.{$statusName}"); - $class = 'default'; - switch ($statusId) { - case INVOICE_STATUS_SENT: - $class = 'info'; - break; - case INVOICE_STATUS_VIEWED: - $class = 'warning'; - break; - case INVOICE_STATUS_PARTIAL: - $class = 'primary'; - break; - case INVOICE_STATUS_PAID: - $class = 'success'; - break; - } - return "

    $statusName

    "; - } - - public function getErrors($input) - { - $contact = (array) $input->client->contacts[0]; - $rules = [ - 'email' => 'email|required_without:first_name', - 'first_name' => 'required_without:email', - ]; - $validator = \Validator::make($contact, $rules); - - if ($validator->fails()) { - return $validator; - } - - $invoice = (array) $input; - $invoiceId = isset($invoice['public_id']) && $invoice['public_id'] ? Invoice::getPrivateId($invoice['public_id']) : null; - $rules = [ - 'invoice_number' => 'required|unique:invoices,invoice_number,'.$invoiceId.',id,account_id,'.\Auth::user()->account_id, - 'discount' => 'positive', - ]; - - if ($invoice['is_recurring'] && $invoice['start_date'] && $invoice['end_date']) { - $rules['end_date'] = 'after:'.$invoice['start_date']; - } - - $validator = \Validator::make($invoice, $rules); - - if ($validator->fails()) { - return $validator; - } - - return false; - } - - public function save($publicId, $data, $entityType) - { - if ($publicId) { - $invoice = Invoice::scope($publicId)->firstOrFail(); - } else { - $invoice = Invoice::createNew(); - - if ($entityType == ENTITY_QUOTE) { - $invoice->is_quote = true; - } - } - $account = \Auth::user()->account; + $publicId = isset($data['public_id']) ? $data['public_id'] : false; + + $isNew = !$publicId || $publicId == '-1'; + + if ($isNew) { + $entityType = ENTITY_INVOICE; + if (isset($data['is_recurring']) && filter_var($data['is_recurring'], FILTER_VALIDATE_BOOLEAN)) { + $entityType = ENTITY_RECURRING_INVOICE; + } elseif (isset($data['is_quote']) && filter_var($data['is_quote'], FILTER_VALIDATE_BOOLEAN)) { + $entityType = ENTITY_QUOTE; + } + $invoice = $account->createInvoice($entityType, $data['client_id']); + if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) { + $invoice->has_tasks = true; + } + if (isset($data['has_expenses']) && filter_var($data['has_expenses'], FILTER_VALIDATE_BOOLEAN)) { + $invoice->has_expenses = true; + } + } else { + $invoice = Invoice::scope($publicId)->firstOrFail(); + } if ((isset($data['set_default_terms']) && $data['set_default_terms']) || (isset($data['set_default_footer']) && $data['set_default_footer'])) { if (isset($data['set_default_terms']) && $data['set_default_terms']) { - $account->invoice_terms = trim($data['terms']); + $account->{"{$invoice->getEntityType()}_terms"} = trim($data['terms']); } if (isset($data['set_default_footer']) && $data['set_default_footer']) { $account->invoice_footer = trim($data['invoice_footer']); } $account->save(); - } + } - if (isset($data['invoice_number'])) { + if (isset($data['invoice_number']) && !$invoice->is_recurring) { $invoice->invoice_number = trim($data['invoice_number']); } - $invoice->discount = round(Utils::parseFloat($data['discount']), 2); - $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; - $invoice->partial = round(Utils::parseFloat($data['partial']), 2); - $invoice->invoice_date = isset($data['invoice_date_sql']) ? $data['invoice_date_sql'] : Utils::toSqlDate($data['invoice_date']); - $invoice->has_tasks = isset($data['has_tasks']) ? $data['has_tasks'] : false; - - if (!$publicId) { - $invoice->client_id = $data['client_id']; - $invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false; + if (isset($data['discount'])) { + $invoice->discount = round(Utils::parseFloat($data['discount']), 2); } - + if (isset($data['is_amount_discount'])) { + $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; + } + if (isset($data['partial'])) { + $invoice->partial = round(Utils::parseFloat($data['partial']), 2); + } + if (isset($data['invoice_date_sql'])) { + $invoice->invoice_date = $data['invoice_date_sql']; + } elseif (isset($data['invoice_date'])) { + $invoice->invoice_date = Utils::toSqlDate($data['invoice_date']); + } + if ($invoice->is_recurring) { + if ($invoice->start_date && $invoice->start_date != Utils::toSqlDate($data['start_date'])) { + $invoice->last_sent_date = null; + } + $invoice->frequency_id = $data['frequency_id'] ? $data['frequency_id'] : 0; $invoice->start_date = Utils::toSqlDate($data['start_date']); $invoice->end_date = Utils::toSqlDate($data['end_date']); - $invoice->due_date = null; + $invoice->auto_bill = isset($data['auto_bill']) && $data['auto_bill'] ? true : false; + + if (isset($data['recurring_due_date'])) { + $invoice->due_date = $data['recurring_due_date']; + } elseif (isset($data['due_date'])) { + $invoice->due_date = $data['due_date']; + } } else { - $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']); + if (isset($data['due_date']) || isset($data['due_date_sql'])) { + $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']); + } $invoice->frequency_id = 0; $invoice->start_date = null; $invoice->end_date = null; } - $invoice->terms = trim($data['terms']) ? trim($data['terms']) : (!$publicId && $account->invoice_terms ? $account->invoice_terms : ''); - $invoice->invoice_footer = trim($data['invoice_footer']) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : ''); - $invoice->public_notes = trim($data['public_notes']); + $invoice->terms = (isset($data['terms']) && trim($data['terms'])) ? trim($data['terms']) : (!$publicId && $account->invoice_terms ? $account->invoice_terms : ''); + $invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : ''); + $invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null; // process date variables $invoice->terms = Utils::processVariables($invoice->terms); $invoice->invoice_footer = Utils::processVariables($invoice->invoice_footer); $invoice->public_notes = Utils::processVariables($invoice->public_notes); - $invoice->po_number = trim($data['po_number']); - $invoice->invoice_design_id = $data['invoice_design_id']; + if (isset($data['po_number'])) { + $invoice->po_number = trim($data['po_number']); + } + + $invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id; if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) { $invoice->tax_rate = Utils::parseFloat($data['tax_rate']); @@ -355,13 +332,29 @@ class InvoiceRepository $total -= $invoice->discount; } else { $total *= (100 - $invoice->discount) / 100; + $total = round($total, 2); } } - $invoice->custom_value1 = round($data['custom_value1'], 2); - $invoice->custom_value2 = round($data['custom_value2'], 2); - $invoice->custom_taxes1 = $data['custom_taxes1'] ? true : false; - $invoice->custom_taxes2 = $data['custom_taxes2'] ? true : false; + if (isset($data['custom_value1'])) { + $invoice->custom_value1 = round($data['custom_value1'], 2); + if ($isNew) { + $invoice->custom_taxes1 = $account->custom_invoice_taxes1 ?: false; + } + } + if (isset($data['custom_value2'])) { + $invoice->custom_value2 = round($data['custom_value2'], 2); + if ($isNew) { + $invoice->custom_taxes2 = $account->custom_invoice_taxes2 ?: false; + } + } + + if (isset($data['custom_text_value1'])) { + $invoice->custom_text_value1 = trim($data['custom_text_value1']); + } + if (isset($data['custom_text_value2'])) { + $invoice->custom_text_value2 = trim($data['custom_text_value2']); + } // custom fields charged taxes if ($invoice->custom_value1 && $invoice->custom_taxes1) { @@ -407,16 +400,27 @@ class InvoiceRepository $task->invoice_id = $invoice->id; $task->client_id = $invoice->client_id; $task->save(); - } else if ($item['product_key'] && !$invoice->has_tasks) { - $product = Product::findProductByKey(trim($item['product_key'])); + } - if (\Auth::user()->account->update_products) { + if (isset($item['expense_public_id']) && $item['expense_public_id']) { + $expense = Expense::scope($item['expense_public_id'])->where('invoice_id', '=', null)->firstOrFail(); + $expense->invoice_id = $invoice->id; + $expense->client_id = $invoice->client_id; + $expense->save(); + } + + if ($item['product_key']) { + $productKey = trim($item['product_key']); + if (\Auth::user()->account->update_products && ! strtotime($productKey)) { + $product = Product::findProductByKey($productKey); if (!$product) { $product = Product::createNew(); $product->product_key = trim($item['product_key']); } - $product->notes = $item['notes']; + $product->notes = $invoice->has_tasks ? '' : $item['notes']; + $product->notes = $invoice->has_expenses ? '' : $item['notes']; + $product->cost = $item['cost']; $product->save(); } @@ -424,7 +428,7 @@ class InvoiceRepository $invoiceItem = InvoiceItem::createNew(); $invoiceItem->product_id = isset($product) ? $product->id : null; - $invoiceItem->product_key = trim($invoice->is_recurring ? $item['product_key'] : Utils::processVariables($item['product_key'])); + $invoiceItem->product_key = isset($item['product_key']) ? (trim($invoice->is_recurring ? $item['product_key'] : Utils::processVariables($item['product_key']))) : ''; $invoiceItem->notes = trim($invoice->is_recurring ? $item['notes'] : Utils::processVariables($item['notes'])); $invoiceItem->cost = Utils::parseFloat($item['cost']); $invoiceItem->qty = Utils::parseFloat($item['qty']); @@ -449,19 +453,22 @@ class InvoiceRepository $clone = Invoice::createNew($invoice); $clone->balance = $invoice->amount; - // if the invoice prefix is diff than quote prefix, use the same number for the invoice - if (($account->invoice_number_prefix || $account->quote_number_prefix) - && $account->invoice_number_prefix != $account->quote_number_prefix - && $account->share_counter) { - + // if the invoice prefix is diff than quote prefix, use the same number for the invoice (if it's available) + $invoiceNumber = false; + if ($account->hasInvoicePrefix() && $account->share_counter) { $invoiceNumber = $invoice->invoice_number; if ($account->quote_number_prefix && strpos($invoiceNumber, $account->quote_number_prefix) === 0) { $invoiceNumber = substr($invoiceNumber, strlen($account->quote_number_prefix)); } - $clone->invoice_number = $account->invoice_number_prefix.$invoiceNumber; - } else { - $clone->invoice_number = $account->getNextInvoiceNumber(); + $invoiceNumber = $account->invoice_number_prefix.$invoiceNumber; + if (Invoice::scope(false, $account->id) + ->withTrashed() + ->whereInvoiceNumber($invoiceNumber) + ->first()) { + $invoiceNumber = false; + } } + $clone->invoice_number = $invoiceNumber ?: $account->getNextInvoiceNumber($clone); foreach ([ 'client_id', @@ -486,7 +493,9 @@ class InvoiceRepository 'custom_value2', 'custom_taxes1', 'custom_taxes2', - 'partial'] as $field) { + 'partial', + 'custom_text_value1', + 'custom_text_value2', ] as $field) { $clone->$field = $invoice->$field; } @@ -529,31 +538,32 @@ class InvoiceRepository return $clone; } - public function bulk($ids, $action, $statusId = false) + public function markSent($invoice) { - if (!$ids) { - return 0; + $invoice->markInvitationsSent(); + } + + public function findInvoiceByInvitation($invitationKey) + { + $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); + + if (!$invitation) { + return false; } - $invoices = Invoice::withTrashed()->scope($ids)->get(); - - foreach ($invoices as $invoice) { - if ($action == 'mark') { - $invoice->invoice_status_id = $statusId; - $invoice->save(); - } elseif ($action == 'restore') { - $invoice->restore(); - } else { - if ($action == 'delete') { - $invoice->is_deleted = true; - $invoice->save(); - } - - $invoice->delete(); - } + $invoice = $invitation->invoice; + if (!$invoice || $invoice->is_deleted) { + return false; } - return count($invoices); + $invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country'); + $client = $invoice->client; + + if (!$client || $client->is_deleted) { + return false; + } + + return $invitation; } public function findOpenInvoices($clientId) @@ -588,7 +598,7 @@ class InvoiceRepository $invoice = Invoice::createNew($recurInvoice); $invoice->client_id = $recurInvoice->client_id; $invoice->recurring_invoice_id = $recurInvoice->id; - $invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber(false, 'R'); + $invoice->invoice_number = 'R'.$recurInvoice->account->getNextInvoiceNumber($recurInvoice); $invoice->amount = $recurInvoice->amount; $invoice->balance = $recurInvoice->amount; $invoice->invoice_date = date_create()->format('Y-m-d'); @@ -600,20 +610,14 @@ class InvoiceRepository $invoice->tax_name = $recurInvoice->tax_name; $invoice->tax_rate = $recurInvoice->tax_rate; $invoice->invoice_design_id = $recurInvoice->invoice_design_id; - $invoice->custom_value1 = $recurInvoice->custom_value1; - $invoice->custom_value2 = $recurInvoice->custom_value2; - $invoice->custom_taxes1 = $recurInvoice->custom_taxes1; - $invoice->custom_taxes2 = $recurInvoice->custom_taxes2; + $invoice->custom_value1 = $recurInvoice->custom_value1 ?: 0; + $invoice->custom_value2 = $recurInvoice->custom_value2 ?: 0; + $invoice->custom_taxes1 = $recurInvoice->custom_taxes1 ?: 0; + $invoice->custom_taxes2 = $recurInvoice->custom_taxes2 ?: 0; + $invoice->custom_text_value1 = $recurInvoice->custom_text_value1; + $invoice->custom_text_value2 = $recurInvoice->custom_text_value2; $invoice->is_amount_discount = $recurInvoice->is_amount_discount; - - if ($invoice->client->payment_terms != 0) { - $days = $invoice->client->payment_terms; - if ($days == -1) { - $days = 0; - } - $invoice->due_date = date_create()->modify($days.' day')->format('Y-m-d'); - } - + $invoice->due_date = $recurInvoice->getDueDate(); $invoice->save(); foreach ($recurInvoice->invoice_items as $recurItem) { @@ -635,9 +639,37 @@ class InvoiceRepository $invoice->invitations()->save($invitation); } - $recurInvoice->last_sent_date = Carbon::now()->toDateTimeString(); + $recurInvoice->last_sent_date = date('Y-m-d'); $recurInvoice->save(); + if ($recurInvoice->auto_bill) { + if ($this->paymentService->autoBillInvoice($invoice)) { + // update the invoice reference to match its actual state + // this is to ensure a 'payment received' email is sent + $invoice->invoice_status_id = INVOICE_STATUS_PAID; + } + } + return $invoice; } + + public function findNeedingReminding($account) + { + $dates = []; + + for ($i=1; $i<=3; $i++) { + if ($date = $account->getReminderDate($i)) { + $field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date'; + $dates[] = "$field = '$date'"; + } + } + + $sql = implode(' OR ', $dates); + $invoices = Invoice::whereAccountId($account->id) + ->where('balance', '>', 0) + ->whereRaw('('.$sql.')') + ->get(); + + return $invoices; + } } diff --git a/app/Ninja/Repositories/NinjaRepository.php b/app/Ninja/Repositories/NinjaRepository.php new file mode 100644 index 000000000000..3f9c1fa4f19e --- /dev/null +++ b/app/Ninja/Repositories/NinjaRepository.php @@ -0,0 +1,18 @@ +first(); + + if (!$account) { + return; + } + + $account->pro_plan_paid = $proPlanPaid; + $account->save(); + } +} diff --git a/app/Ninja/Repositories/PaymentRepository.php b/app/Ninja/Repositories/PaymentRepository.php index c8f69325c813..a080dd89c3d9 100644 --- a/app/Ninja/Repositories/PaymentRepository.php +++ b/app/Ninja/Repositories/PaymentRepository.php @@ -1,16 +1,24 @@ join('accounts', 'accounts.id', '=', 'payments.account_id') ->join('clients', 'clients.id', '=', 'payments.client_id') ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') @@ -21,11 +29,30 @@ class PaymentRepository ->where('clients.deleted_at', '=', null) ->where('contacts.is_primary', '=', true) ->where('contacts.deleted_at', '=', null) - ->select('payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id', 'payments.deleted_at', 'payments.is_deleted', 'invoices.is_deleted as invoice_is_deleted', 'gateways.name as gateway_name'); + ->where('invoices.is_deleted', '=', false) + ->select('payments.public_id', + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'payments.transaction_reference', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'payments.amount', + 'payments.payment_date', + 'invoices.public_id as invoice_public_id', + 'invoices.invoice_number', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'payment_types.name as payment_type', + 'payments.account_gateway_id', + 'payments.deleted_at', + 'payments.is_deleted', + 'invoices.is_deleted as invoice_is_deleted', + 'gateways.name as gateway_name' + ); if (!\Session::get('show_trash:payment')) { - $query->where('payments.deleted_at', '=', null) - ->where('invoices.deleted_at', '=', null); + $query->where('payments.deleted_at', '=', null); } if ($clientPublicId) { @@ -43,7 +70,8 @@ class PaymentRepository public function findForContact($contactId = null, $filter = null) { - $query = \DB::table('payments') + $query = DB::table('payments') + ->join('accounts', 'accounts.id', '=', 'payments.account_id') ->join('clients', 'clients.id', '=', 'payments.client_id') ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') @@ -57,7 +85,24 @@ class PaymentRepository ->where('invitations.deleted_at', '=', null) ->where('invoices.deleted_at', '=', null) ->where('invitations.contact_id', '=', $contactId) - ->select('invitations.invitation_key', 'payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id'); + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'invitations.invitation_key', + 'payments.public_id', + 'payments.transaction_reference', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'payments.amount', + 'payments.payment_date', + 'invoices.public_id as invoice_public_id', + 'invoices.invoice_number', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'payment_types.name as payment_type', + 'payments.account_gateway_id' + ); if ($filter) { $query->where(function ($query) use ($filter) { @@ -68,34 +113,10 @@ class PaymentRepository return $query; } - public function getErrors($input) - { - $rules = array( - 'client' => 'required', - 'invoice' => 'required', - 'amount' => 'required', - ); - - if ($input['payment_type_id'] == PAYMENT_TYPE_CREDIT) { - $rules['payment_type_id'] = 'has_credit:'.$input['client'].','.$input['amount']; - } - - if (isset($input['invoice']) && $input['invoice']) { - $invoice = Invoice::scope($input['invoice'])->firstOrFail(); - $rules['amount'] .= "|less_than:{$invoice->balance}"; - } - - $validator = \Validator::make($input, $rules); - - if ($validator->fails()) { - return $validator; - } - - return false; - } - - public function save($publicId = null, $input) + public function save($input) { + $publicId = isset($input['public_id']) ? $input['public_id'] : false; + if ($publicId) { $payment = Payment::scope($publicId)->firstOrFail(); } else { @@ -116,10 +137,12 @@ class PaymentRepository $payment->payment_date = date('Y-m-d'); } - $payment->transaction_reference = trim($input['transaction_reference']); + if (isset($input['transaction_reference'])) { + $payment->transaction_reference = trim($input['transaction_reference']); + } if (!$publicId) { - $clientId = Client::getPrivateId($input['client']); + $clientId = $input['client_id']; $amount = Utils::parseFloat($input['amount']); if ($paymentTypeId == PAYMENT_TYPE_CREDIT) { @@ -136,8 +159,8 @@ class PaymentRepository } } + $payment->invoice_id = $input['invoice_id']; $payment->client_id = $clientId; - $payment->invoice_id = isset($input['invoice']) && $input['invoice'] != "-1" ? Invoice::getPrivateId($input['invoice']) : null; $payment->amount = $amount; } @@ -146,27 +169,23 @@ class PaymentRepository return $payment; } - public function bulk($ids, $action) + public function delete($payment) { - if (!$ids) { - return 0; + if ($payment->invoice->is_deleted) { + return false; } - $payments = Payment::withTrashed()->scope($ids)->get(); - - foreach ($payments as $payment) { - if ($action == 'restore') { - $payment->restore(); - } else { - if ($action == 'delete') { - $payment->is_deleted = true; - $payment->save(); - } - - $payment->delete(); - } - } - - return count($payments); + parent::delete($payment); } + + public function restore($payment) + { + if ($payment->invoice->is_deleted) { + return false; + } + + parent::restore($payment); + } + + } diff --git a/app/Ninja/Repositories/PaymentTermRepository.php b/app/Ninja/Repositories/PaymentTermRepository.php new file mode 100644 index 000000000000..e631e9f1627a --- /dev/null +++ b/app/Ninja/Repositories/PaymentTermRepository.php @@ -0,0 +1,22 @@ +where('payment_terms.account_id', '=', $accountId) + ->where('payment_terms.deleted_at', '=', null) + ->select('payment_terms.public_id', 'payment_terms.name', 'payment_terms.num_days', 'payment_terms.deleted_at'); + } +} diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php new file mode 100644 index 000000000000..417b49f23640 --- /dev/null +++ b/app/Ninja/Repositories/ProductRepository.php @@ -0,0 +1,32 @@ +leftJoin('tax_rates', function($join) { + $join->on('tax_rates.id', '=', 'products.default_tax_rate_id') + ->whereNull('tax_rates.deleted_at'); + }) + ->where('products.account_id', '=', $accountId) + ->where('products.deleted_at', '=', null) + ->select( + 'products.public_id', + 'products.product_key', + 'products.notes', + 'products.cost', + 'tax_rates.name as tax_name', + 'tax_rates.rate as tax_rate', + 'products.deleted_at' + ); + } +} \ No newline at end of file diff --git a/app/Ninja/Repositories/ReferralRepository.php b/app/Ninja/Repositories/ReferralRepository.php new file mode 100644 index 000000000000..c847f3386b39 --- /dev/null +++ b/app/Ninja/Repositories/ReferralRepository.php @@ -0,0 +1,31 @@ +where('referral_user_id', $userId) + ->get(['id', 'pro_plan_paid']); + + $counts = [ + 'free' => 0, + 'pro' => 0 + ]; + + foreach ($accounts as $account) { + $counts['free']++; + if (Utils::withinPastYear($account->pro_plan_paid)) { + $counts['pro']++; + } + } + + return $counts; + } + + + +} \ No newline at end of file diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php index 47761900e1e6..47a052378ff1 100644 --- a/app/Ninja/Repositories/TaskRepository.php +++ b/app/Ninja/Repositories/TaskRepository.php @@ -23,7 +23,23 @@ class TaskRepository }) ->where('contacts.deleted_at', '=', null) ->where('clients.deleted_at', '=', null) - ->select('tasks.public_id', 'clients.name as client_name', 'clients.public_id as client_public_id', 'contacts.first_name', 'contacts.email', 'contacts.last_name', 'invoices.invoice_status_id', 'tasks.description', 'tasks.is_deleted', 'tasks.deleted_at', 'invoices.invoice_number', 'invoices.public_id as invoice_public_id', 'tasks.is_running', 'tasks.time_log', 'tasks.created_at'); + ->select( + 'tasks.public_id', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'contacts.first_name', + 'contacts.email', + 'contacts.last_name', + 'invoices.invoice_status_id', + 'tasks.description', + 'tasks.is_deleted', + 'tasks.deleted_at', + 'invoices.invoice_number', + 'invoices.public_id as invoice_public_id', + 'tasks.is_running', + 'tasks.time_log', + 'tasks.created_at' + ); if ($clientPublicId) { $query->where('clients.public_id', '=', $clientPublicId); @@ -45,8 +61,22 @@ class TaskRepository return $query; } + public function getErrors($input) + { + $rules = [ + 'time_log' => 'time_log', + ]; + $validator = \Validator::make($input, $rules); + + if ($validator->fails()) { + return $validator; + } + + return false; + } + public function save($publicId, $data) - { + { if ($publicId) { $task = Task::scope($publicId)->firstOrFail(); } else { @@ -67,16 +97,20 @@ class TaskRepository } else { $timeLog = []; } + + array_multisort($timeLog); - if ($data['action'] == 'start') { - $task->is_running = true; - $timeLog[] = [strtotime('now'), false]; - } else if ($data['action'] == 'resume') { - $task->is_running = true; - $timeLog[] = [strtotime('now'), false]; - } else if ($data['action'] == 'stop' && $task->is_running) { - $timeLog[count($timeLog)-1][1] = time(); - $task->is_running = false; + if (isset($data['action'])) { + if ($data['action'] == 'start') { + $task->is_running = true; + $timeLog[] = [strtotime('now'), false]; + } else if ($data['action'] == 'resume') { + $task->is_running = true; + $timeLog[] = [strtotime('now'), false]; + } else if ($data['action'] == 'stop' && $task->is_running) { + $timeLog[count($timeLog)-1][1] = time(); + $task->is_running = false; + } } $task->time_log = json_encode($timeLog); diff --git a/app/Ninja/Repositories/TaxRateRepository.php b/app/Ninja/Repositories/TaxRateRepository.php index d5bd4dc24287..1b9fa8df773b 100644 --- a/app/Ninja/Repositories/TaxRateRepository.php +++ b/app/Ninja/Repositories/TaxRateRepository.php @@ -1,10 +1,26 @@ where('tax_rates.account_id', '=', $accountId) + ->where('tax_rates.deleted_at', '=', null) + ->select('tax_rates.public_id', 'tax_rates.name', 'tax_rates.rate', 'tax_rates.deleted_at'); + } + + /* public function save($taxRates) { $taxRateIds = []; @@ -39,4 +55,5 @@ class TaxRateRepository } } } + */ } diff --git a/app/Ninja/Repositories/TokenRepository.php b/app/Ninja/Repositories/TokenRepository.php new file mode 100644 index 000000000000..5237eb7a0369 --- /dev/null +++ b/app/Ninja/Repositories/TokenRepository.php @@ -0,0 +1,27 @@ +where('account_tokens.account_id', '=', $accountId); + + if (!Session::get('show_trash:token')) { + $query->where('account_tokens.deleted_at', '=', null); + } + + return $query->select('account_tokens.public_id', 'account_tokens.name', 'account_tokens.token', 'account_tokens.public_id', 'account_tokens.deleted_at'); + } +} diff --git a/app/Ninja/Repositories/UserRepository.php b/app/Ninja/Repositories/UserRepository.php new file mode 100644 index 000000000000..01b7017fa0a0 --- /dev/null +++ b/app/Ninja/Repositories/UserRepository.php @@ -0,0 +1,29 @@ +where('users.account_id', '=', $accountId); + + if (!Session::get('show_trash:user')) { + $query->where('users.deleted_at', '=', null); + } + + $query->select('users.public_id', 'users.first_name', 'users.last_name', 'users.email', 'users.confirmed', 'users.public_id', 'users.deleted_at'); + + return $query; + } +} diff --git a/app/Ninja/Repositories/VendorContactRepository.php b/app/Ninja/Repositories/VendorContactRepository.php new file mode 100644 index 000000000000..242b1b9d0c54 --- /dev/null +++ b/app/Ninja/Repositories/VendorContactRepository.php @@ -0,0 +1,26 @@ +send_invoice = true; + $contact->vendor_id = $data['vendor_id']; + $contact->is_primary = VendorContact::scope()->where('vendor_id', '=', $contact->vendor_id)->count() == 0; + } else { + $contact = VendorContact::scope($publicId)->firstOrFail(); + } + + $contact->fill($data); + $contact->save(); + + return $contact; + } +} \ No newline at end of file diff --git a/app/Ninja/Repositories/VendorRepository.php b/app/Ninja/Repositories/VendorRepository.php new file mode 100644 index 000000000000..c7fc5bb90b5f --- /dev/null +++ b/app/Ninja/Repositories/VendorRepository.php @@ -0,0 +1,90 @@ +with('user', 'vendorcontacts', 'country') + ->withTrashed() + ->where('is_deleted', '=', false) + ->get(); + } + + public function find($filter = null) + { + $query = DB::table('vendors') + ->join('accounts', 'accounts.id', '=', 'vendors.account_id') + ->join('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id') + ->where('vendors.account_id', '=', \Auth::user()->account_id) + ->where('vendor_contacts.is_primary', '=', true) + ->where('vendor_contacts.deleted_at', '=', null) + ->select( + DB::raw('COALESCE(vendors.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(vendors.country_id, accounts.country_id) country_id'), + 'vendors.public_id', + 'vendors.name', + 'vendor_contacts.first_name', + 'vendor_contacts.last_name', + 'vendors.created_at', + 'vendors.work_phone', + 'vendor_contacts.email', + 'vendors.deleted_at', + 'vendors.is_deleted' + ); + + if (!\Session::get('show_trash:vendor')) { + $query->where('vendors.deleted_at', '=', null); + } + + if ($filter) { + $query->where(function ($query) use ($filter) { + $query->where('vendors.name', 'like', '%'.$filter.'%') + ->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%') + ->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%') + ->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%'); + }); + } + + return $query; + } + + public function save($data) + { + $publicId = isset($data['public_id']) ? $data['public_id'] : false; + + if (!$publicId || $publicId == '-1') { + $vendor = Vendor::createNew(); + } else { + $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail(); + } + + $vendor->fill($data); + $vendor->save(); + + if ( ! isset($data['vendorcontact']) && ! isset($data['vendorcontacts'])) { + return $vendor; + } + + $first = true; + $vendorcontacts = isset($data['vendorcontact']) ? [$data['vendorcontact']] : $data['vendorcontacts']; + + foreach ($vendorcontacts as $vendorcontact) { + $vendorcontact = $vendor->addVendorContact($vendorcontact, $first); + $first = false; + } + + return $vendor; + } +} diff --git a/app/Ninja/Serializers/ArraySerializer.php b/app/Ninja/Serializers/ArraySerializer.php new file mode 100644 index 000000000000..bdbbbfe99981 --- /dev/null +++ b/app/Ninja/Serializers/ArraySerializer.php @@ -0,0 +1,18 @@ + $data) : $data; + } + + public function item($resourceKey, array $data) + { + return $data; + //return ($resourceKey && $resourceKey !== 'data') ? array($resourceKey => $data) : $data; + } +} diff --git a/app/Ninja/Transformers/AccountTokenTransformer.php b/app/Ninja/Transformers/AccountTokenTransformer.php new file mode 100644 index 000000000000..e6ca7e8bfda7 --- /dev/null +++ b/app/Ninja/Transformers/AccountTokenTransformer.php @@ -0,0 +1,17 @@ + $account_token->name, + 'token' => $account_token->token + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/AccountTransformer.php b/app/Ninja/Transformers/AccountTransformer.php new file mode 100644 index 000000000000..eed474346550 --- /dev/null +++ b/app/Ninja/Transformers/AccountTransformer.php @@ -0,0 +1,92 @@ +serializer); + return $this->includeCollection($account->users, $transformer, 'users'); + } + + public function includeClients(Account $account) + { + $transformer = new ClientTransformer($account, $this->serializer); + return $this->includeCollection($account->clients, $transformer, 'clients'); + } + + public function includeInvoices(Account $account) + { + $transformer = new InvoiceTransformer($account, $this->serializer); + return $this->includeCollection($account->invoices, $transformer, 'invoices'); + } + + public function includeProducts(Account $account) + { + $transformer = new ProductTransformer($account, $this->serializer); + return $this->includeCollection($account->products, $transformer, 'products'); + } + + public function includeTaxRates(Account $account) + { + $transformer = new TaxRateTransformer($account, $this->serializer); + return $this->includeCollection($account->tax_rates, $transformer, 'taxRates'); + } + + public function transform(Account $account) + { + return [ + 'account_key' => $account->account_key, + 'name' => $account->present()->name, + 'currency_id' => (int) $account->currency_id, + 'timezone_id' => (int) $account->timezone_id, + 'date_format_id' => (int) $account->date_format_id, + 'datetime_format_id' => (int) $account->datetime_format_id, + 'updated_at' => $this->getTimestamp($account->updated_at), + 'archived_at' => $this->getTimestamp($account->deleted_at), + 'address1' => $account->address1, + 'address2' => $account->address2, + 'city' => $account->city, + 'state' => $account->state, + 'postal_code' => $account->postal_code, + 'country_id' => (int) $account->country_id, + 'invoice_terms' => $account->invoice_terms, + 'email_footer' => $account->email_footer, + 'industry_id' => (int) $account->industry_id, + 'size_id' => (int) $account->size_id, + 'invoice_taxes' => (bool) $account->invoice_taxes, + 'invoice_item_taxes' => (bool) $account->invoice_item_taxes, + 'invoice_design_id' => (int) $account->invoice_design_id, + 'client_view_css' => (string) $account->client_view_css, + 'work_phone' => $account->work_phone, + 'work_email' => $account->work_email, + 'language_id' => (int) $account->language_id, + 'fill_products' => (bool) $account->fill_products, + 'update_products' => (bool) $account->update_products, + 'vat_number' => $account->vat_number, + 'custom_invoice_label1' => $account->custom_invoice_label1, + 'custom_invoice_label2' => $account->custom_invoice_label2, + 'custom_invoice_taxes1' => $account->custom_invoice_taxes1, + 'custom_invoice_taxes2' => $account->custom_invoice_taxes1, + 'custom_label1' => $account->custom_label1, + 'custom_label2' => $account->custom_label2, + 'custom_value1' => $account->custom_value1, + 'custom_value2' => $account->custom_value2 + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/ClientTransformer.php b/app/Ninja/Transformers/ClientTransformer.php new file mode 100644 index 000000000000..4289545673c7 --- /dev/null +++ b/app/Ninja/Transformers/ClientTransformer.php @@ -0,0 +1,91 @@ +account, $this->serializer); + return $this->includeCollection($client->contacts, $transformer, ENTITY_CONTACT); + } + + public function includeInvoices(Client $client) + { + $transformer = new InvoiceTransformer($this->account, $this->serializer); + return $this->includeCollection($client->invoices, $transformer, ENTITY_INVOICE); + } + + public function transform(Client $client) + { + return [ + 'id' => (int) $client->public_id, + 'name' => $client->name, + 'balance' => (float) $client->balance, + 'paid_to_date' => (float) $client->paid_to_date, + 'user_id' => (int) $client->user->public_id + 1, + 'account_key' => $this->account->account_key, + 'updated_at' => $this->getTimestamp($client->updated_at), + 'archived_at' => $this->getTimestamp($client->deleted_at), + 'address1' => $client->address1, + 'address2' => $client->address2, + 'city' => $client->city, + 'state' => $client->state, + 'postal_code' => $client->postal_code, + 'country_id' => (int) $client->country_id, + 'work_phone' => $client->work_phone, + 'private_notes' => $client->private_notes, + 'last_login' => $client->last_login, + 'website' => $client->website, + 'industry_id' => (int) $client->industry_id, + 'size_id' => (int) $client->size_id, + 'is_deleted' => (bool) $client->is_deleted, + 'payment_terms' => (int) $client->payment_terms, + 'vat_number' => $client->vat_number, + 'id_number' => $client->id_number, + 'language_id' => (int) $client->language_id, + 'currency_id' => (int) $client->currency_id + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/ContactTransformer.php b/app/Ninja/Transformers/ContactTransformer.php new file mode 100644 index 000000000000..75e3620308f6 --- /dev/null +++ b/app/Ninja/Transformers/ContactTransformer.php @@ -0,0 +1,24 @@ + (int) $contact->public_id, + 'first_name' => $contact->first_name, + 'last_name' => $contact->last_name, + 'email' => $contact->email, + 'updated_at' => $this->getTimestamp($contact->updated_at), + 'archived_at' => $this->getTimestamp($contact->deleted_at), + 'is_primary' => (bool) $contact->is_primary, + 'phone' => $contact->phone, + 'last_login' => $contact->last_login, + 'account_key' => $this->account->account_key, + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/EntityTransformer.php b/app/Ninja/Transformers/EntityTransformer.php new file mode 100644 index 000000000000..ea0dba263d97 --- /dev/null +++ b/app/Ninja/Transformers/EntityTransformer.php @@ -0,0 +1,40 @@ +account = $account; + $this->serializer = $serializer; + } + + protected function includeCollection($data, $transformer, $entityType) + { + if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) { + $entityType = null; + } + + return $this->collection($data, $transformer, $entityType); + } + + protected function includeItem($data, $transformer, $entityType) + { + if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) { + $entityType = null; + } + + return $this->item($data, $transformer, $entityType); + } + + protected function getTimestamp($date) + { + return $date ? $date->getTimestamp() : null; + } +} diff --git a/app/Ninja/Transformers/InvoiceItemTransformer.php b/app/Ninja/Transformers/InvoiceItemTransformer.php new file mode 100644 index 000000000000..66d9fe137dd8 --- /dev/null +++ b/app/Ninja/Transformers/InvoiceItemTransformer.php @@ -0,0 +1,26 @@ + (int) $item->public_id, + 'product_key' => $item->product_key, + 'account_key' => $this->account->account_key, + 'user_id' => (int) $item->user_id, + 'updated_at' => $this->getTimestamp($item->updated_at), + 'archived_at' => $this->getTimestamp($item->deleted_at), + 'product_key' => $item->product_key, + 'notes' => $item->notes, + 'cost' => (float) $item->cost, + 'qty' => (float) $item->qty, + 'tax_name' => $item->tax_name, + 'tax_rate' => (float) $item->tax_rate + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php new file mode 100644 index 000000000000..8108115b2f01 --- /dev/null +++ b/app/Ninja/Transformers/InvoiceTransformer.php @@ -0,0 +1,83 @@ +account, $this->serializer); + return $this->includeCollection($invoice->invoice_items, $transformer, ENTITY_INVOICE_ITEMS); + } + + public function includePayments(Invoice $invoice) + { + $transformer = new PaymentTransformer($this->account, $this->serializer); + return $this->includeCollection($invoice->payments, $transformer, ENTITY_PAYMENT); + } + + public function transform(Invoice $invoice) + { + return [ + 'id' => (int) $invoice->public_id, + 'amount' => (float) $invoice->amount, + 'balance' => (float) $invoice->balance, + 'client_id' => (int) $invoice->client->public_id, + 'invoice_status_id' => (int) $invoice->invoice_status_id, + 'updated_at' => $this->getTimestamp($invoice->updated_at), + 'archived_at' => $this->getTimestamp($invoice->deleted_at), + 'invoice_number' => $invoice->invoice_number, + 'discount' => (double) $invoice->discount, + 'po_number' => $invoice->po_number, + 'invoice_date' => $invoice->invoice_date, + 'due_date' => $invoice->due_date, + 'terms' => $invoice->terms, + 'public_notes' => $invoice->public_notes, + 'is_deleted' => (bool) $invoice->is_deleted, + 'is_quote' => (bool) $invoice->is_quote, + 'is_recurring' => (bool) $invoice->is_recurring, + 'frequency_id' => (int) $invoice->frequency_id, + 'start_date' => $invoice->start_date, + 'end_date' => $invoice->end_date, + 'last_sent_date' => $invoice->last_sent_date, + 'recurring_invoice_id' => (int) $invoice->recurring_invoice_id, + 'tax_name' => $invoice->tax_name, + 'tax_rate' => (float) $invoice->tax_rate, + 'amount' => (float) $invoice->amount, + 'balance' => (float) $invoice->balance, + 'is_amount_discount' => (bool) $invoice->is_amount_discount, + 'invoice_footer' => $invoice->invoice_footer, + 'partial' => (float) $invoice->partial, + 'has_tasks' => (bool) $invoice->has_tasks, + 'auto_bill' => (bool) $invoice->auto_bill, + 'account_key' => $this->account->account_key, + 'user_id' => (int) $invoice->user->public_id + 1, + 'custom_value1' => (float) $invoice->custom_value1, + 'custom_value2' => (float) $invoice->custom_value2, + 'custom_taxes1' => (bool) $invoice->custom_taxes1, + 'custom_taxes2' => (bool) $invoice->custom_taxes2, + 'has_expenses' => (bool) $invoice->has_expenses, + ]; + } +} diff --git a/app/Ninja/Transformers/PaymentTransformer.php b/app/Ninja/Transformers/PaymentTransformer.php new file mode 100644 index 000000000000..a2750eaad8af --- /dev/null +++ b/app/Ninja/Transformers/PaymentTransformer.php @@ -0,0 +1,57 @@ +account, $this->serializer); + return $this->includeItem($payment->invoice, $transformer, 'invoice'); + } + + public function includeClient(Payment $payment) + { + $transformer = new ClientTransformer($this->account, $this->serializer); + return $this->includeItem($payment->client, $transformer, 'client'); + } + + public function transform(Payment $payment) + { + return [ + 'id' => (int) $payment->public_id, + 'amount' => (float) $payment->amount, + 'account_key' => $this->account->account_key, + 'user_id' => (int) $payment->user->public_id + 1, + 'transaction_reference' => $payment->transaction_reference, + 'payment_date' => $payment->payment_date, + 'updated_at' => $this->getTimestamp($payment->updated_at), + 'archived_at' => $this->getTimestamp($payment->deleted_at), + 'is_deleted' => (bool) $payment->is_deleted, + 'payment_type_id' => (int) $payment->payment_type_id, + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/ProductTransformer.php b/app/Ninja/Transformers/ProductTransformer.php new file mode 100644 index 000000000000..76bf436066aa --- /dev/null +++ b/app/Ninja/Transformers/ProductTransformer.php @@ -0,0 +1,21 @@ + (int) $product->public_id, + 'product_key' => $product->product_key, + 'notes' => $product->notes, + 'cost' => $product->cost, + 'qty' => $product->qty, + 'account_key' =>$this->account->account_key, + 'default_tax_rate_id' =>$product->default_tax_rate_id, + 'updated_at' =>$this->getTimestamp($product->updated_at), + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/QuoteTransformer.php b/app/Ninja/Transformers/QuoteTransformer.php new file mode 100644 index 000000000000..2c92640e805a --- /dev/null +++ b/app/Ninja/Transformers/QuoteTransformer.php @@ -0,0 +1,27 @@ +account, $this->serializer); + return $this->includeCollection($invoice->invoice_items, $transformer, 'invoice_items'); + } + + public function transform(Invoice $invoice) + { + return [ + 'id' => (int) $invoice->public_id, + 'quote_number' => $invoice->invoice_number, + 'amount' => (float) $invoice->amount, + 'quote_terms' => $invoice->terms, + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/TaskTransformer.php b/app/Ninja/Transformers/TaskTransformer.php new file mode 100644 index 000000000000..908a8118aaea --- /dev/null +++ b/app/Ninja/Transformers/TaskTransformer.php @@ -0,0 +1,50 @@ +client) { + $transformer = new ClientTransformer($this->account, $this->serializer); + return $this->includeItem($task->client, $transformer, 'client'); + } else { + return null; + } + } + + public function transform(Task $task) + { + return [ + 'id' => (int) $task->public_id, + 'account_key' => $this->account->account_key, + 'user_id' => (int) $task->user->public_id + 1, + 'description' => $task->description, + 'duration' => $task->getDuration() + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/TaxRateTransformer.php b/app/Ninja/Transformers/TaxRateTransformer.php new file mode 100644 index 000000000000..8f4a375c7a72 --- /dev/null +++ b/app/Ninja/Transformers/TaxRateTransformer.php @@ -0,0 +1,33 @@ + (int) $taxRate->public_id, + 'name' => $taxRate->name, + 'rate' => (float) $taxRate->rate, + 'updated_at' => $this->getTimestamp($taxRate->updated_at), + 'archived_at' => $this->getTimestamp($taxRate->deleted_at), + 'account_key' => $this->account->account_key, + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/UserAccountTransformer.php b/app/Ninja/Transformers/UserAccountTransformer.php new file mode 100644 index 000000000000..bc49a96c546a --- /dev/null +++ b/app/Ninja/Transformers/UserAccountTransformer.php @@ -0,0 +1,39 @@ +tokenName = $tokenName; + } + + public function includeUser(User $user) + { + $transformer = new UserTransformer($this->account, $this->serializer); + return $this->includeItem($user, $transformer, 'user'); + } + + public function transform(User $user) + { + return [ + 'account_key' => $user->account->account_key, + 'name' => $user->account->present()->name, + 'token' => $user->account->getToken($this->tokenName), + 'default_url' => SITE_URL + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/UserTransformer.php b/app/Ninja/Transformers/UserTransformer.php new file mode 100644 index 000000000000..dd3c6775dcb1 --- /dev/null +++ b/app/Ninja/Transformers/UserTransformer.php @@ -0,0 +1,27 @@ + (int) ($user->public_id + 1), + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'email' => $user->email, + 'account_key' => $user->account->account_key, + 'updated_at' => $this->getTimestamp($user->updated_at), + 'deleted_at' => $this->getTimestamp($user->deleted_at), + 'phone' => $user->phone, + 'username' => $user->username, + 'registered' => (bool) $user->registered, + 'confirmed' => (bool) $user->confirmed, + 'oauth_user_id' => $user->oauth_user_id, + 'oauth_provider_id' => $user->oauth_provider_id + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/VendorContactTransformer.php b/app/Ninja/Transformers/VendorContactTransformer.php new file mode 100644 index 000000000000..0166883aba4d --- /dev/null +++ b/app/Ninja/Transformers/VendorContactTransformer.php @@ -0,0 +1,24 @@ + (int) $contact->public_id, + 'first_name' => $contact->first_name, + 'last_name' => $contact->last_name, + 'email' => $contact->email, + 'updated_at' => $this->getTimestamp($contact->updated_at), + 'archived_at' => $this->getTimestamp($contact->deleted_at), + 'is_primary' => (bool) $contact->is_primary, + 'phone' => $contact->phone, + 'last_login' => $contact->last_login, + 'account_key' => $this->account->account_key, + ]; + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/VendorTransformer.php b/app/Ninja/Transformers/VendorTransformer.php new file mode 100644 index 000000000000..c1714b27a120 --- /dev/null +++ b/app/Ninja/Transformers/VendorTransformer.php @@ -0,0 +1,82 @@ +account, $this->serializer); + return $this->includeCollection($vendor->contacts, $transformer, ENTITY_CONTACT); + } + + public function includeInvoices(Vendor $vendor) + { + $transformer = new InvoiceTransformer($this->account, $this->serializer); + return $this->includeCollection($vendor->invoices, $transformer, ENTITY_INVOICE); + } + + public function transform(Vendor $vendor) + { + return [ + 'id' => (int) $vendor->public_id, + 'name' => $vendor->name, + 'balance' => (float) $vendor->balance, + 'paid_to_date' => (float) $vendor->paid_to_date, + 'user_id' => (int) $vendor->user->public_id + 1, + 'account_key' => $this->account->account_key, + 'updated_at' => $this->getTimestamp($vendor->updated_at), + 'archived_at' => $this->getTimestamp($vendor->deleted_at), + 'address1' => $vendor->address1, + 'address2' => $vendor->address2, + 'city' => $vendor->city, + 'state' => $vendor->state, + 'postal_code' => $vendor->postal_code, + 'country_id' => (int) $vendor->country_id, + 'work_phone' => $vendor->work_phone, + 'private_notes' => $vendor->private_notes, + 'last_login' => $vendor->last_login, + 'website' => $vendor->website, + 'is_deleted' => (bool) $vendor->is_deleted, + 'vat_number' => $vendor->vat_number, + 'id_number' => $vendor->id_number, + 'currency_id' => (int) $vendor->currency_id + ]; + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 56a8e8bca115..a094b01a6079 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -33,28 +33,31 @@ class AppServiceProvider extends ServiceProvider { $types = $type.'s'; $Type = ucfirst($type); $Types = ucfirst($types); - $class = ( Request::is($types) || Request::is('*'.$type.'*')) && !Request::is('*advanced_settings*') ? ' active' : ''; + $class = ( Request::is($types) || Request::is('*'.$type.'*')) && !Request::is('*settings*') ? ' active' : ''; $str = '
    @stop \ No newline at end of file diff --git a/resources/views/accounts/invoice_settings.blade.php b/resources/views/accounts/invoice_settings.blade.php index 8fc5a4f6b920..af9bdefadae9 100644 --- a/resources/views/accounts/invoice_settings.blade.php +++ b/resources/views/accounts/invoice_settings.blade.php @@ -1,13 +1,16 @@ -@extends('accounts.nav') +@extends('header') @section('head') @parent @@ -15,104 +18,254 @@ @section('content') @parent - @include('accounts.nav_advanced') + @include('accounts.nav', ['selected' => ACCOUNT_INVOICE_SETTINGS, 'advanced' => true]) - {!! Former::open()->addClass('warn-on-exit') !!} - {{ Former::populate($account) }} - {{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }} - {{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }} + {!! Former::open()->rules(['iframe_url' => 'url'])->addClass('warn-on-exit') !!} + {{ Former::populate($account) }} + {{ Former::populateField('auto_convert_quote', intval($account->auto_convert_quote)) }} + {{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }} + {{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }} {{ Former::populateField('share_counter', intval($account->share_counter)) }} - {{ Former::populateField('pdf_email_attachment', intval($account->pdf_email_attachment)) }} - -
    -
    -
    -
    -

    {!! trans('texts.invoice_fields') !!}

    -
    -
    - {!! Former::text('custom_invoice_label1')->label(trans('texts.field_label')) - ->append(Former::checkbox('custom_invoice_taxes1')->raw() . trans('texts.charge_taxes')) !!} - {!! Former::text('custom_invoice_label2')->label(trans('texts.field_label')) - ->append(Former::checkbox('custom_invoice_taxes2')->raw() . ' ' . trans('texts.charge_taxes')) !!} -
    -
    - -
    -
    -

    {!! trans('texts.client_fields') !!}

    -
    -
    - {!! Former::text('custom_client_label1')->label(trans('texts.field_label')) !!} - {!! Former::text('custom_client_label2')->label(trans('texts.field_label')) !!} -
    -
    - - -
    -
    -

    {!! trans('texts.company_fields') !!}

    -
    -
    - {!! Former::text('custom_label1')->label(trans('texts.field_label')) !!} - {!! Former::text('custom_value1')->label(trans('texts.field_value')) !!} -

     

    - {!! Former::text('custom_label2')->label(trans('texts.field_label')) !!} - {!! Former::text('custom_value2')->label(trans('texts.field_value')) !!} +
    +

    {!! trans('texts.invoice_quote_number') !!}

    -
    +
    -
    -
    + +
    +
    +
    + {!! Former::inline_radios('invoice_number_type') + ->onchange('onInvoiceNumberTypeChange()') + ->label(trans('texts.type')) + ->radios([ + trans('texts.prefix') => ['value' => 'prefix', 'name' => 'invoice_number_type'], + trans('texts.pattern') => ['value' => 'pattern', 'name' => 'invoice_number_type'], + ])->check($account->invoice_number_pattern ? 'pattern' : 'prefix') !!} + + {!! Former::text('invoice_number_prefix') + ->addGroupClass('invoice-prefix') + ->label(' ') !!} + {!! Former::text('invoice_number_pattern') + ->appendIcon('question-sign') + ->addGroupClass('invoice-pattern') + ->label(' ') + ->addGroupClass('number-pattern') !!} + {!! Former::text('invoice_number_counter') + ->label(trans('texts.counter')) + ->help(trans('texts.invoice_number_help') . ' ' . + trans('texts.next_invoice_number', ['number' => $account->previewNextInvoiceNumber()])) !!} + +
    +
    +
    +
    + {!! Former::inline_radios('quote_number_type') + ->onchange('onQuoteNumberTypeChange()') + ->label(trans('texts.type')) + ->radios([ + trans('texts.prefix') => ['value' => 'prefix', 'name' => 'quote_number_type'], + trans('texts.pattern') => ['value' => 'pattern', 'name' => 'quote_number_type'], + ])->check($account->quote_number_pattern ? 'pattern' : 'prefix') !!} + + {!! Former::text('quote_number_prefix') + ->addGroupClass('quote-prefix') + ->label(' ') !!} + {!! Former::text('quote_number_pattern') + ->appendIcon('question-sign') + ->addGroupClass('quote-pattern') + ->addGroupClass('number-pattern') + ->label(' ') !!} + {!! Former::text('quote_number_counter') + ->label(trans('texts.counter')) + ->addGroupClass('pad-checkbox') + ->append(Former::checkbox('share_counter')->raw() + ->onclick('setQuoteNumberEnabled()') . ' ' . trans('texts.share_invoice_counter')) + ->help(trans('texts.quote_number_help') . ' ' . + trans('texts.next_quote_number', ['number' => $account->previewNextInvoiceNumber(ENTITY_QUOTE)])) !!} + + +
    +
    +
    -
    -
    -

    {!! trans('texts.invoice_number') !!}

    -
    -
    - {!! Former::text('invoice_number_prefix')->label(trans('texts.prefix')) !!} - {!! Former::text('invoice_number_counter')->label(trans('texts.counter')) !!}
    - - -
    -
    -

    {!! trans('texts.quote_number') !!}

    -
    -
    - {!! Former::text('quote_number_prefix')->label(trans('texts.prefix')) !!} - {!! Former::text('quote_number_counter')->label(trans('texts.counter')) - ->append(Former::checkbox('share_counter')->raw()->onclick('setQuoteNumberEnabled()') . ' ' . trans('texts.share_invoice_counter')) !!} -
    -
    - - -
    -
    -

    {!! trans('texts.pdf_settings') !!}

    -
    -
    - {!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!} -
    -
    -
    -
    - @if (Auth::user()->isPro()) -
    - {!! Button::success(trans('texts.save'))->large()->submit()->appendIcon(Icon::create('floppy-disk')) !!} -
    - @else - - @endif +
    +
    +

    {!! trans('texts.custom_fields') !!}

    +
    +
    + + +
    +
    +
    + + {!! Former::text('custom_client_label1') + ->label(trans('texts.field_label')) !!} + {!! Former::text('custom_client_label2') + ->label(trans('texts.field_label')) + ->help(trans('texts.custom_client_fields_helps')) !!} + +
    +
    +
    +
    + + {!! Former::text('custom_label1') + ->label(trans('texts.field_label')) !!} + {!! Former::text('custom_value1') + ->label(trans('texts.field_value')) !!} +

     

    + {!! Former::text('custom_label2') + ->label(trans('texts.field_label')) !!} + {!! Former::text('custom_value2') + ->label(trans('texts.field_value')) + ->help(trans('texts.custom_account_fields_helps')) !!} + +
    +
    +
    +
    + + {!! Former::text('custom_invoice_text_label1') + ->label(trans('texts.field_label')) !!} + {!! Former::text('custom_invoice_text_label2') + ->label(trans('texts.field_label')) + ->help(trans('texts.custom_invoice_fields_helps')) !!} + +
    +
    +
    +
    + + {!! Former::text('custom_invoice_label1') + ->label(trans('texts.field_label')) + ->addGroupClass('pad-checkbox') + ->append(Former::checkbox('custom_invoice_taxes1') + ->raw() . trans('texts.charge_taxes')) !!} + {!! Former::text('custom_invoice_label2') + ->label(trans('texts.field_label')) + ->addGroupClass('pad-checkbox') + ->append(Former::checkbox('custom_invoice_taxes2') + ->raw() . trans('texts.charge_taxes')) + ->help(trans('texts.custom_invoice_charges_helps')) !!} + +
    +
    +
    +
    +
    + +
    +
    +

    {!! trans('texts.quote_settings') !!}

    +
    +
    + {!! Former::checkbox('auto_convert_quote') + ->text(trans('texts.enable')) + ->blockHelp(trans('texts.auto_convert_quote_help')) !!} +
    +
    + +
    +
    +

    {!! trans('texts.default_messages') !!}

    +
    +
    + + +
    +
    +
    + {!! Former::textarea('invoice_terms') + ->label(trans('texts.default_invoice_terms')) + ->rows(4) !!} +
    +
    +
    +
    + {!! Former::textarea('invoice_footer') + ->label(trans('texts.default_invoice_footer')) + ->rows(4) !!} +
    +
    +
    +
    + {!! Former::textarea('quote_terms') + ->label(trans('texts.default_quote_terms')) + ->rows(4) !!} +
    +
    +
    +
    +
    + + + + @if (Auth::user()->isPro()) +
    + {!! Button::success(trans('texts.save'))->large()->submit()->appendIcon(Icon::create('floppy-disk')) !!} +
    + @endif + + + {!! Former::close() !!} @@ -125,9 +278,37 @@ $('#quote_number_counter').val(disabled ? '' : '{!! $account->quote_number_counter !!}'); } + function onInvoiceNumberTypeChange() { + var val = $('input[name=invoice_number_type]:checked').val() + if (val == 'prefix') { + $('.invoice-prefix').show(); + $('.invoice-pattern').hide(); + } else { + $('.invoice-prefix').hide(); + $('.invoice-pattern').show(); + } + } + + function onQuoteNumberTypeChange() { + var val = $('input[name=quote_number_type]:checked').val() + if (val == 'prefix') { + $('.quote-prefix').show(); + $('.quote-pattern').hide(); + } else { + $('.quote-prefix').hide(); + $('.quote-pattern').show(); + } + } + + $('.number-pattern .input-group-addon').click(function() { + $('#patternHelpModal').modal('show'); + }); + $(function() { setQuoteNumberEnabled(); - }); + onInvoiceNumberTypeChange(); + onQuoteNumberTypeChange(); + }); @@ -136,4 +317,4 @@ @section('onReady') $('#custom_invoice_label1').focus(); -@stop \ No newline at end of file +@stop diff --git a/resources/views/accounts/localization.blade.php b/resources/views/accounts/localization.blade.php new file mode 100644 index 000000000000..1ad5de55ff17 --- /dev/null +++ b/resources/views/accounts/localization.blade.php @@ -0,0 +1,46 @@ +@extends('header') + +@section('content') + @parent + + {!! Former::open_for_files()->addClass('warn-on-exit') !!} + {{ Former::populate($account) }} + {{ Former::populateField('military_time', intval($account->military_time)) }} + + @include('accounts.nav', ['selected' => ACCOUNT_LOCALIZATION]) + +
    + +
    +
    +

    {!! trans('texts.localization') !!}

    +
    +
    + + {!! Former::select('currency_id')->addOption('','') + ->fromQuery($currencies, 'name', 'id') !!} + {!! Former::select('language_id')->addOption('','') + ->fromQuery($languages, 'name', 'id') !!} + {!! Former::select('timezone_id')->addOption('','') + ->fromQuery($timezones, 'location', 'id') !!} + {!! Former::select('date_format_id')->addOption('','') + ->fromQuery($dateFormats, 'label', 'id') !!} + {!! Former::select('datetime_format_id')->addOption('','') + ->fromQuery($datetimeFormats, 'label', 'id') !!} + {!! Former::checkbox('military_time')->text(trans('texts.enable')) !!} + +
    +
    +
    + +
    + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
    + + {!! Former::close() !!} + +@stop + +@section('onReady') + $('#currency_id').focus(); +@stop \ No newline at end of file diff --git a/resources/views/accounts/nav.blade.php b/resources/views/accounts/nav.blade.php index 7341d37231af..ae7f38c2fa8b 100644 --- a/resources/views/accounts/nav.blade.php +++ b/resources/views/accounts/nav.blade.php @@ -1,16 +1,37 @@ -@extends('header') +@if (!Utils::isPro() && isset($advanced) && $advanced) +
    +
    + {!! trans('texts.pro_plan_advanced_settings', ['link'=>''.trans('texts.pro_plan.remove_logo_link').'']) !!} +
    +
    +@endif -@section('content') +
    - +
    + @foreach([ + BASIC_SETTINGS => \App\Models\Account::$basicSettings, + ADVANCED_SETTINGS => \App\Models\Account::$advancedSettings, + ] as $type => $settings) +
    +
    + {{ trans("texts.{$type}") }} + @if ($type === ADVANCED_SETTINGS && !Utils::isPro()) + {{ strtoupper(trans('texts.pro')) }} + @endif +
    +
    + @foreach ($settings as $section) + {{ trans("texts.{$section}") }} + @endforeach + @if ($type === ADVANCED_SETTINGS && !Utils::isNinjaProd()) + {{ trans("texts.system_settings") }} + @endif +
    +
    + @endforeach +
    -
    - -@stop \ No newline at end of file +
    \ No newline at end of file diff --git a/resources/views/accounts/nav_advanced.blade.php b/resources/views/accounts/nav_advanced.blade.php deleted file mode 100644 index 6726837af01c..000000000000 --- a/resources/views/accounts/nav_advanced.blade.php +++ /dev/null @@ -1,17 +0,0 @@ - -

     

    - -@if (!Auth::user()->account->isPro()) -
    -
    {!! trans('texts.pro_plan_advanced_settings', ['link'=>''.trans('texts.pro_plan.remove_logo_link').'']) !!}
    -  

      -

    -@endif - -
    \ No newline at end of file diff --git a/resources/views/accounts/notifications.blade.php b/resources/views/accounts/notifications.blade.php index 52c38e9618c1..2b1ebd4847d9 100644 --- a/resources/views/accounts/notifications.blade.php +++ b/resources/views/accounts/notifications.blade.php @@ -1,9 +1,11 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent - {!! Former::open()->addClass('col-md-8 col-md-offset-2 warn-on-exit') !!} + @include('accounts.nav', ['selected' => ACCOUNT_NOTIFICATIONS]) + + {!! Former::open()->addClass('warn-on-exit') !!} {{ Former::populate($account) }} {{ Former::populateField('notify_sent', intval(Auth::user()->notify_sent)) }} {{ Former::populateField('notify_viewed', intval(Auth::user()->notify_viewed)) }} @@ -24,43 +26,29 @@ - - -
    -
    -

    {!! trans('texts.custom_messages') !!}

    -
    -
    - {!! Former::textarea('invoice_terms')->label(trans('texts.default_invoice_terms'))->rows(4) - ->onchange("$('#invoice_terms').val(wordWrapText($('#invoice_terms').val(), 300))") !!} - {!! Former::textarea('invoice_footer')->label(trans('texts.default_invoice_footer'))->rows(4) - ->onchange("$('#invoice_footer').val(wordWrapText($('#invoice_footer').val(), 600))") !!} - {!! Former::textarea('email_footer')->label(trans('texts.default_email_footer'))->rows(4) !!} + +
    - + --> + {!! Former::actions( Button::success(trans('texts.save')) ->submit()->large() diff --git a/resources/views/accounts/partials/map.blade.php b/resources/views/accounts/partials/map.blade.php new file mode 100644 index 000000000000..fab25f4158ad --- /dev/null +++ b/resources/views/accounts/partials/map.blade.php @@ -0,0 +1,66 @@ +
    +
    +

    {!! trans("texts.import_{$entityType}s") !!}

    +
    +
    + + + +

     

    + + + + + + + + + + @for ($i=0; $i + + + + + @endfor +
    {{ trans('texts.column') }}{{ trans('texts.sample') }}{{ trans('texts.import_to') }}
    {{ $headers[$i] }}{{ $data[1][$i] }}{!! Former::select('map['.$entityType.'][' . $i . ']')->options($columns, $mapped[$i])->raw() !!}
    + +

     

    + + + +
    +
    + + diff --git a/resources/views/accounts/payment_term.blade.php b/resources/views/accounts/payment_term.blade.php new file mode 100644 index 000000000000..a1939995dcbb --- /dev/null +++ b/resources/views/accounts/payment_term.blade.php @@ -0,0 +1,47 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_PAYMENT_TERMS]) + + {!! Former::open($url)->method($method) + ->rules([ + 'name' => 'required', + 'num_days' => 'required' + ]) + ->addClass('warn-on-exit') !!} + + +
    +
    +

    {!! $title !!}

    +
    +
    + + @if ($paymentTerm) + {{ Former::populate($paymentTerm) }} + @endif + + {!! Former::text('name')->label('texts.name') !!} + {!! Former::text('num_days')->label('texts.num_days') !!} + +
    +
    + + {!! Former::actions( + Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/payment_terms'))->appendIcon(Icon::create('remove-circle')), + Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) + ) !!} + + {!! Former::close() !!} + + + +@stop \ No newline at end of file diff --git a/resources/views/accounts/payment_terms.blade.php b/resources/views/accounts/payment_terms.blade.php new file mode 100644 index 000000000000..f88016991f13 --- /dev/null +++ b/resources/views/accounts/payment_terms.blade.php @@ -0,0 +1,33 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_PAYMENT_TERMS]) + + {!! Button::primary(trans('texts.create_payment_term')) + ->asLinkTo(URL::to('/payment_terms/create')) + ->withAttributes(['class' => 'pull-right']) + ->appendIcon(Icon::create('plus-sign')) !!} + + @include('partials.bulk_form', ['entityType' => ENTITY_PAYMENT_TERM]) + + {!! Datatable::table() + ->addColumn( + trans('texts.name'), + trans('texts.num_days'), + trans('texts.action')) + ->setUrl(url('api/payment_terms/')) + ->setOptions('sPaginationType', 'bootstrap') + ->setOptions('bFilter', false) + ->setOptions('bAutoWidth', false) + ->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]]) + ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]]) + ->render('datatable') !!} + + + + +@stop \ No newline at end of file diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index 0783bdb492f1..d2fcde895200 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -1,15 +1,8 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent - - {!! Former::open('gateways/delete')->addClass('user-form') !!} - -
    - {!! Former::text('accountGatewayPublicId') !!} -
    - {!! Former::close() !!} - + @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) @if ($showAdd) {!! Button::primary(trans('texts.add_gateway')) @@ -18,6 +11,8 @@ ->appendIcon(Icon::create('plus-sign')) !!} @endif + @include('partials.bulk_form', ['entityType' => ENTITY_ACCOUNT_GATEWAY]) + {!! Datatable::table() ->addColumn( trans('texts.name'), @@ -32,32 +27,7 @@ ->render('datatable') !!} @stop \ No newline at end of file diff --git a/resources/views/accounts/product.blade.php b/resources/views/accounts/product.blade.php index cac6cf81d903..d970ea7cbbb9 100644 --- a/resources/views/accounts/product.blade.php +++ b/resources/views/accounts/product.blade.php @@ -1,18 +1,20 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent + @include('accounts.nav', ['selected' => ACCOUNT_PRODUCTS]) + {!! Former::open($url)->method($method) ->rules(['product_key' => 'required|max:255']) - ->addClass('col-md-8 col-md-offset-2 warn-on-exit') !!} + ->addClass('warn-on-exit') !!}

    {!! $title !!}

    -
    +
    @if ($product) {{ Former::populate($product) }} @@ -23,11 +25,18 @@ {!! Former::textarea('notes') !!} {!! Former::text('cost') !!} + @if ($account->invoice_item_taxes) + {!! Former::select('default_tax_rate_id') + ->addOption('', '') + ->label(trans('texts.tax_rate')) + ->fromQuery($taxRates, function($model) { return $model->name . ' ' . $model->rate . '%'; }, 'id') !!} + @endif +
    {!! Former::actions( - Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/company/products'))->appendIcon(Icon::create('remove-circle')), + Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/products'))->appendIcon(Icon::create('remove-circle')), Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) ) !!} diff --git a/resources/views/accounts/products.blade.php b/resources/views/accounts/products.blade.php index 3047008dc3c0..03a465413fe5 100644 --- a/resources/views/accounts/products.blade.php +++ b/resources/views/accounts/products.blade.php @@ -1,8 +1,10 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent + @include('accounts.nav', ['selected' => ACCOUNT_PRODUCTS]) + {!! Former::open()->addClass('warn-on-exit') !!} {{ Former::populateField('fill_products', intval($account->fill_products)) }} {{ Former::populateField('update_products', intval($account->update_products)) }} @@ -17,7 +19,7 @@ {!! Former::checkbox('fill_products')->text(trans('texts.fill_products_help')) !!} {!! Former::checkbox('update_products')->text(trans('texts.update_products_help')) !!}   - {!! Former::actions( Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) ) !!} + {!! Former::actions( Button::success(trans('texts.save'))->submit()->appendIcon(Icon::create('floppy-disk')) ) !!} {!! Former::close() !!}
    @@ -27,31 +29,20 @@ ->withAttributes(['class' => 'pull-right']) ->appendIcon(Icon::create('plus-sign')) !!} + @include('partials.bulk_form', ['entityType' => ENTITY_PRODUCT]) + {!! Datatable::table() - ->addColumn( - trans('texts.product'), - trans('texts.description'), - trans('texts.unit_cost'), - trans('texts.action')) + ->addColumn($columns) ->setUrl(url('api/products/')) ->setOptions('sPaginationType', 'bootstrap') ->setOptions('bFilter', false) ->setOptions('bAutoWidth', false) - ->setOptions('aoColumns', [[ "sWidth"=> "20%" ], [ "sWidth"=> "45%" ], ["sWidth"=> "20%"], ["sWidth"=> "15%" ]]) + //->setOptions('aoColumns', [[ "sWidth"=> "15%" ], [ "sWidth"=> "35%" ]]) ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[3]]]) ->render('datatable') !!} diff --git a/resources/views/accounts/system_settings.blade.php b/resources/views/accounts/system_settings.blade.php new file mode 100644 index 000000000000..31be6a9d0962 --- /dev/null +++ b/resources/views/accounts/system_settings.blade.php @@ -0,0 +1,37 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_SYSTEM_SETTINGS]) + +
    + + {!! Former::open('/update_setup') + ->addClass('warn-on-exit') + ->autocomplete('off') + ->rules([ + 'app[url]' => 'required', + //'database[default]' => 'required', + 'database[type][host]' => 'required', + 'database[type][database]' => 'required', + 'database[type][username]' => 'required', + 'database[type][password]' => 'required', + ]) !!} + + + @include('partials.system_settings') + +
    + +
    + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
    + + {!! Former::close() !!} + +@stop + +@section('onReady') + $('#app\\[url\\]').focus(); +@stop \ No newline at end of file diff --git a/resources/views/accounts/tax_rate.blade.php b/resources/views/accounts/tax_rate.blade.php new file mode 100644 index 000000000000..5889c7aecb96 --- /dev/null +++ b/resources/views/accounts/tax_rate.blade.php @@ -0,0 +1,47 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_TAX_RATES]) + + {!! Former::open($url)->method($method) + ->rules([ + 'name' => 'required', + 'rate' => 'required' + ]) + ->addClass('warn-on-exit') !!} + + +
    +
    +

    {!! $title !!}

    +
    +
    + + @if ($taxRate) + {{ Former::populate($taxRate) }} + @endif + + {!! Former::text('name')->label('texts.name') !!} + {!! Former::text('rate')->label('texts.rate')->append('%') !!} + +
    +
    + + {!! Former::actions( + Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/tax_rates'))->appendIcon(Icon::create('remove-circle')), + Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) + ) !!} + + {!! Former::close() !!} + + + +@stop \ No newline at end of file diff --git a/resources/views/accounts/tax_rates.blade.php b/resources/views/accounts/tax_rates.blade.php new file mode 100644 index 000000000000..2079d72f2155 --- /dev/null +++ b/resources/views/accounts/tax_rates.blade.php @@ -0,0 +1,72 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_TAX_RATES]) + + {!! Former::open()->addClass('warn-on-exit') !!} + {{ Former::populate($account) }} + {{ Former::populateField('invoice_taxes', intval($account->invoice_taxes)) }} + {{ Former::populateField('invoice_item_taxes', intval($account->invoice_item_taxes)) }} + {{ Former::populateField('show_item_taxes', intval($account->show_item_taxes)) }} + + +
    +
    +

    {!! trans('texts.tax_settings') !!}

    +
    +
    + + {!! Former::checkbox('invoice_taxes') + ->text(trans('texts.enable_invoice_tax')) + ->label(' ') !!} + + {!! Former::checkbox('invoice_item_taxes') + ->text(trans('texts.enable_line_item_tax')) + ->label(' ') !!} + + {!! Former::checkbox('show_item_taxes') + ->text(trans('texts.show_line_item_tax')) + ->label(' ') !!} + +   + + {!! Former::select('default_tax_rate_id') + ->style('max-width: 250px') + ->addOption('', '') + ->fromQuery($taxRates, function($model) { return $model->name . ': ' . $model->rate . '%'; }, 'id') !!} + + +   + {!! Former::actions( Button::success(trans('texts.save'))->submit()->appendIcon(Icon::create('floppy-disk')) ) !!} + {!! Former::close() !!} +
    +
    + + {!! Button::primary(trans('texts.create_tax_rate')) + ->asLinkTo(URL::to('/tax_rates/create')) + ->withAttributes(['class' => 'pull-right']) + ->appendIcon(Icon::create('plus-sign')) !!} + + @include('partials.bulk_form', ['entityType' => ENTITY_TAX_RATE]) + + {!! Datatable::table() + ->addColumn( + trans('texts.name'), + trans('texts.rate'), + trans('texts.action')) + ->setUrl(url('api/tax_rates/')) + ->setOptions('sPaginationType', 'bootstrap') + ->setOptions('bFilter', false) + ->setOptions('bAutoWidth', false) + ->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]]) + ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]]) + ->render('datatable') !!} + + + + +@stop \ No newline at end of file diff --git a/resources/views/accounts/template.blade.php b/resources/views/accounts/template.blade.php new file mode 100644 index 000000000000..0d4294af5b6f --- /dev/null +++ b/resources/views/accounts/template.blade.php @@ -0,0 +1,107 @@ +
    +
    + @if (isset($isReminder) && $isReminder) + + {!! Former::populateField('enable_' . $field, intval($account->{'enable_' . $field})) !!} + +
    +
    + {!! Former::checkbox('enable_' . $field) + ->text(trans('texts.enable'))->label('') !!} + + {!! Former::plaintext('schedule') + ->value( + Former::input('num_days_' . $field) + ->addClass('enable-' . $field) + ->style('float:left;width:20%') + ->raw() . + Former::select('direction_' . $field) + ->addOption(trans('texts.days_before'), REMINDER_DIRECTION_BEFORE) + ->addOption(trans('texts.days_after'), REMINDER_DIRECTION_AFTER) + ->addClass('enable-' . $field) + ->style('float:left;width:40%') + ->raw() . + '' . + Former::select('field_' . $field) + ->addOption(trans('texts.field_due_date'), REMINDER_FIELD_DUE_DATE) + ->addOption(trans('texts.field_invoice_date'), REMINDER_FIELD_INVOICE_DATE) + ->addClass('enable-' . $field) + ->style('float:left;width:40%') + ->raw() + ) !!} +
    +
    + @endif +
    +
    + + {!! Former::text('email_subject_' . $field) + ->label(trans('texts.subject')) + ->appendIcon('question-sign') + ->addGroupClass('email-subject') + ->addClass('enable-' . $field) !!} +
    +
    +

     

    +

    +
    +
    +
    +
    +
    + + {!! Former::textarea('email_template_' . $field) + ->label(trans('texts.body')) + ->addClass('enable-' . $field) + ->style('display:none') !!} +
    +
    +
    +
    +

     

    +

    +
    +
    +
    +
    +

     

    + @include('partials/quill_toolbar', ['name' => $field]) +

    +
    +
    +
    + + \ No newline at end of file diff --git a/resources/views/accounts/templates_and_reminders.blade.php b/resources/views/accounts/templates_and_reminders.blade.php new file mode 100644 index 000000000000..6b88ab70f9be --- /dev/null +++ b/resources/views/accounts/templates_and_reminders.blade.php @@ -0,0 +1,256 @@ +@extends('header') + +@section('head') + @parent + + @include('money_script') + + + + + + + +@stop + +@section('content') + @parent + @include('accounts.nav', ['selected' => ACCOUNT_TEMPLATES_AND_REMINDERS, 'advanced' => true]) + + + {!! Former::vertical_open()->addClass('warn-on-exit') !!} + {!! Former::populate($account) !!} + + @foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) + @foreach (['subject', 'template'] as $field) + {!! Former::populateField("email_{$field}_{$type}", $templates[$type][$field]) !!} + @endforeach + @endforeach + +
    +
    +

    {!! trans('texts.email_templates') !!}

    +
    +
    +
    +
    + +
    + @include('accounts.template', ['field' => 'invoice', 'active' => true]) + @include('accounts.template', ['field' => 'quote']) + @include('accounts.template', ['field' => 'payment']) +
    +
    +
    +
    +
    + +

     

    + +
    +
    +

    {!! trans('texts.reminder_emails') !!}

    +
    +
    +
    +
    + +
    + @include('accounts.template', ['field' => 'reminder1', 'isReminder' => true, 'active' => true]) + @include('accounts.template', ['field' => 'reminder2', 'isReminder' => true]) + @include('accounts.template', ['field' => 'reminder3', 'isReminder' => true]) +
    +
    +
    +
    +
    + + + + + @if (Auth::user()->isPro()) +
    + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
    + @else + + @endif + + {!! Former::close() !!} + + + +@stop diff --git a/resources/views/accounts/token.blade.php b/resources/views/accounts/token.blade.php index ce2227269705..8fc4a3b14efe 100644 --- a/resources/views/accounts/token.blade.php +++ b/resources/views/accounts/token.blade.php @@ -1,10 +1,10 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent - @include('accounts.nav_advanced') + @include('accounts.nav', ['selected' => ACCOUNT_API_TOKENS]) - {!! Former::open($url)->method($method)->addClass('col-md-8 col-md-offset-2 warn-on-exit')->rules(array( + {!! Former::open($url)->method($method)->addClass('warn-on-exit')->rules(array( 'name' => 'required', )); !!} @@ -12,7 +12,7 @@

    {!! trans($title) !!}

    -
    +
    @if ($token) {!! Former::populate($token) !!} @@ -22,11 +22,20 @@
    + + @if (Auth::user()->isPro()) + {!! Former::actions( + Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/api_tokens'))->appendIcon(Icon::create('remove-circle'))->large(), + Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) + ) !!} + @else + + @endif - {!! Former::actions( - Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/company/advanced_settings/token_management'))->appendIcon(Icon::create('remove-circle'))->large(), - Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) - ) !!} {!! Former::close() !!} diff --git a/resources/views/accounts/user_details.blade.php b/resources/views/accounts/user_details.blade.php new file mode 100644 index 000000000000..346fc342107a --- /dev/null +++ b/resources/views/accounts/user_details.blade.php @@ -0,0 +1,241 @@ +@extends('header') + +@section('content') + @parent + + {!! Former::open_for_files()->addClass('warn-on-exit')->rules(array( + 'email' => 'email|required' + )) !!} + + {{ Former::populate($account) }} + {{ Former::populateField('first_name', $user->first_name) }} + {{ Former::populateField('last_name', $user->last_name) }} + {{ Former::populateField('email', $user->email) }} + {{ Former::populateField('phone', $user->phone) }} + + @if (Utils::isNinjaDev()) + {{ Former::populateField('dark_mode', intval($user->dark_mode)) }} + @endif + + @if (Input::has('affiliate')) + {{ Former::populateField('referral_code', true) }} + @endif + + @include('accounts.nav', ['selected' => ACCOUNT_USER_DETAILS]) + +
    +
    + +
    +
    +

    {!! trans('texts.user_details') !!}

    +
    +
    + {!! Former::text('first_name') !!} + {!! Former::text('last_name') !!} + {!! Former::text('email') !!} + {!! Former::text('phone') !!} + +
    + + @if (Utils::isOAuthEnabled()) + {!! Former::plaintext('oneclick_login')->value( + $user->oauth_provider_id ? + $oauthProviderName . ' - ' . link_to('#', trans('texts.disable'), ['onclick' => 'disableSocialLogin()']) : + DropdownButton::primary(trans('texts.enable'))->withContents($oauthLoginUrls)->small() + )->help('oneclick_login_help') + !!} + @endif + + @if (Utils::isNinja()) + @if ($user->referral_code) + {{ Former::setOption('capitalize_translations', false) }} + {!! Former::plaintext('referral_code') + ->help($referralCounts['free'] . ' ' . trans('texts.free') . ' | ' . + $referralCounts['pro'] . ' ' . trans('texts.pro') . + '' . Icon::create('question-sign') . ' ') + ->value(NINJA_APP_URL . '/invoice_now?rc=' . $user->referral_code) !!} + @else + {!! Former::checkbox('referral_code') + ->help(trans('texts.referral_code_help')) + ->text(trans('texts.enable') . ' ' . Icon::create('question-sign') . '') !!} + @endif + @endif + + @if (false && Utils::isNinjaDev()) + {!! Former::checkbox('dark_mode')->text(trans('texts.dark_mode_help')) !!} + @endif + +
    +
    + +
    +
    + @if (Auth::user()->confirmed) + {!! Button::primary(trans('texts.change_password')) + ->appendIcon(Icon::create('lock')) + ->large()->withAttributes(['onclick'=>'showChangePassword()']) !!} + @elseif (Auth::user()->registered && Utils::isNinja()) + {!! Button::primary(trans('texts.resend_confirmation')) + ->appendIcon(Icon::create('send')) + ->asLinkTo(URL::to('/resend_confirmation'))->large() !!} + @endif + {!! Button::success(trans('texts.save')) + ->submit()->large() + ->appendIcon(Icon::create('floppy-disk')) !!} +
    +
    + + + + + {!! Former::close() !!} + + + +@stop + +@section('onReady') + $('#first_name').focus(); +@stop \ No newline at end of file diff --git a/resources/views/accounts/user_management.blade.php b/resources/views/accounts/user_management.blade.php index 44946f7b5011..969154faec0b 100644 --- a/resources/views/accounts/user_management.blade.php +++ b/resources/views/accounts/user_management.blade.php @@ -1,19 +1,11 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent - @include('accounts.nav_advanced') - - {!! Former::open('users/delete')->addClass('user-form') !!} - -
    - {!! Former::text('userPublicId') !!} -
    - {!! Former::close() !!} + @include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT, 'advanced' => true])
    - {!! Button::normal(trans('texts.api_tokens'))->asLinkTo(URL::to('/company/advanced_settings/token_management'))->appendIcon(Icon::create('cloud')) !!} @if (Utils::isPro()) {!! Button::primary(trans('texts.add_user'))->asLinkTo(URL::to('/users/create'))->appendIcon(Icon::create('plus-sign')) !!} @endif @@ -22,9 +14,10 @@ + @include('partials.bulk_form', ['entityType' => ENTITY_USER]) {!! Datatable::table() ->addColumn( @@ -41,30 +34,14 @@ ->render('datatable') !!} @stop diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index b3ac449db155..fe275fe1d54f 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -62,8 +62,11 @@ @section('body')
    + @include('partials.warn_session', ['redirectTo' => '/login']) - {!! Former::open('login')->rules(['email' => 'required|email', 'password' => 'required'])->addClass('form-signin') !!} + {!! Former::open('login') + ->rules(['email' => 'required|email', 'password' => 'required']) + ->addClass('form-signin') !!} {{ Former::populateField('remember', 'true') }} - @endif + @endif @if (Session::has('warning'))
    {{ Session::get('warning') }}
    @@ -156,7 +173,15 @@ } else { $('#email').focus(); } + + /* + var authProvider = localStorage.getItem('auth_provider'); + if (authProvider) { + $('#' + authProvider + 'LoginButton').removeClass('btn-primary').addClass('btn-success'); + } + */ }) + @endsection \ No newline at end of file diff --git a/resources/views/clients/edit.blade.php b/resources/views/clients/edit.blade.php index e3cf16d7ee14..72835f170f3d 100644 --- a/resources/views/clients/edit.blade.php +++ b/resources/views/clients/edit.blade.php @@ -6,16 +6,25 @@ @stop @section('content') + +@if ($errors->first('contacts')) +
    {{ trans($errors->first('contacts')) }}
    +@endif +
    {!! Former::open($url) + ->autocomplete('off') ->rules( ['email' => 'email'] )->addClass('col-md-12 warn-on-exit') ->method($method) !!} + + @include('partials.autocomplete_fix') @if ($client) {!! Former::populate($client) !!} + {!! Former::hidden('public_id') !!} @endif
    @@ -34,7 +43,7 @@ {!! Former::text('website') !!} {!! Former::text('work_phone') !!} - @if (Auth::user()->isPro()) + @if (Auth::user()->isPro()) @if ($customLabel1) {!! Former::text('custom_value1')->label($customLabel1) !!} @endif @@ -74,11 +83,16 @@
    - {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('email')->data_bind('value: email, valueUpdate: \'afterkeydown\', attr: {id:\'email\'+$index()}') !!} - {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") !!} + {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', + attr: {name: 'contacts[' + \$index() + '][public_id]'}") !!} + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown', + attr: {name: 'contacts[' + \$index() + '][first_name]'}") !!} + {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown', + attr: {name: 'contacts[' + \$index() + '][last_name]'}") !!} + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown', + attr: {name: 'contacts[' + \$index() + '][email]', id:'email'+\$index()}") !!} + {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', + attr: {name: 'contacts[' + \$index() + '][phone]'}") !!}
    @@ -102,7 +116,11 @@
    {!! Former::select('currency_id')->addOption('','') + ->placeholder($account->currency ? $account->currency->name : '') ->fromQuery($currencies, 'name', 'id') !!} + {!! Former::select('language_id')->addOption('','') + ->placeholder($account->language ? $account->language->name : '') + ->fromQuery($languages, 'name', 'id') !!} {!! Former::select('payment_terms')->addOption('','') ->fromQuery($paymentTerms, 'name', 'num_days') ->help(trans('texts.payment_terms_help')) !!} @@ -111,6 +129,21 @@ {!! Former::select('industry_id')->addOption('','') ->fromQuery($industries, 'name', 'id') !!} {!! Former::textarea('private_notes') !!} + + + @if (isset($proPlanPaid)) + {!! Former::populateField('pro_plan_paid', $proPlanPaid) !!} + {!! Former::text('pro_plan_paid') + ->data_date_format('yyyy-mm-dd') + ->addGroupClass('pro_plan_paid_date') + ->append('') !!} + + @endif +
    @@ -135,13 +168,14 @@ self.phone = ko.observable(''); if (data) { - ko.mapping.fromJS(data, {}, this); - } + ko.mapping.fromJS(data, {}, this); + } } - function ContactsModel(data) { + function ClientModel(data) { var self = this; - self.contacts = ko.observableArray(); + + self.contacts = ko.observableArray(); self.mapping = { 'contacts': { @@ -149,10 +183,10 @@ return new ContactModel(options.data); } } - } + } if (data) { - ko.mapping.fromJS(data, self.mapping, this); + ko.mapping.fromJS(data, self.mapping, this); } else { self.contacts.push(new ContactModel()); } @@ -168,7 +202,11 @@ }); } - window.model = new ContactsModel({!! $client !!}); + @if ($data) + window.model = new ClientModel({!! $data !!}); + @else + window.model = new ClientModel({!! $client !!}); + @endif model.showContact = function(elem) { if (elem.nodeType === 1) $(elem).hide().slideDown() } model.hideContact = function(elem) { if (elem.nodeType === 1) $(elem).slideUp(function() { $(elem).remove(); }) } diff --git a/resources/views/clients/show.blade.php b/resources/views/clients/show.blade.php index 2c507d504048..06b0e7774abb 100644 --- a/resources/views/clients/show.blade.php +++ b/resources/views/clients/show.blade.php @@ -1,13 +1,31 @@ @extends('header') -@section('content') +@section('head') + @parent + @if ($client->hasAddress()) + + + + @endif +@stop + + +@section('content')
    {!! Former::open('clients/bulk')->addClass('mainForm') !!}
    {!! Former::text('action') !!} - {!! Former::text('id')->value($client->public_id) !!} + {!! Former::text('public_id')->value($client->public_id) !!}
    @if ($gatewayLink) @@ -60,17 +78,11 @@ @if ($client->address2) {{ $client->address2 }}
    @endif - @if ($client->city) - {{ $client->city }}, - @endif - @if ($client->state) - {{ $client->state }} - @endif - @if ($client->postal_code) - {{ $client->postal_code }} + @if ($client->getCityState()) + {{ $client->getCityState() }}
    @endif @if ($client->country) -
    {{ $client->country->name }} + {{ $client->country->name }}
    @endif @if ($client->account->custom_client_label1 && $client->custom_value1) @@ -81,7 +93,7 @@ @endif @if ($client->work_phone) - {{ Utils::formatPhoneNumber($client->work_phone) }} + {{ $client->work_phone }} @endif @if ($client->private_notes) @@ -96,7 +108,11 @@ @endif @if ($client->website) -

    {!! $client->getWebsite() !!}

    +

    {!! Utils::formatWebsite($client->website) !!}

    + @endif + + @if ($client->language) +

    {{ $client->language->name }}

    @endif

    {{ $client->payment_terms ? trans('texts.payment_terms') . ": Net " . $client->payment_terms : '' }}

    @@ -112,14 +128,14 @@ {!! HTML::mailto($contact->email, $contact->email) !!}
    @endif @if ($contact->phone) - {!! Utils::formatPhoneNumber($contact->phone) !!}
    + {{ $contact->phone }}
    @endif @endforeach
    -
    +

    {{ trans('texts.standing') }} - +
    @@ -136,12 +152,16 @@ @endif
    {{ trans('texts.paid_to_date') }} {{ Utils::formatMoney($client->paid_to_date, $client->getCurrencyId()) }}

    -
    + @if ($client->hasAddress()) +
    +
    + @endif +
    @@ -61,6 +64,9 @@ $('#amount').focus(); @endif + $('.credit_date .input-group-addon').click(function() { + toggleDatePicker('credit_date'); + }); }); diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index f4e52f16e67f..56f9690610ec 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,5 +1,6 @@ @extends('header') + @section('content')
    @@ -66,13 +67,13 @@

     

    -
    +

    - {{ trans('texts.notifications') }} + {{ trans('texts.activity') }}
    - {{ $invoicesSent }} {{ Utils::pluralize('invoice', $invoicesSent) }} {{ trans('texts.sent') }} + {{ trans_choice('texts.invoices_sent', $invoicesSent) }}

    @@ -80,11 +81,14 @@ @foreach ($activities as $activity)
  • {{ Utils::timestampToDateString(strtotime($activity->created_at)) }}: - {!! Utils::decodeActivity($activity->message) !!} + {!! $activity->getMessage() !!}
  • @endforeach
    +
    + +

    @@ -112,7 +116,40 @@

    - +
    +
    + +
    +
    +
    +
    +

    + {{ trans('texts.upcoming_invoices') }} +

    +
    +
    + + + + + + + + + @foreach ($upcoming as $invoice) + @if (!$invoice->is_quote) + + + + + + + @endif + @endforeach + +
    {{ trans('texts.invoice_number_short') }}{{ trans('texts.client') }}{{ trans('texts.due_date') }}{{ trans('texts.balance_due') }}
    {!! \App\Models\Invoice::calcLink($invoice) !!}{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}{{ Utils::fromSqlDate($invoice->due_date) }}{{ Utils::formatMoney($invoice->balance, $invoice->currency_id ?: ($account->currency_id ?: DEFAULT_CURRENCY)) }}
    +
    +
    @@ -131,52 +168,95 @@ @foreach ($pastDue as $invoice) - - {!! \App\Models\Invoice::calcLink($invoice) !!} - {!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!} - {{ Utils::fromSqlDate($invoice->due_date) }} - {{ Utils::formatMoney($invoice->balance, $invoice->currency_id ?: ($account->currency_id ?: DEFAULT_CURRENCY)) }} - + @if (!$invoice->is_quote) + + {!! \App\Models\Invoice::calcLink($invoice) !!} + {!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!} + {{ Utils::fromSqlDate($invoice->due_date) }} + {{ Utils::formatMoney($invoice->balance, $invoice->currency_id ?: ($account->currency_id ?: DEFAULT_CURRENCY)) }} + + @endif @endforeach
    -
    -
    -

    - {{ trans('texts.upcoming_invoices') }} -

    -
    -
    - - - - - - - - - @foreach ($upcoming as $invoice) - - - - - - - @endforeach - -
    {{ trans('texts.invoice_number_short') }}{{ trans('texts.client') }}{{ trans('texts.due_date') }}{{ trans('texts.balance_due') }}
    {!! \App\Models\Invoice::calcLink($invoice) !!}{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}{{ Utils::fromSqlDate($invoice->due_date) }}{{ Utils::formatMoney($invoice->balance, $invoice->currency_id ?: ($account->currency_id ?: DEFAULT_CURRENCY)) }}
    +
    +
    + +@if ($hasQuotes) +
    +
    +
    +
    +

    + {{ trans('texts.upcoming_quotes') }} +

    +
    +
    + + + + + + + + + @foreach ($upcoming as $invoice) + @if ($invoice->is_quote) + + + + + + + @endif + @endforeach + +
    {{ trans('texts.quote_number_short') }}{{ trans('texts.client') }}{{ trans('texts.valid_until') }}{{ trans('texts.amount') }}
    {!! \App\Models\Invoice::calcLink($invoice) !!}{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}{{ Utils::fromSqlDate($invoice->due_date) }}{{ Utils::formatMoney($invoice->balance, $invoice->currency_id ?: ($account->currency_id ?: DEFAULT_CURRENCY)) }}
    +
    - +
    +
    +
    +

    + {{ trans('texts.expired_quotes') }} +

    +
    +
    + + + + + + + + + @foreach ($pastDue as $invoice) + @if ($invoice->is_quote) + + + + + + + @endif + @endforeach + +
    {{ trans('texts.quote_number_short') }}{{ trans('texts.client') }}{{ trans('texts.valid_until') }}{{ trans('texts.amount') }}
    {!! \App\Models\Invoice::calcLink($invoice) !!}{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}{{ Utils::fromSqlDate($invoice->due_date) }}{{ Utils::formatMoney($invoice->balance, $invoice->currency_id ?: ($account->currency_id ?: DEFAULT_CURRENCY)) }}
    +
    +
    +
    -
    +@endif -
    -
    -
    -
    - -@stop + +@stop \ No newline at end of file diff --git a/resources/views/datatable.blade.php b/resources/views/datatable.blade.php index cac84815a8c0..f21f6c8cfff4 100644 --- a/resources/views/datatable.blade.php +++ b/resources/views/datatable.blade.php @@ -32,8 +32,17 @@ \ No newline at end of file diff --git a/resources/views/emails/confirm_html.blade.php b/resources/views/emails/confirm_html.blade.php index fe4b73a847aa..b7e53f7df76a 100644 --- a/resources/views/emails/confirm_html.blade.php +++ b/resources/views/emails/confirm_html.blade.php @@ -1,25 +1,29 @@ - - - - - - -@if (false && !$invitationMessage) - @include('emails.confirm_action', ['user' => $user]) -@endif +@extends('emails.master_user') -

    {{ trans('texts.confirmation_header') }}

    +@section('markup') + @if (!$invitationMessage) + @include('emails.confirm_action', ['user' => $user]) + @endif +@stop -

    - {{ $invitationMessage . trans('texts.confirmation_message') }}
    - - {!! URL::to("user/confirm/{$user->confirmation_code}")!!} - -

    - - {{ trans('texts.email_signature') }}
    - {{ trans('texts.email_from') }} -

    - - - \ No newline at end of file +@section('body') +

    {{ trans('texts.confirmation_header') }}

    +
    + {{ $invitationMessage . trans('texts.button_confirmation_message') }} +
    +   +
    +
    + @include('partials.email_button', [ + 'link' => URL::to("user/confirm/{$user->confirmation_code}"), + 'field' => 'confirm', + 'color' => '#36c157', + ]) +
    +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/confirm_text.blade.php b/resources/views/emails/confirm_text.blade.php index 652815a1f94f..7809cbfce1f7 100644 --- a/resources/views/emails/confirm_text.blade.php +++ b/resources/views/emails/confirm_text.blade.php @@ -1,7 +1,7 @@ -{{ trans('texts.confirmation_header') }} +{!! trans('texts.confirmation_header') !!} -{{ $invitationMessage . trans('texts.confirmation_message') }} +{!! $invitationMessage . trans('texts.confirmation_message') !!} {!! URL::to("user/confirm/{$user->confirmation_code}") !!} -{{ trans('texts.email_signature') }} -{{ trans('texts.email_from') }} \ No newline at end of file +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} diff --git a/resources/views/emails/contact_html.blade.php b/resources/views/emails/contact_html.blade.php index 76e2e1b75280..ab1c8cf099a5 100644 --- a/resources/views/emails/contact_html.blade.php +++ b/resources/views/emails/contact_html.blade.php @@ -1 +1 @@ -{!! nl2br($text) !!} +{{ nl2br($text) }} \ No newline at end of file diff --git a/resources/views/emails/design1_html.blade.php b/resources/views/emails/design1_html.blade.php new file mode 100644 index 000000000000..62b621545479 --- /dev/null +++ b/resources/views/emails/design1_html.blade.php @@ -0,0 +1,69 @@ +@extends('emails.master') + +@section('markup') + @if ($account->enable_email_markup) + @include('emails.partials.client_view_action') + @endif +@stop + +@section('content') + +   + + + + + + + + + +
    +

    + @if ($invoice->due_date) + + {{ strtoupper(trans('texts.due_by', ['date' => $account->formatDate($invoice->due_date)])) }} +
    + @endif + + {{ trans("texts.{$entityType}") }} {{ $invoice->invoice_number }} + +

    +
    +

    + + {{ trans('texts.' . $invoice->present()->balanceDueLabel) }}: +
    + + {{ $account->formatMoney($invoice->getRequestedAmount(), $client) }} + +

    +
    + + + + +
    {!! $body !!}
    + + +@stop + +@section('footer') +

    + {{ $account->address1 }} + @if ($account->address1 && $account->getCityState()) + - + @endif + {{ $account->getCityState() }} + @if ($account->address1 || $account->getCityState()) +
    + @endif + + @if ($account->website) + {{ $account->website }} + @endif +

    +@stop \ No newline at end of file diff --git a/resources/views/emails/design1_text.blade.php b/resources/views/emails/design1_text.blade.php new file mode 100644 index 000000000000..9b258c53a812 --- /dev/null +++ b/resources/views/emails/design1_text.blade.php @@ -0,0 +1 @@ +{!! strip_tags($body) !!} \ No newline at end of file diff --git a/resources/views/emails/design2_html.blade.php b/resources/views/emails/design2_html.blade.php new file mode 100644 index 000000000000..550b5fed472a --- /dev/null +++ b/resources/views/emails/design2_html.blade.php @@ -0,0 +1,69 @@ +@extends('emails.master') + +@section('markup') + @if ($account->enable_email_markup) + @include('emails.partials.client_view_action') + @endif +@stop + +@section('content') + +   + + + + + + + + + +
    +

    + @if ($invoice->due_date) + + {{ strtoupper(trans('texts.due_by', ['date' => $account->formatDate($invoice->due_date)])) }} +
    + @endif + + {{ trans("texts.{$entityType}") }} {{ $invoice->invoice_number }} + +

    +
    +

    + + {{ strtoupper(trans('texts.' . $invoice->present()->balanceDueLabel)) }}: +
    + + {{ $account->formatMoney($invoice->getRequestedAmount(), $client) }} + +

    +
    + + + + +
    {!! $body !!}
    + + +@stop + +@section('footer') +

    + {{ $account->address1 }} + @if ($account->address1 && $account->getCityState()) + - + @endif + {{ $account->getCityState() }} + @if ($account->address1 || $account->getCityState()) +
    + @endif + + @if ($account->website) + {{ $account->website }} + @endif +

    +@stop \ No newline at end of file diff --git a/resources/views/emails/design2_text.blade.php b/resources/views/emails/design2_text.blade.php new file mode 100644 index 000000000000..9b258c53a812 --- /dev/null +++ b/resources/views/emails/design2_text.blade.php @@ -0,0 +1 @@ +{!! strip_tags($body) !!} \ No newline at end of file diff --git a/resources/views/emails/email_bounced_html.blade.php b/resources/views/emails/email_bounced_html.blade.php new file mode 100644 index 000000000000..c87a0e9cf8d5 --- /dev/null +++ b/resources/views/emails/email_bounced_html.blade.php @@ -0,0 +1,20 @@ +@extends('emails.master_user') + +@section('body') +
    + {{ trans('texts.email_salutation', ['name' => $userName]) }} +
    +   +
    + {{ trans("texts.notification_{$entityType}_bounced", ['contact' => $contactName, 'invoice' => $invoiceNumber]) }} +
    +   +
    + {{ $emailError }} +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/email_bounced_text.blade.php b/resources/views/emails/email_bounced_text.blade.php new file mode 100644 index 000000000000..f5dc30869f12 --- /dev/null +++ b/resources/views/emails/email_bounced_text.blade.php @@ -0,0 +1,8 @@ +{!! trans('texts.email_salutation', ['name' => $userName]) !!} + +{!! trans("texts.notification_{$entityType}_bounced", ['contact' => $contactName, 'invoice' => $invoiceNumber]) !!} + +{!! $emailError !!} + +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} \ No newline at end of file diff --git a/resources/views/emails/invoice_html.blade.php b/resources/views/emails/invoice_html.blade.php index a0d2526a6e26..762726e95d81 100644 --- a/resources/views/emails/invoice_html.blade.php +++ b/resources/views/emails/invoice_html.blade.php @@ -4,8 +4,8 @@ - @if (false) - @include('emails.view_action', ['link' => $link, 'entityType' => $entityType]) + @if ($account->enable_email_markup) + @include('emails.partials.client_view_action') @endif {!! $body !!} diff --git a/resources/views/emails/invoice_paid_html.blade.php b/resources/views/emails/invoice_paid_html.blade.php index 07e478b0338d..76dabc82db1d 100644 --- a/resources/views/emails/invoice_paid_html.blade.php +++ b/resources/views/emails/invoice_paid_html.blade.php @@ -1,23 +1,32 @@ - - - - - - - @if (false) - @include('emails.view_action', ['link' => $invoiceLink, 'entityType' => $entityType]) - @endif - {{ trans('texts.email_salutation', ['name' => $userName]) }}

    +@extends('emails.master_user') - {{ trans("texts.notification_{$entityType}_paid", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }}

    +@section('markup') + @if ($account->enable_email_markup) + @include('emails.partials.user_view_action') + @endif +@stop - {{ trans("texts.{$entityType}_link_message") }}
    - {{ $invoiceLink }}

    - - {{ trans('texts.email_signature') }}
    - {{ trans('texts.email_from') }}

    - {{ trans('texts.user_email_footer') }}

    - - - - \ No newline at end of file +@section('body') +

    + {{ trans('texts.email_salutation', ['name' => $userName]) }} +
    +   +
    + {{ trans("texts.notification_{$entityType}_paid", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +
    +   +
    +
    + @include('partials.email_button', [ + 'link' => $invoiceLink, + 'field' => "view_{$entityType}", + 'color' => '#0b4d78', + ]) +
    +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/invoice_paid_text.blade.php b/resources/views/emails/invoice_paid_text.blade.php index 77f29aed49c4..f604076c1ead 100644 --- a/resources/views/emails/invoice_paid_text.blade.php +++ b/resources/views/emails/invoice_paid_text.blade.php @@ -1,11 +1,11 @@ -{{ trans('texts.email_salutation', ['name' => $userName]) }} +{!! trans('texts.email_salutation', ['name' => $userName]) !!} -{{ trans("texts.notification_{$entityType}_paid", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +{!! trans("texts.notification_{$entityType}_paid", ['amount' => $paymentAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) !!} -{{ trans("texts.{$entityType}_link_message") }} -{{ $invoiceLink }} +{!! trans("texts.{$entityType}_link_message") !!} +{!! $invoiceLink !!} -{{ trans('texts.email_signature') }} -{{ trans('texts.email_from') }} +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} -{{ trans('texts.user_email_footer') }} \ No newline at end of file +{!! trans('texts.user_email_footer') !!} \ No newline at end of file diff --git a/resources/views/emails/invoice_sent_html.blade.php b/resources/views/emails/invoice_sent_html.blade.php index 3426820e1db3..99b065afa44c 100644 --- a/resources/views/emails/invoice_sent_html.blade.php +++ b/resources/views/emails/invoice_sent_html.blade.php @@ -1,20 +1,32 @@ - - - - - - - @if (false) - @include('emails.view_action', ['link' => $invoiceLink, 'entityType' => $entityType]) - @endif - {{ trans('texts.email_salutation', ['name' => $userName]) }}

    +@extends('emails.master_user') - {{ trans("texts.notification_{$entityType}_sent", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }}

    +@section('markup') + @if ($account->enable_email_markup) + @include('emails.partials.user_view_action') + @endif +@stop - {{ trans('texts.email_signature') }}
    - {{ trans('texts.email_from') }}

    - - {{ trans('texts.user_email_footer') }}

    - - - \ No newline at end of file +@section('body') +

    + {{ trans('texts.email_salutation', ['name' => $userName]) }} +
    +   +
    + {{ trans("texts.notification_{$entityType}_sent", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +
    +   +
    +
    + @include('partials.email_button', [ + 'link' => $invoiceLink, + 'field' => "view_{$entityType}", + 'color' => '#0b4d78', + ]) +
    +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/invoice_sent_text.blade.php b/resources/views/emails/invoice_sent_text.blade.php index 72a595078029..caea95dd1fe8 100644 --- a/resources/views/emails/invoice_sent_text.blade.php +++ b/resources/views/emails/invoice_sent_text.blade.php @@ -1,8 +1,8 @@ -{{ trans('texts.email_salutation', ['name' => $userName]) }} +{!! trans('texts.email_salutation', ['name' => $userName]) !!} -{{ trans("texts.notification_{$entityType}_sent", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +{!! trans("texts.notification_{$entityType}_sent", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) !!} -{{ trans('texts.email_signature') }} -{{ trans('texts.email_from') }} +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} -{{ trans('texts.user_email_footer') }} \ No newline at end of file +{!! trans('texts.user_email_footer') !!} \ No newline at end of file diff --git a/resources/views/emails/invoice_viewed_html.blade.php b/resources/views/emails/invoice_viewed_html.blade.php index 60fd6faaae65..377752a7d41e 100644 --- a/resources/views/emails/invoice_viewed_html.blade.php +++ b/resources/views/emails/invoice_viewed_html.blade.php @@ -1,20 +1,32 @@ - - - - - - - @if (false) - @include('emails.view_action', ['link' => $invoiceLink, 'entityType' => $entityType]) - @endif - {{ trans('texts.email_salutation', ['name' => $userName]) }}

    +@extends('emails.master_user') - {{ trans("texts.notification_{$entityType}_viewed", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }}

    +@section('markup') + @if ($account->enable_email_markup) + @include('emails.partials.user_view_action') + @endif +@stop - {{ trans('texts.email_signature') }}
    - {{ trans('texts.email_from') }}

    - - {{ trans('texts.user_email_footer') }}

    - - - \ No newline at end of file +@section('body') +

    + {{ trans('texts.email_salutation', ['name' => $userName]) }} +
    +   +
    + {{ trans("texts.notification_{$entityType}_viewed", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +
    +   +
    +
    + @include('partials.email_button', [ + 'link' => $invoiceLink, + 'field' => "view_{$entityType}", + 'color' => '#0b4d78', + ]) +
    +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/invoice_viewed_text.blade.php b/resources/views/emails/invoice_viewed_text.blade.php index 7b9dadd21cdf..1de6bd5049b9 100644 --- a/resources/views/emails/invoice_viewed_text.blade.php +++ b/resources/views/emails/invoice_viewed_text.blade.php @@ -1,8 +1,8 @@ -{{ trans('texts.email_salutation', ['name' => $userName]) }} +{!! trans('texts.email_salutation', ['name' => $userName]) !!} -{{ trans("texts.notification_{$entityType}_viewed", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +{!! trans("texts.notification_{$entityType}_viewed", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) !!} -{{ trans('texts.email_signature') }} -{{ trans('texts.email_from') }} +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} -{{ trans('texts.user_email_footer') }} \ No newline at end of file +{!! trans('texts.user_email_footer') !!} \ No newline at end of file diff --git a/resources/views/emails/license_confirmation_html.blade.php b/resources/views/emails/license_confirmation_html.blade.php index f7950158958c..2dc6f8e80722 100644 --- a/resources/views/emails/license_confirmation_html.blade.php +++ b/resources/views/emails/license_confirmation_html.blade.php @@ -1,18 +1,20 @@ - - - - - - +@extends('emails.master_user') - {{ $client }},

    - - {{ trans('texts.payment_message', ['amount' => $amount]) }}

    - - {{ $license }}

    - - {{ trans('texts.email_signature') }}
    - {{ $account }} - - - \ No newline at end of file +@section('body') +

    + {{ $client }}, +
    +   +
    + {{ trans('texts.payment_message', ['amount' => $amount]) }} +
    +   +
    + {{ $license }} +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/license_confirmation_text.blade.php b/resources/views/emails/license_confirmation_text.blade.php index d8ee711c9aa8..0f35bb3d6619 100644 --- a/resources/views/emails/license_confirmation_text.blade.php +++ b/resources/views/emails/license_confirmation_text.blade.php @@ -1,8 +1,8 @@ -{{ $client }}, +{!! $client !!}, -{{ trans('texts.payment_message', ['amount' => $amount]) }} +{!! trans('texts.payment_message', ['amount' => $amount]) !!} -{{ $license }} +{!! $license !!} -{{ trans('texts.email_signature') }} -{{ $account }} \ No newline at end of file +{!! trans('texts.email_signature') !!} +{!! $account !!} \ No newline at end of file diff --git a/resources/views/emails/master.blade.php b/resources/views/emails/master.blade.php new file mode 100644 index 000000000000..5f5c3df90a2d --- /dev/null +++ b/resources/views/emails/master.blade.php @@ -0,0 +1,72 @@ + + + + + + + + + + @yield('markup') + + + +
    + + + + @yield('content') + + + + + +
    +
    + + + + \ No newline at end of file diff --git a/resources/views/emails/master_user.blade.php b/resources/views/emails/master_user.blade.php new file mode 100644 index 000000000000..15a0aa4a1677 --- /dev/null +++ b/resources/views/emails/master_user.blade.php @@ -0,0 +1,38 @@ +@extends('emails.master') + +@section('content') + +   + + + + + + + +
    + + + + +
    + @yield('body') +
    + + +@stop + +@section('footer') +

    + facebook + twitter + github +

    + +

    + © {{ date('Y') }} Invoice Ninja
    + {{ strtoupper(trans('texts.email_preferences')) }} +

    +@stop \ No newline at end of file diff --git a/resources/views/emails/partials/account_logo.blade.php b/resources/views/emails/partials/account_logo.blade.php new file mode 100644 index 000000000000..cfe158153fe7 --- /dev/null +++ b/resources/views/emails/partials/account_logo.blade.php @@ -0,0 +1,11 @@ +@if ($account->hasLogo()) + @if ($account->website) + + @endif + + + + @if ($account->website) + + @endif +@endif diff --git a/resources/views/emails/partials/client_view_action.blade.php b/resources/views/emails/partials/client_view_action.blade.php new file mode 100644 index 000000000000..c2b00faf19c7 --- /dev/null +++ b/resources/views/emails/partials/client_view_action.blade.php @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/resources/views/emails/partials/user_view_action.blade.php b/resources/views/emails/partials/user_view_action.blade.php new file mode 100644 index 000000000000..1338ebee5292 --- /dev/null +++ b/resources/views/emails/partials/user_view_action.blade.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/resources/views/emails/password.blade.php b/resources/views/emails/password.blade.php index b42fc337f5e8..f3fd34362430 100644 --- a/resources/views/emails/password.blade.php +++ b/resources/views/emails/password.blade.php @@ -1,9 +1,26 @@ -{{ trans('texts.email_salutation', ['name' => $user->username]) }}

    +@extends('emails.master_user') -{{ trans('texts.reset_password') }}
    -{!! url('password/reset/'.$token) !!}

    - -{{ trans('texts.email_signature') }}
    -{{ trans('texts.email_from') }}

    - -{{ trans('texts.reset_password_footer') }}

    \ No newline at end of file +@section('body') +

    + {{ trans('texts.reset_password') }} +
    +   +
    +
    + @include('partials.email_button', [ + 'link' => URL::to("password/reset/{$token}"), + 'field' => 'reset', + 'color' => '#36c157', + ]) +
    +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +   +
    + {{ trans('texts.reset_password_footer') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/payment_confirmation_html.blade.php b/resources/views/emails/payment_confirmation_html.blade.php index 7e944a2f39fb..87af7aada2c1 100644 --- a/resources/views/emails/payment_confirmation_html.blade.php +++ b/resources/views/emails/payment_confirmation_html.blade.php @@ -3,5 +3,10 @@ -{!! $body !!} + + @if ($account->enable_email_markup) + @include('emails.partials.client_view_action', ['link' => $link]) + @endif + {!! $body !!} + diff --git a/resources/views/emails/quote_approved_html.blade.php b/resources/views/emails/quote_approved_html.blade.php index 4f1396a1a81b..758ff76a5d98 100644 --- a/resources/views/emails/quote_approved_html.blade.php +++ b/resources/views/emails/quote_approved_html.blade.php @@ -1,20 +1,32 @@ - - - - - - - @if (false) - @include('emails.view_action', ['link' => $invoiceLink, 'entityType' => $entityType]) - @endif - {{ trans('texts.email_salutation', ['name' => $userName]) }}

    +@extends('emails.master_user') - {{ trans("texts.notification_{$entityType}_approved", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }}

    +@section('markup') + @if ($account->enable_email_markup) + @include('emails.partials.user_view_action') + @endif +@stop - {{ trans('texts.email_signature') }}
    - {{ trans('texts.email_from') }}

    - - {{ trans('texts.user_email_footer') }}

    - - - \ No newline at end of file +@section('body') +

    + {{ trans('texts.email_salutation', ['name' => $userName]) }} +
    +   +
    + {{ trans("texts.notification_quote_approved", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +
    +   +
    +
    + @include('partials.email_button', [ + 'link' => $invoiceLink, + 'field' => "view_{$entityType}", + 'color' => '#0b4d78', + ]) +
    +
    +   +
    + {{ trans('texts.email_signature') }}
    + {{ trans('texts.email_from') }} +
    +@stop \ No newline at end of file diff --git a/resources/views/emails/quote_approved_text.blade.php b/resources/views/emails/quote_approved_text.blade.php index 826b18e7d58e..7ae4689362a1 100644 --- a/resources/views/emails/quote_approved_text.blade.php +++ b/resources/views/emails/quote_approved_text.blade.php @@ -1,8 +1,8 @@ -{{ trans('texts.email_salutation', ['name' => $userName]) }} +{!! trans('texts.email_salutation', ['name' => $userName]) !!} -{{ trans("texts.notification_{$entityType}_approved", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) }} +{!! trans("texts.notification_{$entityType}_approved", ['amount' => $invoiceAmount, 'client' => $clientName, 'invoice' => $invoiceNumber]) !!} -{{ trans('texts.email_signature') }} -{{ trans('texts.email_from') }} +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} -{{ trans('texts.user_email_footer') }} \ No newline at end of file +{!! trans('texts.user_email_footer') !!} \ No newline at end of file diff --git a/resources/views/emails/view_action.blade.php b/resources/views/emails/view_action.blade.php deleted file mode 100644 index 718af5a5968d..000000000000 --- a/resources/views/emails/view_action.blade.php +++ /dev/null @@ -1,17 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/error.blade.php b/resources/views/error.blade.php index 13ef9e95dd10..149472fd5011 100644 --- a/resources/views/error.blade.php +++ b/resources/views/error.blade.php @@ -8,8 +8,8 @@

    Something went wrong...

    - {{ $error }} -

    If you'd like help please email us at contact@invoiceninja.com.

    +

    {{ $error }}

    +

    If you'd like help please email us at {{ env('MAIL_USERNAME') }}.

    diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php new file mode 100644 index 000000000000..949e9b73ebe4 --- /dev/null +++ b/resources/views/expenses/edit.blade.php @@ -0,0 +1,221 @@ +@extends('header') + +@section('head') + @parent + + @include('money_script') +@stop + +@section('content') + + {!! Former::open($url)->addClass('warn-on-exit main-form')->method($method) !!} +
    + {!! Former::text('action') !!} +
    + + @if ($expense) + {!! Former::populate($expense) !!} + {!! Former::populateField('should_be_invoiced', intval($expense->should_be_invoiced)) !!} + {!! Former::hidden('public_id') !!} + @endif + +
    +
    +
    +
    + {!! Former::select('vendor_id')->addOption('', '') + ->data_bind('combobox: vendor_id') + ->label(trans('texts.vendor')) + ->addGroupClass('vendor-select') !!} + + {!! Former::text('amount') + ->label(trans('texts.amount')) + ->data_bind("value: amount, valueUpdate: 'afterkeydown'") + ->addGroupClass('amount') + ->append($account->present()->currencyCode) !!} + + {!! Former::text('expense_date') + ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT)) + ->addGroupClass('expense_date') + ->label(trans('texts.date')) + ->append('') !!} + + {!! Former::select('client_id') + ->addOption('', '') + ->label(trans('texts.client')) + ->data_bind('combobox: client_id') + ->addGroupClass('client-select') !!} + + @if (!$expense || ($expense && !$expense->invoice_id)) + {!! Former::checkbox('should_be_invoiced') + ->text(trans('texts.should_be_invoiced')) + ->data_bind('checked: should_be_invoiced() || client_id(), enable: !client_id()') + ->label(' ') !!}
    + @endif + + + {!! Former::select('currency_id')->addOption('','') + ->data_bind('combobox: currency_id, disable: true') + ->fromQuery($currencies, 'name', 'id') !!} + + + {!! Former::plaintext('test') + ->value('') + ->style('min-height:46px') + ->label(trans('texts.currency_id')) !!} + + + {!! Former::text('exchange_rate') + ->data_bind("value: exchange_rate, enable: enableExchangeRate, valueUpdate: 'afterkeydown'") !!} + + {!! Former::text('invoice_amount') + ->addGroupClass('converted-amount') + ->data_bind("value: convertedAmount, enable: enableExchangeRate") + ->append('') !!} + +
    +
    + + {!! Former::textarea('public_notes')->rows(9) !!} + {!! Former::textarea('private_notes')->rows(9) !!} +
    +
    +
    +
    + +
    + {!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/expenses'))->appendIcon(Icon::create('remove-circle')) !!} + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} + @if ($expense) + {!! DropdownButton::normal(trans('texts.more_actions')) + ->withContents($actions) + ->large() + ->dropup() !!} + @endif +
    + + {!! Former::close() !!} + + + +@stop \ No newline at end of file diff --git a/resources/views/export.blade.php b/resources/views/export.blade.php new file mode 100644 index 000000000000..f2fa8dbbe7fe --- /dev/null +++ b/resources/views/export.blade.php @@ -0,0 +1,43 @@ + + + + {{ $title }} + + + + @if (isset($clients) && $clients && count($clients)) + {{ strtoupper(trans('texts.clients')) }} + @include('export.clients') + @endif + + @if (isset($contacts) && $contacts && count($contacts)) + {{ strtoupper(trans('texts.contacts')) }} + @include('export.contacts') + @endif + + @if (isset($credits) && $credits && count($credits)) + {{ strtoupper(trans('texts.credits')) }} + @include('export.credits') + @endif + + @if (isset($tasks) && $tasks && count($tasks)) + {{ strtoupper(trans('texts.tasks')) }} + @include('export.tasks') + @endif + + @if (isset($invoices) && $invoices && count($invoices)) + {{ strtoupper(trans('texts.invoices')) }} + @include('export.invoices') + @endif + + @if (isset($quotes) && $quotes && count($quotes)) + {{ strtoupper(trans('texts.quotes')) }} + @include('export.invoices', ['entityType' => ENTITY_QUOTE]) + @endif + + @if (isset($payments) && $payments && count($payments)) + {{ strtoupper(trans('texts.payments')) }} + @include('export.payments') + @endif + + \ No newline at end of file diff --git a/resources/views/export/clients.blade.php b/resources/views/export/clients.blade.php new file mode 100644 index 000000000000..f0d2a05b75fe --- /dev/null +++ b/resources/views/export/clients.blade.php @@ -0,0 +1,45 @@ + + {{ trans('texts.name') }} + @if ($multiUser) + {{ trans('texts.user') }} + @endif + {{ trans('texts.balance') }} + {{ trans('texts.paid_to_date') }} + {{ trans('texts.address1') }} + {{ trans('texts.address2') }} + {{ trans('texts.city') }} + {{ trans('texts.state') }} + {{ trans('texts.postal_code') }} + {{ trans('texts.country') }} + @if ($account->custom_client_label1) + {{ $account->custom_client_label1 }} + @endif + @if ($account->custom_client_label2) + {{ $account->custom_client_label2 }} + @endif + + +@foreach ($clients as $client) + + {{ $client->getDisplayName() }} + @if ($multiUser) + {{ $client->user->getDisplayName() }} + @endif + {{ $account->formatMoney($client->balance, $client) }} + {{ $account->formatMoney($client->paid_to_date, $client) }} + {{ $client->address1 }} + {{ $client->address2 }} + {{ $client->city }} + {{ $client->state }} + {{ $client->postal_code }} + {{ $client->present()->country }} + @if ($account->custom_client_label1) + {{ $client->custom_value1 }} + @endif + @if ($account->custom_client_label2) + {{ $client->custom_value2 }} + @endif + +@endforeach + + \ No newline at end of file diff --git a/resources/views/export/contacts.blade.php b/resources/views/export/contacts.blade.php new file mode 100644 index 000000000000..b35f59045a26 --- /dev/null +++ b/resources/views/export/contacts.blade.php @@ -0,0 +1,27 @@ + + {{ trans('texts.client') }} + @if ($multiUser) + {{ trans('texts.user') }} + @endif + {{ trans('texts.first_name') }} + {{ trans('texts.last_name') }} + {{ trans('texts.email') }} + {{ trans('texts.phone') }} + + +@foreach ($contacts as $contact) + @if (!$contact->client->is_deleted) + + {{ $contact->client->getDisplayName() }} + @if ($multiUser) + {{ $contact->user->getDisplayName() }} + @endif + {{ $contact->first_name }} + {{ $contact->last_name }} + {{ $contact->email }} + {{ $contact->phone }} + + @endif +@endforeach + + \ No newline at end of file diff --git a/resources/views/export/credits.blade.php b/resources/views/export/credits.blade.php new file mode 100644 index 000000000000..d47eba057042 --- /dev/null +++ b/resources/views/export/credits.blade.php @@ -0,0 +1,25 @@ + + {{ trans('texts.name') }} + @if ($multiUser) + {{ trans('texts.user') }} + @endif + {{ trans('texts.amount') }} + {{ trans('texts.balance') }} + {{ trans('texts.credit_date') }} + + +@foreach ($credits as $credit) + @if (!$credit->client->is_deleted) + + {{ $credit->client->getDisplayName() }} + @if ($multiUser) + {{ $credit->user->getDisplayName() }} + @endif + {{ $account->formatMoney($credit->amount, $credit->client) }} + {{ $account->formatMoney($credit->balance, $credit->client) }} + {{ $credit->present()->credit_date }} + + @endif +@endforeach + + \ No newline at end of file diff --git a/resources/views/export/invoices.blade.php b/resources/views/export/invoices.blade.php new file mode 100644 index 000000000000..fab37fbf8cb7 --- /dev/null +++ b/resources/views/export/invoices.blade.php @@ -0,0 +1,57 @@ + + {{ trans('texts.client') }} + @if ($multiUser) + {{ trans('texts.user') }} + @endif + {{ trans(isset($entityType) && $entityType == ENTITY_QUOTE ? 'texts.quote_number' : 'texts.invoice_number') }} + {{ trans('texts.balance') }} + {{ trans('texts.amount') }} + {{ trans('texts.po_number') }} + {{ trans('texts.status') }} + {{ trans(isset($entityType) && $entityType == ENTITY_QUOTE ? 'texts.quote_date' : 'texts.invoice_date') }} + {{ trans('texts.due_date') }} + @if ($account->custom_invoice_label1) + {{ $account->custom_invoice_label1 }} + @endif + @if ($account->custom_invoice_label2) + {{ $account->custom_invoice_label2 }} + @endif + @if ($account->custom_invoice_text_label1) + {{ $account->custom_invoice_text_label1 }} + @endif + @if ($account->custom_invoice_text_label2) + {{ $account->custom_invoice_text_label2 }} + @endif + + +@foreach ($invoices as $invoice) + @if (!$invoice->client->is_deleted) + + {{ $invoice->present()->client }} + @if ($multiUser) + {{ $invoice->present()->user }} + @endif + {{ $invoice->invoice_number }} + {{ $account->formatMoney($invoice->balance, $invoice->client) }} + {{ $account->formatMoney($invoice->amount, $invoice->client) }} + {{ $invoice->po_number }} + {{ $invoice->present()->status }} + {{ $invoice->present()->invoice_date }} + {{ $invoice->present()->due_date }} + @if ($account->custom_invoice_label1) + {{ $invoice->custom_value1 }} + @endif + @if ($account->custom_invoice_label2) + {{ $invoice->custom_value2 }} + @endif + @if ($account->custom_invoice_label1) + {{ $invoice->custom_text_value1 }} + @endif + @if ($account->custom_invoice_label2) + {{ $invoice->custom_text_value2 }} + @endif + + @endif +@endforeach + + \ No newline at end of file diff --git a/resources/views/export/payments.blade.php b/resources/views/export/payments.blade.php new file mode 100644 index 000000000000..66a51f8179dd --- /dev/null +++ b/resources/views/export/payments.blade.php @@ -0,0 +1,29 @@ + + {{ trans('texts.client') }} + @if ($multiUser) + {{ trans('texts.user') }} + @endif + {{ trans('texts.invoice_number') }} + {{ trans('texts.amount') }} + {{ trans('texts.payment_date') }} + {{ trans('texts.method') }} + {{ trans('texts.transaction_reference') }} + + +@foreach ($payments as $payment) + @if (!$payment->client->is_deleted) + + {{ $payment->present()->client }} + @if ($multiUser) + {{ $payment->user->getDisplayName() }} + @endif + {{ $payment->invoice->invoice_number }} + {{ $account->formatMoney($payment->amount, $payment->client) }} + {{ $payment->present()->payment_date }} + {{ $payment->present()->method }} + {{ $payment->transaction_reference }} + + @endif +@endforeach + + \ No newline at end of file diff --git a/resources/views/export/tasks.blade.php b/resources/views/export/tasks.blade.php new file mode 100644 index 000000000000..3cacc198b935 --- /dev/null +++ b/resources/views/export/tasks.blade.php @@ -0,0 +1,25 @@ + + {{ trans('texts.client') }} + @if ($multiUser) + {{ trans('texts.user') }} + @endif + {{ trans('texts.start_date') }} + {{ trans('texts.duration') }} + {{ trans('texts.description') }} + + +@foreach ($tasks as $task) + @if (!$task->client || !$task->client->is_deleted) + + {{ $task->present()->client }} + @if ($multiUser) + {{ $task->present()->user }} + @endif + {{ $task->getStartTime() }} + {{ $task->getDuration() }} + {{ $task->description }} + + @endif +@endforeach + + \ No newline at end of file diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 91e97364997b..0236388c8786 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -3,6 +3,7 @@ @section('head') + - @include('script') - @@ -356,6 +373,7 @@ {!! HTML::nav_link('dashboard', 'dashboard') !!} {!! HTML::menu_link('client') !!} {!! HTML::menu_link('task') !!} + {!! HTML::menu_link('expense') !!} {!! HTML::menu_link('invoice') !!} {!! HTML::menu_link('payment') !!} @@ -428,16 +446,14 @@ @@ -445,7 +461,7 @@
    - -
    -
    +
    + + @include('partials.warn_session', ['redirectTo' => '/dashboard']) @if (Session::has('warning'))
    {!! Session::get('warning') !!}
    @@ -499,11 +515,11 @@ @endif @if (Session::has('error')) -
    {!! Session::get('error') !!}
    +
    {!! Session::get('error') !!}
    @endif @if (!isset($showBreadcrumbs) || $showBreadcrumbs) - {!! HTML::breadcrumbs() !!} + {!! HTML::breadcrumbs() !!} @endif @yield('content') @@ -522,7 +538,7 @@

    - {!! Former::open('signup/submit')->addClass('signUpForm') !!} + {!! Former::open('signup/submit')->addClass('signUpForm')->autocomplete('on') !!} @if (Auth::check()) {!! Former::populateField('new_first_name', Auth::user()->first_name) !!} @@ -535,11 +551,56 @@ {!! Former::text('go_pro') !!}
    - {!! Former::text('new_first_name')->label(trans('texts.first_name')) !!} - {!! Former::text('new_last_name')->label(trans('texts.last_name')) !!} - {!! Former::text('new_email')->label(trans('texts.email')) !!} - {!! Former::password('new_password')->label(trans('texts.password')) !!} - {!! Former::checkbox('terms_checkbox')->label(' ')->text(trans('texts.agree_to_terms', ['terms' => ''.trans('texts.terms_of_service').''])) !!} + +

     

    - {{ trans('texts.powered_by') }} InvoiceNinja.com | + {{ trans('texts.powered_by') }} InvoiceNinja.com - + {!! link_to(RELEASES_URL, 'v' . NINJA_VERSION, ['target' => '_blank']) !!} | @if (Auth::user()->account->isWhiteLabel()) {{ trans('texts.white_labeled') }} @else @@ -651,8 +714,18 @@
    -
    +

    {{ trans('texts.white_label_text')}}

    +
    +
    +

    {{ trans('texts.before') }}

    + {!! HTML::image('images/pro_plan/white_label_before.png', 'before', ['width' => '100%']) !!} +
    +
    +

    {{ trans('texts.after') }}

    + {!! HTML::image('images/pro_plan/white_label_after.png', 'after', ['width' => '100%']) !!} +
    +
    -

     

    - -
    +
    - + - - + + @@ -152,30 +217,39 @@ - - @@ -192,32 +266,44 @@
    {!! Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'") - ->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!} + ->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!}
    - {!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: default_terms, valueUpdate: 'afterkeydown'") + {!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'") ->label(false)->style('resize: none; min-width: 450px')->rows(3) - ->help('') !!} + ->help('
    + + +
    ') !!}
    - @@ -229,21 +315,21 @@ - @if (($account->custom_invoice_label1 || ($invoice && floatval($invoice->custom_value1)) != 0) && $account->custom_invoice_taxes1) + @if ($account->showCustomField('custom_invoice_label1', $invoice) && $account->custom_invoice_taxes1) - + @endif - @if (($account->custom_invoice_label2 || ($invoice && floatval($invoice->custom_value2)) != 0) && $account->custom_invoice_taxes2) + @if ($account->showCustomField('custom_invoice_label2', $invoice) && $account->custom_invoice_taxes2) - + @endif @@ -258,44 +344,48 @@ @endif - + - @if (($account->custom_invoice_label1 || ($invoice && floatval($invoice->custom_value1)) != 0) && !$account->custom_invoice_taxes1) + @if ($account->showCustomField('custom_invoice_label1', $invoice) && !$account->custom_invoice_taxes1) - + @endif - @if (($account->custom_invoice_label2 || ($invoice && floatval($invoice->custom_value2)) != 0) && !$account->custom_invoice_taxes2) + @if ($account->showCustomField('custom_invoice_label2', $invoice) && !$account->custom_invoice_taxes2) - + @endif @if (!$account->hide_paid_to_date) - + @endif - + @@ -307,45 +397,50 @@ - +

     

    {!! Former::populateField('entityType', $entityType) !!} + {!! Former::text('entityType') !!} {!! Former::text('action') !!} - {!! Former::text('data')->data_bind("value: ko.mapping.toJSON(model)") !!} - {!! Former::text('pdfupload') !!} - - @if ($invoice && $invoice->id) - {!! Former::populateField('id', $invoice->public_id) !!} - {!! Former::text('id') !!} - @endif + {!! Former::text('public_id')->data_bind('value: public_id') !!} + {!! Former::text('is_recurring')->data_bind('value: is_recurring') !!} + {!! Former::text('is_quote')->data_bind('value: is_quote') !!} + {!! Former::text('has_tasks')->data_bind('value: has_tasks') !!} + {!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!} + {!! Former::text('has_expenses')->data_bind('value: has_expenses') !!} + {!! Former::text('pdfupload') !!}
    + @if ($account->hasLargeFont()) + + @endif @if (!Utils::isPro() || \App\Models\InvoiceDesign::count() == COUNT_FREE_DESIGNS_SELF_HOST) {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id")->addOption(trans('texts.more_designs') . '...', '-1') !!} - @else + @else {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") !!} @endif - {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} - - @if (!$invoice || (!$invoice->trashed() && !$invoice->client->trashed())) + {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} + @if ($invoice->isClientTrashed()) + + @elseif ($invoice->trashed()) + {!! Button::success(trans('texts.restore'))->withAttributes(['onclick' => 'submitBulkAction("restore")'])->appendIcon(Icon::create('cloud-download')) !!} + @elseif (!$invoice->trashed()) {!! Button::success(trans("texts.save_{$entityType}"))->withAttributes(array('id' => 'saveButton', 'onclick' => 'onSaveClick()'))->appendIcon(Icon::create('floppy-disk')) !!} {!! Button::info(trans("texts.email_{$entityType}"))->withAttributes(array('id' => 'emailButton', 'onclick' => 'onEmailClick()'))->appendIcon(Icon::create('send')) !!} - - @if ($invoice && $invoice->id) + @if ($invoice->id) {!! DropdownButton::normal(trans('texts.more_actions')) ->withContents($actions) ->dropup() !!} - @endif - - @elseif ($invoice && $invoice->trashed() && !$invoice->is_deleted == '1') - {!! Button::success(trans('texts.restore'))->withAttributes(['onclick' => 'submitAction("restore")'])->appendIcon(Icon::create('cloud-download')) !!} + @endif @endif
    @@ -374,34 +469,65 @@
    - {!! Former::text('name')->data_bind("value: name, valueUpdate: 'afterkeydown', attr { placeholder: name.placeholder }")->label('client_name') !!} + {!! Former::hidden('client_public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', + attr: {name: 'client[public_id]'}") !!} + {!! Former::text('client[name]') + ->data_bind("value: name, valueUpdate: 'afterkeydown', attr { placeholder: name.placeholder }") + ->label('client_name') !!} + - {!! Former::text('id_number')->data_bind("value: id_number, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('vat_number')->data_bind("value: vat_number, valueUpdate: 'afterkeydown'") !!} - - {!! Former::text('website')->data_bind("value: website, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('work_phone')->data_bind("value: work_phone, valueUpdate: 'afterkeydown'") !!} - @if (Auth::user()->isPro()) - @if ($account->custom_client_label1) - {!! Former::text('custom_value1')->label($account->custom_client_label1) - ->data_bind("value: custom_value1, valueUpdate: 'afterkeydown'") !!} - @endif - @if ($account->custom_client_label2) - {!! Former::text('custom_value2')->label($account->custom_client_label2) - ->data_bind("value: custom_value2, valueUpdate: 'afterkeydown'") !!} - @endif - @endif + {!! Former::text('client[id_number]') + ->label('id_number') + ->data_bind("value: id_number, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[vat_number]') + ->label('vat_number') + ->data_bind("value: vat_number, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[website]') + ->label('website') + ->data_bind("value: website, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[work_phone]') + ->label('work_phone') + ->data_bind("value: work_phone, valueUpdate: 'afterkeydown'") !!} + + + + @if (Auth::user()->isPro()) + @if ($account->custom_client_label1) + {!! Former::text('client[custom_value1]') + ->label($account->custom_client_label1) + ->data_bind("value: custom_value1, valueUpdate: 'afterkeydown'") !!} + @endif + @if ($account->custom_client_label2) + {!! Former::text('client[custom_value2]') + ->label($account->custom_client_label2) + ->data_bind("value: custom_value2, valueUpdate: 'afterkeydown'") !!} + @endif + @endif + +   - {!! Former::text('address1')->data_bind("value: address1, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('address2')->data_bind("value: address2, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('city')->data_bind("value: city, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('state')->data_bind("value: state, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('postal_code')->data_bind("value: postal_code, valueUpdate: 'afterkeydown'") !!} - {!! Former::select('country_id')->addOption('','')->addGroupClass('country_select') - ->fromQuery($countries, 'name', 'id')->data_bind("dropdown: country_id") !!} + {!! Former::text('client[address1]') + ->label(trans('texts.address1')) + ->data_bind("value: address1, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[address2]') + ->label(trans('texts.address2')) + ->data_bind("value: address2, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[city]') + ->label(trans('texts.city')) + ->data_bind("value: city, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[state]') + ->label(trans('texts.state')) + ->data_bind("value: state, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('client[postal_code]') + ->label(trans('texts.postal_code')) + ->data_bind("value: postal_code, valueUpdate: 'afterkeydown'") !!} + {!! Former::select('client[country_id]') + ->label(trans('texts.country_id')) + ->addOption('','')->addGroupClass('country_select') + ->fromQuery(Cache::get('countries'), 'name', 'id')->data_bind("dropdown: country_id") !!}
    @@ -410,16 +536,24 @@
    - {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") !!} - {!! Former::text('email')->data_bind('value: email, valueUpdate: \'afterkeydown\', attr: {id:\'email\'+$index()}') !!} - {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") !!} + + {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', + attr: {name: 'client[contacts][' + \$index() + '][public_id]'}") !!} + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown', + attr: {name: 'client[contacts][' + \$index() + '][first_name]'}") !!} + {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown', + attr: {name: 'client[contacts][' + \$index() + '][last_name]'}") !!} + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown', + attr: {name: 'client[contacts][' + \$index() + '][email]', id:'email'+\$index()}") + ->addClass('client-email') !!} + {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', + attr: {name: 'client[contacts][' + \$index() + '][phone]'}") !!} +
    {!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!} - + {!! link_to('#', trans('texts.add_contact').' +', array('data-bind'=>'click: $parent.addContact')) !!} @@ -427,22 +561,35 @@
    - +   - {!! Former::select('currency_id')->addOption('','')->data_bind('value: currency_id') - ->fromQuery($currencies, 'name', 'id') !!} + {!! Former::select('client[currency_id]')->addOption('','') + ->placeholder($account->currency ? $account->currency->name : '') + ->label(trans('texts.currency_id')) + ->data_bind('value: currency_id') + ->fromQuery($currencies, 'name', 'id') !!} - - {!! Former::select('payment_terms')->addOption('','')->data_bind('value: payment_terms') - ->fromQuery($paymentTerms, 'name', 'num_days') - ->help(trans('texts.payment_terms_help')) !!} - {!! Former::select('size_id')->addOption('','')->data_bind('value: size_id') - ->fromQuery($sizes, 'name', 'id') !!} - {!! Former::select('industry_id')->addOption('','')->data_bind('value: industry_id') - ->fromQuery($industries, 'name', 'id') !!} - {!! Former::textarea('private_notes')->data_bind('value: private_notes') !!} + + {!! Former::select('client[language_id]')->addOption('','') + ->placeholder($account->language ? $account->language->name : '') + ->label(trans('texts.language_id')) + ->data_bind('value: language_id') + ->fromQuery($languages, 'name', 'id') !!} + {!! Former::select('client[payment_terms]')->addOption('','')->data_bind('value: payment_terms') + ->fromQuery($paymentTerms, 'name', 'num_days') + ->label(trans('texts.payment_terms')) + ->help(trans('texts.payment_terms_help')) !!} + {!! Former::select('client[size_id]')->addOption('','')->data_bind('value: size_id') + ->label(trans('texts.size_id')) + ->fromQuery($sizes, 'name', 'id') !!} + {!! Former::select('client[industry_id]')->addOption('','')->data_bind('value: industry_id') + ->label(trans('texts.industry_id')) + ->fromQuery($industries, 'name', 'id') !!} + {!! Former::textarea('client_private_notes') + ->label(trans('texts.private_notes')) + ->data_bind("value: private_notes, attr:{ name: 'client[private_notes]'}") !!}
    @@ -454,66 +601,13 @@   - + - + -
    {{ $invoiceLabels['item'] }} {{ $invoiceLabels['description'] }}{{ $invoiceLabels['unit_cost'] }}{{ $invoiceLabels['quantity'] }} {{ trans('texts.tax') }} {{ trans('texts.line_total') }}
    - {!! Former::text('product_key')->useDatalist($products->toArray(), 'product_key')->onkeyup('onItemChange()') - ->raw()->data_bind("value: product_key, valueUpdate: 'afterkeydown'")->addClass('datalist') !!} + {!! Former::text('product_key')->useDatalist($products->toArray(), 'product_key') + ->data_bind("value: product_key, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + \$index() + '][product_key]'}") + ->addClass('datalist') + ->raw() + !!} - + + + - + - + - + + + +
      - {{ trans('texts.subtotal') }}
    {{ $account->custom_invoice_label1 }}
    {{ $account->custom_invoice_label2 }}
    - {{ trans('texts.tax') }} + + + +
    {{ $account->custom_invoice_label1 }}
    {{ $account->custom_invoice_label2 }}
    - {{ trans('texts.paid_to_date') }}
    - {{ trans($entityType == ENTITY_INVOICE ? 'texts.balance_due' : 'texts.total') }}
    - - - - - - - - - - - - - - - - -
    {{ trans('texts.name') }}{{ trans('texts.rate') }}
    - - - - -   -
    -   - - {!! Former::checkbox('invoice_taxes')->text(trans('texts.enable_invoice_tax')) - ->label(trans('texts.settings'))->data_bind('checked: $root.invoice_taxes, enable: $root.tax_rates().length > 1') !!} - {!! Former::checkbox('invoice_item_taxes')->text(trans('texts.enable_line_item_tax')) - ->label(' ')->data_bind('checked: $root.invoice_item_taxes, enable: $root.tax_rates().length > 1') !!} - -
    - -
    - - - -
    -
    -
    - + + {!! Former::close() !!} + {!! Former::open("{$entityType}s/bulk")->addClass('bulkForm') !!} + {!! Former::populateField('bulk_public_id', $invoice->public_id) !!} + + {!! Former::text('bulk_public_id') !!} + {!! Former::text('bulk_action') !!} + + {!! Former::close() !!} +
    + @include('invoices.knockout') + diff --git a/resources/views/invoices/history.blade.php b/resources/views/invoices/history.blade.php index c869933fb6cf..d59d59000852 100644 --- a/resources/views/invoices/history.blade.php +++ b/resources/views/invoices/history.blade.php @@ -3,14 +3,16 @@ @section('head') @parent - - - - + @include('money_script') +@foreach (Auth::user()->account->getFontFolders() as $font) + +@endforeach + diff --git a/resources/views/invoices/pdf.blade.php b/resources/views/invoices/pdf.blade.php index f60e7ac096d2..eba6f4453187 100644 --- a/resources/views/invoices/pdf.blade.php +++ b/resources/views/invoices/pdf.blade.php @@ -62,7 +62,7 @@ logoImages.imageLogoWidth3 =325/2; logoImages.imageLogoHeight3 = 81/2; - @if (file_exists($account->getLogoPath())) + @if ($account->hasLogo()) window.accountLogo = "{{ HTML::image_data($account->getLogoPath()) }}"; if (window.invoice) { invoice.image = window.accountLogo; @@ -72,30 +72,34 @@ @endif var NINJA = NINJA || {}; - NINJA.primaryColor = "{{ $account->primary_color }}"; - NINJA.secondaryColor = "{{ $account->secondary_color }}"; - NINJA.fontSize = {{ $account->font_size }}; - + @if ($account->isPro()) + NINJA.primaryColor = "{{ $account->primary_color }}"; + NINJA.secondaryColor = "{{ $account->secondary_color }}"; + NINJA.fontSize = {{ $account->font_size }}; + NINJA.headerFont = {!! json_encode($account->getHeaderFontName()) !!}; + NINJA.bodyFont = {!! json_encode($account->getBodyFontName()) !!}; + @endif var invoiceLabels = {!! json_encode($account->getInvoiceLabels()) !!}; if (window.invoice) { - invoiceLabels.item = invoice.has_tasks ? invoiceLabels.date : invoiceLabels.item_orig; + //invoiceLabels.item = invoice.has_tasks ? invoiceLabels.date : invoiceLabels.item_orig; invoiceLabels.quantity = invoice.has_tasks ? invoiceLabels.hours : invoiceLabels.quantity_orig; - invoiceLabels.unit_cost = invoice.has_tasks ? invoiceLabels.rate : invoiceLabels.unit_cost_orig; + invoiceLabels.unit_cost = invoice.has_tasks ? invoiceLabels.rate : invoiceLabels.unit_cost_orig; } var isRefreshing = false; var needsRefresh = false; function refreshPDF(force) { - getPDFString(refreshPDFCB, force); + //console.log('refresh PDF - force: ' + force + ' ' + (new Date()).getTime()) + return getPDFString(refreshPDFCB, force); } function refreshPDFCB(string) { if (!string) return; PDFJS.workerSrc = '{{ asset('js/pdf_viewer.worker.js') }}'; - if ({{ Auth::check() && Auth::user()->force_pdfjs ? 'false' : 'true' }} && (isFirefox || (isChrome && !isChromium))) { - $('#theFrame').attr('src', string).show(); + if ({{ Auth::check() && Auth::user()->force_pdfjs ? 'false' : 'true' }} && (isFirefox || isChrome)) { + $('#theFrame').attr('src', string).show(); } else { if (isRefreshing) { //needsRefresh = true; diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index 0d01cbd3c779..04ad660ae1ba 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -3,13 +3,13 @@ @section('head') @parent - @include('script') + @include('money_script') - - - - - + @foreach ($invoice->client->account->getFontFolders() as $font) + + @endforeach + + - -{!! Former::vertical_open($url)->rules(array( -'first_name' => 'required', -'last_name' => 'required', -'card_number' => 'required', -'expiration_month' => 'required', -'expiration_year' => 'required', -'cvv' => 'required', -'address1' => 'required', -'city' => 'required', -'state' => 'required', -'postal_code' => 'required', -'country_id' => 'required', -'phone' => 'required', -'email' => 'required|email' -)) !!} +{!! Former::vertical_open($url) + ->autocomplete('on') + ->addClass('payment-form') + ->rules(array( + 'first_name' => 'required', + 'last_name' => 'required', + 'card_number' => 'required', + 'expiration_month' => 'required', + 'expiration_year' => 'required', + 'cvv' => 'required', + 'address1' => 'required', + 'city' => 'required', + 'state' => 'required', + 'postal_code' => 'required', + 'country_id' => 'required', + 'phone' => 'required', + 'email' => 'required|email' + )) !!} @if ($client) {{ Former::populate($client) }} @@ -161,6 +91,8 @@ header h3 em {

     

    + +
    @@ -169,7 +101,7 @@ header h3 em {
    @if ($client)

    {{ $client->getDisplayName() }}

    -

    {{ trans('texts.invoice') . ' ' . $invoiceNumber }}|  {{ trans('texts.amount_due') }}: {{ Utils::formatMoney($amount, $currencyId) }} {{ $currencyCode }}

    +

    {{ trans('texts.invoice') . ' ' . $invoiceNumber }}|  {{ trans('texts.amount_due') }}: {{ $account->formatMoney($amount, $client, true) }}

    @elseif ($paymentTitle)

    {{ $paymentTitle }}
    {{ $paymentSubtitle }}

    @endif @@ -191,16 +123,25 @@ header h3 em {

    {{ trans('texts.contact_information') }}

    - {!! Former::text('first_name')->placeholder(trans('texts.first_name'))->label('') !!} + {!! Former::text('first_name') + ->placeholder(trans('texts.first_name')) + ->autocomplete('given-name') + ->label('') !!}
    - {!! Former::text('last_name')->placeholder(trans('texts.last_name'))->label('') !!} + {!! Former::text('last_name') + ->placeholder(trans('texts.last_name')) + ->autocomplete('family-name') + ->label('') !!}
    @if (isset($paymentTitle))
    - {!! Former::text('email')->placeholder(trans('texts.email'))->label('') !!} + {!! Former::text('email') + ->placeholder(trans('texts.email')) + ->autocomplete('email') + ->label('') !!}
    @endif @@ -211,26 +152,45 @@ header h3 em {

    {{ trans('texts.billing_address') }}  {{ trans('texts.payment_footer1') }}

    - {!! Former::text('address1')->placeholder(trans('texts.address1'))->label('') !!} + {!! Former::text('address1') + ->autocomplete('address-line1') + ->placeholder(trans('texts.address1')) + ->label('') !!}
    - {!! Former::text('address2')->placeholder(trans('texts.address2'))->label('') !!} -
    -
    -
    -
    - {!! Former::text('city')->placeholder(trans('texts.city'))->label('') !!} -
    -
    - {!! Former::text('state')->placeholder(trans('texts.state'))->label('') !!} + {!! Former::text('address2') + ->autocomplete('address-line2') + ->placeholder(trans('texts.address2')) + ->label('') !!}
    - {!! Former::text('postal_code')->placeholder(trans('texts.postal_code'))->label('') !!} + {!! Former::text('city') + ->autocomplete('address-level2') + ->placeholder(trans('texts.city')) + ->label('') !!}
    - {!! Former::select('country_id')->placeholder(trans('texts.country_id'))->fromQuery($countries, 'name', 'id')->label('') !!} + {!! Former::text('state') + ->autocomplete('address-level1') + ->placeholder(trans('texts.state')) + ->label('') !!} +
    +
    +
    +
    + {!! Former::text('postal_code') + ->autocomplete('postal-code') + ->placeholder(trans('texts.postal_code')) + ->label('') !!} +
    +
    + {!! Former::select('country_id') + ->placeholder(trans('texts.country_id')) + ->fromQuery($countries, 'name', 'id') + ->addGroupClass('country-select') + ->label('') !!}
    @@ -240,58 +200,72 @@ header h3 em {

    {{ trans('texts.billing_method') }}

    - {!! Former::text('card_number')->placeholder(trans('texts.card_number'))->label('') !!} + {!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'card_number') + ->placeholder(trans('texts.card_number')) + ->autocomplete('cc-number') + ->data_stripe('number') + ->label('') !!}
    - {!! Former::text('cvv')->placeholder(trans('texts.cvv'))->label('') !!} + {!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'cvv') + ->placeholder(trans('texts.cvv')) + ->autocomplete('off') + ->data_stripe('cvc') + ->label('') !!}
    - {!! Former::select('expiration_month')->placeholder(trans('texts.expiration_month')) - ->addOption('01 - January', '1') - ->addOption('02 - February', '2') - ->addOption('03 - March', '3') - ->addOption('04 - April', '4') - ->addOption('05 - May', '5') - ->addOption('06 - June', '6') - ->addOption('07 - July', '7') - ->addOption('08 - August', '8') - ->addOption('09 - September', '9') - ->addOption('10 - October', '10') - ->addOption('11 - November', '11') - ->addOption('12 - December', '12')->label('') - !!} + {!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_month') + ->autocomplete('cc-exp-month') + ->data_stripe('exp-month') + ->placeholder(trans('texts.expiration_month')) + ->addOption('01 - January', '1') + ->addOption('02 - February', '2') + ->addOption('03 - March', '3') + ->addOption('04 - April', '4') + ->addOption('05 - May', '5') + ->addOption('06 - June', '6') + ->addOption('07 - July', '7') + ->addOption('08 - August', '8') + ->addOption('09 - September', '9') + ->addOption('10 - October', '10') + ->addOption('11 - November', '11') + ->addOption('12 - December', '12')->label('') + !!}
    - {!! Former::select('expiration_year')->placeholder(trans('texts.expiration_year')) - ->addOption('2015', '2015') - ->addOption('2016', '2016') - ->addOption('2017', '2017') - ->addOption('2018', '2018') - ->addOption('2019', '2019') - ->addOption('2020', '2020') - ->addOption('2021', '2021') - ->addOption('2022', '2022') - ->addOption('2023', '2023') - ->addOption('2024', '2024') - ->addOption('2025', '2025')->label('') - !!} + {!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_year') + ->autocomplete('cc-exp-year') + ->data_stripe('exp-year') + ->placeholder(trans('texts.expiration_year')) + ->addOption('2015', '2015') + ->addOption('2016', '2016') + ->addOption('2017', '2017') + ->addOption('2018', '2018') + ->addOption('2019', '2019') + ->addOption('2020', '2020') + ->addOption('2021', '2021') + ->addOption('2022', '2022') + ->addOption('2023', '2023') + ->addOption('2024', '2024') + ->addOption('2025', '2025')->label('') + !!}
    - @if ($client && $account->showTokenCheckbox()) + @if ($client && $account->showTokenCheckbox()) selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top"> {!! trans('texts.token_billing_secure', ['stripe_link' => link_to('https://stripe.com/', 'Stripe.com', ['target' => '_blank'])]) !!} - @endif + @endif
    -
    - @if (isset($acceptedCreditCardTypes)) +
    + @if (isset($acceptedCreditCardTypes))
    @foreach ($acceptedCreditCardTypes as $card) {{ $card['alt'] }} @@ -306,7 +280,7 @@ header h3 em {
    - {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . Utils::formatMoney($amount, $currencyId) ))->submit()->block()->large() !!} + {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) ))->submit()->block()->large() !!}
    @@ -322,16 +296,6 @@ header h3 em {
    - - {!! Former::close() !!} diff --git a/resources/views/payments/payment_css.blade.php b/resources/views/payments/payment_css.blade.php new file mode 100644 index 000000000000..e22a15464561 --- /dev/null +++ b/resources/views/payments/payment_css.blade.php @@ -0,0 +1,130 @@ + diff --git a/resources/views/public/header.blade.php b/resources/views/public/header.blade.php index cc6a59e4b269..d60487b7c3ae 100644 --- a/resources/views/public/header.blade.php +++ b/resources/views/public/header.blade.php @@ -1,123 +1,15 @@ @extends('master') @section('head') - - - - + @if (!empty($clientFontUrl)) + + @else + + @endif + + @if (!empty($clientViewCSS)) + + @endif @stop @section('body') @@ -144,10 +36,6 @@ table.table thead .sorting_desc_disabled:after { content: '' !important } $('[name="guest_key"]').val(localStorage.getItem('guest_key')); } - @if (isset($invoiceNow) && $invoiceNow) - getStarted(); - @endif - function isStorageSupported() { if ('localStorage' in window && window['localStorage'] !== null) { var storage = window.localStorage; @@ -188,6 +76,9 @@ table.table thead .sorting_desc_disabled:after { content: '' !important } -
    + + @include('partials.warn_session', ['redirectTo' => '/']) + @if (Session::has('warning'))
    {!! Session::get('warning') !!}
    @endif @@ -223,10 +116,10 @@ table.table thead .sorting_desc_disabled:after { content: '' !important }
    - @if (!isset($hideLogo) || !$hideLogo) + @if (!isset($hideLogo) || !$hideLogo)
    diff --git a/resources/views/public/invoice_now.blade.php b/resources/views/public/invoice_now.blade.php new file mode 100644 index 000000000000..b18c37965395 --- /dev/null +++ b/resources/views/public/invoice_now.blade.php @@ -0,0 +1,37 @@ +@extends('master') + +@section('body') + +{!! Form::open(array('url' => 'get_started', 'id' => 'startForm')) !!} +{!! Form::hidden('guest_key') !!} +{!! Form::hidden('sign_up', Input::get('sign_up')) !!} +{!! Form::hidden('redirect_to', Input::get('redirect_to')) !!} +{!! Form::close() !!} + + + +@stop \ No newline at end of file diff --git a/resources/views/public/license.blade.php b/resources/views/public/license.blade.php index a915157ffc86..60e8f3514e50 100644 --- a/resources/views/public/license.blade.php +++ b/resources/views/public/license.blade.php @@ -112,6 +112,7 @@ header h3 em { + @endif - {!! Former::open()->rules(['start_date' => 'required', 'end_date' => 'required'])->addClass('warn-on-exit') !!} - -
    - {!! Former::text('action') !!} -
    - - {!! Former::populateField('start_date', $startDate) !!} - {!! Former::populateField('end_date', $endDate) !!} - {!! Former::populateField('enable_report', intval($enableReport)) !!} - {!! Former::populateField('enable_chart', intval($enableChart)) !!} - - {!! Former::text('start_date')->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT)) - ->append('') !!} - {!! Former::text('end_date')->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT)) - ->append('') !!} - -

     

    - {!! Former::checkbox('enable_report')->text(trans('texts.enable')) !!} - {!! Former::select('report_type')->options($reportTypes, $reportType)->label(trans('texts.group_by')) !!} - -

     

    - {!! Former::checkbox('enable_chart')->text(trans('texts.enable')) !!} - {!! Former::select('group_by')->options($dateTypes, $groupBy) !!} - {!! Former::select('chart_type')->options($chartTypes, $chartType) !!} - -

     

    - @if (Auth::user()->isPro()) - {!! Former::actions( - Button::primary(trans('texts.export'))->withAttributes(array('onclick' => 'onExportClick()'))->appendIcon(Icon::create('export')), - Button::success(trans('texts.run'))->withAttributes(array('id' => 'submitButton'))->submit()->appendIcon(Icon::create('play')) - ) !!} - @else - - @endif - - {!! Former::close() !!} +
    +
    + {!! Former::checkbox('enable_report')->text(trans('texts.enable')) !!} + {!! Former::select('report_type')->options($reportTypes, $reportType)->label(trans('texts.group_by')) !!} +

     

    + {!! Former::checkbox('enable_chart')->text(trans('texts.enable')) !!} + {!! Former::select('group_by')->options($dateTypes, $groupBy) !!} + {!! Former::select('chart_type')->options($chartTypes, $chartType) !!} + + {!! Former::close() !!}
    -
    -
    - - @if ($enableReport) -
    -
    - - - - @foreach ($columns as $column) - - @endforeach - - - - @foreach ($displayData as $record) - - @foreach ($record as $field) - - @endforeach - + + + @if ($enableReport) +
    +
    +
    - {{ trans("texts.{$column}") }} -
    - {!! $field !!} -
    + + + @foreach ($columns as $column) + @endforeach - - + + + + @foreach ($displayData as $record) - - @if (!$reportType) - - - @endif - - - - - -
    + {{ trans("texts.{$column}") }} +
    {{ trans('texts.totals') }} - @foreach ($reportTotals['amount'] as $currencyId => $total) - {{ Utils::formatMoney($total, $currencyId) }}
    - @endforeach -
    - @foreach ($reportTotals['paid'] as $currencyId => $total) - {{ Utils::formatMoney($total, $currencyId) }}
    - @endforeach -
    - @foreach ($reportTotals['balance'] as $currencyId => $total) - {{ Utils::formatMoney($total, $currencyId) }}
    - @endforeach -
    + @foreach ($record as $field) + + {!! $field !!} + + @endforeach + + @endforeach + + + + {{ trans('texts.totals') }} + @if ($reportType != ENTITY_CLIENT) + + + @endif + + @foreach ($reportTotals['amount'] as $currencyId => $total) + {{ Utils::formatMoney($total, $currencyId) }}
    + @endforeach + + @if ($reportType == ENTITY_PAYMENT) + + @endif + + @foreach ($reportTotals['paid'] as $currencyId => $total) + {{ Utils::formatMoney($total, $currencyId) }}
    + @endforeach + + @if ($reportType != ENTITY_PAYMENT) + + @foreach ($reportTotals['balance'] as $currencyId => $total) + {{ Utils::formatMoney($total, $currencyId) }}
    + @endforeach + + @endif + + + +
    +
    + @endif + + @if ($enableChart) +
    +
    + +

     

    +
    +
    +
     Invoices
    +
    +
    +
    +
     Payments
    -
    - @endif - - @if ($enableChart) -
    -
    - -

     

    -
    -
    -
     Invoices
    -
    -
    -
    -
     Payments
    -
    -
    -
    -
     Credits
    -
    - +
    +
    +
     Credits
    -
    - @endif -
    +
    +
    + @endif
    @@ -163,29 +161,41 @@ $('#action').val(''); } - var ctx = document.getElementById('monthly-reports').getContext('2d'); - var chart = { - labels: {!! json_encode($labels) !!}, - datasets: [ - @foreach ($datasets as $dataset) - { - data: {!! json_encode($dataset['totals']) !!}, - fillColor : "rgba({!! $dataset['colors'] !!},0.5)", - strokeColor : "rgba({!! $dataset['colors'] !!},1)", - }, - @endforeach - ] - } + @if ($enableChart) + var ctx = document.getElementById('monthly-reports').getContext('2d'); + var chart = { + labels: {!! json_encode($labels) !!}, + datasets: [ + @foreach ($datasets as $dataset) + { + data: {!! json_encode($dataset['totals']) !!}, + fillColor : "rgba({!! $dataset['colors'] !!},0.5)", + strokeColor : "rgba({!! $dataset['colors'] !!},1)", + }, + @endforeach + ] + } - var options = { - scaleOverride: true, - scaleSteps: 10, - scaleStepWidth: {!! $scaleStepWidth !!}, - scaleStartValue: 0, - scaleLabel : "<%=value%>", - }; + var options = { + scaleOverride: true, + scaleSteps: 10, + scaleStepWidth: {!! $scaleStepWidth !!}, + scaleStartValue: 0, + scaleLabel : "<%=value%>", + }; + + new Chart(ctx).{!! $chartType !!}(chart, options); + @endif + + $(function() { + $('.start_date .input-group-addon').click(function() { + toggleDatePicker('start_date'); + }); + $('.end_date .input-group-addon').click(function() { + toggleDatePicker('end_date'); + }); + }) - new Chart(ctx).{!! $chartType !!}(chart, options); diff --git a/resources/views/reports/d3.blade.php b/resources/views/reports/d3.blade.php index 0b75bef85cb9..6685441321ce 100644 --- a/resources/views/reports/d3.blade.php +++ b/resources/views/reports/d3.blade.php @@ -1,8 +1,9 @@ -@extends('accounts.nav') +@extends('header') @section('head') @parent + @include('money_script') @@ -41,10 +49,16 @@
    -

    {{ $task->getStartTime() }}

    +

    {{ $task->getStartTime() }} - + @if (Auth::user()->account->timezone_id) + {{ $timezone }} + @else + {!! link_to('/settings/localization?focus=timezone_id', $timezone, ['target' => '_blank']) !!} + @endif +

    @if ($task->hasPreviousDuration()) - {{ trans('texts.duration') . ': ' . gmdate('H:i:s', $task->getDuration()) }}
    + {{ trans('texts.duration') . ': ' . Utils::formatTime($task->getDuration()) }}
    @endif @if (!$task->is_running) @@ -77,13 +91,13 @@

    -
    -
    @@ -111,6 +125,9 @@ @if ($task && $task->is_running) {!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!} {!! Button::primary(trans('texts.stop'))->large()->appendIcon(Icon::create('stop'))->withAttributes(['id' => 'stop-button']) !!} + @elseif ($task && $task->trashed()) + {!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!} + {!! Button::success(trans('texts.restore'))->large()->withAttributes(['onclick' => 'submitAction("restore")'])->appendIcon(Icon::create('cloud-download')) !!} @else {!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!} @if ($task) @@ -121,7 +138,7 @@ ->large() ->dropup() !!} @else - {!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button', 'style' => 'display:none']) !!} + {!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!} {!! Button::success(trans('texts.start'))->large()->appendIcon(Icon::create('play'))->withAttributes(['id' => 'start-button']) !!} @endif @endif @@ -131,6 +148,46 @@ -@stop \ No newline at end of file +@stop diff --git a/resources/views/user_account.blade.php b/resources/views/user_account.blade.php index 4848a190e5c9..9ab41f845b7a 100644 --- a/resources/views/user_account.blade.php +++ b/resources/views/user_account.blade.php @@ -2,7 +2,7 @@ @if (isset($user_id) && $user_id != Auth::user()->id) @else - + @endif @if (file_exists($logo_path)) diff --git a/resources/views/users/account_management.blade.php b/resources/views/users/account_management.blade.php index 3839a0bb4377..328ca9b920e2 100644 --- a/resources/views/users/account_management.blade.php +++ b/resources/views/users/account_management.blade.php @@ -4,7 +4,9 @@
    - {!! Button::success(trans('texts.add_company'))->asLinkTo('/login?new_company=true') !!} + @if (!session(SESSION_USER_ACCOUNTS) || count(session(SESSION_USER_ACCOUNTS)) < 5) + {!! Button::success(trans('texts.add_company'))->asLinkTo('/login?new_company=true') !!} + @endif

     

    diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index ac7c621e2260..71efd2fee6a8 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -1,10 +1,10 @@ -@extends('accounts.nav') +@extends('header') @section('content') @parent - @include('accounts.nav_advanced') + @include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT]) - {!! Former::open($url)->method($method)->addClass('col-md-8 col-md-offset-2 warn-on-exit')->rules(array( + {!! Former::open($url)->method($method)->addClass('warn-on-exit')->rules(array( 'first_name' => 'required', 'last_name' => 'required', 'email' => 'required|email', @@ -18,7 +18,7 @@

    {!! $title !!}

    -
    +
    {!! Former::text('first_name') !!} {!! Former::text('last_name') !!} @@ -28,7 +28,7 @@
    {!! Former::actions( - Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/company/advanced_settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(), + Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(), Button::success(trans($user && $user->confirmed ? 'texts.save' : 'texts.send_invite'))->submit()->large()->appendIcon(Icon::create($user && $user->confirmed ? 'floppy-disk' : 'send')) )!!} diff --git a/resources/views/vendor.blade.php b/resources/views/vendor.blade.php new file mode 100644 index 000000000000..3248b39f4d3b --- /dev/null +++ b/resources/views/vendor.blade.php @@ -0,0 +1,99 @@ +{!!-- // vendor --!!} +
    +
    + + {!! Former::legend('Organization') !!} + {!! Former::text('name') !!} + {!! Former::text('id_number') !!} + {!! Former::text('vat_number') !!} + {!! Former::text('work_phone')->label('Phone') !!} + {!! Former::textarea('notes') !!} + + + {!! Former::legend('Address') !!} + {!! Former::text('address1')->label('Street') !!} + {!! Former::text('address2')->label('Apt/Floor') !!} + {!! Former::text('city') !!} + {!! Former::text('state') !!} + {!! Former::text('postal_code') !!} + {!! Former::select('country_id')->addOption('','')->label('Country') + ->fromQuery($countries, 'name', 'id') !!} + + +
    +
    + + {!! Former::legend('VendorContacts') !!} +
    + {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") !!} + +
    +
    + + {!! link_to('#', 'Remove contact', array('data-bind'=>'click: $parent.removeContact')) !!} + + + {!! link_to('#', 'Add contact', array('onclick'=>'return addContact()')) !!} + +
    +
    + +
    + +
    +
    + + +{!! Former::hidden('data')->data_bind("value: ko.toJSON(model)") !!} + + + diff --git a/resources/views/vendor/swaggervel/.gitkeep b/resources/views/vendor/swaggervel/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/resources/views/vendor/swaggervel/index.blade.php b/resources/views/vendor/swaggervel/index.blade.php new file mode 100644 index 000000000000..a9c0d1a67e25 --- /dev/null +++ b/resources/views/vendor/swaggervel/index.blade.php @@ -0,0 +1,123 @@ + + + + + + Swagger UI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
     
    +
    + + diff --git a/resources/views/vendors/edit.blade.php b/resources/views/vendors/edit.blade.php new file mode 100644 index 000000000000..e0286c8d5366 --- /dev/null +++ b/resources/views/vendors/edit.blade.php @@ -0,0 +1,218 @@ +@extends('header') + + +@section('onReady') + $('input#name').focus(); +@stop + +@section('content') + +@if ($errors->first('vendorcontacts')) +
    {{ trans($errors->first('vendorcontacts')) }}
    +@endif + +
    + + {!! Former::open($url) + ->autocomplete('off') + ->rules( + ['email' => 'email'] + )->addClass('col-md-12 warn-on-exit') + ->method($method) !!} + + @include('partials.autocomplete_fix') + + @if ($vendor) + {!! Former::populate($vendor) !!} + {!! Former::hidden('public_id') !!} + @endif + +
    +
    + + +
    +
    +

    {!! trans('texts.organization') !!}

    +
    +
    + + {!! Former::text('name')->data_bind("attr { placeholder: placeholderName }") !!} + {!! Former::text('id_number') !!} + {!! Former::text('vat_number') !!} + {!! Former::text('website') !!} + {!! Former::text('work_phone') !!} + +
    +
    + +
    +
    +

    {!! trans('texts.address') !!}

    +
    +
    + + {!! Former::text('address1') !!} + {!! Former::text('address2') !!} + {!! Former::text('city') !!} + {!! Former::text('state') !!} + {!! Former::text('postal_code') !!} + {!! Former::select('country_id')->addOption('','') + ->fromQuery($countries, 'name', 'id') !!} + +
    +
    +
    +
    + + +
    +
    +

    {!! trans('texts.contacts') !!}

    +
    +
    + +
    + {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][public_id]'}") !!} + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][first_name]'}") !!} + {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][last_name]'}") !!} + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][email]', id:'email'+\$index()}") !!} + {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][phone]'}") !!} + +
    +
    + + {!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!} + + + {!! link_to('#', trans('texts.add_contact').' +', array('onclick'=>'return addContact()')) !!} + +
    +
    +
    +
    +
    + + +
    +
    +

    {!! trans('texts.additional_info') !!}

    +
    +
    + + {!! Former::select('currency_id')->addOption('','') + ->placeholder($account->currency ? $account->currency->name : '') + ->fromQuery($currencies, 'name', 'id') !!} + {!! Former::textarea('private_notes')->rows(6) !!} + + + @if (isset($proPlanPaid)) + {!! Former::populateField('pro_plan_paid', $proPlanPaid) !!} + {!! Former::text('pro_plan_paid') + ->data_date_format('yyyy-mm-dd') + ->addGroupClass('pro_plan_paid_date') + ->append('') !!} + + @endif + +
    +
    + +
    +
    + + + {!! Former::hidden('data')->data_bind("value: ko.toJSON(model)") !!} + + + +
    + {!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/vendors/' . ($vendor ? $vendor->public_id : '')))->appendIcon(Icon::create('remove-circle')) !!} + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
    + + {!! Former::close() !!} +
    +@stop diff --git a/resources/views/vendors/show.blade.php b/resources/views/vendors/show.blade.php new file mode 100644 index 000000000000..1cff74275f69 --- /dev/null +++ b/resources/views/vendors/show.blade.php @@ -0,0 +1,258 @@ +@extends('header') + +@section('head') + @parent + + @if ($vendor->hasAddress()) + + + + @endif +@stop + + +@section('content') + +
    + {!! Former::open('vendors/bulk')->addClass('mainForm') !!} +
    + {!! Former::text('action') !!} + {!! Former::text('public_id')->value($vendor->public_id) !!} +
    + + @if ($vendor->trashed()) + {!! Button::primary(trans('texts.restore_vendor'))->withAttributes(['onclick' => 'onRestoreClick()']) !!} + @else + {!! DropdownButton::normal(trans('texts.edit_vendor')) + ->withAttributes(['class'=>'normalDropDown']) + ->withContents([ + ['label' => trans('texts.archive_vendor'), 'url' => "javascript:onArchiveClick()"], + ['label' => trans('texts.delete_vendor'), 'url' => "javascript:onDeleteClick()"], + ] + )->split() !!} + + {!! DropdownButton::primary(trans('texts.new_expense')) + ->withAttributes(['class'=>'primaryDropDown']) + ->withContents($actionLinks)->split() !!} + @endif + {!! Former::close() !!} + +
    + + +

    {{ $vendor->getDisplayName() }}

    +
    +
    +
    +
    +

    {{ trans('texts.details') }}

    + @if ($vendor->id_number) +

    {{ trans('texts.id_number').': '.$vendor->id_number }}

    + @endif + @if ($vendor->vat_number) +

    {{ trans('texts.vat_number').': '.$vendor->vat_number }}

    + @endif + + @if ($vendor->address1) + {{ $vendor->address1 }}
    + @endif + @if ($vendor->address2) + {{ $vendor->address2 }}
    + @endif + @if ($vendor->getCityState()) + {{ $vendor->getCityState() }}
    + @endif + @if ($vendor->country) + {{ $vendor->country->name }}
    + @endif + + @if ($vendor->account->custom_vendor_label1 && $vendor->custom_value1) + {{ $vendor->account->custom_vendor_label1 . ': ' . $vendor->custom_value1 }}
    + @endif + @if ($vendor->account->custom_vendor_label2 && $vendor->custom_value2) + {{ $vendor->account->custom_vendor_label2 . ': ' . $vendor->custom_value2 }}
    + @endif + + @if ($vendor->work_phone) + {{ $vendor->work_phone }} + @endif + + @if ($vendor->private_notes) +

    {{ $vendor->private_notes }}

    + @endif + + @if ($vendor->vendor_industry) + {{ $vendor->vendor_industry->name }}
    + @endif + @if ($vendor->vendor_size) + {{ $vendor->vendor_size->name }}
    + @endif + + @if ($vendor->website) +

    {!! Utils::formatWebsite($vendor->website) !!}

    + @endif + + @if ($vendor->language) +

    {{ $vendor->language->name }}

    + @endif + +

    {{ $vendor->payment_terms ? trans('texts.payment_terms') . ": " . trans('texts.payment_terms_net') . " " . $vendor->payment_terms : '' }}

    +
    + +
    +

    {{ trans('texts.contacts') }}

    + @foreach ($vendor->vendorcontacts as $contact) + @if ($contact->first_name || $contact->last_name) + {{ $contact->first_name.' '.$contact->last_name }}
    + @endif + @if ($contact->email) + {!! HTML::mailto($contact->email, $contact->email) !!}
    + @endif + @if ($contact->phone) + {{ $contact->phone }}
    + @endif + @endforeach +
    + +
    +

    {{ trans('texts.standing') }} + + + + + +
    {{ trans('texts.balance') }}{{ Utils::formatMoney($totalexpense, $vendor->getCurrencyId()) }}
    +

    +
    +
    +
    +
    + + @if ($vendor->hasAddress()) +
    +
    + @endif + + + +
    +
    + {!! Datatable::table() + ->addColumn( + trans('texts.expense_date'), + trans('texts.amount'), + trans('texts.public_notes')) + ->setUrl(url('api/expenseVendor/' . $vendor->public_id)) + ->setCustomValues('entityType', 'expenses') + ->setOptions('sPaginationType', 'bootstrap') + ->setOptions('bFilter', false) + ->setOptions('aaSorting', [['0', 'asc']]) + ->render('datatable') + !!} +
    +
    + + + +@stop diff --git a/storage/pdfcache/.gitignore b/storage/pdfcache/.gitignore deleted file mode 100755 index c96a04f008ee..000000000000 --- a/storage/pdfcache/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/storage/templates/bold.js b/storage/templates/bold.js index 1001ff708a8f..b877f5f4b86c 100644 --- a/storage/templates/bold.js +++ b/storage/templates/bold.js @@ -3,17 +3,12 @@ { "columns": [ { - "image": "$accountLogo", - "width": 80, - "margin": [60, -40, 0, 0] - }, - { - "width": 300, + "width": 380, "stack": [ {"text":"$yourInvoiceLabelUC", "style": "yourInvoice"}, "$clientDetails" ], - "margin": [-32, 120, 0, 0] + "margin": [60, 100, 0, 10] }, { "canvas": [ @@ -29,14 +24,14 @@ } ], "width":10, - "margin":[-10,120,0,0] + "margin":[-10,100,0,10] }, { "table": { "body": "$invoiceDetails" }, "layout": "noBorders", - "margin": [0, 130, 0, 0] + "margin": [0, 110, 0, 0] } ] }, @@ -44,7 +39,7 @@ "style": "invoiceLineItemsTable", "table": { "headerRows": 1, - "widths": "$invoiceLineItemColumns", + "widths": ["22%", "*", "14%", "$quantityWidth", "$taxWidth", "22%"], "body": "$invoiceLineItems" }, "layout": { @@ -53,7 +48,7 @@ "paddingLeft": "$amount:8", "paddingRight": "$amount:8", "paddingTop": "$amount:14", - "paddingBottom": "$amount:14" + "paddingBottom": "$amount:14" } }, { @@ -79,38 +74,67 @@ }] } ], - "footer": [ - {"canvas": [{ "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 100,"lineColor":"#2e2b2b"}]}, + "footer": + [ + {"canvas": [{ "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 100,"lineColor":"$secondaryColor:#292526"}]}, { - "text": "$invoiceFooter", - "margin": [40, -20, 40, 0], - "alignment": "left", - "color": "#FFFFFF" + "columns": + [ + { + "text": "$invoiceFooter", + "margin": [40, -40, 40, 0], + "alignment": "left", + "color": "#FFFFFF" + } + ] } ], "header": [ - {"canvas": [{ "type": "line", "x1": 0, "y1": 0, "x2": 50, "y2":0,"lineWidth": 200,"lineColor":"#2e2b2b"}],"width":100,"margin":[0,0,0,0]}, - {"canvas": [{ "type": "line", "x1": 0, "y1": 0, "x2": 150, "y2":0,"lineWidth": 60,"lineColor":"#2e2b2b"}],"width":100,"margin":[0,0,0,0]}, - {"canvas": [{ "type": "line", "x1": 149, "y1": 0, "x2": 600, "y2":0,"lineWidth": 200,"lineColor":"#2e2b2b"}],"width":10,"margin":[0,0,0,0]}, - { + { + "canvas": [ + { + "type": "line", + "x1": 0, + "y1": 0, + "x2": 600, + "y2": 0, + "lineWidth": 200, + "lineColor": "$secondaryColor:#292526" + } + ], + "width": 10 + }, + { "columns": [ - { - "text": " ", - "width": 260 - }, - { - "stack": "$accountDetails", - "margin": [0, 16, 0, 0], - "width": 140 - }, - { - "stack": "$accountAddress", - "margin": [20, 16, 0, 0] - } + { + "image": "$accountLogo", + "fit": [120, 80], + "margin": [30, 20, 0, 0] + }, + { + "stack": "$accountDetails", + "margin": [ + 0, + 16, + 0, + 0 + ], + "width": 140 + }, + { + "stack": "$accountAddress", + "margin": [ + 20, + 16, + 0, + 0 + ] + } ] - } - ], + } + ], "defaultStyle": { + "font": "$bodyFont", "fontSize": "$fontSize", "margin": [8, 4, 8, 4] }, @@ -120,16 +144,19 @@ }, "accountName": { "bold": true, - "margin": [4, 2, 4, 2], + "margin": [4, 2, 4, 1], "color": "$primaryColor:#36a498" }, "accountDetails": { - "margin": [4, 2, 4, 2], - "color": "#AAA9A9" + "margin": [4, 2, 4, 1], + "color": "#FFFFFF" }, "accountAddress": { - "margin": [4, 2, 4, 2], - "color": "#AAA9A9" + "margin": [4, 2, 4, 1], + "color": "#FFFFFF" + }, + "clientDetails": { + "margin": [0, 2, 0, 1] }, "odd": { "fillColor": "#ebebeb", @@ -163,12 +190,26 @@ "fontSize": 12, "bold": true }, + "costTableHeader": { + "alignment": "right" + }, + "qtyTableHeader": { + "alignment": "right" + }, + "taxTableHeader": { + "alignment": "right" + }, + "lineTotalTableHeader": { + "alignment": "right", + "margin": [0, 0, 40, 0] + }, "productKey": { "color": "$primaryColor:#36a498", "margin": [40,0,0,0], "bold": true }, "yourInvoice": { + "font": "$headerFont", "bold": true, "fontSize": 14, "color": "$primaryColor:#36a498", @@ -176,7 +217,7 @@ }, "invoiceLineItemsTable": { "margin": [0, 26, 0, 16] - }, + }, "clientName": { "bold": true }, @@ -191,16 +232,29 @@ }, "lineTotal": { "alignment": "right", - "margin": [0,0,40,0] + "margin": [0, 0, 40, 0] }, "subtotals": { "alignment": "right", "margin": [0,0,40,0] - }, + }, "termsLabel": { "bold": true, "margin": [0, 0, 0, 4] - } + }, + "header": { + "font": "$headerFont", + "fontSize": "$fontSizeLargest", + "bold": true + }, + "subheader": { + "font": "$headerFont", + "fontSize": "$fontSizeLarger" + }, + "help": { + "fontSize": "$fontSizeSmaller", + "color": "#737373" + } }, "pageMargins": [0, 80, 0, 40] } \ No newline at end of file diff --git a/storage/templates/clean.js b/storage/templates/clean.js index 36bd4dafa3c8..e367a5893217 100644 --- a/storage/templates/clean.js +++ b/storage/templates/clean.js @@ -30,7 +30,7 @@ "table": { "body": "$invoiceDetails" }, - "margin": [0, 4, 12, 4], + "margin": [0, 0, 12, 0], "layout": "noBorders" }, { @@ -48,8 +48,8 @@ "hLineColor": "#D8D8D8", "paddingLeft": "$amount:8", "paddingRight": "$amount:8", - "paddingTop": "$amount:4", - "paddingBottom": "$amount:4" + "paddingTop": "$amount:6", + "paddingBottom": "$amount:6" } }, { @@ -90,6 +90,7 @@ } ], "defaultStyle": { + "font": "$bodyFont", "fontSize": "$fontSize", "margin": [8, 4, 8, 4] }, @@ -104,6 +105,7 @@ }, "styles": { "entityTypeLabel": { + "font": "$headerFont", "fontSize": "$fontSizeLargest", "color": "$primaryColor:#37a3c6" }, @@ -150,6 +152,18 @@ "bold": true, "fontSize": "$fontSizeLarger" }, + "costTableHeader": { + "alignment": "right" + }, + "qtyTableHeader": { + "alignment": "right" + }, + "taxTableHeader": { + "alignment": "right" + }, + "lineTotalTableHeader": { + "alignment": "right" + }, "invoiceLineItemsTable": { "margin": [0, 16, 0, 16] }, @@ -173,7 +187,20 @@ }, "termsLabel": { "bold": true - } + }, + "header": { + "font": "$headerFont", + "fontSize": "$fontSizeLargest", + "bold": true + }, + "subheader": { + "font": "$headerFont", + "fontSize": "$fontSizeLarger" + }, + "help": { + "fontSize": "$fontSizeSmaller", + "color": "#737373" + } }, "pageMargins": [40, 40, 40, 60] } \ No newline at end of file diff --git a/storage/templates/modern.js b/storage/templates/modern.js index 4a95e5af5f97..9023121aff64 100644 --- a/storage/templates/modern.js +++ b/storage/templates/modern.js @@ -1,20 +1,18 @@ { "content": [ - { - "columns": [ { - "image": "$accountLogo", - "fit": [120, 80], - "margin": [0, 60, 0, 30] + "columns": [ + { + "image": "$accountLogo", + "fit": [120, 80], + "margin": [0, 60, 0, 30] + }, + { + "stack": "$clientDetails", + "margin": [0, 80, 0, 0] + } + ] }, - { - "stack": "$clientDetails", - "margin": [260, 80, 0, 0] - } - ] - }, - { - "canvas": [{ "type": "rect", "x": 0, "y": 0, "w": 515, "h": 26, "r":0, "lineWidth": 1, "color":"#403d3d"}],"width":10,"margin":[0,25,0,-30]}, { "style": "invoiceLineItemsTable", "table": { @@ -24,8 +22,9 @@ }, "layout": { "hLineWidth": "$notFirst:.5", - "vLineWidth": "$none", + "vLineWidth": "$notFirstAndLastColumn:.5", "hLineColor": "#888888", + "vLineColor": "#FFFFFF", "paddingLeft": "$amount:8", "paddingRight": "$amount:8", "paddingTop": "$amount:8", @@ -63,7 +62,7 @@ "h": 26, "r": 0, "lineWidth": 1, - "color": "#403d3d" + "color": "$secondaryColor:#403d3d" } ], "width": 10, @@ -92,17 +91,22 @@ "canvas": [ { "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 100,"lineColor":"$primaryColor:#f26621" - }] - ,"width":10 - }, - { + }] + ,"width":10 + }, + { "columns": [ - { - "text": "$invoiceFooter", - "margin": [40, -30, 40, 0], - "alignment": "left", - "color": "#FFFFFF", - "width": 350 + { + "width": 350, + "stack": [ + { + "text": "$invoiceFooter", + "margin": [40, -40, 40, 0], + "alignment": "left", + "color": "#FFFFFF" + + } + ] }, { "stack": "$accountDetails", @@ -124,7 +128,7 @@ { "columns": [ { - "text": "$accountName", "bold": true,"fontSize":30,"color":"#ffffff","margin":[40,20,0,0],"width":350 + "text": "$accountName", "bold": true,"font":"$headerFont","fontSize":30,"color":"#ffffff","margin":[40,20,0,0],"width":350 } ] }, @@ -138,6 +142,7 @@ } ], "defaultStyle": { + "font": "$bodyFont", "fontSize": "$fontSize", "margin": [8, 4, 8, 4] }, @@ -175,8 +180,21 @@ "tableHeader": { "bold": true, "color": "#FFFFFF", - "fontSize": "$fontSizeLargest" + "fontSize": "$fontSizeLargest", + "fillColor": "$secondaryColor:#403d3d" }, + "costTableHeader": { + "alignment": "right" + }, + "qtyTableHeader": { + "alignment": "right" + }, + "taxTableHeader": { + "alignment": "right" + }, + "lineTotalTableHeader": { + "alignment": "right" + }, "balanceDueLabel": { "fontSize": "$fontSizeLargest", "color":"#FFFFFF", @@ -213,8 +231,20 @@ }, "invoiceNumber": { "bold": true + }, + "header": { + "font": "$headerFont", + "fontSize": "$fontSizeLargest", + "bold": true + }, + "subheader": { + "font": "$headerFont", + "fontSize": "$fontSizeLarger" + }, + "help": { + "fontSize": "$fontSizeSmaller", + "color": "#737373" } - }, "pageMargins": [40, 80, 40, 50] } \ No newline at end of file diff --git a/storage/templates/plain.js b/storage/templates/plain.js index d21275c4e122..b260a6727e75 100644 --- a/storage/templates/plain.js +++ b/storage/templates/plain.js @@ -91,6 +91,7 @@ "margin": [40, -20, 40, 40] }, "defaultStyle": { + "font": "$bodyFont", "fontSize": "$fontSize", "margin": [8, 4, 8, 4] }, @@ -110,6 +111,15 @@ "tableHeader": { "bold": true }, + "costTableHeader": { + "alignment": "right" + }, + "qtyTableHeader": { + "alignment": "right" + }, + "lineTotalTableHeader": { + "alignment": "right" + }, "invoiceLineItemsTable": { "margin": [0, 16, 0, 16] }, @@ -143,6 +153,19 @@ }, "balanceDue": { "fillColor": "#e6e6e6" + }, + "header": { + "font": "$headerFont", + "fontSize": "$fontSizeLargest", + "bold": true + }, + "subheader": { + "font": "$headerFont", + "fontSize": "$fontSizeLarger" + }, + "help": { + "fontSize": "$fontSizeSmaller", + "color": "#737373" } }, "pageMargins": [40, 40, 40, 60] diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php.default similarity index 63% rename from tests/_bootstrap.php rename to tests/_bootstrap.php.default index 464fcb4d7f7a..193f2f90f10c 100644 --- a/tests/_bootstrap.php +++ b/tests/_bootstrap.php.default @@ -3,4 +3,6 @@ use Codeception\Util\Fixtures; Fixtures::add('username', 'user@example.com'); -Fixtures::add('password', 'password'); \ No newline at end of file +Fixtures::add('password', 'password'); + +Fixtures::add('gateway_key', ''); \ No newline at end of file diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php index ac8fe46eb605..fd89ac0d0de0 100644 --- a/tests/_support/AcceptanceTester.php +++ b/tests/_support/AcceptanceTester.php @@ -31,7 +31,7 @@ class AcceptanceTester extends \Codeception\Actor $I->amOnPage('/login'); $I->fillField(['name' => 'email'], Fixtures::get('username')); $I->fillField(['name' => 'password'], Fixtures::get('password')); - $I->click('Let\'s go'); + $I->click('Login'); //$I->saveSessionSnapshot('login'); } diff --git a/tests/_support/FunctionalTester.php b/tests/_support/FunctionalTester.php index 173b7a82e7a6..87e4f0234c27 100644 --- a/tests/_support/FunctionalTester.php +++ b/tests/_support/FunctionalTester.php @@ -27,11 +27,10 @@ class FunctionalTester extends \Codeception\Actor function checkIfLogin(\FunctionalTester $I) { //if ($I->loadSessionSnapshot('login')) return; - $I->amOnPage('/login'); $I->fillField(['name' => 'email'], Fixtures::get('username')); $I->fillField(['name' => 'password'], Fixtures::get('password')); - $I->click('Let\'s go'); + $I->click('#loginButton'); //$I->saveSessionSnapshot('login'); } diff --git a/tests/_support/_generated/FunctionalTesterActions.php b/tests/_support/_generated/FunctionalTesterActions.php index 06431340137a..edf786bc5c7e 100644 --- a/tests/_support/_generated/FunctionalTesterActions.php +++ b/tests/_support/_generated/FunctionalTesterActions.php @@ -1,4 +1,4 @@ -faker = Factory::create(); + + Debug::debug('Create/get token'); + $data = new stdClass; + $data->email = Fixtures::get('username'); + $data->password = Fixtures::get('password'); + $data->api_secret = Fixtures::get('api_secret'); + $data->token_name = 'iOS Token'; + + $response = $this->sendRequest('login', $data); + $userAccounts = $response->data; + + PHPUnit_Framework_Assert::assertGreaterThan(0, count($userAccounts)); + + $userAccount = $userAccounts[0]; + $this->token = $userAccount->token; + + Debug::debug("Token: {$this->token}"); + } + + public function testAPI(AcceptanceTester $I) + { + $I->wantTo('test the API'); + + $data = new stdClass; + $data->contact = new stdClass; + $data->contact->email = $this->faker->safeEmail; + $clientId = $this->createEntity('client', $data); + $this->listEntities('client'); + + $data = new stdClass; + $data->client_id = $clientId; + $data->description = $this->faker->realText(100); + $this->createEntity('task', $data); + $this->listEntities('task'); + + $lineItem = new stdClass; + $lineItem->qty = $this->faker->numberBetween(1, 10); + $lineItem->cost = $this->faker->numberBetween(1, 10); + $data = new stdClass; + $data->client_id = $clientId; + $data->invoice_items = [ + $lineItem + ]; + $invoiceId = $this->createEntity('invoice', $data); + $this->listEntities('invoice'); + + $data = new stdClass; + $data->invoice_id = $invoiceId; + $data->amount = 1; + $this->createEntity('payment', $data); + $this->listEntities('payment'); + + $this->listEntities('account'); + } + + private function createEntity($entityType, $data) + { + Debug::debug("Create {$entityType}"); + + $response = $this->sendRequest("{$entityType}s", $data); + $entityId = $response->data->id; + PHPUnit_Framework_Assert::assertGreaterThan(0, $entityId); + + return $entityId; + } + + private function listEntities($entityType) + { + Debug::debug("List {$entityType}s"); + $response = $this->sendRequest("{$entityType}s", null, 'GET'); + + PHPUnit_Framework_Assert::assertGreaterThan(0, count($response->data)); + + return $response; + } + + private function sendRequest($url, $data, $type = 'POST') + { + $url = Fixtures::get('url') . '/api/v1/' . $url; + $data = json_encode($data); + $curl = curl_init(); + + $opts = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => $type, + CURLOPT_POST => $type === 'POST' ? 1 : 0, + CURLOPT_POSTFIELDS => $data, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data), + 'X-Ninja-Token: '. $this->token, + ], + ]; + + curl_setopt_array($curl, $opts); + $response = curl_exec($curl); + curl_close($curl); + + return json_decode($response); + } +} \ No newline at end of file diff --git a/tests/acceptance/AllPagesCept.php b/tests/acceptance/AllPagesCept.php index 07c8871471da..24d43a9eccf8 100644 --- a/tests/acceptance/AllPagesCept.php +++ b/tests/acceptance/AllPagesCept.php @@ -1,16 +1,9 @@ checkIfLogin($I); $I->wantTo('Test all pages load'); -$I->amOnPage('/login'); -//$I->see(trans('texts.forgot_password')); - -// Login as test user -$I->fillField(['name' => 'email'], 'hillelcoren@gmail.com'); -$I->fillField(['name' => 'password'], '4uejs%2ksl#271df'); -$I->click('Let\'s go'); -$I->see('Dashboard'); // Top level navigation $I->amOnPage('/dashboard'); @@ -59,31 +52,31 @@ $I->see('Payments', 'li'); $I->see('Create'); // Settings pages -$I->amOnPage('/company/details'); +$I->amOnPage('/settings/company_details'); $I->see('Details'); $I->amOnPage('/gateways/create'); $I->see('Add Gateway'); -$I->amOnPage('/company/products'); +$I->amOnPage('/settings/products'); $I->see('Product Settings'); -$I->amOnPage('/company/import_export'); +$I->amOnPage('/settings/import_export'); $I->see('Import'); -$I->amOnPage('/company/advanced_settings/invoice_settings'); +$I->amOnPage('/settings/invoice_settings'); $I->see('Invoice Fields'); -$I->amOnPage('/company/advanced_settings/invoice_design'); +$I->amOnPage('/settings/invoice_design'); $I->see('Invoice Design'); -$I->amOnPage('/company/advanced_settings/email_templates'); +$I->amOnPage('/settings/templates_and_reminders'); $I->see('Invoice Email'); -$I->amOnPage('/company/advanced_settings/charts_and_reports'); +$I->amOnPage('/settings/charts_and_reports'); $I->see('Data Visualizations'); -$I->amOnPage('/company/advanced_settings/user_management'); +$I->amOnPage('/settings/user_management'); $I->see('Add User'); //try to logout diff --git a/tests/acceptance/ChartsAndReportsCest.php b/tests/acceptance/ChartsAndReportsCest.php deleted file mode 100644 index 19c0b8a1b8c5..000000000000 --- a/tests/acceptance/ChartsAndReportsCest.php +++ /dev/null @@ -1,103 +0,0 @@ -checkIfLogin($I); - - $this->faker = Factory::create(); - } - - public function _after(AcceptanceTester $I) - { - - } - - // tests - public function updateChartsAndReportsPage(AcceptanceTester $I) - { - - $faker = Faker\Factory::create(); - - $I->wantTo('Run the report'); - - $I->amOnPage('/company/advanced_settings/charts_and_reports'); - - /* - $format = 'M d,Y'; - $start_date = date ( $format, strtotime ( '-7 day' . $format)); - $I->fillField(['name' => 'start_date'],$start_date); - $I->fillField(['name' => 'start_date'], 'April 15, 2015'); - $I->fillField(['name' => 'end_date'], date('M d,Y')); - $I->fillField(['name' => 'end_date'], 'August 29, 2015'); - */ - - $I->checkOption(['name' => 'enable_report']); - $I->selectOption("#report_type", 'Client'); - $I->checkOption(['name' => 'enable_chart']); - - $rand = ['DAYOFYEAR', 'WEEK', 'MONTH']; - $I->selectOption("#group_by", $rand[array_rand($rand)]); - - $rand = ['Bar', 'Line']; - $I->selectOption("#chart_type", $rand[array_rand($rand)]); - - $I->click('Run'); - $I->see('Start Date'); - } - - /* - public function showDataVisualization(AcceptanceTester $I) { - - $I->wantTo('Display pdf data'); - $I->amOnPage('/company/advanced_settings/data_visualizations'); - - $optionTest = "1"; // This is the option to test! - $I->selectOption('#groupBySelect', $optionTest); - $models = ['Client', 'Invoice', 'Product']; - - //$all = Helper::getRandom($models[array_rand($models)], 'all'); - $all = Helper::getRandom('Client', 'all'); - $labels = $this->getLabels($optionTest); - - $all_items = true; - $I->seeElement('div.svg-div svg g:nth-child(2)'); - - for ($i = 0; $i < count($labels); $i++) { - $text = $I->grabTextFrom('div.svg-div svg g:nth-child('.($i+2).') text'); - //$I->seeInField('div.svg-div svg g:nth-child('.($i+2).') text', $labels[$i]); - if (!in_array($text, $labels)) { - $all_items = false; - break; - } - } - - if (!$all_items) { - $I->see('Fail', 'Fail'); - } - } - - private function getLabels($option) { - - $invoices = \App\Models\Invoice::where('user_id', '1')->get(); - $clients = []; - - foreach ($invoices as $invoice) { - $clients[] = \App\Models\Client::where('public_id', $invoice->client_id)->get(); - } - - $clientNames = []; - foreach ($clients as $client) { - $clientNames[] = $client[0]->name; - } - - return $clientNames; - } - */ -} \ No newline at end of file diff --git a/tests/acceptance/CheckBalanceCest.php b/tests/acceptance/CheckBalanceCest.php new file mode 100644 index 000000000000..6bac648864f9 --- /dev/null +++ b/tests/acceptance/CheckBalanceCest.php @@ -0,0 +1,94 @@ +checkIfLogin($I); + + $this->faker = Factory::create(); + } + + public function checkBalance(AcceptanceTester $I) + { + $I->wantTo('ensure the balance is correct'); + + $clientEmail = $this->faker->safeEmail; + $productKey = $this->faker->text(10); + $productPrice = $this->faker->numberBetween(1, 20); + + // create client + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->wait(1); + $I->see($clientEmail); + + $clientId = $I->grabFromCurrentUrl('~clients/(\d+)~'); + + // create product + $I->amOnPage('/products/create'); + $I->fillField(['name' => 'product_key'], $productKey); + $I->fillField(['name' => 'notes'], $this->faker->text(80)); + $I->fillField(['name' => 'cost'], $productPrice); + $I->click('Save'); + $I->wait(1); + $I->see($productKey); + + // create invoice + $I->amOnPage('/invoices/create'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); + $I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey); + $I->click('Save'); + $I->wait(1); + $I->see($clientEmail); + $invoiceId = $I->grabFromCurrentUrl('~invoices/(\d+)~'); + $I->amOnPage("/clients/{$clientId}"); + $I->see('Balance $' . $productPrice); + + // update the invoice + $I->amOnPage('/invoices/' . $invoiceId); + $I->fillField(['name' => 'invoice_items[0][qty]'], 2); + $I->click('Save'); + $I->wait(1); + $I->amOnPage("/clients/{$clientId}"); + $I->see('Balance $' . ($productPrice * 2)); + + // enter payment + $I->amOnPage("/payments/create/{$clientId}/{$invoiceId}"); + $I->click('Save'); + $I->wait(1); + $I->see('Balance $0.00'); + $I->see('Paid to Date $' . ($productPrice * 2)); + + // archive the invoice + $I->amOnPage('/invoices/' . $invoiceId); + $I->executeJS('submitBulkAction("archive")'); + $I->wait(1); + $I->amOnPage("/clients/{$clientId}"); + $I->see('Balance $0.00'); + $I->see('Paid to Date $' . ($productPrice * 2)); + + // delete the invoice + $I->amOnPage('/invoices/' . $invoiceId); + $I->executeJS('submitBulkAction("delete")'); + $I->wait(1); + $I->amOnPage("/clients/{$clientId}"); + $I->see('Balance $0.00'); + $I->see('Paid to Date $0.00'); + + // restore the invoice + $I->amOnPage('/invoices/' . $invoiceId); + $I->executeJS('submitBulkAction("restore")'); + $I->wait(1); + $I->amOnPage("/clients/{$clientId}"); + $I->see('Balance $0.00'); + $I->see('Paid to Date $' . ($productPrice * 2)); + } +} \ No newline at end of file diff --git a/tests/acceptance/ClientCest.php b/tests/acceptance/ClientCest.php index 70eb2f8e7d54..e8b17e8dd214 100644 --- a/tests/acceptance/ClientCest.php +++ b/tests/acceptance/ClientCest.php @@ -32,10 +32,10 @@ class ClientCest $I->fillField(['name' => 'work_phone'], $this->faker->phoneNumber); //Contacts - $I->fillField(['name' => 'first_name'], $this->faker->firstName); - $I->fillField(['name' => 'last_name'], $this->faker->lastName); - $I->fillField(['name' => 'email'], $this->faker->companyEmail); - $I->fillField(['name' => 'phone'], $this->faker->phoneNumber); + $I->fillField(['name' => 'contacts[0][first_name]'], $this->faker->firstName); + $I->fillField(['name' => 'contacts[0][last_name]'], $this->faker->lastName); + $I->fillField(['name' => 'contacts[0][email]'], $this->faker->companyEmail); + $I->fillField(['name' => 'contacts[0][phone]'], $this->faker->phoneNumber); //Additional Contact //$I->click('Add contact +'); diff --git a/tests/acceptance/CreditCest.php b/tests/acceptance/CreditCest.php index 738c8e6fb03f..3c47d9ede362 100644 --- a/tests/acceptance/CreditCest.php +++ b/tests/acceptance/CreditCest.php @@ -19,12 +19,17 @@ class CreditCest public function create(AcceptanceTester $I) { $note = $this->faker->catchPhrase; - $clientName = $I->grabFromDatabase('clients', 'name'); + $clientEmail = $this->faker->safeEmail; $I->wantTo('Create a credit'); - $I->amOnPage('/credits/create'); - $I->selectDropdown($I, $clientName, '.client-select .dropdown-toggle'); + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->see($clientEmail); + + $I->amOnPage('/credits/create'); + $I->selectDropdown($I, $clientEmail, '.client-select .dropdown-toggle'); $I->fillField(['name' => 'amount'], rand(50, 200)); $I->fillField(['name' => 'private_notes'], $note); $I->selectDataPicker($I, '#credit_date', 'now + 1 day'); @@ -35,7 +40,7 @@ class CreditCest $I->amOnPage('/credits'); $I->seeCurrentUrlEquals('/credits'); - $I->see($clientName); + $I->see($clientEmail); } } \ No newline at end of file diff --git a/tests/acceptance/GoProCest.php b/tests/acceptance/GoProCest.php new file mode 100644 index 000000000000..3ccc7c557717 --- /dev/null +++ b/tests/acceptance/GoProCest.php @@ -0,0 +1,60 @@ +faker = Factory::create(); + } + + public function signUpAndGoPro(AcceptanceTester $I) + { + $userEmail = $this->faker->safeEmail; + $userPassword = $this->faker->password; + + $I->wantTo('test purchasing a pro plan'); + $I->amOnPage('/invoice_now'); + + $I->click('Sign Up'); + $I->wait(1); + + $I->checkOption('#terms_checkbox'); + $I->fillField(['name' =>'new_first_name'], $this->faker->firstName); + $I->fillField(['name' =>'new_last_name'], $this->faker->lastName); + $I->fillField(['name' =>'new_email'], $userEmail); + $I->fillField(['name' =>'new_password'], $userPassword); + $I->click('Save'); + $I->wait(1); + + $I->amOnPage('/dashboard'); + $I->click('Go Pro'); + $I->wait(1); + + $I->click('Upgrade Now!'); + $I->wait(1); + + $I->fillField(['name' => 'address1'], $this->faker->streetAddress); + $I->fillField(['name' => 'address2'], $this->faker->streetAddress); + $I->fillField(['name' => 'city'], $this->faker->city); + $I->fillField(['name' => 'state'], $this->faker->state); + $I->fillField(['name' => 'postal_code'], $this->faker->postcode); + $I->selectDropdown($I, 'United States', '.country-select .dropdown-toggle'); + $I->fillField(['name' => 'card_number'], '4242424242424242'); + $I->fillField(['name' => 'cvv'], '1234'); + $I->selectOption('#expiration_month', 12); + $I->selectOption('#expiration_year', date('Y')); + $I->click('.btn-success'); + $I->wait(1); + + $I->see('Successfully applied payment'); + + $I->amOnPage('/dashboard'); + $I->dontSee('Go Pro'); + } +} \ No newline at end of file diff --git a/tests/acceptance/InvoiceCest.php b/tests/acceptance/InvoiceCest.php index ad7a4048be80..5604b50830d8 100644 --- a/tests/acceptance/InvoiceCest.php +++ b/tests/acceptance/InvoiceCest.php @@ -19,14 +19,20 @@ class InvoiceCest public function createInvoice(AcceptanceTester $I) { - $clientName = $I->grabFromDatabase('clients', 'name'); + $clientEmail = $this->faker->safeEmail; $I->wantTo('create an invoice'); + + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->see($clientEmail); + $I->amOnPage('/invoices/create'); $invoiceNumber = $I->grabAttributeFrom('#invoice_number', 'value'); - $I->selectDropdown($I, $clientName, '.client_select .dropdown-toggle'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); $I->selectDataPicker($I, '#invoice_date'); $I->selectDataPicker($I, '#due_date', '+ 15 day'); $I->fillField('#po_number', rand(100, 200)); @@ -41,12 +47,17 @@ class InvoiceCest public function createRecurringInvoice(AcceptanceTester $I) { - $clientName = $I->grabFromDatabase('clients', 'name'); + $clientEmail = $this->faker->safeEmail; $I->wantTo('create a recurring invoice'); - $I->amOnPage('/recurring_invoices/create'); + + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->see($clientEmail); - $I->selectDropdown($I, $clientName, '.client_select .dropdown-toggle'); + $I->amOnPage('/recurring_invoices/create'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); $I->selectDataPicker($I, '#end_date', '+ 1 week'); $I->fillField('#po_number', rand(100, 200)); $I->fillField('#discount', rand(0, 20)); @@ -55,16 +66,16 @@ class InvoiceCest $I->executeJS("submitAction('email')"); $I->wait(1); - $I->see($clientName); + $I->see($clientEmail); $invoiceNumber = $I->grabAttributeFrom('#invoice_number', 'value'); $I->click('Recurring Invoice'); - $I->see($clientName); + $I->see($clientEmail); - $I->click('#lastInvoiceSent'); + $I->click('#lastSent'); $I->see($invoiceNumber); } - + public function editInvoice(AcceptanceTester $I) { $I->wantTo('edit an invoice'); @@ -73,7 +84,7 @@ class InvoiceCest //change po_number with random number $po_number = rand(100, 300); - $I->fillField('po_number', $po_number); + $I->fillField('#po_number', $po_number); //save $I->executeJS('submitAction()'); @@ -86,7 +97,7 @@ class InvoiceCest public function cloneInvoice(AcceptanceTester $I) { $I->wantTo('clone an invoice'); - $I->amOnPage('invoices/1/clone'); + $I->amOnPage('/invoices/1/clone'); $invoiceNumber = $I->grabAttributeFrom('#invoice_number', 'value'); @@ -95,7 +106,6 @@ class InvoiceCest $I->see($invoiceNumber); } - /* public function deleteInvoice(AcceptanceTester $I) diff --git a/tests/acceptance/InvoiceDesignCest.php b/tests/acceptance/InvoiceDesignCest.php index dfe9f789ecc0..36dc6799d126 100644 --- a/tests/acceptance/InvoiceDesignCest.php +++ b/tests/acceptance/InvoiceDesignCest.php @@ -1,5 +1,4 @@ wantTo('Design my invoice'); - $I->amOnPage('/company/advanced_settings/invoice_design'); + $I->amOnPage('/settings/invoice_design'); $I->click('select#invoice_design_id'); $I->click('select#invoice_design_id option:nth-child(2)'); @@ -42,15 +41,16 @@ class InvoiceDesignCest //$I->executeJS('$("#secondary_color + .sp-replacer .sp-preview-inner").attr("style", "background-color: rgb(254,0,50);")'); $I->executeJS('$(".sp-container:nth-child(2) .sp-choose").click()'); + /* $I->fillField(['name' => 'labels_item'], $this->faker->text(6)); $I->fillField(['name' => 'labels_description'], $this->faker->text(12)); $I->fillField(['name' => 'labels_unit_cost'], $this->faker->text(12)); $I->fillField(['name' => 'labels_quantity'], $this->faker->text(8)); $I->uncheckOption('#hide_quantity'); - $I->checkOption('#hide_paid_to_date'); - + */ + $I->click('Save'); $I->wait(3); diff --git a/tests/acceptance/OnlinePaymentCest.php b/tests/acceptance/OnlinePaymentCest.php new file mode 100644 index 000000000000..e56fda7e6a51 --- /dev/null +++ b/tests/acceptance/OnlinePaymentCest.php @@ -0,0 +1,96 @@ +/checkIfLogin($I); + + $this->faker = Factory::create(); + } + + public function onlinePayment(AcceptanceTester $I) + { + $I->wantTo('test an online payment'); + + $clientEmail = $this->faker->safeEmail; + $productKey = $this->faker->text(10); + + // set gateway info + $I->wantTo('create a gateway'); + $I->amOnPage('/settings/online_payments'); + + if (strpos($I->grabFromCurrentUrl(), 'create') !== false) { + $I->fillField(['name' =>'23_apiKey'], Fixtures::get('gateway_key')); + $I->selectOption('#token_billing_type_id', 4); + $I->click('Save'); + $I->see('Successfully created gateway'); + } + + // create client + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->see($clientEmail); + + // create product + $I->amOnPage('/products/create'); + $I->fillField(['name' => 'product_key'], $productKey); + $I->fillField(['name' => 'notes'], $this->faker->text(80)); + $I->fillField(['name' => 'cost'], $this->faker->numberBetween(1, 20)); + $I->click('Save'); + $I->wait(1); + $I->see($productKey); + + // create invoice + $I->amOnPage('/invoices/create'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); + $I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey); + $I->click('Save'); + $I->see($clientEmail); + + // enter payment + $clientId = $I->grabFromDatabase('contacts', 'client_id', ['email' => $clientEmail]); + $invoiceId = $I->grabFromDatabase('invoices', 'id', ['client_id' => $clientId]); + $invitationKey = $I->grabFromDatabase('invitations', 'invitation_key', ['invoice_id' => $invoiceId]); + + $clientSession = $I->haveFriend('client'); + $clientSession->does(function(AcceptanceTester $I) use ($invitationKey) { + $I->amOnPage('/view/' . $invitationKey); + $I->click('Pay Now'); + + $I->fillField(['name' => 'first_name'], $this->faker->firstName); + $I->fillField(['name' => 'last_name'], $this->faker->lastName); + $I->fillField(['name' => 'address1'], $this->faker->streetAddress); + $I->fillField(['name' => 'address2'], $this->faker->streetAddress); + $I->fillField(['name' => 'city'], $this->faker->city); + $I->fillField(['name' => 'state'], $this->faker->state); + $I->fillField(['name' => 'postal_code'], $this->faker->postcode); + $I->selectDropdown($I, 'United States', '.country-select .dropdown-toggle'); + $I->fillField(['name' => 'card_number'], '4242424242424242'); + $I->fillField(['name' => 'cvv'], '1234'); + $I->selectOption('#expiration_month', 12); + $I->selectOption('#expiration_year', date('Y')); + $I->click('.btn-success'); + $I->see('Successfully applied payment'); + }); + + $I->wait(1); + + // create recurring invoice and auto-bill + $I->amOnPage('/recurring_invoices/create'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); + $I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey); + $I->checkOption('#auto_bill'); + $I->executeJS('preparePdfData(\'email\')'); + $I->wait(2); + $I->see("$0.00"); + + } +} \ No newline at end of file diff --git a/tests/acceptance/PaymentCest.php b/tests/acceptance/PaymentCest.php index cd8664505ccb..2808c7c8d33e 100644 --- a/tests/acceptance/PaymentCest.php +++ b/tests/acceptance/PaymentCest.php @@ -17,16 +17,38 @@ class PaymentCest public function create(AcceptanceTester $I) { - $clientName = $I->grabFromDatabase('clients', 'name'); - $amount = rand(1, 30); + $clientEmail = $this->faker->safeEmail; + $productKey = $this->faker->text(10); + $amount = rand(1, 10); $I->wantTo('enter a payment'); - $I->amOnPage('/payments/create'); - $I->selectDropdown($I, $clientName, '.client-select .dropdown-toggle'); + // create client + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->see($clientEmail); + + // create product + $I->amOnPage('/products/create'); + $I->fillField(['name' => 'product_key'], $productKey); + $I->fillField(['name' => 'notes'], $this->faker->text(80)); + $I->fillField(['name' => 'cost'], $this->faker->numberBetween(11, 20)); + $I->click('Save'); + $I->see($productKey); + + // create invoice + $I->amOnPage('/invoices/create'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); + $I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey); + $I->click('Save'); + $I->see($clientEmail); + + $I->amOnPage('/payments/create'); + $I->selectDropdown($I, $clientEmail, '.client-select .dropdown-toggle'); $I->selectDropdownRow($I, 1, '.invoice-select .combobox-container'); $I->fillField(['name' => 'amount'], $amount); - $I->selectDropdownRow($I, 1, 'div.panel-body div.form-group:nth-child(4) .combobox-container'); + $I->selectDropdown($I, 'Cash', '.payment-type-select .dropdown-toggle'); $I->selectDataPicker($I, '#payment_date', 'now + 1 day'); $I->fillField(['name' => 'transaction_reference'], $this->faker->text(12)); diff --git a/tests/acceptance/TaxRatesCest.php b/tests/acceptance/TaxRatesCest.php new file mode 100644 index 000000000000..1ee9883da21d --- /dev/null +++ b/tests/acceptance/TaxRatesCest.php @@ -0,0 +1,88 @@ +/checkIfLogin($I); + + $this->faker = Factory::create(); + } + + public function taxRates(AcceptanceTester $I) + { + $I->wantTo('test tax rates'); + + $clientEmail = $this->faker->safeEmail; + $productKey = $this->faker->text(10); + $itemTaxRate = $this->faker->randomFloat(2, 5, 15); + $itemTaxName = $this->faker->word(); + $invoiceTaxRate = $this->faker->randomFloat(2, 5, 15); + $invoiceTaxName = $this->faker->word(); + $itemCost = $this->faker->numberBetween(1, 20); + + $total = $itemCost; + $total += round($itemCost * $itemTaxRate / 100, 2); + $total += round($itemCost * $invoiceTaxRate / 100, 2); + + // create tax rates + $I->amOnPage('/tax_rates/create'); + $I->fillField(['name' => 'name'], $itemTaxName); + $I->fillField(['name' => 'rate'], $itemTaxRate); + $I->click('Save'); + $I->see($itemTaxName); + + $I->amOnPage('/tax_rates/create'); + $I->fillField(['name' => 'name'], $invoiceTaxName); + $I->fillField(['name' => 'rate'], $invoiceTaxRate); + $I->click('Save'); + $I->see($invoiceTaxName); + + // enable line item taxes + $I->amOnPage('/settings/tax_rates'); + $I->checkOption('#invoice_item_taxes'); + $I->click('Save'); + + // create product + $I->amOnPage('/products/create'); + $I->fillField(['name' => 'product_key'], $productKey); + $I->fillField(['name' => 'notes'], $this->faker->text(80)); + $I->fillField(['name' => 'cost'], $itemCost); + $I->selectOption('select[name=default_tax_rate_id]', $itemTaxName . ' ' . $itemTaxRate . '%'); + $I->click('Save'); + $I->wait(1); + $I->see($productKey); + + // create client + $I->amOnPage('/clients/create'); + $I->fillField(['name' => 'contacts[0][email]'], $clientEmail); + $I->click('Save'); + $I->see($clientEmail); + + // create invoice + $I->amOnPage('/invoices/create'); + $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); + $I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey); + $I->selectOption('#taxRateSelect', $invoiceTaxName . ' ' . $invoiceTaxRate . '%'); + $I->wait(2); + + // check total is right before saving + $I->see("\${$total}"); + $I->click('Save'); + $I->see($clientEmail); + + // check total is right after saving + $I->see("\${$total}"); + $I->amOnPage('/invoices'); + + // check total is right in list view + $I->see("\${$total}"); + } + +} \ No newline at end of file diff --git a/tests/functional.suite.yml b/tests/functional.suite.yml index 809044371ff4..7c84b1000840 100644 --- a/tests/functional.suite.yml +++ b/tests/functional.suite.yml @@ -9,7 +9,9 @@ modules: enabled: - \Helper\Functional - PhpBrowser: - url: 'http://ninja.dev/' + url: 'http://ninja.dev' + curl: + CURLOPT_RETURNTRANSFER: true - Laravel5: environment_file: '.env' cleanup: false \ No newline at end of file diff --git a/tests/functional/SettingsCest.php b/tests/functional/SettingsCest.php index a0c71f419011..7de52ef9d94c 100644 --- a/tests/functional/SettingsCest.php +++ b/tests/functional/SettingsCest.php @@ -14,10 +14,11 @@ class SettingsCest $this->faker = Factory::create(); } + /* public function companyDetails(FunctionalTester $I) { $I->wantTo('update the company details'); - $I->amOnPage('/company/details'); + $I->amOnPage('/settings/company_details'); $name = $this->faker->company; @@ -29,28 +30,191 @@ class SettingsCest $I->fillField(['name' => 'city'], $this->faker->city); $I->fillField(['name' => 'state'], $this->faker->state); $I->fillField(['name' => 'postal_code'], $this->faker->postcode); + $I->click('Save'); - $I->fillField(['name' => 'first_name'], $this->faker->firstName); + $I->seeResponseCodeIs(200); + $I->seeRecord('accounts', array('name' => $name)); + } + */ + + public function userDetails(FunctionalTester $I) + { + $I->wantTo('update the user details'); + $I->amOnPage('/settings/user_details'); + + $firstName = $this->faker->firstName; + + $I->fillField(['name' => 'first_name'], $firstName); $I->fillField(['name' => 'last_name'], $this->faker->lastName); $I->fillField(['name' => 'phone'], $this->faker->phoneNumber); $I->click('Save'); $I->seeResponseCodeIs(200); - $I->see('Successfully updated settings'); + $I->seeRecord('users', array('first_name' => $firstName)); + } + + /* + public function localization(FunctionalTester $I) + { + $I->wantTo('update the localization'); + $I->amOnPage('/settings/localization'); + + $name = $this->faker->company; + + $I->fillField(['name' => 'name'], $name); + $I->click('Save'); + + $I->seeResponseCodeIs(200); $I->seeRecord('accounts', array('name' => $name)); } + */ + public function productSettings(FunctionalTester $I) { $I->wantTo('update the product settings'); - $I->amOnPage('/company/products'); + $I->amOnPage('/settings/products'); $I->click('Save'); $I->seeResponseCodeIs(200); - $I->see('Successfully updated settings'); } + public function createProduct(FunctionalTester $I) + { + $I->wantTo('create a product'); + $I->amOnPage('/products/create'); + + $productKey = $this->faker->text(10); + + $I->fillField(['name' => 'product_key'], $productKey); + $I->fillField(['name' => 'notes'], $this->faker->text(80)); + $I->fillField(['name' => 'cost'], $this->faker->numberBetween(1, 20)); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('products', array('product_key' => $productKey)); + } + + public function updateProduct(FunctionalTester $I) + { + return; + + $I->wantTo('update a product'); + $I->amOnPage('/products/1/edit'); + + $productKey = $this->faker->text(10); + + $I->fillField(['name' => 'product_key'], $productKey); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('products', array('product_key' => $productKey)); + } + + /* + public function updateNotifications(FunctionalTester $I) + { + $I->wantTo('update notification settings'); + $I->amOnPage('/settings/notifications'); + + $terms = $this->faker->text(80); + + $I->fillField(['name' => 'invoice_terms'], $terms); + $I->fillField(['name' => 'invoice_footer'], $this->faker->text(60)); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('accounts', array('invoice_terms' => $terms)); + } + */ + + public function updateInvoiceDesign(FunctionalTester $I) + { + $I->wantTo('update invoice design'); + $I->amOnPage('/settings/invoice_design'); + + $color = $this->faker->hexcolor; + + $I->fillField(['name' => 'labels_item'], $this->faker->text(14)); + $I->fillField(['name' => 'primary_color'], $color); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('accounts', array('primary_color' => $color)); + } + + public function updateInvoiceSettings(FunctionalTester $I) + { + $I->wantTo('update invoice settings'); + $I->amOnPage('/settings/invoice_settings'); + + $label = $this->faker->text(10); + + $I->fillField(['name' => 'custom_client_label1'], $label); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('accounts', array('custom_client_label1' => $label)); + + //$I->amOnPage('/clients/create'); + //$I->see($label); + } + + public function updateEmailTemplates(FunctionalTester $I) + { + $I->wantTo('update email templates'); + $I->amOnPage('/settings/templates_and_reminders'); + + $string = $this->faker->text(100); + + $I->fillField(['name' => 'email_template_invoice'], $string); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('accounts', array('email_template_invoice' => $string)); + } + + public function runReport(FunctionalTester $I) + { + $I->wantTo('run the report'); + $I->amOnPage('/settings/charts_and_reports'); + + $I->click('Run'); + $I->seeResponseCodeIs(200); + } + + public function createUser(FunctionalTester $I) + { + $I->wantTo('create a user'); + $I->amOnPage('/users/create'); + + $email = $this->faker->safeEmail; + + $I->fillField(['name' => 'first_name'], $this->faker->firstName); + $I->fillField(['name' => 'last_name'], $this->faker->lastName); + $I->fillField(['name' => 'email'], $email); + $I->click('Send invitation'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('users', array('email' => $email)); + } + + public function createToken(FunctionalTester $I) + { + $I->wantTo('create a token'); + $I->amOnPage('/tokens/create'); + + $name = $this->faker->firstName; + + $I->fillField(['name' => 'name'], $name); + $I->click('Save'); + + $I->seeResponseCodeIs(200); + $I->seeRecord('account_tokens', array('name' => $name)); + } + + /* public function onlinePayments(FunctionalTester $I) { $gateway = $I->grabRecord('account_gateways', array('gateway_id' => 23)); @@ -69,125 +233,18 @@ class SettingsCest $I->see('Successfully created gateway'); $I->seeRecord('account_gateways', array('gateway_id' => 23)); } else { - $config = json_decode($gateway->config); + $config = $gateway->getConfig(); $apiKey = $config->apiKey; } - /* - $I->amOnPage('/gateways/1/edit'); - $I->click('Save'); + // $I->amOnPage('/gateways/1/edit'); + // $I->click('Save'); - $I->seeResponseCodeIs(200); - $I->see('Successfully updated gateway'); - $I->seeRecord('account_gateways', array('config' => '{"apiKey":"ASHHOWAH"}')); - */ + // $I->seeResponseCodeIs(200); + // $I->see('Successfully updated gateway'); + // $I->seeRecord('account_gateways', array('config' => '{"apiKey":"ASHHOWAH"}')); } + */ + - public function createProduct(FunctionalTester $I) - { - $I->wantTo('create a product'); - $I->amOnPage('/products/create'); - - $productKey = $this->faker->text(10); - - $I->fillField(['name' => 'product_key'], $productKey); - $I->fillField(['name' => 'notes'], $this->faker->text(80)); - $I->fillField(['name' => 'cost'], $this->faker->numberBetween(1,20)); - $I->click('Save'); - - $I->seeResponseCodeIs(200); - $I->see('Successfully created product'); - $I->seeRecord('products', array('product_key' => $productKey)); - } - - public function updateProduct(FunctionalTester $I) - { - return; - - $I->wantTo('update a product'); - $I->amOnPage('/products/1/edit'); - - $productKey = $this->faker->text(10); - - $I->fillField(['name' => 'product_key'], $productKey); - $I->click('Save'); - - $I->seeResponseCodeIs(200); - $I->see('Successfully updated product'); - $I->seeRecord('products', array('product_key' => $productKey)); - } - - public function updateNotifications(FunctionalTester $I) - { - $I->wantTo('update notification settings'); - $I->amOnPage('/company/notifications'); - - $terms = $this->faker->text(80); - - $I->fillField(['name' => 'invoice_terms'], $terms); - $I->fillField(['name' => 'invoice_footer'], $this->faker->text(60)); - $I->click('Save'); - - $I->seeResponseCodeIs(200); - $I->see('Successfully updated settings'); - $I->seeRecord('accounts', array('invoice_terms' => $terms)); - } - - public function updateInvoiceDesign(FunctionalTester $I) - { - $I->wantTo('update invoice design'); - $I->amOnPage('/company/advanced_settings/invoice_design'); - - $color = $this->faker->hexcolor; - - $I->fillField(['name' => 'labels_item'], $this->faker->text(14)); - $I->fillField(['name' => 'primary_color'], $color); - $I->click('Save'); - - $I->seeResponseCodeIs(200); - $I->see('Successfully updated settings'); - $I->seeRecord('accounts', array('primary_color' => $color)); - } - - public function updateInvoiceSettings(FunctionalTester $I) - { - $I->wantTo('update invoice settings'); - $I->amOnPage('/company/advanced_settings/invoice_settings'); - - $label = $this->faker->text(10); - - $I->fillField(['name' => 'custom_client_label1'], $label); - $I->click('Save'); - - $I->seeResponseCodeIs(200); - $I->see('Successfully updated settings'); - $I->seeRecord('accounts', array('custom_client_label1' => $label)); - - $I->amOnPage('/clients/create'); - $I->see($label); - } - - public function updateEmailTemplates(FunctionalTester $I) - { - $I->wantTo('update email templates'); - $I->amOnPage('/company/advanced_settings/email_templates'); - - $string = $this->faker->text(100); - - $I->fillField(['name' => 'email_template_payment'], $string); - $I->click('Save'); - - $I->seeResponseCodeIs(200); - $I->see('Successfully updated settings'); - $I->seeRecord('accounts', array('email_template_payment' => $string)); - } - - public function runReport(FunctionalTester $I) - { - $I->wantTo('run the report'); - $I->amOnPage('/company/advanced_settings/charts_and_reports'); - - $I->click('Run'); - $I->seeResponseCodeIs(200); - } }