diff --git a/.env.example b/.env.example index e8a53eaa4..1dc6d99bd 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 fe7a9dd0c..5a86589d7 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 500686664..27a6945d3 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 4de932ca4..266606679 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 eaa9f1e36..83e679591 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 018bc2192..5bc485011 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 9fe826cc9..000000000 --- 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 6acd87aa0..abf493d1c 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 000000000..3243ac016 --- /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 cf9a968d5..1e8ea1b49 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 000000000..637451fbb --- /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 9235cbf87..e281afb92 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 000000000..03ebdc09c --- /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 000000000..5c2d37001 --- /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 000000000..b87063c49 --- /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 000000000..385a0472a --- /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 000000000..7e4790da6 --- /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 000000000..2c680905b --- /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 000000000..bc20b312d --- /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 000000000..e26a5d3ab --- /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 000000000..8d17d961e --- /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 000000000..a4b2af4bd --- /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 000000000..ab462fe60 --- /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 000000000..1549b483b --- /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 000000000..b52a2d119 --- /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 000000000..1066d90de --- /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 000000000..da0031249 --- /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 000000000..bbf7e23c3 --- /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 000000000..7587c071a --- /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 cbe08d052..cfd943bcf 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 8d9f129e7..316b1b5c5 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 000000000..dc30f6a55 --- /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 000000000..5d75b4b24 --- /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 12b5384b3..87a0f8f20 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 4dced7347..b8bb693df 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 000000000..619d33e95 --- /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 000000000..e12647c86 --- /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 000000000..711bdbb67 --- /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 000000000..5e69fe9c7 --- /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 000000000..5ce1c6860 --- /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 000000000..3cd84b0e1 --- /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 000000000..285a61250 --- /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 000000000..d17ef9c13 --- /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 000000000..ce3685d7a --- /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 000000000..19b1ec12d --- /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 000000000..0f13a65b4 --- /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 000000000..f01b98226 --- /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 02c3a0195..ead79b390 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 000000000..99e8b2245 --- /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 000000000..b2d7e81c9 --- /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 000000000..553bece3c --- /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 000000000..88c24693e --- /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 000000000..eb90a68f4 --- /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 e0db4f0a8..ba656f6c0 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 000000000..3517be1af --- /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 cf647dcaa..d03b13fa9 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 ff4c84c64..86c76ecf2 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 9704b3ada..44d87429d 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 703bd5fdd..35f1f9b0a 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 a5897ac68..5ed231ced 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 000000000..3b0f190ef --- /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 000000000..4d7835560 --- /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 0cc63c7c5..1a2f6c8dc 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 065e3e073..84145afe9 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 505400718..e08c136b5 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 189b277c2..483b73a6c 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 000000000..c3dc77b23 --- /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 000000000..540e38d97 --- /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 1308a4378..571ac7319 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 000000000..b078fb6f9 --- /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 de30afdfa..740c91e36 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 4c13259ae..52b1811ca 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 b7e734ca1..a17453be8 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 1fb81bf78..2d4d45fa7 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 712f19dce..8d769c126 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 000000000..623ca1bf4 --- /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 d971115b2..e25f48668 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 000000000..834a89bea --- /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 83e5e8781..3e3cfa580 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 d84ce2fec..47b5c1a33 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 000000000..c59370647 --- /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 25114f8d3..1757cb585 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 000000000..a302944d2 --- /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 c9e65148a..fc822df5d 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 000000000..85d3c7903 --- /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 3b9d54684..604b94d57 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 85b4cede7..afad67b0d 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 000000000..80236226d --- /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 000000000..bbd69ed23 --- /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 2050070ab..000000000 --- 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 c7da91c94..000000000 --- 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 517dc905e..5632e7de4 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 68ddfc7df..bfda7bcb4 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 bc70cdf6a..e1cd17f5d 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 000000000..6fb7060e4 --- /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 000000000..f2dc44d31 --- /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 000000000..78f6eeee7 --- /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 000000000..4a11ea560 --- /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 000000000..52e9d313b --- /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 000000000..d85817931 --- /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 000000000..7186077fc --- /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 000000000..be925c032 --- /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 000000000..b73e019c4 --- /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 000000000..7b67ca892 --- /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 000000000..68fa2acaa --- /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 000000000..29ac70e85 --- /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 000000000..568166735 --- /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 63a678f35..4bd84629f 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 000000000..734a27be3 --- /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 a5eb3b9a4..113a78671 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 000000000..3bc86b843 --- /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 000000000..52c2e26f9 --- /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 000000000..bed71a47f --- /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 000000000..c8b0e7db5 --- /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 d072abd00..000000000 --- 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 119936e95..000000000 --- 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 47ee62a85..000000000 --- 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 3a49aa9b5..000000000 --- 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 26f7cc455..bf39d7e34 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 993e30141..425983349 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 000000000..08961e161 --- /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 000000000..9e40bbbe6 --- /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 000000000..aba304457 --- /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 000000000..5dfa0e45a --- /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 000000000..fab5a2c57 --- /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 000000000..b52c2fd5f --- /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 fbd10efcd..9af3350dd 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 16d72076c..a4027db6e 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 909cfbfe1..dd9a98800 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 0dd5ec54b..921c037d5 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 000000000..0ba99f73a --- /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 000000000..01ae612dc --- /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 554f74556..6655a9ef2 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 0856a6d43..a95f40bab 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 219251a44..8a87500e3 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 9a507bc6b..c46095e80 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 550de1d3c..b8e7d651a 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 000000000..ce1241b11 --- /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 000000000..b9518a91b --- /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 7941c81f4..2cc710295 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 aeebfaed6..39fedd489 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 396a1c713..b7b3c8ffc 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 d1e757936..084c2fe86 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 000000000..7f1144832 --- /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 ee382dece..a2e8b2591 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 de8cced5d..dbb788aef 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 4ccbf9688..17b667558 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 bb74c89f5..751cdb320 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 93b263a55..129ddb0ab 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 000000000..bc34fbd2b --- /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 000000000..5546b27d2 --- /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 000000000..8e17bfeec --- /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 000000000..b480d5e88 --- /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 000000000..e58bfe335 --- /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 000000000..7acd3d88f --- /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 000000000..464274e5a --- /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 000000000..d71be4bef --- /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 000000000..06c4af967 --- /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 000000000..1f69fdbac --- /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 000000000..8c1363edc --- /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 000000000..c083360aa --- /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 000000000..fb8200ec3 --- /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 000000000..6baf883c9 --- /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 000000000..850eeede5 --- /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 000000000..0efd44288 --- /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 000000000..3aa0b0b36 --- /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 000000000..efab1e6b6 --- /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 000000000..515eb8353 --- /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 000000000..e9054f1b8 --- /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 000000000..d6232d05b --- /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 000000000..dec1b62d1 --- /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 000000000..7e462ceef --- /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 000000000..f6697a7e9 --- /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 000000000..c52494cdc --- /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 000000000..1ec4a2876 --- /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 000000000..74705a597 --- /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 000000000..a3e3bc913 --- /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 000000000..04e783361 --- /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 000000000..8c1363edc --- /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 000000000..b97f08119 --- /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 000000000..f79523830 --- /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 000000000..5a4ff6ce2 --- /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 000000000..c04101456 --- /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 000000000..817de03d6 --- /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 000000000..f76ba9c48 --- /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 000000000..b10585aa7 --- /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 000000000..522fe8ff9 --- /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 000000000..f2fe2f43e --- /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 000000000..689bd1cf1 --- /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 000000000..f6fc3c44a --- /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 000000000..a8fc74962 --- /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 000000000..811a9f7ff --- /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 f68633e83..5e75a8112 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 9995a27c4..c30c9a10d 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 3f2b4290a..ce0c53834 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 000000000..dc9cacbb8 --- /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 000000000..bb6e7db06 --- /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 000000000..7e38205b1 --- /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 000000000..9cede24d0 --- /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 000000000..fb292a885 --- /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 000000000..a0a58663e --- /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 000000000..09b860a1a --- /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 000000000..b3da402be --- /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 000000000..b61f854f2 --- /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 593d11307..1fd6c7117 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 000000000..b51c8bbf2 --- /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 000000000..5ab3148e3 --- /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 000000000..bec95fb96 --- /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 ab0baacda..3e43d8f34 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 000000000..49b73e91a --- /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 700467620..1c33cb19e 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 000000000..3c65f1c25 --- /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 df73c6f6a..9c1a6dcc0 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 000000000..3f9c1fa4f --- /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 c8f69325c..a080dd89c 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 000000000..e631e9f16 --- /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 000000000..417b49f23 --- /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 000000000..c847f3386 --- /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 47761900e..47a052378 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 d5bd4dc24..1b9fa8df7 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 000000000..5237eb7a0 --- /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 000000000..01b7017fa --- /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 000000000..242b1b9d0 --- /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 000000000..c7fc5bb90 --- /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 000000000..bdbbbfe99 --- /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 000000000..e6ca7e8bf --- /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 000000000..eed474346 --- /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 000000000..428954567 --- /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 000000000..75e362030 --- /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 000000000..ea0dba263 --- /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 000000000..66d9fe137 --- /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 000000000..8108115b2 --- /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 000000000..a2750eaad --- /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 000000000..76bf43606 --- /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 000000000..2c92640e8 --- /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 000000000..908a8118a --- /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 000000000..8f4a375c7 --- /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 000000000..bc49a96c5 --- /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 000000000..dd3c6775d --- /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 000000000..0166883ab --- /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 000000000..c1714b27a --- /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 56a8e8bca..a094b01a6 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 8fc5a4f6b..af9bdefad 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 000000000..1ad5de55f --- /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 7341d3723..ae7f38c2f 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 6726837af..000000000 --- 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 52c38e961..2b1ebd484 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 000000000..fab25f415 --- /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 000000000..a1939995d --- /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 000000000..f88016991 --- /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 0783bdb49..d2fcde895 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 cac6cf81d..d970ea7cb 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 3047008dc..03a465413 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 000000000..31be6a9d0 --- /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 000000000..5889c7aec --- /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 000000000..2079d72f2 --- /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 000000000..0d4294af5 --- /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 000000000..6b88ab70f --- /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 ce2227269..8fc4a3b14 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 000000000..346fc3421 --- /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 44946f7b5..969154fae 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 b3ac449db..fe275fe1d 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 e3cf16d7e..72835f170 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 2c507d504..06b0e7774 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 f4e52f16e..56f969061 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 cac84815a..f21f6c8cf 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 fe4b73a84..b7e53f7df 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 652815a1f..7809cbfce 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 76e2e1b75..ab1c8cf09 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 000000000..62b621545 --- /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 000000000..9b258c53a --- /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 000000000..550b5fed4 --- /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 000000000..9b258c53a --- /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 000000000..c87a0e9cf --- /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 000000000..f5dc30869 --- /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 a0d2526a6..762726e95 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 07e478b03..76dabc82d 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 77f29aed4..f604076c1 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 3426820e1..99b065afa 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 72a595078..caea95dd1 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 60fd6faaa..377752a7d 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 7b9dadd21..1de6bd504 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 f79501589..2dc6f8e80 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 d8ee711c9..0f35bb3d6 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 000000000..5f5c3df90 --- /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 000000000..15a0aa4a1 --- /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 000000000..cfe158153 --- /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 000000000..c2b00faf1 --- /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 000000000..1338ebee5 --- /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 b42fc337f..f3fd34362 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 7e944a2f3..87af7aada 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 4f1396a1a..758ff76a5 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 826b18e7d..7ae468936 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 718af5a59..000000000 --- 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 13ef9e95d..149472fd5 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 000000000..949e9b73e --- /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 000000000..f2fa8dbbe --- /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 000000000..f0d2a05b7 --- /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 000000000..b35f59045 --- /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 000000000..d47eba057 --- /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 000000000..fab37fbf8 --- /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 000000000..66a51f817 --- /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 000000000..3cacc198b --- /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 91e973649..0236388c8 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 c869933fb..d59d59000 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 f60e7ac09..eba6f4453 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 0d01cbd3c..04ad660ae 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 000000000..e22a15464 --- /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 cc6a59e4b..d60487b7c 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 000000000..b18c37965 --- /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 a915157ff..60e8f3514 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 0b75bef85..668544132 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 4848a190e..9ab41f845 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 3839a0bb4..328ca9b92 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 ac7c621e2..71efd2fee 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 000000000..3248b39f4 --- /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 000000000..e69de29bb diff --git a/resources/views/vendor/swaggervel/index.blade.php b/resources/views/vendor/swaggervel/index.blade.php new file mode 100644 index 000000000..a9c0d1a67 --- /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 000000000..e0286c8d5 --- /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 000000000..1cff74275 --- /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 c96a04f00..000000000 --- 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 1001ff708..b877f5f4b 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 36bd4dafa..e367a5893 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 4a95e5af5..9023121af 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 d21275c4e..b260a6727 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 464fcb4d7..193f2f90f 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 ac8fe46eb..fd89ac0d0 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 173b7a82e..87e4f0234 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 064313401..edf786bc5 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 07c887147..24d43a9ec 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 19c0b8a1b..000000000 --- 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 000000000..6bac64886 --- /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 70eb2f8e7..e8b17e8dd 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 738c8e6fb..3c47d9ede 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 000000000..3ccc7c557 --- /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 ad7a4048b..5604b5083 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 dfe9f789e..36dc6799d 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 000000000..e56fda7e6 --- /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 cd8664505..2808c7c8d 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 000000000..1ee9883da --- /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 809044371..7c84b1000 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 a0c71f419..7de52ef9d 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); - } }