diff --git a/.env.example b/.env.example index e8a53eaa41c5..1dc6d99bd419 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,9 @@ APP_DEBUG=false APP_URL=http://ninja.dev APP_CIPHER=rijndael-128 APP_KEY=SomeRandomString -APP_TIMEZONE DB_TYPE=mysql +DB_STRICT=false DB_HOST=localhost DB_DATABASE=ninja DB_USERNAME @@ -19,3 +19,11 @@ MAIL_USERNAME MAIL_FROM_ADDRESS MAIL_FROM_NAME MAIL_PASSWORD + +PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address' +LOG=single +REQUIRE_HTTPS=false + +GOOGLE_CLIENT_ID +GOOGLE_CLIENT_SECRET +GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe7a9dd0cbcc..5a86589d7a21 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ /vendor /node_modules /.DS_Store -/Thumbs.db +Thumbs.db /.env /.env.development.php /.env.php @@ -29,4 +29,10 @@ /_ide_helper.php /.idea /.project -tests/_output/ \ No newline at end of file +tests/_output/ +tests/_bootstrap.php + +# composer stuff +/c3.php + +_ide_helper.php \ No newline at end of file diff --git a/.htaccess b/.htaccess index 500686664f68..27a6945d38f8 100644 --- a/.htaccess +++ b/.htaccess @@ -2,4 +2,7 @@ RewriteEngine On RewriteRule "^.env" - [F,L] RewriteRule "^storage" - [F,L] + + # https://coderwall.com/p/erbaig/laravel-s-htaccess-to-remove-public-from-url + # RewriteRule ^(.*)$ public/$1 [L] diff --git a/Gruntfile.js b/Gruntfile.js index 4de932ca45a6..26660667929c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,6 +2,39 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), + dump_dir: (function() { + var out = {}; + + grunt.file.expand({ filter: 'isDirectory'}, 'public/fonts/invoice-fonts/*').forEach(function(path) { + var fontName = /[^/]*$/.exec(path)[0], + files = {}, + license=''; + + // Add license text + grunt.file.expand({ filter: 'isFile'}, path+'/*.txt').forEach(function(path) { + var licenseText = grunt.file.read(path); + + // Fix anything that could escape from the comment + licenseText = licenseText.replace(/\*\//g,'*\\/'); + + license += "/*\n"+licenseText+"\n*/"; + }); + + // Create files list + files['public/js/vfs_fonts/'+fontName+'.js'] = [path+'/*.ttf']; + + out[fontName] = { + options: { + pre: license+'window.ninjaFontVfs=window.ninjaFontVfs||{};window.ninjaFontVfs.'+fontName+'=', + rootPath: path+'/' + }, + files: files + }; + }); + + // Return the computed object + return out; + }()), concat: { options: { process: function(src, filepath) { @@ -67,6 +100,7 @@ module.exports = function(grunt) { 'public/vendor/spectrum/spectrum.js', 'public/vendor/jspdf/dist/jspdf.min.js', 'public/vendor/moment/min/moment.min.js', + 'public/vendor/moment-timezone/builds/moment-timezone-with-data.min.js', //'public/vendor/moment-duration-format/lib/moment-duration-format.js', //'public/vendor/handsontable/dist/jquery.handsontable.full.min.js', //'public/vendor/pdfmake/build/pdfmake.min.js', @@ -119,25 +153,33 @@ module.exports = function(grunt) { src: [ 'public/vendor/bootstrap/dist/css/bootstrap.min.css', 'public/vendor/font-awesome/css/font-awesome.min.css', - /* - 'public/css/bootstrap.splash.css', - 'public/css/splash.css', - */ 'public/css/bootstrap-combobox.css', 'public/vendor/datatables/media/css/jquery.dataTables.css', 'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css', + 'public/css/public.style.css', ], dest: 'public/css/built.public.css', nonull: true, options: { process: false } + }, + js_pdf: { + src: [ + 'public/js/pdf_viewer.js', + 'public/js/compatibility.js', + 'public/js/pdfmake.min.js', + 'public/js/vfs_fonts.js', + ], + dest: 'public/js/pdf.built.js', + nonull: true } } }); grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-dump-dir'); - grunt.registerTask('default', ['concat']); + grunt.registerTask('default', ['dump_dir', 'concat']); }; diff --git a/LICENSE b/LICENSE index eaa9f1e3672c..83e6795919db 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Attribution Assurance License -Copyright (c) 2014 by Hillel Coren +Copyright (c) 2015 by Hillel Coren http://www.hillelcoren.com All Rights Reserved diff --git a/app/Commands/Command.php b/app/Commands/Command.php index 018bc2192435..5bc48501167e 100644 --- a/app/Commands/Command.php +++ b/app/Commands/Command.php @@ -1,7 +1,6 @@ -info(date('Y-m-d') . ' Running CheckData...'); - $today = new DateTime(); if (!$this->option('client_id')) { - // update client paid_to_date value - $clients = DB::table('clients') - ->join('payments', 'payments.client_id', '=', 'clients.id') - ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') - ->where('payments.is_deleted', '=', 0) - ->where('invoices.is_deleted', '=', 0) - ->groupBy('clients.id') - ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') - ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); - $this->info(count($clients) . ' clients with incorrect paid to date'); - - if ($this->option('fix') == 'true') { - foreach ($clients as $client) { - DB::table('clients') - ->where('id', $client->id) - ->update(['paid_to_date' => $client->amount]); + $this->checkPaidToDate(); + } + + $this->checkBalances(); + + $this->checkAccountData(); + + $this->info('Done'); + } + + private function checkAccountData() + { + $tables = [ + 'activities' => [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_CONTACT, + ENTITY_PAYMENT, + ENTITY_INVITATION, + ENTITY_USER + ], + 'invoices' => [ + ENTITY_CLIENT, + ENTITY_USER + ], + 'payments' => [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_USER, + ENTITY_INVITATION, + ENTITY_CONTACT + ], + 'tasks' => [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_USER + ], + 'credits' => [ + ENTITY_CLIENT, + ENTITY_USER + ], + ]; + + foreach ($tables as $table => $entityTypes) { + foreach ($entityTypes as $entityType) { + $records = DB::table($table) + ->join("{$entityType}s", "{$entityType}s.id", '=', "{$table}.{$entityType}_id"); + + if ($entityType != ENTITY_CLIENT) { + $records = $records->join('clients', 'clients.id', '=', "{$table}.client_id"); + } + + $records = $records->where("{$table}.account_id", '!=', DB::raw("{$entityType}s.account_id")) + ->get(["{$table}.id", "clients.account_id", "clients.user_id"]); + + if (count($records)) { + $this->info(count($records) . " {$table} records with incorrect {$entityType} account id"); + + if ($this->option('fix') == 'true') { + foreach ($records as $record) { + DB::table($table) + ->where('id', $record->id) + ->update([ + 'account_id' => $record->account_id, + 'user_id' => $record->user_id, + ]); + } + } } } } + } + private function checkPaidToDate() + { + // update client paid_to_date value + $clients = DB::table('clients') + ->join('payments', 'payments.client_id', '=', 'clients.id') + ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->where('payments.is_deleted', '=', 0) + ->where('invoices.is_deleted', '=', 0) + ->groupBy('clients.id') + ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') + ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); + $this->info(count($clients) . ' clients with incorrect paid to date'); + + if ($this->option('fix') == 'true') { + foreach ($clients as $client) { + DB::table('clients') + ->where('id', $client->id) + ->update(['paid_to_date' => $client->amount]); + } + } + } + + private function checkBalances() + { // find all clients where the balance doesn't equal the sum of the outstanding invoices $clients = DB::table('clients') ->join('invoices', 'invoices.client_id', '=', 'clients.id') @@ -98,7 +174,7 @@ class CheckData extends Command { $activities = DB::table('activities') ->where('client_id', '=', $client->id) ->orderBy('activities.id') - ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.message', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); + ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); //$this->info(var_dump($activities)); foreach ($activities as $activity) { @@ -111,7 +187,7 @@ class CheckData extends Command { ->first(['invoices.amount', 'invoices.is_recurring', 'invoices.is_quote', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']); // Check if this invoice was once set as recurring invoice - if (!$invoice->is_recurring && DB::table('invoices') + if ($invoice && !$invoice->is_recurring && DB::table('invoices') ->where('recurring_invoice_id', '=', $activity->invoice_id) ->first(['invoices.id'])) { $invoice->is_recurring = 1; @@ -197,7 +273,7 @@ class CheckData extends Command { $activityFix = 0; } } else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) { - // **Fix for delting payment after deleting invoice** + // **Fix for deleting payment after deleting invoice** if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) { $this->info("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); $foundProblem = true; @@ -235,7 +311,6 @@ class CheckData extends Command { 'updated_at' => new Carbon, 'account_id' => $client->account_id, 'client_id' => $client->id, - 'message' => 'Recovered update to invoice [details]', 'adjustment' => $client->actual_balance - $activity->balance, 'balance' => $client->actual_balance, ]); @@ -250,8 +325,6 @@ class CheckData extends Command { ->update($data); } } - - $this->info('Done'); } protected function getArguments() diff --git a/app/Console/Commands/CreateRandomData.php b/app/Console/Commands/CreateRandomData.php deleted file mode 100644 index 9fe826cc9b5b..000000000000 --- a/app/Console/Commands/CreateRandomData.php +++ /dev/null @@ -1,88 +0,0 @@ -info(date('Y-m-d') . ' Running CreateRandomData...'); - - $user = User::first(); - - if (!$user) { - $this->error("Error: please create user account by logging in"); - return; - } - - $productNames = ['Arkansas', 'New York', 'Arizona', 'California', 'Colorado', 'Alabama', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'Alaska', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; - $clientNames = ['IBM', 'Nestle', 'Mitsubishi UFJ Financial', 'Vodafone', 'Eni', 'Procter & Gamble', 'Johnson & Johnson', 'American International Group', 'Banco Santander', 'BHP Billiton', 'Pfizer', 'Itaú Unibanco Holding', 'Ford Motor', 'BMW Group', 'Commonwealth Bank', 'EDF', 'Statoil', 'Google', 'Siemens', 'Novartis', 'Royal Bank of Canada', 'Sumitomo Mitsui Financial', 'Comcast', 'Sberbank', 'Goldman Sachs Group', 'Westpac Banking Group', 'Nippon Telegraph & Tel', 'Ping An Insurance Group', 'Banco Bradesco', 'Anheuser-Busch InBev', 'Bank of Communications', 'China Life Insurance', 'General Motors', 'Telefónica', 'MetLife', 'Honda Motor', 'Enel', 'BASF', 'Softbank', 'National Australia Bank', 'ANZ', 'ConocoPhillips', 'TD Bank Group', 'Intel', 'UBS', 'Hewlett-Packard', 'Coca-Cola', 'Cisco Systems', 'UnitedHealth Group', 'Boeing', 'Zurich Insurance Group', 'Hyundai Motor', 'Sanofi', 'Credit Agricole', 'United Technologies', 'Roche Holding', 'Munich Re', 'PepsiCo', 'Oracle', 'Bank of Nova Scotia']; - - foreach ($productNames as $i => $value) { - $product = Product::createNew($user); - $product->id = $i+1; - $product->product_key = $value; - $product->save(); - } - - foreach ($clientNames as $i => $value) { - $client = Client::createNew($user); - $client->name = $value; - $client->save(); - - $contact = Contact::createNew($user); - $contact->email = "client@aol.com"; - $contact->is_primary = 1; - $client->contacts()->save($contact); - - $numInvoices = rand(1, 25); - if ($numInvoices == 4 || $numInvoices == 10 || $numInvoices == 25) { - // leave these - } else if ($numInvoices % 3 == 0) { - $numInvoices = 1; - } else if ($numInvoices > 10) { - $numInvoices = $numInvoices % 2; - } - - $paidUp = rand(0, 1) == 1; - - for ($j=1; $j<=$numInvoices; $j++) { - - $price = rand(10, 1000); - if ($price < 900) { - $price = rand(10, 150); - } - - $invoice = Invoice::createNew($user); - $invoice->invoice_number = $user->account->getNextInvoiceNumber(); - $invoice->amount = $invoice->balance = $price; - $invoice->created_at = date('Y-m-d', strtotime(date("Y-m-d") . ' - ' . rand(1, 100) . ' days')); - $client->invoices()->save($invoice); - - $productId = rand(0, 40); - if ($productId > 20) { - $productId = ($productId % 2) + rand(0, 2); - } - - $invoiceItem = InvoiceItem::createNew($user); - $invoiceItem->product_id = $productId+1; - $invoiceItem->product_key = $productNames[$invoiceItem->product_id]; - $invoiceItem->cost = $invoice->amount; - $invoiceItem->qty = 1; - $invoice->invoice_items()->save($invoiceItem); - - if ($paidUp || rand(0,2) > 1) { - $payment = Payment::createNew($user); - $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->amount; - $client->payments()->save($payment); - } - } - } - } -} \ No newline at end of file diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 6acd87aa0c6e..abf493d1ca54 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -33,16 +33,22 @@ class SendRecurringInvoices extends Command $today = new DateTime(); $invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user') - ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get(); + ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today)) + ->orderBy('id', 'asc') + ->get(); $this->info(count($invoices).' recurring invoice(s) found'); foreach ($invoices as $recurInvoice) { - $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); - + if (!$recurInvoice->user->confirmed) { + continue; + } + + $recurInvoice->account->loadLocalizationSettings($recurInvoice->client); + $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); - if ($invoice) { - $recurInvoice->account->loadLocalizationSettings(); + if ($invoice && !$invoice->isPaid()) { + $this->info('Sending Invoice'); $this->mailer->sendInvoice($invoice); } } diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php new file mode 100644 index 000000000000..3243ac016eaf --- /dev/null +++ b/app/Console/Commands/SendReminders.php @@ -0,0 +1,70 @@ +mailer = $mailer; + $this->invoiceRepo = $invoiceRepo; + $this->accountRepo = $accountRepo; + } + + public function fire() + { + $this->info(date('Y-m-d').' Running SendReminders...'); + $today = new DateTime(); + + $accounts = $this->accountRepo->findWithReminders(); + $this->info(count($accounts).' accounts found'); + + foreach ($accounts as $account) { + if (!$account->isPro()) { + continue; + } + + $invoices = $this->invoiceRepo->findNeedingReminding($account); + $this->info($account->name . ': ' . count($invoices).' invoices found'); + + foreach ($invoices as $invoice) { + if ($reminder = $account->getInvoiceReminder($invoice)) { + $this->info('Send to ' . $invoice->id); + $this->mailer->sendInvoice($invoice, $reminder); + } + } + } + + $this->info('Done'); + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + //array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null), + ); + } +} diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php index cf9a968d56ba..1e8ea1b49eb1 100644 --- a/app/Console/Commands/SendRenewalInvoices.php +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -28,14 +28,34 @@ class SendRenewalInvoices extends Command { $this->info(date('Y-m-d').' Running SendRenewalInvoices...'); $today = new DateTime(); + $sentTo = []; - $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355')->get(); + // get all accounts with pro plans expiring in 10 days + $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355') + ->orderBy('id') + ->get(); $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { + // don't send multiple invoices to multi-company users + if ($userAccountId = $this->accountRepo->getUserAccountId($account)) { + if (isset($sentTo[$userAccountId])) { + continue; + } else { + $sentTo[$userAccountId] = true; + } + } + $client = $this->accountRepo->getNinjaClient($account); $invitation = $this->accountRepo->createNinjaInvoice($client); - $this->mailer->sendInvoice($invitation->invoice); + + // set the due date to 10 days from now + $invoice = $invitation->invoice; + $invoice->due_date = date('Y-m-d', strtotime('+ 10 days')); + $invoice->save(); + + $this->mailer->sendInvoice($invoice); + $this->info("Sent invoice to {$client->getDisplayName()}"); } $this->info('Done'); diff --git a/app/Console/Commands/TestOFX.php b/app/Console/Commands/TestOFX.php new file mode 100644 index 000000000000..637451fbba68 --- /dev/null +++ b/app/Console/Commands/TestOFX.php @@ -0,0 +1,30 @@ +bankAccountService = $bankAccountService; + } + + public function fire() + { + $this->info(date('Y-m-d').' Running TestOFX...'); + + $bankId = env('TEST_BANK_ID'); + $username = env('TEST_BANK_USERNAME'); + $password = env('TEST_BANK_PASSWORD'); + + $data = $this->bankAccountService->loadBankAccounts($bankId, $username, $password, false); + + print "
".print_r($data, 1).""; + } +} \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 9235cbf87b58..e281afb926d4 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -1,33 +1,51 @@ -command('inspire') - // ->hourly(); - } + /** + * Define the application's command schedule. + * + * @param \Illuminate\Console\Scheduling\Schedule $schedule + * @return void + */ + protected function schedule(Schedule $schedule) + { + $logFile = storage_path() . '/logs/cron.log'; + $schedule + ->command('ninja:send-invoices --force') + ->sendOutputTo($logFile) + ->withoutOverlapping() + ->hourly(); + + $schedule + ->command('ninja:send-reminders --force') + ->sendOutputTo($logFile) + ->daily(); + + if (Utils::isNinja()) { + $schedule + ->command('ninja:send-renewals --force') + ->sendOutputTo($logFile) + ->daily(); + } + } } diff --git a/app/Events/ClientWasArchived.php b/app/Events/ClientWasArchived.php new file mode 100644 index 000000000000..03ebdc09cd18 --- /dev/null +++ b/app/Events/ClientWasArchived.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasCreated.php b/app/Events/ClientWasCreated.php new file mode 100644 index 000000000000..5c2d3700172b --- /dev/null +++ b/app/Events/ClientWasCreated.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasDeleted.php b/app/Events/ClientWasDeleted.php new file mode 100644 index 000000000000..b87063c4979d --- /dev/null +++ b/app/Events/ClientWasDeleted.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasRestored.php b/app/Events/ClientWasRestored.php new file mode 100644 index 000000000000..385a0472ab4c --- /dev/null +++ b/app/Events/ClientWasRestored.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/ClientWasUpdated.php b/app/Events/ClientWasUpdated.php new file mode 100644 index 000000000000..7e4790da6885 --- /dev/null +++ b/app/Events/ClientWasUpdated.php @@ -0,0 +1,21 @@ +client = $client; + } +} diff --git a/app/Events/CreditWasArchived.php b/app/Events/CreditWasArchived.php new file mode 100644 index 000000000000..2c680905b33a --- /dev/null +++ b/app/Events/CreditWasArchived.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/CreditWasCreated.php b/app/Events/CreditWasCreated.php new file mode 100644 index 000000000000..bc20b312dc5f --- /dev/null +++ b/app/Events/CreditWasCreated.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/CreditWasDeleted.php b/app/Events/CreditWasDeleted.php new file mode 100644 index 000000000000..e26a5d3ab053 --- /dev/null +++ b/app/Events/CreditWasDeleted.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/CreditWasRestored.php b/app/Events/CreditWasRestored.php new file mode 100644 index 000000000000..8d17d961e7ff --- /dev/null +++ b/app/Events/CreditWasRestored.php @@ -0,0 +1,23 @@ +credit = $credit; + } + +} diff --git a/app/Events/ExpenseWasArchived.php b/app/Events/ExpenseWasArchived.php new file mode 100644 index 000000000000..a4b2af4bdf31 --- /dev/null +++ b/app/Events/ExpenseWasArchived.php @@ -0,0 +1,22 @@ +expense = $expense; + } + +} diff --git a/app/Events/ExpenseWasCreated.php b/app/Events/ExpenseWasCreated.php new file mode 100644 index 000000000000..ab462fe60253 --- /dev/null +++ b/app/Events/ExpenseWasCreated.php @@ -0,0 +1,21 @@ +expense = $expense; + } +} diff --git a/app/Events/ExpenseWasDeleted.php b/app/Events/ExpenseWasDeleted.php new file mode 100644 index 000000000000..1549b483b497 --- /dev/null +++ b/app/Events/ExpenseWasDeleted.php @@ -0,0 +1,23 @@ +expense = $expense; + } + +} diff --git a/app/Events/ExpenseWasRestored.php b/app/Events/ExpenseWasRestored.php new file mode 100644 index 000000000000..b52a2d119a2d --- /dev/null +++ b/app/Events/ExpenseWasRestored.php @@ -0,0 +1,23 @@ +expense = $expense; + } + +} diff --git a/app/Events/ExpenseWasUpdated.php b/app/Events/ExpenseWasUpdated.php new file mode 100644 index 000000000000..1066d90de4f7 --- /dev/null +++ b/app/Events/ExpenseWasUpdated.php @@ -0,0 +1,21 @@ +expense = $expense; + } +} diff --git a/app/Events/InvoiceInvitationWasEmailed.php b/app/Events/InvoiceInvitationWasEmailed.php new file mode 100644 index 000000000000..da0031249217 --- /dev/null +++ b/app/Events/InvoiceInvitationWasEmailed.php @@ -0,0 +1,23 @@ +invitation = $invitation; + } + +} diff --git a/app/Events/InvoiceInvitationWasViewed.php b/app/Events/InvoiceInvitationWasViewed.php new file mode 100644 index 000000000000..bbf7e23c3353 --- /dev/null +++ b/app/Events/InvoiceInvitationWasViewed.php @@ -0,0 +1,25 @@ +invoice = $invoice; + $this->invitation = $invitation; + } + +} diff --git a/app/Events/InvoiceWasArchived.php b/app/Events/InvoiceWasArchived.php new file mode 100644 index 000000000000..7587c071a66e --- /dev/null +++ b/app/Events/InvoiceWasArchived.php @@ -0,0 +1,22 @@ +invoice = $invoice; + } + +} diff --git a/app/Events/InvoiceSent.php b/app/Events/InvoiceWasCreated.php similarity index 88% rename from app/Events/InvoiceSent.php rename to app/Events/InvoiceWasCreated.php index cbe08d0528f3..cfd943bcffbf 100644 --- a/app/Events/InvoiceSent.php +++ b/app/Events/InvoiceWasCreated.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class InvoiceSent extends Event { +class InvoiceWasCreated extends Event { use SerializesModels; - public $invoice; /** diff --git a/app/Events/InvoiceViewed.php b/app/Events/InvoiceWasDeleted.php similarity index 61% rename from app/Events/InvoiceViewed.php rename to app/Events/InvoiceWasDeleted.php index 8d9f129e764a..316b1b5c5001 100644 --- a/app/Events/InvoiceViewed.php +++ b/app/Events/InvoiceWasDeleted.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class InvoiceViewed extends Event { +class InvoiceWasDeleted extends Event { use SerializesModels; - public $invoice; /** @@ -15,9 +14,9 @@ class InvoiceViewed extends Event { * * @return void */ - public function __construct($invoice) - { - $this->invoice = $invoice; - } + public function __construct($invoice) + { + $this->invoice = $invoice; + } } diff --git a/app/Events/InvoiceWasEmailed.php b/app/Events/InvoiceWasEmailed.php new file mode 100644 index 000000000000..dc30f6a55869 --- /dev/null +++ b/app/Events/InvoiceWasEmailed.php @@ -0,0 +1,22 @@ +invoice = $invoice; + } + +} diff --git a/app/Events/InvoiceWasRestored.php b/app/Events/InvoiceWasRestored.php new file mode 100644 index 000000000000..5d75b4b246b4 --- /dev/null +++ b/app/Events/InvoiceWasRestored.php @@ -0,0 +1,25 @@ +invoice = $invoice; + $this->fromDeleted = $fromDeleted; + } + +} diff --git a/app/Events/QuoteApproved.php b/app/Events/InvoiceWasUpdated.php similarity index 87% rename from app/Events/QuoteApproved.php rename to app/Events/InvoiceWasUpdated.php index 12b5384b35e3..87a0f8f20136 100644 --- a/app/Events/QuoteApproved.php +++ b/app/Events/InvoiceWasUpdated.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class QuoteApproved extends Event { +class InvoiceWasUpdated extends Event { use SerializesModels; - public $invoice; /** diff --git a/app/Events/InvoicePaid.php b/app/Events/PaymentWasArchived.php similarity index 87% rename from app/Events/InvoicePaid.php rename to app/Events/PaymentWasArchived.php index 4dced73471a0..b8bb693dfc78 100644 --- a/app/Events/InvoicePaid.php +++ b/app/Events/PaymentWasArchived.php @@ -4,10 +4,9 @@ use App\Events\Event; use Illuminate\Queue\SerializesModels; -class InvoicePaid extends Event { +class PaymentWasArchived extends Event { use SerializesModels; - public $payment; /** diff --git a/app/Events/PaymentWasCreated.php b/app/Events/PaymentWasCreated.php new file mode 100644 index 000000000000..619d33e95890 --- /dev/null +++ b/app/Events/PaymentWasCreated.php @@ -0,0 +1,22 @@ +payment = $payment; + } + +} diff --git a/app/Events/PaymentWasDeleted.php b/app/Events/PaymentWasDeleted.php new file mode 100644 index 000000000000..e12647c86011 --- /dev/null +++ b/app/Events/PaymentWasDeleted.php @@ -0,0 +1,22 @@ +payment = $payment; + } + +} diff --git a/app/Events/PaymentWasRestored.php b/app/Events/PaymentWasRestored.php new file mode 100644 index 000000000000..711bdbb67fa5 --- /dev/null +++ b/app/Events/PaymentWasRestored.php @@ -0,0 +1,25 @@ +payment = $payment; + $this->fromDeleted = $fromDeleted; + } + +} diff --git a/app/Events/QuoteInvitationWasApproved.php b/app/Events/QuoteInvitationWasApproved.php new file mode 100644 index 000000000000..5e69fe9c7895 --- /dev/null +++ b/app/Events/QuoteInvitationWasApproved.php @@ -0,0 +1,27 @@ +quote = $quote; + $this->invoice = $invoice; + $this->invitation = $invitation; + } + +} diff --git a/app/Events/QuoteInvitationWasEmailed.php b/app/Events/QuoteInvitationWasEmailed.php new file mode 100644 index 000000000000..5ce1c68602fb --- /dev/null +++ b/app/Events/QuoteInvitationWasEmailed.php @@ -0,0 +1,23 @@ +invitation = $invitation; + } + +} diff --git a/app/Events/QuoteInvitationWasViewed.php b/app/Events/QuoteInvitationWasViewed.php new file mode 100644 index 000000000000..3cd84b0e1189 --- /dev/null +++ b/app/Events/QuoteInvitationWasViewed.php @@ -0,0 +1,25 @@ +quote = $quote; + $this->invitation = $invitation; + } + +} diff --git a/app/Events/QuoteWasArchived.php b/app/Events/QuoteWasArchived.php new file mode 100644 index 000000000000..285a61250c04 --- /dev/null +++ b/app/Events/QuoteWasArchived.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasCreated.php b/app/Events/QuoteWasCreated.php new file mode 100644 index 000000000000..d17ef9c1318c --- /dev/null +++ b/app/Events/QuoteWasCreated.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasDeleted.php b/app/Events/QuoteWasDeleted.php new file mode 100644 index 000000000000..ce3685d7a212 --- /dev/null +++ b/app/Events/QuoteWasDeleted.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasEmailed.php b/app/Events/QuoteWasEmailed.php new file mode 100644 index 000000000000..19b1ec12d6a5 --- /dev/null +++ b/app/Events/QuoteWasEmailed.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasRestored.php b/app/Events/QuoteWasRestored.php new file mode 100644 index 000000000000..0f13a65b437e --- /dev/null +++ b/app/Events/QuoteWasRestored.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/QuoteWasUpdated.php b/app/Events/QuoteWasUpdated.php new file mode 100644 index 000000000000..f01b9822601f --- /dev/null +++ b/app/Events/QuoteWasUpdated.php @@ -0,0 +1,22 @@ +quote = $quote; + } + +} diff --git a/app/Events/UserSettingsChanged.php b/app/Events/UserSettingsChanged.php index 02c3a0195875..ead79b390898 100644 --- a/app/Events/UserSettingsChanged.php +++ b/app/Events/UserSettingsChanged.php @@ -8,14 +8,16 @@ class UserSettingsChanged extends Event { use SerializesModels; + public $user; + /** * Create a new event instance. * * @return void */ - public function __construct() + public function __construct($user = false) { - // + $this->user = $user; } } diff --git a/app/Events/UserSignedUp.php b/app/Events/UserSignedUp.php new file mode 100644 index 000000000000..99e8b22456c2 --- /dev/null +++ b/app/Events/UserSignedUp.php @@ -0,0 +1,21 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasCreated.php b/app/Events/VendorWasCreated.php new file mode 100644 index 000000000000..b2d7e81c9394 --- /dev/null +++ b/app/Events/VendorWasCreated.php @@ -0,0 +1,22 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasDeleted.php b/app/Events/VendorWasDeleted.php new file mode 100644 index 000000000000..553bece3ccdc --- /dev/null +++ b/app/Events/VendorWasDeleted.php @@ -0,0 +1,22 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasRestored.php b/app/Events/VendorWasRestored.php new file mode 100644 index 000000000000..88c24693e611 --- /dev/null +++ b/app/Events/VendorWasRestored.php @@ -0,0 +1,22 @@ +vendor = $vendor; + } +} diff --git a/app/Events/VendorWasUpdated.php b/app/Events/VendorWasUpdated.php new file mode 100644 index 000000000000..eb90a68f46c0 --- /dev/null +++ b/app/Events/VendorWasUpdated.php @@ -0,0 +1,21 @@ +vendor = $vendor; + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index e0db4f0a89f5..ba656f6c0131 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -27,11 +27,13 @@ class Handler extends ExceptionHandler { */ public function report(Exception $e) { - Utils::logError(Utils::getErrorString($e)); - return false; - - //return parent::report($e); - } + if (Utils::isNinja()) { + Utils::logError(Utils::getErrorString($e)); + return false; + } else { + return parent::report($e); + } + } /** * Render an exception into an HTTP response. @@ -41,13 +43,21 @@ class Handler extends ExceptionHandler { * @return \Illuminate\Http\Response */ public function render($request, Exception $e) - { - + { if ($e instanceof ModelNotFoundException) { return Redirect::to('/'); + } elseif ($e instanceof \Illuminate\Session\TokenMismatchException) { + // https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e + return redirect() + ->back() + ->withInput($request->except('password', '_token')) + ->with([ + 'warning' => trans('texts.token_expired') + ]); } - if (Utils::isNinjaProd()) { + // In production, except for maintenance mode, we'll show a custom error screen + if (Utils::isNinjaProd() && !Utils::isDownForMaintenance()) { $data = [ 'error' => get_class($e), 'hideHeader' => true, diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php new file mode 100644 index 000000000000..3517be1af9f3 --- /dev/null +++ b/app/Http/Controllers/AccountApiController.php @@ -0,0 +1,104 @@ +accountRepo = $accountRepo; + } + + public function login(Request $request) + { + if ( ! env(API_SECRET) || $request->api_secret !== env(API_SECRET)) { + sleep(ERROR_DELAY); + return 'Invalid secret'; + } + + if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) { + return $this->processLogin($request); + } else { + sleep(ERROR_DELAY); + return 'Invalid credentials'; + } + } + + private function processLogin(Request $request) + { + // Create a new token only if one does not already exist + $user = Auth::user(); + $this->accountRepo->createTokens($user, $request->token_name); + + $users = $this->accountRepo->findUsers($user, 'account.account_tokens'); + $transformer = new UserAccountTransformer($user->account, $request->serializer, $request->token_name); + $data = $this->createCollection($users, $transformer, 'user_account'); + + return $this->response($data); + } + + public function show(Request $request) + { + $account = Auth::user()->account; + $updatedAt = $request->updated_at ? date('Y-m-d H:i:s', $request->updated_at) : false; + + $map = [ + 'users' => [], + 'clients' => ['contacts'], + 'invoices' => ['invoice_items', 'user', 'client', 'payments'], + 'products' => [], + 'tax_rates' => [], + ]; + + foreach ($map as $key => $values) { + $account->load([$key => function($query) use ($values, $updatedAt) { + $query->withTrashed()->with($values); + if ($updatedAt) { + $query->where('updated_at', '>=', $updatedAt); + } + }]); + } + + $transformer = new AccountTransformer(null, $request->serializer); + $account = $this->createItem($account, $transformer, 'account'); + + return $this->response($account); + } + + public function getStaticData() + { + $data = []; + + $cachedTables = unserialize(CACHED_TABLES); + foreach ($cachedTables as $name => $class) { + $data[$name] = Cache::get($name); + } + + return $this->response($data); + } + + public function getUserAccounts(Request $request) + { + return $this->processLogin($request); + } +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index cf647dcaa8c6..d03b13fa9e72 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,7 +1,6 @@ accountRepo = $accountRepo; $this->userMailer = $userMailer; $this->contactMailer = $contactMailer; + $this->referralRepository = $referralRepository; } public function demo() @@ -85,7 +75,7 @@ class AccountController extends BaseController if (!Utils::isNinja() && (Account::count() > 0 && !$prevUserId)) { return Redirect::to('/login'); } - + if ($guestKey && !$prevUserId) { $user = User::where('password', '=', $guestKey)->first(); @@ -107,9 +97,10 @@ class AccountController extends BaseController } Auth::login($user, true); - Event::fire(new UserLoggedIn()); - - $redirectTo = Input::get('redirect_to', 'invoices/create'); + event(new UserLoggedIn()); + + $redirectTo = Input::get('redirect_to') ?: 'invoices/create'; + return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up')); } @@ -117,13 +108,6 @@ class AccountController extends BaseController { $invitation = $this->accountRepo->enableProPlan(); - /* - if ($invoice) - { - $this->contactMailer->sendInvoice($invoice); - } - */ - return $invitation->invitation_key; } @@ -132,9 +116,9 @@ class AccountController extends BaseController Session::put("show_trash:{$entityType}", $visible == 'true'); if ($entityType == 'user') { - return Redirect::to('company/'.ACCOUNT_ADVANCED_SETTINGS.'/'.ACCOUNT_USER_MANAGEMENT); + return Redirect::to('settings/'.ACCOUNT_USER_MANAGEMENT); } elseif ($entityType == 'token') { - return Redirect::to('company/'.ACCOUNT_ADVANCED_SETTINGS.'/'.ACCOUNT_TOKEN_MANAGEMENT); + return Redirect::to('settings/'.ACCOUNT_API_TOKENS); } else { return Redirect::to("{$entityType}s"); } @@ -143,170 +127,417 @@ class AccountController extends BaseController public function getSearchData() { $data = $this->accountRepo->getSearchData(); + return Response::json($data); } - public function showSection($section = ACCOUNT_DETAILS, $subSection = false) + public function showSection($section = false) { - if ($section == ACCOUNT_DETAILS) { - $primaryUser = Auth::user()->account->users()->orderBy('id')->first(); - $data = [ - 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), - 'countries' => Cache::get('countries'), - 'sizes' => Cache::get('sizes'), - 'industries' => Cache::get('industries'), - 'timezones' => Cache::get('timezones'), - 'dateFormats' => Cache::get('dateFormats'), - 'datetimeFormats' => Cache::get('datetimeFormats'), - 'currencies' => Cache::get('currencies'), - 'languages' => Cache::get('languages'), - 'showUser' => Auth::user()->id === $primaryUser->id, - 'title' => trans('texts.company_details'), - 'primaryUser' => $primaryUser, - ]; + if (!$section) { + return Redirect::to('/settings/'.ACCOUNT_COMPANY_DETAILS, 301); + } - return View::make('accounts.details', $data); + if ($section == ACCOUNT_COMPANY_DETAILS) { + return self::showCompanyDetails(); + } elseif ($section == ACCOUNT_USER_DETAILS) { + return self::showUserDetails(); + } elseif ($section == ACCOUNT_LOCALIZATION) { + return self::showLocalization(); } elseif ($section == ACCOUNT_PAYMENTS) { - - $account = Auth::user()->account; - $account->load('account_gateways'); - $count = count($account->account_gateways); - - if ($count == 0) { - return Redirect::to('gateways/create'); - } else { - return View::make('accounts.payments', [ - 'showAdd' => $count < 3, - 'title' => trans('texts.online_payments') - ]); - } - } elseif ($section == ACCOUNT_NOTIFICATIONS) { - $data = [ - 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), - 'title' => trans('texts.notifications'), - ]; - - return View::make('accounts.notifications', $data); + return self::showOnlinePayments(); + } elseif ($section == ACCOUNT_BANKS) { + return self::showBankAccounts(); + } elseif ($section == ACCOUNT_INVOICE_SETTINGS) { + return self::showInvoiceSettings(); } elseif ($section == ACCOUNT_IMPORT_EXPORT) { return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); - } elseif ($section == ACCOUNT_ADVANCED_SETTINGS) { - $account = Auth::user()->account->load('country'); + } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { + return self::showInvoiceDesign($section); + } elseif ($section == ACCOUNT_CLIENT_PORTAL) { + return self::showClientViewStyling(); + } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { + return self::showTemplates(); + } elseif ($section === ACCOUNT_PRODUCTS) { + return self::showProducts(); + } elseif ($section === ACCOUNT_TAX_RATES) { + return self::showTaxRates(); + } elseif ($section === ACCOUNT_PAYMENT_TERMS) { + return self::showPaymentTerms(); + } elseif ($section === ACCOUNT_SYSTEM_SETTINGS) { + return self::showSystemSettings(); + } else { $data = [ - 'account' => $account, - 'feature' => $subSection, - 'title' => trans('texts.invoice_settings'), + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans("texts.{$section}"), + 'section' => $section, ]; - if ($subSection == ACCOUNT_INVOICE_DESIGN - || $subSection == ACCOUNT_CUSTOMIZE_DESIGN) { - $invoice = new stdClass(); - $client = new stdClass(); - $contact = new stdClass(); - $invoiceItem = new stdClass(); - - $client->name = 'Sample Client'; - $client->address1 = ''; - $client->city = ''; - $client->state = ''; - $client->postal_code = ''; - $client->work_phone = ''; - $client->work_email = ''; - - $invoice->invoice_number = $account->getNextInvoiceNumber(); - $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); - $invoice->account = json_decode($account->toJson()); - $invoice->amount = $invoice->balance = 100; - - $invoice->terms = trim($account->invoice_terms); - $invoice->invoice_footer = trim($account->invoice_footer); - - $contact->email = 'contact@gmail.com'; - $client->contacts = [$contact]; - - $invoiceItem->cost = 100; - $invoiceItem->qty = 1; - $invoiceItem->notes = 'Notes'; - $invoiceItem->product_key = 'Item'; - - $invoice->client = $client; - $invoice->invoice_items = [$invoiceItem]; - - $data['account'] = $account; - $data['invoice'] = $invoice; - $data['invoiceLabels'] = json_decode($account->invoice_labels) ?: []; - $data['title'] = trans('texts.invoice_design'); - $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); - - $design = false; - foreach ($data['invoiceDesigns'] as $item) { - if ($item->id == $account->invoice_design_id) { - $design = $item->javascript; - break; - } - } - - if ($subSection == ACCOUNT_CUSTOMIZE_DESIGN) { - $data['customDesign'] = ($account->custom_design && !$design) ? $account->custom_design : $design; - } - } else if ($subSection == ACCOUNT_EMAIL_TEMPLATES) { - $data['invoiceEmail'] = $account->getEmailTemplate(ENTITY_INVOICE); - $data['quoteEmail'] = $account->getEmailTemplate(ENTITY_QUOTE); - $data['paymentEmail'] = $account->getEmailTemplate(ENTITY_PAYMENT); - $data['emailFooter'] = $account->getEmailFooter(); - $data['title'] = trans('texts.email_templates'); - } else if ($subSection == ACCOUNT_USER_MANAGEMENT) { - $data['title'] = trans('texts.users_and_tokens'); - } - - return View::make("accounts.{$subSection}", $data); - } elseif ($section == ACCOUNT_PRODUCTS) { - $data = [ - 'account' => Auth::user()->account, - 'title' => trans('texts.product_library'), - ]; - - return View::make('accounts.products', $data); + return View::make("accounts.{$section}", $data); } } - public function doSection($section = ACCOUNT_DETAILS, $subSection = false) + private function showSystemSettings() { - if ($section == ACCOUNT_DETAILS) { - return AccountController::saveDetails(); - } elseif ($section == ACCOUNT_IMPORT_EXPORT) { - return AccountController::importFile(); - } elseif ($section == ACCOUNT_MAP) { - return AccountController::mapFile(); - } elseif ($section == ACCOUNT_NOTIFICATIONS) { - return AccountController::saveNotifications(); - } elseif ($section == ACCOUNT_EXPORT) { - return AccountController::export(); - } elseif ($section == ACCOUNT_ADVANCED_SETTINGS) { - if ($subSection == ACCOUNT_INVOICE_SETTINGS) { - return AccountController::saveInvoiceSettings(); - } elseif ($subSection == ACCOUNT_INVOICE_DESIGN) { - return AccountController::saveInvoiceDesign(); - } elseif ($subSection == ACCOUNT_CUSTOMIZE_DESIGN) { - return AccountController::saveCustomizeDesign(); - } elseif ($subSection == ACCOUNT_EMAIL_TEMPLATES) { - return AccountController::saveEmailTemplates(); + if (Utils::isNinjaProd()) { + return Redirect::to('/'); + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans("texts.system_settings"), + 'section' => ACCOUNT_SYSTEM_SETTINGS, + ]; + + return View::make("accounts.system_settings", $data); + } + + private function showInvoiceSettings() + { + $account = Auth::user()->account; + $recurringHours = []; + + for ($i = 0; $i<24; $i++) { + if ($account->military_time) { + $format = 'H:i'; + } else { + $format = 'g:i a'; } - } elseif ($section == ACCOUNT_PRODUCTS) { - return AccountController::saveProducts(); + $recurringHours[$i] = date($format, strtotime("{$i}:00")); + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans("texts.invoice_settings"), + 'section' => ACCOUNT_INVOICE_SETTINGS, + 'recurringHours' => $recurringHours, + ]; + + return View::make("accounts.invoice_settings", $data); + } + + private function showCompanyDetails() + { + // check that logo is less than the max file size + $account = Auth::user()->account; + if ($account->isLogoTooLarge()) { + Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize().'KB'])); + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'countries' => Cache::get('countries'), + 'sizes' => Cache::get('sizes'), + 'industries' => Cache::get('industries'), + 'title' => trans('texts.company_details'), + ]; + + return View::make('accounts.details', $data); + } + + private function showUserDetails() + { + $oauthLoginUrls = []; + foreach (AuthService::$providers as $provider) { + $oauthLoginUrls[] = ['label' => $provider, 'url' => '/auth/'.strtolower($provider)]; + } + + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'title' => trans('texts.user_details'), + 'user' => Auth::user(), + 'oauthProviderName' => AuthService::getProviderName(Auth::user()->oauth_provider_id), + 'oauthLoginUrls' => $oauthLoginUrls, + 'referralCounts' => $this->referralRepository->getCounts(Auth::user()->id), + ]; + + return View::make('accounts.user_details', $data); + } + + private function showLocalization() + { + $data = [ + 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), + 'timezones' => Cache::get('timezones'), + 'dateFormats' => Cache::get('dateFormats'), + 'datetimeFormats' => Cache::get('datetimeFormats'), + 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), + 'title' => trans('texts.localization'), + ]; + + return View::make('accounts.localization', $data); + } + + private function showBankAccounts() + { + $account = Auth::user()->account; + $account->load('bank_accounts'); + $count = count($account->bank_accounts); + + if ($count == 0) { + return Redirect::to('bank_accounts/create'); + } else { + return View::make('accounts.banks', [ + 'title' => trans('texts.bank_accounts') + ]); } } - private function saveCustomizeDesign() { + private function showOnlinePayments() + { + $account = Auth::user()->account; + $account->load('account_gateways'); + $count = count($account->account_gateways); + + if ($accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) { + if (! $accountGateway->getPublishableStripeKey()) { + Session::flash('warning', trans('texts.missing_publishable_key')); + } + } + + if ($count == 0) { + return Redirect::to('gateways/create'); + } else { + return View::make('accounts.payments', [ + 'showAdd' => $count < count(Gateway::$paymentTypes), + 'title' => trans('texts.online_payments') + ]); + } + } + + private function showProducts() + { + $columns = ['product', 'description', 'unit_cost']; + if (Auth::user()->account->invoice_item_taxes) { + $columns[] = 'tax_rate'; + } + $columns[] = 'action'; + + $data = [ + 'account' => Auth::user()->account, + 'title' => trans('texts.product_library'), + 'columns' => Utils::trans($columns), + ]; + + return View::make('accounts.products', $data); + } + + private function showTaxRates() + { + $data = [ + 'account' => Auth::user()->account, + 'title' => trans('texts.tax_rates'), + 'taxRates' => TaxRate::scope()->get(['id', 'name', 'rate']), + ]; + + return View::make('accounts.tax_rates', $data); + } + + private function showPaymentTerms() + { + $data = [ + 'account' => Auth::user()->account, + 'title' => trans('texts.payment_terms'), + 'taxRates' => PaymentTerm::scope()->get(['id', 'name', 'num_days']), + ]; + + return View::make('accounts.payment_terms', $data); + } + + private function showInvoiceDesign($section) + { + $account = Auth::user()->account->load('country'); + $invoice = new stdClass(); + $client = new stdClass(); + $contact = new stdClass(); + $invoiceItem = new stdClass(); + + $client->name = 'Sample Client'; + $client->address1 = trans('texts.address1'); + $client->city = trans('texts.city'); + $client->state = trans('texts.state'); + $client->postal_code = trans('texts.postal_code'); + $client->work_phone = trans('texts.work_phone'); + $client->work_email = trans('texts.work_id'); + + $invoice->invoice_number = '0000'; + $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); + $invoice->account = json_decode($account->toJson()); + $invoice->amount = $invoice->balance = 100; + + $invoice->terms = trim($account->invoice_terms); + $invoice->invoice_footer = trim($account->invoice_footer); + + $contact->email = 'contact@gmail.com'; + $client->contacts = [$contact]; + + $invoiceItem->cost = 100; + $invoiceItem->qty = 1; + $invoiceItem->notes = 'Notes'; + $invoiceItem->product_key = 'Item'; + + $invoice->client = $client; + $invoice->invoice_items = [$invoiceItem]; + + $data['account'] = $account; + $data['invoice'] = $invoice; + $data['invoiceLabels'] = json_decode($account->invoice_labels) ?: []; + $data['title'] = trans('texts.invoice_design'); + $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); + $data['invoiceFonts'] = Cache::get('fonts'); + $data['section'] = $section; + + $design = false; + foreach ($data['invoiceDesigns'] as $item) { + if ($item->id == $account->invoice_design_id) { + $design = $item->javascript; + break; + } + } + + if ($section == ACCOUNT_CUSTOMIZE_DESIGN) { + $data['customDesign'] = ($account->custom_design && !$design) ? $account->custom_design : $design; + } + + return View::make("accounts.{$section}", $data); + } + + private function showClientViewStyling() + { + $account = Auth::user()->account->load('country'); + $css = $account->client_view_css ? $account->client_view_css : ''; + + if (Utils::isNinja() && $css) { + // Unescape the CSS for display purposes + $css = str_replace( + array('\3C ', '\3E ', '\26 '), + array('<', '>', '&'), + $css + ); + } + + $data = [ + 'client_view_css' => $css, + 'title' => trans("texts.client_portal"), + 'section' => ACCOUNT_CLIENT_PORTAL, + ]; + + return View::make("accounts.client_portal", $data); + } + + private function showTemplates() + { + $account = Auth::user()->account->load('country'); + $data['account'] = $account; + $data['templates'] = []; + $data['defaultTemplates'] = []; + foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { + $data['templates'][$type] = [ + 'subject' => $account->getEmailSubject($type), + 'template' => $account->getEmailTemplate($type), + ]; + $data['defaultTemplates'][$type] = [ + 'subject' => $account->getDefaultEmailSubject($type), + 'template' => $account->getDefaultEmailTemplate($type), + ]; + } + $data['emailFooter'] = $account->getEmailFooter(); + $data['title'] = trans('texts.email_templates'); + + return View::make('accounts.templates_and_reminders', $data); + } + + public function doSection($section = ACCOUNT_COMPANY_DETAILS) + { + if ($section === ACCOUNT_COMPANY_DETAILS) { + return AccountController::saveDetails(); + } elseif ($section === ACCOUNT_USER_DETAILS) { + return AccountController::saveUserDetails(); + } elseif ($section === ACCOUNT_LOCALIZATION) { + return AccountController::saveLocalization(); + } elseif ($section === ACCOUNT_NOTIFICATIONS) { + return AccountController::saveNotifications(); + } elseif ($section === ACCOUNT_EXPORT) { + return AccountController::export(); + } elseif ($section === ACCOUNT_INVOICE_SETTINGS) { + return AccountController::saveInvoiceSettings(); + } elseif ($section === ACCOUNT_EMAIL_SETTINGS) { + return AccountController::saveEmailSettings(); + } elseif ($section === ACCOUNT_INVOICE_DESIGN) { + return AccountController::saveInvoiceDesign(); + } elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) { + return AccountController::saveCustomizeDesign(); + } elseif ($section === ACCOUNT_CLIENT_PORTAL) { + return AccountController::saveClientPortal(); + } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { + return AccountController::saveEmailTemplates(); + } elseif ($section === ACCOUNT_PRODUCTS) { + return AccountController::saveProducts(); + } elseif ($section === ACCOUNT_TAX_RATES) { + return AccountController::saveTaxRates(); + } elseif ($section === ACCOUNT_PAYMENT_TERMS) { + return AccountController::savePaymetTerms(); + } + } + + private function saveCustomizeDesign() + { if (Auth::user()->account->isPro()) { $account = Auth::user()->account; $account->custom_design = Input::get('custom_design'); $account->invoice_design_id = CUSTOM_DESIGN; $account->save(); - + Session::flash('message', trans('texts.updated_settings')); } - return Redirect::to('company/advanced_settings/customize_design'); + return Redirect::to('settings/'.ACCOUNT_CUSTOMIZE_DESIGN); + } + + private function saveClientPortal() + { + // Only allowed for pro Invoice Ninja users or white labeled self-hosted users + if ((Utils::isNinja() && Auth::user()->account->isPro()) || Auth::user()->account->isWhiteLabel()) { + $input_css = Input::get('client_view_css'); + if (Utils::isNinja()) { + // Allow referencing the body element + $input_css = preg_replace('/(? + // + + // Create a new configuration object + $config = \HTMLPurifier_Config::createDefault(); + $config->set('Filter.ExtractStyleBlocks', true); + $config->set('CSS.AllowImportant', true); + $config->set('CSS.AllowTricky', true); + $config->set('CSS.Trusted', true); + + // Create a new purifier instance + $purifier = new \HTMLPurifier($config); + + // Wrap our CSS in style tags and pass to purifier. + // we're not actually interested in the html response though + $html = $purifier->purify(''); + + // The "style" blocks are stored seperately + $output_css = $purifier->context->get('StyleBlocks'); + + // Get the first style block + $sanitized_css = count($output_css) ? $output_css[0] : ''; + } else { + $sanitized_css = $input_css; + } + + $account = Auth::user()->account; + $account->client_view_css = $sanitized_css; + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + } + + return Redirect::to('settings/'.ACCOUNT_CLIENT_PORTAL); } private function saveEmailTemplates() @@ -314,16 +545,48 @@ class AccountController extends BaseController if (Auth::user()->account->isPro()) { $account = Auth::user()->account; - $account->email_template_invoice = Input::get('email_template_invoice', $account->getEmailTemplate(ENTITY_INVOICE)); - $account->email_template_quote = Input::get('email_template_quote', $account->getEmailTemplate(ENTITY_QUOTE)); - $account->email_template_payment = Input::get('email_template_payment', $account->getEmailTemplate(ENTITY_PAYMENT)); + foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { + $subjectField = "email_subject_{$type}"; + $subject = Input::get($subjectField, $account->getEmailSubject($type)); + $account->$subjectField = ($subject == $account->getDefaultEmailSubject($type) ? null : $subject); + + $bodyField = "email_template_{$type}"; + $body = Input::get($bodyField, $account->getEmailTemplate($type)); + $account->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body); + } + + foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) { + $enableField = "enable_{$type}"; + $account->$enableField = Input::get($enableField) ? true : false; + + if ($account->$enableField) { + $account->{"num_days_{$type}"} = Input::get("num_days_{$type}"); + $account->{"field_{$type}"} = Input::get("field_{$type}"); + $account->{"direction_{$type}"} = Input::get("field_{$type}") == REMINDER_FIELD_INVOICE_DATE ? REMINDER_DIRECTION_AFTER : Input::get("direction_{$type}"); + } + } $account->save(); Session::flash('message', trans('texts.updated_settings')); } - - return Redirect::to('company/advanced_settings/email_templates'); + + return Redirect::to('settings/'.ACCOUNT_TEMPLATES_AND_REMINDERS); + } + + private function saveTaxRates() + { + $account = Auth::user()->account; + + $account->invoice_taxes = Input::get('invoice_taxes') ? true : false; + $account->invoice_item_taxes = Input::get('invoice_item_taxes') ? true : false; + $account->show_item_taxes = Input::get('show_item_taxes') ? true : false; + $account->default_tax_rate_id = Input::get('default_tax_rate_id'); + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_TAX_RATES); } private function saveProducts() @@ -335,48 +598,124 @@ class AccountController extends BaseController $account->save(); Session::flash('message', trans('texts.updated_settings')); - return Redirect::to('company/products'); + + return Redirect::to('settings/'.ACCOUNT_PRODUCTS); } - private function saveInvoiceSettings() + private function saveEmailSettings() { if (Auth::user()->account->isPro()) { - $account = Auth::user()->account; + $rules = []; + $user = Auth::user(); + $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); + $iframeURL = rtrim($iframeURL, "/"); - $account->custom_label1 = trim(Input::get('custom_label1')); - $account->custom_value1 = trim(Input::get('custom_value1')); - $account->custom_label2 = trim(Input::get('custom_label2')); - $account->custom_value2 = trim(Input::get('custom_value2')); - $account->custom_client_label1 = trim(Input::get('custom_client_label1')); - $account->custom_client_label2 = trim(Input::get('custom_client_label2')); - $account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1')); - $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); - $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; - $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; - - $account->invoice_number_prefix = Input::get('invoice_number_prefix'); - $account->invoice_number_counter = Input::get('invoice_number_counter'); - $account->quote_number_prefix = Input::get('quote_number_prefix'); - $account->share_counter = Input::get('share_counter') ? true : false; - - $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; - $account->auto_wrap = Input::get('auto_wrap') ? true : false; - - if (!$account->share_counter) { - $account->quote_number_counter = Input::get('quote_number_counter'); + $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); + if ($iframeURL || !$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { + $subdomain = null; + } + if ($subdomain) { + $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id"; } - if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) { - Session::flash('error', trans('texts.invalid_counter')); + $validator = Validator::make(Input::all(), $rules); - return Redirect::to('company/advanced_settings/invoice_settings')->withInput(); + if ($validator->fails()) { + return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS) + ->withErrors($validator) + ->withInput(); } else { + $account = Auth::user()->account; + $account->subdomain = $subdomain; + $account->iframe_url = $iframeURL; + $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; + $account->email_design_id = Input::get('email_design_id'); + + if (Utils::isNinja()) { + $account->enable_email_markup = Input::get('enable_email_markup') ? true : false; + } + $account->save(); Session::flash('message', trans('texts.updated_settings')); } } - return Redirect::to('company/advanced_settings/invoice_settings'); + return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS); + } + + private function saveInvoiceSettings() + { + if (Auth::user()->account->isPro()) { + $rules = [ + 'invoice_number_pattern' => 'has_counter', + 'quote_number_pattern' => 'has_counter', + ]; + + $validator = Validator::make(Input::all(), $rules); + + if ($validator->fails()) { + return Redirect::to('settings/'.ACCOUNT_INVOICE_SETTINGS) + ->withErrors($validator) + ->withInput(); + } else { + $account = Auth::user()->account; + $account->custom_label1 = trim(Input::get('custom_label1')); + $account->custom_value1 = trim(Input::get('custom_value1')); + $account->custom_label2 = trim(Input::get('custom_label2')); + $account->custom_value2 = trim(Input::get('custom_value2')); + $account->custom_client_label1 = trim(Input::get('custom_client_label1')); + $account->custom_client_label2 = trim(Input::get('custom_client_label2')); + $account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1')); + $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); + $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; + $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; + $account->custom_invoice_text_label1 = trim(Input::get('custom_invoice_text_label1')); + $account->custom_invoice_text_label2 = trim(Input::get('custom_invoice_text_label2')); + + $account->invoice_number_counter = Input::get('invoice_number_counter'); + $account->quote_number_prefix = Input::get('quote_number_prefix'); + $account->share_counter = Input::get('share_counter') ? true : false; + $account->invoice_terms = Input::get('invoice_terms'); + $account->invoice_footer = Input::get('invoice_footer'); + $account->quote_terms = Input::get('quote_terms'); + $account->auto_convert_quote = Input::get('auto_convert_quote'); + + if (Input::has('recurring_hour')) { + $account->recurring_hour = Input::get('recurring_hour'); + } + + if (!$account->share_counter) { + $account->quote_number_counter = Input::get('quote_number_counter'); + } + + if (Input::get('invoice_number_type') == 'prefix') { + $account->invoice_number_prefix = trim(Input::get('invoice_number_prefix')); + $account->invoice_number_pattern = null; + } else { + $account->invoice_number_pattern = trim(Input::get('invoice_number_pattern')); + $account->invoice_number_prefix = null; + } + + if (Input::get('quote_number_type') == 'prefix') { + $account->quote_number_prefix = trim(Input::get('quote_number_prefix')); + $account->quote_number_pattern = null; + } else { + $account->quote_number_pattern = trim(Input::get('quote_number_pattern')); + $account->quote_number_prefix = null; + } + + if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) { + Session::flash('error', trans('texts.invalid_counter')); + + return Redirect::to('settings/'.ACCOUNT_INVOICE_SETTINGS)->withInput(); + } else { + $account->save(); + Session::flash('message', trans('texts.updated_settings')); + } + } + } + + return Redirect::to('settings/'.ACCOUNT_INVOICE_SETTINGS); } private function saveInvoiceDesign() @@ -385,16 +724,19 @@ class AccountController extends BaseController $account = Auth::user()->account; $account->hide_quantity = Input::get('hide_quantity') ? true : false; $account->hide_paid_to_date = Input::get('hide_paid_to_date') ? true : false; + $account->header_font_id = Input::get('header_font_id'); + $account->body_font_id = Input::get('body_font_id'); $account->primary_color = Input::get('primary_color'); $account->secondary_color = Input::get('secondary_color'); - $account->invoice_design_id = Input::get('invoice_design_id'); + $account->invoice_design_id = Input::get('invoice_design_id'); + if (Input::has('font_size')) { $account->font_size = intval(Input::get('font_size')); } - + $labels = []; - foreach (['item', 'description', 'unit_cost', 'quantity'] as $field) { - $labels[$field] = trim(Input::get("labels_{$field}")); + foreach (['item', 'description', 'unit_cost', 'quantity', 'line_total', 'terms'] as $field) { + $labels[$field] = Input::get("labels_{$field}"); } $account->invoice_labels = json_encode($labels); @@ -403,220 +745,11 @@ class AccountController extends BaseController Session::flash('message', trans('texts.updated_settings')); } - return Redirect::to('company/advanced_settings/invoice_design'); - } - - private function export() - { - $output = fopen('php://output', 'w') or Utils::fatalError(); - header('Content-Type:application/csv'); - header('Content-Disposition:attachment;filename=export.csv'); - - $clients = Client::scope()->get(); - Utils::exportData($output, $clients->toArray()); - - $contacts = Contact::scope()->get(); - Utils::exportData($output, $contacts->toArray()); - - $invoices = Invoice::scope()->get(); - Utils::exportData($output, $invoices->toArray()); - - $invoiceItems = InvoiceItem::scope()->get(); - Utils::exportData($output, $invoiceItems->toArray()); - - $payments = Payment::scope()->get(); - Utils::exportData($output, $payments->toArray()); - - $credits = Credit::scope()->get(); - Utils::exportData($output, $credits->toArray()); - - fclose($output); - exit; - } - - private function importFile() - { - $data = Session::get('data'); - Session::forget('data'); - - $map = Input::get('map'); - $count = 0; - $hasHeaders = Input::get('header_checkbox'); - - $countries = Cache::get('countries'); - $countryMap = []; - - foreach ($countries as $country) { - $countryMap[strtolower($country->name)] = $country->id; - } - - foreach ($data as $row) { - if ($hasHeaders) { - $hasHeaders = false; - continue; - } - - $client = Client::createNew(); - $contact = Contact::createNew(); - $contact->is_primary = true; - $contact->send_invoice = true; - $count++; - - foreach ($row as $index => $value) { - $field = $map[$index]; - $value = trim($value); - - if ($field == Client::$fieldName && !$client->name) { - $client->name = $value; - } elseif ($field == Client::$fieldPhone && !$client->work_phone) { - $client->work_phone = $value; - } elseif ($field == Client::$fieldAddress1 && !$client->address1) { - $client->address1 = $value; - } elseif ($field == Client::$fieldAddress2 && !$client->address2) { - $client->address2 = $value; - } elseif ($field == Client::$fieldCity && !$client->city) { - $client->city = $value; - } elseif ($field == Client::$fieldState && !$client->state) { - $client->state = $value; - } elseif ($field == Client::$fieldPostalCode && !$client->postal_code) { - $client->postal_code = $value; - } elseif ($field == Client::$fieldCountry && !$client->country_id) { - $value = strtolower($value); - $client->country_id = isset($countryMap[$value]) ? $countryMap[$value] : null; - } elseif ($field == Client::$fieldNotes && !$client->private_notes) { - $client->private_notes = $value; - } elseif ($field == Contact::$fieldFirstName && !$contact->first_name) { - $contact->first_name = $value; - } elseif ($field == Contact::$fieldLastName && !$contact->last_name) { - $contact->last_name = $value; - } elseif ($field == Contact::$fieldPhone && !$contact->phone) { - $contact->phone = $value; - } elseif ($field == Contact::$fieldEmail && !$contact->email) { - $contact->email = strtolower($value); - } - } - - $client->save(); - $client->contacts()->save($contact); - Activity::createClient($client, false); - } - - $message = Utils::pluralize('created_client', $count); - Session::flash('message', $message); - - return Redirect::to('clients'); - } - - private function mapFile() - { - $file = Input::file('file'); - - if ($file == null) { - Session::flash('error', trans('texts.select_file')); - - return Redirect::to('company/import_export'); - } - - $name = $file->getRealPath(); - - require_once app_path().'/Includes/parsecsv.lib.php'; - $csv = new parseCSV(); - $csv->heading = false; - $csv->auto($name); - - if (count($csv->data) + Client::scope()->count() > Auth::user()->getMaxNumClients()) { - $message = trans('texts.limit_clients', ['count' => Auth::user()->getMaxNumClients()]); - Session::flash('error', $message); - - return Redirect::to('company/import_export'); - } - - Session::put('data', $csv->data); - - $headers = false; - $hasHeaders = false; - $mapped = array(); - $columns = array('', - Client::$fieldName, - Client::$fieldPhone, - Client::$fieldAddress1, - Client::$fieldAddress2, - Client::$fieldCity, - Client::$fieldState, - Client::$fieldPostalCode, - Client::$fieldCountry, - Client::$fieldNotes, - Contact::$fieldFirstName, - Contact::$fieldLastName, - Contact::$fieldPhone, - Contact::$fieldEmail, - ); - - if (count($csv->data) > 0) { - $headers = $csv->data[0]; - foreach ($headers as $title) { - if (strpos(strtolower($title), 'name') > 0) { - $hasHeaders = true; - break; - } - } - - for ($i = 0; $i
sudo chown www-data:www-data /path/to/ninja/.env
');
+ return Redirect::to('/settings/system_settings');
+ }
+
+ $app = Input::get('app');
+ $db = Input::get('database');
+ $mail = Input::get('mail');
+
+ $_ENV['APP_URL'] = $app['url'];
+ $_ENV['APP_DEBUG'] = Input::get('debug') ? 'true' : 'false';
+
+ $_ENV['DB_TYPE'] = 'mysql'; // $db['default'];
+ $_ENV['DB_HOST'] = $db['type']['host'];
+ $_ENV['DB_DATABASE'] = $db['type']['database'];
+ $_ENV['DB_USERNAME'] = $db['type']['username'];
+ $_ENV['DB_PASSWORD'] = $db['type']['password'];
+
+ if ($mail) {
+ $_ENV['MAIL_DRIVER'] = $mail['driver'];
+ $_ENV['MAIL_PORT'] = $mail['port'];
+ $_ENV['MAIL_ENCRYPTION'] = $mail['encryption'];
+ $_ENV['MAIL_HOST'] = $mail['host'];
+ $_ENV['MAIL_USERNAME'] = $mail['username'];
+ $_ENV['MAIL_FROM_NAME'] = $mail['from']['name'];
+ $_ENV['MAIL_PASSWORD'] = $mail['password'];
+ $_ENV['MAIL_FROM_ADDRESS'] = $mail['username'];
+ }
+
+ $config = '';
+ foreach ($_ENV as $key => $val) {
+ $config .= "{$key}={$val}\n";
+ }
+
+ $fp = fopen(base_path()."/.env", 'w');
+ fwrite($fp, $config);
+ fclose($fp);
+
+ Session::flash('message', trans('texts.updated_settings'));
+ return Redirect::to('/settings/system_settings');
+ }
+
private function testDatabase($database)
{
- $dbType = $database['default'];
-
+ $dbType = 'mysql'; // $database['default'];
Config::set('database.default', $dbType);
-
foreach ($database['connections'][$dbType] as $key => $val) {
Config::set("database.connections.{$dbType}.{$key}", $val);
}
-
+
try {
+ DB::reconnect();
$valid = DB::connection()->getDatabaseName() ? true : false;
} catch (Exception $e) {
return $e->getMessage();
@@ -179,10 +241,13 @@ class AppController extends BaseController
{
if (!Utils::isNinjaProd()) {
try {
+ Cache::flush();
+ Session::flush();
+ Artisan::call('optimize', array('--force' => true));
Artisan::call('migrate', array('--force' => true));
Artisan::call('db:seed', array('--force' => true, '--class' => 'PaymentLibrariesSeeder'));
- Artisan::call('optimize', array('--force' => true));
- Cache::flush();
+ Artisan::call('db:seed', array('--force' => true, '--class' => 'FontsSeeder'));
+ Event::fire(new UserSettingsChanged());
Session::flash('message', trans('texts.processed_updates'));
} catch (Exception $e) {
Response::make($e->getMessage(), 500);
@@ -191,4 +256,19 @@ class AppController extends BaseController
return Redirect::to('/');
}
+
+ public function emailBounced()
+ {
+ $messageId = Input::get('MessageID');
+ $error = Input::get('Name') . ': ' . Input::get('Description');
+ return $this->emailService->markBounced($messageId, $error) ? RESULT_SUCCESS : RESULT_FAILURE;
+ }
+
+ public function emailOpened()
+ {
+ $messageId = Input::get('MessageID');
+ return $this->emailService->markOpened($messageId) ? RESULT_SUCCESS : RESULT_FAILURE;
+
+ return RESULT_SUCCESS;
+ }
}
diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php
index a5897ac6879d..5ed231cedc82 100644
--- a/app/Http/Controllers/Auth/AuthController.php
+++ b/app/Http/Controllers/Auth/AuthController.php
@@ -9,6 +9,7 @@ use App\Models\User;
use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller;
use App\Ninja\Repositories\AccountRepository;
+use App\Services\AuthService;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\Registrar;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
@@ -30,6 +31,7 @@ class AuthController extends Controller {
protected $loginPath = '/login';
protected $redirectTo = '/dashboard';
+ protected $authService;
protected $accountRepo;
/**
@@ -39,15 +41,29 @@ class AuthController extends Controller {
* @param \Illuminate\Contracts\Auth\Registrar $registrar
* @return void
*/
- public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo)
+ public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo, AuthService $authService)
{
$this->auth = $auth;
$this->registrar = $registrar;
$this->accountRepo = $repo;
+ $this->authService = $authService;
//$this->middleware('guest', ['except' => 'getLogout']);
}
+ public function authLogin($provider, Request $request)
+ {
+ return $this->authService->execute($provider, $request->has('code'));
+ }
+
+ public function authUnlink()
+ {
+ $this->accountRepo->unlinkUserFromOauth(Auth::user());
+
+ Session::flash('message', trans('texts.updated_settings'));
+ return redirect()->to('/settings/' . ACCOUNT_USER_DETAILS);
+ }
+
public function getLoginWrapper()
{
if (!Utils::isNinja() && !User::count()) {
@@ -59,11 +75,12 @@ class AuthController extends Controller {
public function postLoginWrapper(Request $request)
{
+
$userId = Auth::check() ? Auth::user()->id : null;
$user = User::where('email', '=', $request->input('email'))->first();
- if ($user && $user->failed_logins >= 3) {
- Session::flash('error', 'These credentials do not match our records.');
+ if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
+ Session::flash('error', trans('texts.invalid_credentials'));
return redirect()->to('login');
}
@@ -74,15 +91,15 @@ class AuthController extends Controller {
$users = false;
// we're linking a new account
- if ($userId && Auth::user()->id != $userId) {
+ if ($request->link_accounts && $userId && Auth::user()->id != $userId) {
$users = $this->accountRepo->associateAccounts($userId, Auth::user()->id);
Session::flash('message', trans('texts.associated_accounts'));
// check if other accounts are linked
} else {
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
}
-
Session::put(SESSION_USER_ACCOUNTS, $users);
+
} elseif ($user) {
$user->failed_logins = $user->failed_logins + 1;
$user->save();
@@ -91,6 +108,7 @@ class AuthController extends Controller {
return $response;
}
+
public function getLogoutWrapper()
{
if (Auth::check() && !Auth::user()->registered) {
diff --git a/app/Http/Controllers/BankAccountController.php b/app/Http/Controllers/BankAccountController.php
new file mode 100644
index 000000000000..3b0f190ef1ec
--- /dev/null
+++ b/app/Http/Controllers/BankAccountController.php
@@ -0,0 +1,152 @@
+bankAccountService = $bankAccountService;
+ }
+
+ public function index()
+ {
+ return Redirect::to('settings/' . ACCOUNT_BANKS);
+ }
+
+ public function getDatatable()
+ {
+ return $this->bankAccountService->getDatatable(Auth::user()->account_id);
+ }
+
+ public function edit($publicId)
+ {
+ $bankAccount = BankAccount::scope($publicId)->firstOrFail();
+ $bankAccount->username = str_repeat('*', 16);
+
+ $data = [
+ 'url' => 'bank_accounts/' . $publicId,
+ 'method' => 'PUT',
+ 'title' => trans('texts.edit_bank_account'),
+ 'banks' => Cache::get('banks'),
+ 'bankAccount' => $bankAccount,
+ ];
+
+ return View::make('accounts.bank_account', $data);
+ }
+
+ public function update($publicId)
+ {
+ return $this->save($publicId);
+ }
+
+ public function store()
+ {
+ return $this->save();
+ }
+
+ /**
+ * Displays the form for account creation
+ *
+ */
+ public function create()
+ {
+ $data = [
+ 'url' => 'bank_accounts',
+ 'method' => 'POST',
+ 'title' => trans('texts.add_bank_account'),
+ 'banks' => Cache::get('banks'),
+ 'bankAccount' => null,
+ ];
+
+ return View::make('accounts.bank_account', $data);
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('bulk_action');
+ $ids = Input::get('bulk_public_id');
+ $count = $this->bankAccountService->bulk($ids, $action);
+
+ Session::flash('message', trans('texts.archived_bank_account'));
+
+ return Redirect::to('settings/' . ACCOUNT_BANKS);
+ }
+
+ /**
+ * Stores new account
+ *
+ */
+ public function save($bankAccountPublicId = false)
+ {
+ $account = Auth::user()->account;
+ $bankId = Input::get('bank_id');
+ $username = Input::get('bank_username');
+
+ $rules = [
+ 'bank_id' => $bankAccountPublicId ? '' : 'required',
+ 'bank_username' => 'required',
+ ];
+
+ $validator = Validator::make(Input::all(), $rules);
+
+ if ($validator->fails()) {
+ return Redirect::to('bank_accounts/create')
+ ->withErrors($validator)
+ ->withInput();
+ } else {
+ if ($bankAccountPublicId) {
+ $bankAccount = BankAccount::scope($bankAccountPublicId)->firstOrFail();
+ } else {
+ $bankAccount = BankAccount::createNew();
+ $bankAccount->bank_id = $bankId;
+ }
+
+ if ($username != str_repeat('*', strlen($username))) {
+ $bankAccount->username = Crypt::encrypt(trim($username));
+ }
+
+ if ($bankAccountPublicId) {
+ $bankAccount->save();
+ $message = trans('texts.updated_bank_account');
+ } else {
+ $account->bank_accounts()->save($bankAccount);
+ $message = trans('texts.created_bank_account');
+ }
+
+ Session::flash('message', $message);
+ return Redirect::to("bank_accounts/{$bankAccount->public_id}/edit");
+ }
+ }
+
+ public function test()
+ {
+ $bankId = Input::get('bank_id');
+ $username = Input::get('bank_username');
+ $password = Input::get('bank_password');
+
+ return json_encode($this->bankAccountService->loadBankAccounts($bankId, $username, $password, false));
+ }
+
+}
diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php
new file mode 100644
index 000000000000..4d783556022e
--- /dev/null
+++ b/app/Http/Controllers/BaseAPIController.php
@@ -0,0 +1,135 @@
+manager = new Manager();
+
+ if ($include = Request::get('include')) {
+ $this->manager->parseIncludes($include);
+ }
+
+ $this->serializer = Request::get('serializer') ?: API_SERIALIZER_ARRAY;
+
+ if ($this->serializer === API_SERIALIZER_JSON) {
+ $this->manager->setSerializer(new JsonApiSerializer());
+ } else {
+ $this->manager->setSerializer(new ArraySerializer());
+ }
+ }
+
+ protected function createItem($data, $transformer, $entityType)
+ {
+ if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) {
+ $entityType = null;
+ }
+
+ $resource = new Item($data, $transformer, $entityType);
+ return $this->manager->createData($resource)->toArray();
+ }
+
+ protected function createCollection($data, $transformer, $entityType, $paginator = false)
+ {
+ if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) {
+ $entityType = null;
+ }
+
+ $resource = new Collection($data, $transformer, $entityType);
+
+ if ($paginator) {
+ $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
+ }
+
+ return $this->manager->createData($resource)->toArray();
+ }
+
+ protected function response($response)
+ {
+ $index = Request::get('index') ?: 'data';
+ $meta = isset($response['meta']) ? $response['meta'] : null;
+ $response = [
+ $index => $response
+ ];
+ if ($meta) {
+ $response['meta'] = $meta;
+ unset($response[$index]['meta']);
+ }
+
+ $response = json_encode($response, JSON_PRETTY_PRINT);
+ $headers = Utils::getApiHeaders();
+
+ return Response::make($response, 200, $headers);
+ }
+
+ protected function getIncluded()
+ {
+ $data = ['user'];
+
+ $included = Request::get('include');
+ $included = explode(',', $included);
+
+ foreach ($included as $include) {
+ if ($include == 'invoices') {
+ $data[] = 'invoices.invoice_items';
+ $data[] = 'invoices.user';
+ } elseif ($include == 'clients') {
+ $data[] = 'clients.contacts';
+ $data[] = 'clients.user';
+ } elseif ($include == 'vendors') {
+ $data[] = 'vendors.vendorcontacts';
+ $data[] = 'vendors.user';
+ }
+ elseif ($include) {
+ $data[] = $include;
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php
index 0cc63c7c5a1a..1a2f6c8dc526 100644
--- a/app/Http/Controllers/BaseController.php
+++ b/app/Http/Controllers/BaseController.php
@@ -1,7 +1,11 @@
clientRepo = $clientRepo;
}
@@ -22,37 +28,80 @@ class ClientApiController extends Controller
return Response::make('', 200, $headers);
}
+ /**
+ * @SWG\Get(
+ * path="/clients",
+ * summary="List of clients",
+ * tags={"client"},
+ * @SWG\Response(
+ * response=200,
+ * description="A list with clients",
+ * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Client"))
+ * ),
+ * @SWG\Response(
+ * response="default",
+ * description="an ""unexpected"" error"
+ * )
+ * )
+ */
public function index()
{
$clients = Client::scope()
- ->with('country', 'contacts', 'industry', 'size', 'currency')
- ->orderBy('created_at', 'desc')
- ->get();
- $clients = Utils::remapPublicIds($clients);
+ ->with($this->getIncluded())
+ ->orderBy('created_at', 'desc');
- $response = json_encode($clients, JSON_PRETTY_PRINT);
- $headers = Utils::getApiHeaders(count($clients));
+ // Filter by email
+ if (Input::has('email')) {
- return Response::make($response, 200, $headers);
+ $email = Input::get('email');
+ $clients = $clients->whereHas('contacts', function ($query) use ($email) {
+ $query->where('email', $email);
+ });
+
+ }
+
+ $clients = $clients->paginate();
+
+ $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer'));
+ $paginator = Client::scope()->paginate();
+
+ $data = $this->createCollection($clients, $transformer, ENTITY_CLIENT, $paginator);
+
+ return $this->response($data);
}
- public function store()
+ /**
+ * @SWG\Post(
+ * path="/clients",
+ * tags={"client"},
+ * summary="Create a client",
+ * @SWG\Parameter(
+ * in="body",
+ * name="body",
+ * @SWG\Schema(ref="#/definitions/Client")
+ * ),
+ * @SWG\Response(
+ * response=200,
+ * description="New client",
+ * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Client"))
+ * ),
+ * @SWG\Response(
+ * response="default",
+ * description="an ""unexpected"" error"
+ * )
+ * )
+ */
+ public function store(CreateClientRequest $request)
{
- $data = Input::all();
- $error = $this->clientRepo->getErrors($data);
+ $client = $this->clientRepo->save($request->input());
- if ($error) {
- $headers = Utils::getApiHeaders();
+ $client = Client::scope($client->public_id)
+ ->with('country', 'contacts', 'industry', 'size', 'currency')
+ ->first();
- return Response::make($error, 500, $headers);
- } else {
- $client = $this->clientRepo->save(isset($data['id']) ? $data['id'] : false, $data, false);
- $client = Client::scope($client->public_id)->with('country', 'contacts', 'industry', 'size', 'currency')->first();
- $client = Utils::remapPublicIds([$client]);
- $response = json_encode($client, JSON_PRETTY_PRINT);
- $headers = Utils::getApiHeaders();
+ $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer'));
+ $data = $this->createItem($client, $transformer, ENTITY_CLIENT);
- return Response::make($response, 200, $headers);
- }
+ return $this->response($data);
}
}
diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php
index 065e3e073a33..84145afe9c1c 100644
--- a/app/Http/Controllers/ClientController.php
+++ b/app/Http/Controllers/ClientController.php
@@ -13,6 +13,7 @@ use Cache;
use App\Models\Activity;
use App\Models\Client;
+use App\Models\Account;
use App\Models\Contact;
use App\Models\Invoice;
use App\Models\Size;
@@ -21,18 +22,23 @@ use App\Models\Industry;
use App\Models\Currency;
use App\Models\Country;
use App\Models\Task;
-
use App\Ninja\Repositories\ClientRepository;
+use App\Services\ClientService;
+
+use App\Http\Requests\CreateClientRequest;
+use App\Http\Requests\UpdateClientRequest;
class ClientController extends BaseController
{
+ protected $clientService;
protected $clientRepo;
- public function __construct(ClientRepository $clientRepo)
+ public function __construct(ClientRepository $clientRepo, ClientService $clientService)
{
parent::__construct();
$this->clientRepo = $clientRepo;
+ $this->clientService = $clientService;
}
/**
@@ -46,56 +52,22 @@ class ClientController extends BaseController
'entityType' => ENTITY_CLIENT,
'title' => trans('texts.clients'),
'sortCol' => '4',
- 'columns' => Utils::trans(['checkbox', 'client', 'contact', 'email', 'date_created', 'last_login', 'balance', 'action']),
+ 'columns' => Utils::trans([
+ 'checkbox',
+ 'client',
+ 'contact',
+ 'email',
+ 'date_created',
+ 'last_login',
+ 'balance',
+ ''
+ ]),
));
}
public function getDatatable()
{
- $clients = $this->clientRepo->find(Input::get('sSearch'));
-
- return Datatable::query($clients)
- ->addColumn('checkbox', function ($model) { return ''; })
- ->addColumn('name', function ($model) { return link_to('clients/'.$model->public_id, $model->name); })
- ->addColumn('first_name', function ($model) { return link_to('clients/'.$model->public_id, $model->first_name.' '.$model->last_name); })
- ->addColumn('email', function ($model) { return link_to('clients/'.$model->public_id, $model->email); })
- ->addColumn('clients.created_at', function ($model) { return Utils::timestampToDateString(strtotime($model->created_at)); })
- ->addColumn('last_login', function ($model) { return Utils::timestampToDateString(strtotime($model->last_login)); })
- ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); })
- ->addColumn('dropdown', function ($model) {
- if ($model->is_deleted) {
- return '';
- }
-
- $str = ' ';
- })
- ->make();
+ return $this->clientService->getDatatable(Input::get('sSearch'));
}
/**
@@ -103,9 +75,13 @@ class ClientController extends BaseController
*
* @return Response
*/
- public function store()
+ public function store(CreateClientRequest $request)
{
- return $this->save();
+ $client = $this->clientService->save($request->input());
+
+ Session::flash('message', trans('texts.created_client'));
+
+ return redirect()->to($client->getRoute());
}
/**
@@ -128,8 +104,10 @@ class ClientController extends BaseController
}
array_push($actionLinks,
+ \DropdownButton::DIVIDER,
['label' => trans('texts.enter_payment'), 'url' => '/payments/create/'.$client->public_id],
- ['label' => trans('texts.enter_credit'), 'url' => '/credits/create/'.$client->public_id]
+ ['label' => trans('texts.enter_credit'), 'url' => '/credits/create/'.$client->public_id],
+ ['label' => trans('texts.enter_expense'), 'url' => '/expenses/create/0/'.$client->public_id]
);
$data = array(
@@ -188,16 +166,25 @@ class ClientController extends BaseController
$data = array_merge($data, self::getViewModel());
+ if (Auth::user()->account->isNinjaAccount()) {
+ if ($account = Account::whereId($client->public_id)->first()) {
+ $data['proPlanPaid'] = $account['pro_plan_paid'];
+ }
+ }
+
return View::make('clients.edit', $data);
}
private static function getViewModel()
{
return [
+ 'data' => Input::old('data'),
+ 'account' => Auth::user()->account,
'sizes' => Cache::get('sizes'),
'paymentTerms' => Cache::get('paymentTerms'),
'industries' => Cache::get('industries'),
'currencies' => Cache::get('currencies'),
+ 'languages' => Cache::get('languages'),
'countries' => Cache::get('countries'),
'customLabel1' => Auth::user()->account->custom_client_label1,
'customLabel2' => Auth::user()->account->custom_client_label2,
@@ -210,97 +197,20 @@ class ClientController extends BaseController
* @param int $id
* @return Response
*/
- public function update($publicId)
+ public function update(UpdateClientRequest $request)
{
- return $this->save($publicId);
- }
-
- private function save($publicId = null)
- {
- $rules = array(
- 'email' => 'email|required_without:first_name',
- 'first_name' => 'required_without:email',
- );
- $validator = Validator::make(Input::all(), $rules);
-
- if ($validator->fails()) {
- $url = $publicId ? 'clients/'.$publicId.'/edit' : 'clients/create';
-
- return Redirect::to($url)
- ->withErrors($validator)
- ->withInput(Input::except('password'));
- } else {
- if ($publicId) {
- $client = Client::scope($publicId)->firstOrFail();
- } else {
- $client = Client::createNew();
- }
-
- $client->name = trim(Input::get('name'));
- $client->id_number = trim(Input::get('id_number'));
- $client->vat_number = trim(Input::get('vat_number'));
- $client->work_phone = trim(Input::get('work_phone'));
- $client->custom_value1 = trim(Input::get('custom_value1'));
- $client->custom_value2 = trim(Input::get('custom_value2'));
- $client->address1 = trim(Input::get('address1'));
- $client->address2 = trim(Input::get('address2'));
- $client->city = trim(Input::get('city'));
- $client->state = trim(Input::get('state'));
- $client->postal_code = trim(Input::get('postal_code'));
- $client->country_id = Input::get('country_id') ?: null;
- $client->private_notes = trim(Input::get('private_notes'));
- $client->size_id = Input::get('size_id') ?: null;
- $client->industry_id = Input::get('industry_id') ?: null;
- $client->currency_id = Input::get('currency_id') ?: null;
- $client->payment_terms = Input::get('payment_terms') ?: 0;
- $client->website = trim(Input::get('website'));
-
- $client->save();
-
- $data = json_decode(Input::get('data'));
- $contactIds = [];
- $isPrimary = true;
-
- foreach ($data->contacts as $contact) {
- if (isset($contact->public_id) && $contact->public_id) {
- $record = Contact::scope($contact->public_id)->firstOrFail();
- } else {
- $record = Contact::createNew();
- }
-
- $record->email = trim($contact->email);
- $record->first_name = trim($contact->first_name);
- $record->last_name = trim($contact->last_name);
- $record->phone = trim($contact->phone);
- $record->is_primary = $isPrimary;
- $isPrimary = false;
-
- $client->contacts()->save($record);
- $contactIds[] = $record->public_id;
- }
-
- foreach ($client->contacts as $contact) {
- if (!in_array($contact->public_id, $contactIds)) {
- $contact->delete();
- }
- }
-
- if ($publicId) {
- Session::flash('message', trans('texts.updated_client'));
- } else {
- Activity::createClient($client);
- Session::flash('message', trans('texts.created_client'));
- }
-
- return Redirect::to('clients/'.$client->public_id);
- }
+ $client = $this->clientService->save($request->input());
+
+ Session::flash('message', trans('texts.updated_client'));
+
+ return redirect()->to($client->getRoute());
}
public function bulk()
{
$action = Input::get('action');
- $ids = Input::get('id') ? Input::get('id') : Input::get('ids');
- $count = $this->clientRepo->bulk($ids, $action);
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+ $count = $this->clientService->bulk($ids, $action);
$message = Utils::pluralize($action.'d_client', $count);
Session::flash('message', $message);
diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php
index 505400718caf..e08c136b5267 100644
--- a/app/Http/Controllers/CreditController.php
+++ b/app/Http/Controllers/CreditController.php
@@ -4,22 +4,26 @@ use Datatable;
use Input;
use Redirect;
use Session;
+use URL;
use Utils;
use View;
use Validator;
use App\Models\Client;
-
+use App\Services\CreditService;
use App\Ninja\Repositories\CreditRepository;
+use App\Http\Requests\CreateCreditRequest;
class CreditController extends BaseController
{
protected $creditRepo;
+ protected $creditService;
- public function __construct(CreditRepository $creditRepo)
+ public function __construct(CreditRepository $creditRepo, CreditService $creditService)
{
parent::__construct();
$this->creditRepo = $creditRepo;
+ $this->creditService = $creditService;
}
/**
@@ -33,46 +37,21 @@ class CreditController extends BaseController
'entityType' => ENTITY_CREDIT,
'title' => trans('texts.credits'),
'sortCol' => '4',
- 'columns' => Utils::trans(['checkbox', 'client', 'credit_amount', 'credit_balance', 'credit_date', 'private_notes', 'action']),
+ 'columns' => Utils::trans([
+ 'checkbox',
+ 'client',
+ 'credit_amount',
+ 'credit_balance',
+ 'credit_date',
+ 'private_notes',
+ ''
+ ]),
));
}
public function getDatatable($clientPublicId = null)
{
- $credits = $this->creditRepo->find($clientPublicId, Input::get('sSearch'));
-
- $table = Datatable::query($credits);
-
- if (!$clientPublicId) {
- $table->addColumn('checkbox', function ($model) { return ''; })
- ->addColumn('client_name', function ($model) { return link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)); });
- }
-
- return $table->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id).''; })
- ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); })
- ->addColumn('credit_date', function ($model) { return Utils::fromSqlDate($model->credit_date); })
- ->addColumn('private_notes', function ($model) { return $model->private_notes; })
- ->addColumn('dropdown', function ($model) {
- if ($model->is_deleted) {
- return '';
- }
-
- $str = '
- ';
- })
- ->make();
+ return $this->creditService->getDatatable($clientPublicId, Input::get('sSearch'));
}
public function create($clientPublicId = 0)
@@ -106,46 +85,20 @@ class CreditController extends BaseController
return View::make('credit.edit', $data);
}
- public function store()
+ public function store(CreateCreditRequest $request)
{
- return $this->save();
- }
-
- public function update($publicId)
- {
- return $this->save($publicId);
- }
-
- private function save($publicId = null)
- {
- $rules = array(
- 'client' => 'required',
- 'amount' => 'required|positive',
- );
-
- $validator = Validator::make(Input::all(), $rules);
-
- if ($validator->fails()) {
- $url = $publicId ? 'credits/'.$publicId.'/edit' : 'credits/create';
-
- return Redirect::to($url)
- ->withErrors($validator)
- ->withInput();
- } else {
- $this->creditRepo->save($publicId, Input::all());
-
- $message = trans('texts.created_credit');
- Session::flash('message', $message);
-
- return Redirect::to('clients/'.Input::get('client'));
- }
+ $credit = $this->creditRepo->save($request->input());
+
+ Session::flash('message', trans('texts.created_credit'));
+
+ return redirect()->to($credit->client->getRoute());
}
public function bulk()
{
$action = Input::get('action');
- $ids = Input::get('id') ? Input::get('id') : Input::get('ids');
- $count = $this->creditRepo->bulk($ids, $action);
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+ $count = $this->creditService->bulk($ids, $action);
if ($count > 0) {
$message = Utils::pluralize($action.'d_credit', $count);
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index 189b277c2c77..483b73a6c2ef 100644
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -11,6 +11,7 @@ class DashboardController extends BaseController
{
public function index()
{
+
// total_income, billed_clients, invoice_sent and active_clients
$select = DB::raw('COUNT(DISTINCT CASE WHEN invoices.id IS NOT NULL THEN clients.id ELSE null END) billed_clients,
SUM(CASE WHEN invoices.invoice_status_id >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent,
@@ -62,6 +63,7 @@ class DashboardController extends BaseController
->get();
$activities = Activity::where('activities.account_id', '=', Auth::user()->account_id)
+ ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account')
->where('activity_type_id', '>', 0)
->orderBy('created_at', 'desc')
->take(50)
@@ -74,12 +76,13 @@ class DashboardController extends BaseController
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
- ->where('invoices.is_quote', '=', false)
+ //->where('invoices.is_quote', '=', false)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
+ ->where('invoices.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '<', date('Y-m-d'))
- ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id'])
+ ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'is_quote'])
->orderBy('invoices.due_date', 'asc')
->take(50)
->get();
@@ -90,15 +93,16 @@ class DashboardController extends BaseController
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
+ ->where('invoices.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
- ->where('invoices.is_quote', '=', false)
+ //->where('invoices.is_quote', '=', false)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '>=', date('Y-m-d'))
->orderBy('invoices.due_date', 'asc')
->take(50)
- ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id'])
+ ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'is_quote'])
->get();
$payments = DB::table('payments')
@@ -106,14 +110,24 @@ class DashboardController extends BaseController
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('payments.account_id', '=', Auth::user()->account_id)
+ ->where('payments.is_deleted', '=', false)
+ ->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->select(['payments.payment_date', 'payments.amount', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id'])
- ->orderBy('payments.id', 'desc')
+ ->orderBy('payments.payment_date', 'desc')
->take(50)
->get();
+ $hasQuotes = false;
+ foreach ([$upcoming, $pastDue] as $data) {
+ foreach ($data as $invoice) {
+ if ($invoice->is_quote) {
+ $hasQuotes = true;
+ }
+ }
+ }
$data = [
'account' => Auth::user()->account,
@@ -127,6 +141,7 @@ class DashboardController extends BaseController
'upcoming' => $upcoming,
'payments' => $payments,
'title' => trans('texts.dashboard'),
+ 'hasQuotes' => $hasQuotes,
];
return View::make('dashboard', $data);
diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php
new file mode 100644
index 000000000000..c3dc77b23ec2
--- /dev/null
+++ b/app/Http/Controllers/ExpenseController.php
@@ -0,0 +1,252 @@
+expenseRepo = $expenseRepo;
+ $this->expenseService = $expenseService;
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return Response
+ */
+ public function index()
+ {
+ return View::make('list', array(
+ 'entityType' => ENTITY_EXPENSE,
+ 'title' => trans('texts.expenses'),
+ 'sortCol' => '1',
+ 'columns' => Utils::trans([
+ 'checkbox',
+ 'vendor',
+ 'client',
+ 'expense_date',
+ 'amount',
+ 'public_notes',
+ 'status',
+ ''
+ ]),
+ ));
+ }
+
+ public function getDatatable($expensePublicId = null)
+ {
+ return $this->expenseService->getDatatable($expensePublicId, Input::get('sSearch'));
+ }
+
+ public function getDatatableVendor($vendorPublicId = null)
+ {
+ return $this->expenseService->getDatatableVendor($vendorPublicId);
+ }
+
+ public function create($vendorPublicId = null, $clientPublicId = null)
+ {
+ if($vendorPublicId != 0) {
+ $vendor = Vendor::scope($vendorPublicId)->with('vendorcontacts')->firstOrFail();
+ } else {
+ $vendor = null;
+ }
+ $data = array(
+ 'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $vendorPublicId,
+ 'expense' => null,
+ 'method' => 'POST',
+ 'url' => 'expenses',
+ 'title' => trans('texts.new_expense'),
+ 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(),
+ 'vendor' => $vendor,
+ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
+ 'clientPublicId' => $clientPublicId,
+ );
+
+ $data = array_merge($data, self::getViewModel());
+
+ return View::make('expenses.edit', $data);
+ }
+
+ public function edit($publicId)
+ {
+ $expense = Expense::scope($publicId)->firstOrFail();
+ $expense->expense_date = Utils::fromSqlDate($expense->expense_date);
+
+ $actions = [];
+ if ($expense->invoice) {
+ $actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans("texts.view_invoice")];
+ } else {
+ $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.invoice_expense")];
+
+ /*
+ // check for any open invoices
+ $invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : [];
+
+ foreach ($invoices as $invoice) {
+ $actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans("texts.add_to_invoice", ["invoice" => $invoice->invoice_number])];
+ }
+ */
+ }
+
+ $actions[] = \DropdownButton::DIVIDER;
+ if (!$expense->trashed()) {
+ $actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_expense')];
+ $actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans('texts.delete_expense')];
+ } else {
+ $actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_expense')];
+ }
+
+ $data = array(
+ 'vendor' => null,
+ 'expense' => $expense,
+ 'method' => 'PUT',
+ 'url' => 'expenses/'.$publicId,
+ 'title' => 'Edit Expense',
+ 'actions' => $actions,
+ 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(),
+ 'vendorPublicId' => $expense->vendor ? $expense->vendor->public_id : null,
+ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
+ 'clientPublicId' => $expense->client ? $expense->client->public_id : null,
+ );
+
+ $data = array_merge($data, self::getViewModel());
+
+ if (Auth::user()->account->isNinjaAccount()) {
+ if ($account = Account::whereId($client->public_id)->first()) {
+ $data['proPlanPaid'] = $account['pro_plan_paid'];
+ }
+ }
+
+ return View::make('expenses.edit', $data);
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function update(UpdateExpenseRequest $request)
+ {
+ $expense = $this->expenseRepo->save($request->input());
+
+ Session::flash('message', trans('texts.updated_expense'));
+
+ $action = Input::get('action');
+ if (in_array($action, ['archive', 'delete', 'restore', 'invoice'])) {
+ return self::bulk();
+ }
+
+ return redirect()->to("expenses/{$expense->public_id}/edit");
+ }
+
+ public function store(CreateExpenseRequest $request)
+ {
+ $expense = $this->expenseRepo->save($request->input());
+
+ Session::flash('message', trans('texts.created_expense'));
+
+ return redirect()->to("expenses/{$expense->public_id}/edit");
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('action');
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+
+ switch($action)
+ {
+ case 'invoice':
+ $expenses = Expense::scope($ids)->get();
+ $clientPublicId = null;
+ $data = [];
+
+ // Validate that either all expenses do not have a client or if there is a client, it is the same client
+ foreach ($expenses as $expense)
+ {
+ if ($expense->client_id) {
+ if (!$clientPublicId) {
+ $clientPublicId = $expense->client_id;
+ } elseif ($clientPublicId != $expense->client_id) {
+ Session::flash('error', trans('texts.expense_error_multiple_clients'));
+ return Redirect::to('expenses');
+ }
+ }
+
+ if ($expense->invoice_id) {
+ Session::flash('error', trans('texts.expense_error_invoiced'));
+ return Redirect::to('expenses');
+ }
+
+ $account = Auth::user()->account;
+ $data[] = [
+ 'publicId' => $expense->public_id,
+ 'description' => $expense->public_notes,
+ 'qty' => 1,
+ 'cost' => $expense->present()->converted_amount,
+ ];
+ }
+
+ return Redirect::to("invoices/create/{$clientPublicId}")->with('expenses', $data);
+ break;
+
+ default:
+ $count = $this->expenseService->bulk($ids, $action);
+ }
+
+ if ($count > 0) {
+ $message = Utils::pluralize($action.'d_expense', $count);
+ Session::flash('message', $message);
+ }
+
+ return Redirect::to('expenses');
+ }
+
+ private static function getViewModel()
+ {
+ return [
+ 'data' => Input::old('data'),
+ 'account' => Auth::user()->account,
+ 'sizes' => Cache::get('sizes'),
+ 'paymentTerms' => Cache::get('paymentTerms'),
+ 'industries' => Cache::get('industries'),
+ 'currencies' => Cache::get('currencies'),
+ 'languages' => Cache::get('languages'),
+ 'countries' => Cache::get('countries'),
+ 'customLabel1' => Auth::user()->account->custom_vendor_label1,
+ 'customLabel2' => Auth::user()->account->custom_vendor_label2,
+ ];
+ }
+
+ public function show($publicId)
+ {
+ Session::reflash();
+
+ return Redirect::to("expenses/{$publicId}/edit");
+ }
+}
diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php
new file mode 100644
index 000000000000..540e38d97296
--- /dev/null
+++ b/app/Http/Controllers/ExportController.php
@@ -0,0 +1,181 @@
+input('format');
+ $date = date('Y-m-d');
+ $fileName = "invoice-ninja-{$date}";
+
+ if ($format === 'JSON') {
+ return $this->returnJSON($request, $fileName);
+ } elseif ($format === 'CSV') {
+ return $this->returnCSV($request, $fileName);
+ } else {
+ return $this->returnXLS($request, $fileName);
+ }
+ }
+
+ private function returnJSON($request, $fileName)
+ {
+ $output = fopen('php://output', 'w') or Utils::fatalError();
+ header('Content-Type:application/json');
+ header("Content-Disposition:attachment;filename={$fileName}.json");
+
+ $manager = new Manager();
+ $manager->setSerializer(new ArraySerializer());
+
+ $account = Auth::user()->account;
+ $account->loadAllData();
+
+ $resource = new Item($account, new AccountTransformer);
+ $data = $manager->createData($resource)->toArray();
+
+ return response()->json($data);
+ }
+
+
+ private function returnCSV($request, $fileName)
+ {
+ $data = $this->getData($request);
+
+ return Excel::create($fileName, function($excel) use ($data) {
+ $excel->sheet('', function($sheet) use ($data) {
+ $sheet->loadView('export', $data);
+ });
+ })->download('csv');
+ }
+
+ private function returnXLS($request, $fileName)
+ {
+ $user = Auth::user();
+ $data = $this->getData($request);
+
+ return Excel::create($fileName, function($excel) use ($user, $data) {
+
+ $excel->setTitle($data['title'])
+ ->setCreator($user->getDisplayName())
+ ->setLastModifiedBy($user->getDisplayName())
+ ->setDescription('')
+ ->setSubject('')
+ ->setKeywords('')
+ ->setCategory('')
+ ->setManager('')
+ ->setCompany($user->account->getDisplayName());
+
+ foreach ($data as $key => $val) {
+ if ($key === 'account' || $key === 'title' || $key === 'multiUser') {
+ continue;
+ }
+ $label = trans("texts.{$key}");
+ $excel->sheet($label, function($sheet) use ($key, $data) {
+ if ($key === 'quotes') {
+ $key = 'invoices';
+ $data['entityType'] = ENTITY_QUOTE;
+ }
+ $sheet->loadView("export.{$key}", $data);
+ });
+ }
+ })->download('xls');
+ }
+
+ private function getData($request)
+ {
+ $account = Auth::user()->account;
+
+ $data = [
+ 'account' => $account,
+ 'title' => 'Invoice Ninja v' . NINJA_VERSION . ' - ' . $account->formatDateTime($account->getDateTime()),
+ 'multiUser' => $account->users->count() > 1
+ ];
+
+ if ($request->input(ENTITY_CLIENT)) {
+ $data['clients'] = Client::scope()
+ ->with('user', 'contacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+
+ $data['contacts'] = Contact::scope()
+ ->with('user', 'client.contacts')
+ ->withTrashed()
+ ->get();
+
+ $data['credits'] = Credit::scope()
+ ->with('user', 'client.contacts')
+ ->get();
+ }
+
+ if ($request->input(ENTITY_TASK)) {
+ $data['tasks'] = Task::scope()
+ ->with('user', 'client.contacts')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
+ if ($request->input(ENTITY_INVOICE)) {
+ $data['invoices'] = Invoice::scope()
+ ->with('user', 'client.contacts', 'invoice_status')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->where('is_quote', '=', false)
+ ->where('is_recurring', '=', false)
+ ->get();
+
+ $data['quotes'] = Invoice::scope()
+ ->with('user', 'client.contacts', 'invoice_status')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->where('is_quote', '=', true)
+ ->where('is_recurring', '=', false)
+ ->get();
+ }
+
+ if ($request->input(ENTITY_PAYMENT)) {
+ $data['payments'] = Payment::scope()
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->with('user', 'client.contacts', 'payment_type', 'invoice', 'account_gateway.gateway')
+ ->get();
+ }
+
+
+ if ($request->input(ENTITY_VENDOR)) {
+ $data['clients'] = Vendor::scope()
+ ->with('user', 'vendorcontacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+
+ $data['vendor_contacts'] = VendorContact::scope()
+ ->with('user', 'vendor.contacts')
+ ->withTrashed()
+ ->get();
+ /*
+ $data['expenses'] = Credit::scope()
+ ->with('user', 'client.contacts')
+ ->get();
+ */
+ }
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 1308a4378b26..571ac731938c 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -1,7 +1,6 @@
true]);
}
+
+ public function viewLogo()
+ {
+ return View::make('public.logo');
+ }
public function invoiceNow()
{
@@ -49,11 +53,18 @@ class HomeController extends BaseController
Auth::logout();
}
+ // Track the referral/campaign code
+ foreach (['rc', 'utm_campaign'] as $code) {
+ if (Input::has($code)) {
+ Session::set(SESSION_REFERRAL_CODE, Input::get($code));
+ }
+ }
+
if (Auth::check()) {
$redirectTo = Input::get('redirect_to', 'invoices/create');
return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up'));
} else {
- return View::make('public.header', ['invoiceNow' => true]);
+ return View::make('public.invoice_now');
}
}
diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php
new file mode 100644
index 000000000000..b078fb6f955e
--- /dev/null
+++ b/app/Http/Controllers/ImportController.php
@@ -0,0 +1,92 @@
+importService = $importService;
+ }
+
+ public function doImport()
+ {
+ $source = Input::get('source');
+ $files = [];
+
+ foreach (ImportService::$entityTypes as $entityType) {
+ if (Input::file("{$entityType}_file")) {
+ $files[$entityType] = Input::file("{$entityType}_file")->getRealPath();
+ if ($source === IMPORT_CSV) {
+ Session::forget("{$entityType}-data");
+ }
+ }
+ }
+
+ try {
+ if ($source === IMPORT_CSV) {
+ $data = $this->importService->mapCSV($files);
+ return View::make('accounts.import_map', ['data' => $data]);
+ } else {
+ $results = $this->importService->import($source, $files);
+ return $this->showResult($results);
+ }
+ } catch (Exception $exception) {
+ Utils::logError($exception);
+ Session::flash('error', $exception->getMessage());
+ return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
+ }
+ }
+
+ public function doImportCSV()
+ {
+ $map = Input::get('map');
+ $headers = Input::get('headers');
+
+ try {
+ $results = $this->importService->importCSV($map, $headers);
+ return $this->showResult($results);
+ } catch (Exception $exception) {
+ Utils::logError($exception);
+ Session::flash('error', $exception->getMessage());
+ return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
+ }
+ }
+
+ private function showResult($results)
+ {
+ $message = '';
+ $skipped = [];
+
+ foreach ($results as $entityType => $entityResults) {
+ if ($count = count($entityResults[RESULT_SUCCESS])) {
+ $message .= trans("texts.created_{$entityType}s", ['count' => $count]) . '
- ', $response->getMessage()); + $this->error('Unknown', $response->getMessage(), $accountGateway); + if ($onSite) { + return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); + } else { + return Redirect::to('view/'.$invitationKey); + } } } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage."
".$e->getMessage()); - Utils::logError('Payment Error [uncaught]:' . Utils::getErrorString($e)); - + $this->error('Uncaught', false, $accountGateway, $e); if ($onSite) { - return Redirect::to('payment/'.$invitationKey)->withInput(); + return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } else { return Redirect::to('view/'.$invitationKey); } } } - private function createPayment($invitation, $ref, $payerId = null) - { - $invoice = $invitation->invoice; - $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); - - if ($invoice->account->account_key == NINJA_ACCOUNT_KEY - && $invoice->amount == PRO_PLAN_PRICE) { - $account = Account::with('users')->find($invoice->client->public_id); - if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); - $account->pro_plan_paid = $date->modify('+1 year')->format('Y-m-d'); - } else { - $account->pro_plan_paid = date_create()->format('Y-m-d'); - } - $account->save(); - - $user = $account->users()->first(); - $this->accountRepo->syncAccounts($user->id, $account->pro_plan_paid); - } - - $payment = Payment::createNew($invitation); - $payment->invitation_id = $invitation->id; - $payment->account_gateway_id = $accountGateway->id; - $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->getRequestedAmount(); - $payment->client_id = $invoice->client_id; - $payment->contact_id = $invitation->contact_id; - $payment->transaction_reference = $ref; - $payment->payment_date = date_create()->format('Y-m-d'); - - if ($payerId) { - $payment->payer_id = $payerId; - } - - $payment->save(); - - Event::fire(new InvoicePaid($payment)); - - return $payment; - } - public function offsite_payment() { $payerId = Request::query('PayerID'); @@ -692,101 +506,102 @@ class PaymentController extends BaseController if (!$token) { $token = Session::pull('transaction_reference'); } - if (!$token) { return redirect(NINJA_WEB_URL); } $invitation = Invitation::with('invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('transaction_reference', '=', $token)->firstOrFail(); $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; - $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); - $gateway = self::createGateway($accountGateway); + if ($payerId) { + $paymentType = PAYMENT_TYPE_PAYPAL; + } else { + $paymentType = Session::get($invitation->id . 'payment_type'); + } + if (!$paymentType) { + $this->error('No-Payment-Type', false, false); + return Redirect::to($invitation->getLink()); + } + $accountGateway = $account->getGatewayByType($paymentType); + $gateway = $this->paymentService->createGateway($accountGateway); // Check for Dwolla payment error if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) { - $errorMessage = trans('texts.payment_error')."\n\n".Input::get('error_description'); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage); - return Redirect::to('view/'.$invitation->invitation_key); + $this->error('Dwolla', Input::get('error_description'), $accountGateway); + return Redirect::to($invitation->getLink()); + } + + // PayFast transaction referencce + if ($accountGateway->isGateway(GATEWAY_PAYFAST) && Request::has('pt')) { + $token = Request::query('pt'); } try { - if (method_exists($gateway, 'completePurchase')) { - $details = self::getPaymentDetails($invitation); - $response = $gateway->completePurchase($details)->send(); - $ref = $response->getTransactionReference(); + if (method_exists($gateway, 'completePurchase') + && !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT) + && !$accountGateway->isGateway(GATEWAY_CHECKOUT_COM)) { + $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway); - if ($response->isSuccessful()) { - $payment = self::createPayment($invitation, $ref, $payerId); + $response = $this->paymentService->completePurchase($gateway, $accountGateway, $details, $token); + + $ref = $response->getTransactionReference() ?: $token; + + if ($response->isCancelled()) { + // do nothing + } elseif ($response->isSuccessful()) { + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId); Session::flash('message', trans('texts.applied_payment')); - - return Redirect::to('view/'.$invitation->invitation_key); } else { - $errorMessage = trans('texts.payment_error')."\n\n".$response->getMessage(); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage); - - return Redirect::to('view/'.$invitation->invitation_key); + $this->error('offsite', $response->getMessage(), $accountGateway); } + return Redirect::to($invitation->getLink()); } else { - $payment = self::createPayment($invitation, $token, $payerId); + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $token, $payerId); Session::flash('message', trans('texts.applied_payment')); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage."\n\n".$e->getMessage()); - return Redirect::to('view/'.$invitation->invitation_key); + $this->error('Offsite-uncaught', false, $accountGateway, $e); + return Redirect::to($invitation->getLink()); } } - public function store() + public function store(CreatePaymentRequest $request) { - return $this->save(); - } + $input = $request->input(); + $input['invoice_id'] = Invoice::getPrivateId($input['invoice']); + $input['client_id'] = Client::getPrivateId($input['client']); + $payment = $this->paymentRepo->save($input); - public function update($publicId) - { - return $this->save($publicId); - } - - private function save($publicId = null) - { - if (!$publicId && $errors = $this->paymentRepo->getErrors(Input::all())) { - $url = $publicId ? 'payments/'.$publicId.'/edit' : 'payments/create'; - - return Redirect::to($url) - ->withErrors($errors) - ->withInput(); + if (Input::get('email_receipt')) { + $this->contactMailer->sendPaymentConfirmation($payment); + Session::flash('message', trans('texts.created_payment_emailed_client')); } else { - $payment = $this->paymentRepo->save($publicId, Input::all()); - - if ($publicId) { - Session::flash('message', trans('texts.updated_payment')); - - return Redirect::to('payments/'); - } else { - if (Input::get('email_receipt')) { - $this->contactMailer->sendPaymentConfirmation($payment); - Session::flash('message', trans('texts.created_payment_emailed_client')); - } else { - Session::flash('message', trans('texts.created_payment')); - } - - return Redirect::to('clients/'.Input::get('client')); - } + Session::flash('message', trans('texts.created_payment')); } + + return redirect()->to($payment->client->getRoute()); + } + + public function update(UpdatePaymentRequest $request) + { + $input = $request->input(); + $payment = $this->paymentRepo->save($input); + + Session::flash('message', trans('texts.updated_payment')); + + return redirect()->to($payment->getRoute()); } public function bulk() { $action = Input::get('action'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->paymentRepo->bulk($ids, $action); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + $count = $this->paymentService->bulk($ids, $action); if ($count > 0) { $message = Utils::pluralize($action.'d_payment', $count); @@ -795,4 +610,16 @@ class PaymentController extends BaseController return Redirect::to('payments'); } + + private function error($type, $error, $accountGateway = false, $exception = false) + { + $message = ''; + if ($accountGateway && $accountGateway->gateway) { + $message = $accountGateway->gateway->name . ': '; + } + $message .= $error ?: trans('texts.payment_error'); + + Session::flash('error', $message); + Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message)); + } } diff --git a/app/Http/Controllers/PaymentTermController.php b/app/Http/Controllers/PaymentTermController.php new file mode 100644 index 000000000000..623ca1bf42da --- /dev/null +++ b/app/Http/Controllers/PaymentTermController.php @@ -0,0 +1,103 @@ +paymentTermService = $paymentTermService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS); + } + + public function getDatatable() + { + return $this->paymentTermService->getDatatable(); + } + + public function edit($publicId) + { + $data = [ + 'paymentTerm' => PaymentTerm::scope($publicId)->firstOrFail(), + 'method' => 'PUT', + 'url' => 'payment_terms/'.$publicId, + 'title' => trans('texts.edit_payment_term'), + ]; + + return View::make('accounts.payment_term', $data); + } + + public function create() + { + $data = [ + 'paymentTerm' => null, + 'method' => 'POST', + 'url' => 'payment_terms', + 'title' => trans('texts.create_payment_term'), + ]; + + return View::make('accounts.payment_term', $data); + } + + public function store() + { + return $this->save(); + } + + public function update($publicId) + { + return $this->save($publicId); + } + + private function save($publicId = false) + { + if ($publicId) { + $paymentTerm = PaymentTerm::scope($publicId)->firstOrFail(); + } else { + $paymentTerm = PaymentTerm::createNew(); + } + + $paymentTerm->name = trim(Input::get('name')); + $paymentTerm->num_days = Utils::parseInt(Input::get('num_days')); + $paymentTerm->save(); + + $message = $publicId ? trans('texts.updated_payment_term') : trans('texts.created_payment_term'); + Session::flash('message', $message); + + return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS); + } + + public function bulk() + { + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->paymentTermService->bulk($ids, $action); + + Session::flash('message', trans('texts.archived_payment_term')); + + return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS); + } + +} diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index d971115b22f1..e25f486688d5 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -12,58 +12,58 @@ use Session; use Redirect; use App\Models\Product; +use App\Models\TaxRate; +use App\Services\ProductService; class ProductController extends BaseController { + protected $productService; + + public function __construct(ProductService $productService) + { + parent::__construct(); + + $this->productService = $productService; + } + + public function index() + { + return Redirect::to('settings/' . ACCOUNT_PRODUCTS); + } + public function getDatatable() { - $query = DB::table('products') - ->where('products.account_id', '=', Auth::user()->account_id) - ->where('products.deleted_at', '=', null) - ->select('products.public_id', 'products.product_key', 'products.notes', 'products.cost'); - - return Datatable::query($query) - ->addColumn('product_key', function ($model) { return link_to('products/'.$model->public_id.'/edit', $model->product_key); }) - ->addColumn('notes', function ($model) { return nl2br(Str::limit($model->notes, 100)); }) - ->addColumn('cost', function ($model) { return Utils::formatMoney($model->cost); }) - ->addColumn('dropdown', function ($model) { - return '
'; - }) - ->orderColumns(['cost', 'product_key', 'cost']) - ->make(); + return $this->productService->getDatatable(Auth::user()->account_id); } public function edit($publicId) { + $account = Auth::user()->account; + $data = [ - 'showBreadcrumbs' => false, - 'product' => Product::scope($publicId)->firstOrFail(), - 'method' => 'PUT', - 'url' => 'products/'.$publicId, - 'title' => trans('texts.edit_product'), - ]; + 'account' => $account, + 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, + 'product' => Product::scope($publicId)->firstOrFail(), + 'method' => 'PUT', + 'url' => 'products/'.$publicId, + 'title' => trans('texts.edit_product'), + ]; return View::make('accounts.product', $data); } public function create() { + $account = Auth::user()->account; + $data = [ - 'showBreadcrumbs' => false, - 'product' => null, - 'method' => 'POST', - 'url' => 'products', - 'title' => trans('texts.create_product'), - ]; + 'account' => $account, + 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, + 'product' => null, + 'method' => 'POST', + 'url' => 'products', + 'title' => trans('texts.create_product'), + ]; return View::make('accounts.product', $data); } @@ -89,21 +89,24 @@ class ProductController extends BaseController $product->product_key = trim(Input::get('product_key')); $product->notes = trim(Input::get('notes')); $product->cost = trim(Input::get('cost')); + $product->default_tax_rate_id = Input::get('default_tax_rate_id'); + $product->save(); $message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product'); Session::flash('message', $message); - return Redirect::to('company/products'); + return Redirect::to('settings/' . ACCOUNT_PRODUCTS); } - public function archive($publicId) + public function bulk() { - $product = Product::scope($publicId)->firstOrFail(); - $product->delete(); + $action = Input::get('bulk_action'); + $ids = Input::get('bulk_public_id'); + $count = $this->productService->bulk($ids, $action); Session::flash('message', trans('texts.archived_product')); - return Redirect::to('company/products'); + return Redirect::to('settings/' . ACCOUNT_PRODUCTS); } } diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php new file mode 100644 index 000000000000..834a89beac59 --- /dev/null +++ b/app/Http/Controllers/PublicClientController.php @@ -0,0 +1,347 @@ +invoiceRepo = $invoiceRepo; + $this->paymentRepo = $paymentRepo; + $this->activityRepo = $activityRepo; + $this->paymentService = $paymentService; + } + + public function view($invitationKey) + { + if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + ]); + } + + $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $invoice->account; + + if (!$account->checkSubdomain(Request::server('HTTP_HOST'))) { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + ]); + } + + if (!Input::has('phantomjs') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { + if ($invoice->is_quote) { + event(new QuoteInvitationWasViewed($invoice, $invitation)); + } else { + event(new InvoiceInvitationWasViewed($invoice, $invitation)); + } + } + + Session::put($invitationKey, true); // track this invitation has been seen + Session::put('invitation_key', $invitationKey); // track current invitation + + $account->loadLocalizationSettings($client); + + $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); + $invoice->due_date = Utils::fromSqlDate($invoice->due_date); + $invoice->is_pro = $account->isPro(); + $invoice->invoice_fonts = $account->getFontsData(); + + if ($invoice->invoice_design_id == CUSTOM_DESIGN) { + $invoice->invoice_design->javascript = $account->custom_design; + } else { + $invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake; + } + $contact = $invitation->contact; + $contact->setVisible([ + 'first_name', + 'last_name', + 'email', + 'phone', + ]); + + $paymentTypes = $this->getPaymentTypes($client, $invitation); + $paymentURL = ''; + if (count($paymentTypes)) { + $paymentURL = $paymentTypes[0]['url']; + if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { + $paymentURL = URL::to($paymentURL); + } + } + + $showApprove = $invoice->quote_invoice_id ? false : true; + if ($invoice->due_date) { + $showApprove = time() < strtotime($invoice->due_date); + } + if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) { + $showApprove = false; + } + + // Checkout.com requires first getting a payment token + $checkoutComToken = false; + $checkoutComKey = false; + if ($accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM)) { + if ($checkoutComToken = $this->paymentService->getCheckoutComToken($invitation)) { + $checkoutComKey = $accountGateway->getConfigField('publicApiKey'); + $invitation->transaction_reference = $checkoutComToken; + $invitation->save(); + } + } + + $data = array( + 'account' => $account, + 'showApprove' => $showApprove, + 'showBreadcrumbs' => false, + 'hideLogo' => $account->isWhiteLabel(), + 'hideHeader' => $account->isNinjaAccount(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'invoice' => $invoice->hidePrivateFields(), + 'invitation' => $invitation, + 'invoiceLabels' => $account->getInvoiceLabels(), + 'contact' => $contact, + 'paymentTypes' => $paymentTypes, + 'paymentURL' => $paymentURL, + 'checkoutComToken' => $checkoutComToken, + 'checkoutComKey' => $checkoutComKey, + 'phantomjs' => Input::has('phantomjs'), + ); + + return View::make('invoices.view', $data); + } + + private function getPaymentTypes($client, $invitation) + { + $paymentTypes = []; + $account = $client->account; + + if ($client->getGatewayToken()) { + $paymentTypes[] = [ + 'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file') + ]; + } + foreach(Gateway::$paymentTypes as $type) { + if ($account->getGatewayByType($type)) { + $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); + $url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"); + + // PayPal doesn't allow being run in an iframe so we need to open in new tab + if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) { + $url = 'javascript:window.open("'.$url.'", "_blank")'; + } + $paymentTypes[] = [ + 'url' => $url, 'label' => trans('texts.'.strtolower($type)) + ]; + } + } + + return $paymentTypes; + } + + public function dashboard() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $invoice = $invitation->invoice; + $client = $invoice->client; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'account' => $account, + 'client' => $client, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + ]; + + return response()->view('invited.dashboard', $data); + } + + public function activityDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + $invoice = $invitation->invoice; + + $query = $this->activityRepo->findByClientId($invoice->client_id); + $query->where('activities.adjustment', '!=', 0); + + return Datatable::query($query) + ->addColumn('activities.id', function ($model) { return Utils::timestampToDateTimeString(strtotime($model->created_at)); }) + ->addColumn('activity_type_id', function ($model) { + $data = [ + 'client' => Utils::getClientDisplayName($model), + 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name), + 'invoice' => trans('texts.invoice') . ' ' . $model->invoice, + 'contact' => Utils::getClientDisplayName($model), + 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), + ]; + + return trans("texts.activity_{$model->activity_type_id}", $data); + }) + ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); }) + ->addColumn('adjustment', function ($model) { return $model->adjustment != 0 ? Utils::wrapAdjustment($model->adjustment, $model->currency_id, $model->country_id) : ''; }) + ->make(); + } + + public function invoiceIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.invoices'), + 'entityType' => ENTITY_INVOICE, + 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']), + ]; + + return response()->view('public_list', $data); + } + + public function invoiceDatatable() + { + if (!$invitation = $this->getInvitation()) { + return ''; + } + + return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_INVOICE, Input::get('sSearch')); + } + + + public function paymentIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'entityType' => ENTITY_PAYMENT, + 'title' => trans('texts.payments'), + 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) + ]; + + return response()->view('public_list', $data); + } + + public function paymentDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + $payments = $this->paymentRepo->findForContact($invitation->contact->id, Input::get('sSearch')); + + return Datatable::query($payments) + ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number) : $model->invoice_number; }) + ->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; }) + ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); }) + ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }) + ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) + ->make(); + } + + public function quoteIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.quotes'), + 'entityType' => ENTITY_QUOTE, + 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), + ]; + + return response()->view('public_list', $data); + } + + + public function quoteDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + + return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, Input::get('sSearch')); + } + + private function returnError() + { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + ]); + } + + private function getInvitation() + { + $invitationKey = session('invitation_key'); + + if (!$invitationKey) { + return false; + } + + $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); + + if (!$invitation || $invitation->is_deleted) { + return false; + } + + $invoice = $invitation->invoice; + + if (!$invoice || $invoice->is_deleted) { + return false; + } + + return $invitation; + } + +} diff --git a/app/Http/Controllers/QuoteApiController.php b/app/Http/Controllers/QuoteApiController.php index 83e5e8781179..3e3cfa580c23 100644 --- a/app/Http/Controllers/QuoteApiController.php +++ b/app/Http/Controllers/QuoteApiController.php @@ -1,32 +1,64 @@ invoiceRepo = $invoiceRepo; } + /** + * @SWG\Get( + * path="/quotes", + * tags={"quote"}, + * summary="List of quotes", + * @SWG\Response( + * response=200, + * description="A list with quotes", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { + $paginator = Invoice::scope(); $invoices = Invoice::scope() - ->with('client', 'user') - ->where('invoices.is_quote', '=', true) - ->orderBy('created_at', 'desc') - ->get(); - $invoices = Utils::remapPublicIds($invoices); + ->with('client', 'invitations', 'user', 'invoice_items') + ->where('invoices.is_quote', '=', true); - $response = json_encode($invoices, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(count($invoices)); + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $invoices->whereHas('client', $filter); + $paginator->whereHas('client', $filter); + } - return Response::make($response, 200, $headers); + $invoices = $invoices->orderBy('created_at', 'desc')->paginate(); + + $transformer = new QuoteTransformer(\Auth::user()->account, Input::get('serializer')); + $paginator = $paginator->paginate(); + + $data = $this->createCollection($invoices, $transformer, 'quotes', $paginator); + + return $this->response($data); } /* diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index d84ce2fece65..47b5c1a338e4 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -24,24 +24,24 @@ use App\Models\Invoice; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; -use App\Ninja\Repositories\TaxRateRepository; -use App\Events\QuoteApproved; +use App\Events\QuoteInvitationWasApproved; +use App\Services\InvoiceService; class QuoteController extends BaseController { protected $mailer; protected $invoiceRepo; protected $clientRepo; - protected $taxRateRepo; + protected $invoiceService; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, TaxRateRepository $taxRateRepo) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService) { parent::__construct(); $this->mailer = $mailer; $this->invoiceRepo = $invoiceRepo; $this->clientRepo = $clientRepo; - $this->taxRateRepo = $taxRateRepo; + $this->invoiceService = $invoiceService; } public function index() @@ -53,40 +53,19 @@ class QuoteController extends BaseController $data = [ 'title' => trans('texts.quotes'), 'entityType' => ENTITY_QUOTE, - 'columns' => Utils::trans(['checkbox', 'quote_number', 'client', 'quote_date', 'quote_total', 'due_date', 'status', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'quote_number', + 'client', + 'quote_date', + 'quote_total', + 'valid_until', + 'status', + 'action' + ]), ]; - /* - if (Invoice::scope()->where('is_recurring', '=', true)->count() > 0) - { - $data['secEntityType'] = ENTITY_RECURRING_INVOICE; - $data['secColumns'] = Utils::trans(['checkbox', 'frequency', 'client', 'start_date', 'end_date', 'quote_total', 'action']); - } - */ - - return View::make('list', $data); - } - - public function clientIndex() - { - $invitationKey = Session::get('invitation_key'); - if (!$invitationKey) { - return Redirect::to('/setup'); - } - - $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $account = $invitation->account; - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - - $data = [ - 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'title' => trans('texts.quotes'), - 'entityType' => ENTITY_QUOTE, - 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), - ]; - - return View::make('public_list', $data); + return response()->view('list', $data); } public function getDatatable($clientPublicId = null) @@ -94,26 +73,7 @@ class QuoteController extends BaseController $accountId = Auth::user()->account_id; $search = Input::get('sSearch'); - return $this->invoiceRepo->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search); - } - - public function getClientDatatable() - { - $search = Input::get('sSearch'); - $invitationKey = Session::get('invitation_key'); - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); - - if (!$invitation || $invitation->is_deleted) { - return []; - } - - $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - return []; - } - - return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, $search); + return $this->invoiceService->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search); } public function create($clientPublicId = 0) @@ -122,25 +82,24 @@ class QuoteController extends BaseController return Redirect::to('/invoices/create'); } - $client = null; - $invoiceNumber = Auth::user()->account->getNextInvoiceNumber(true); - $account = Account::with('country')->findOrFail(Auth::user()->account_id); - + $account = Auth::user()->account; + $clientId = null; if ($clientPublicId) { - $client = Client::scope($clientPublicId)->firstOrFail(); + $clientId = Client::getPrivateId($clientPublicId); } + $invoice = $account->createInvoice(ENTITY_QUOTE, $clientId); + $invoice->public_id = 0; - $data = array( - 'account' => $account, - 'invoice' => null, - 'data' => Input::old('data'), - 'invoiceNumber' => $invoiceNumber, - 'method' => 'POST', - 'url' => 'invoices', - 'title' => trans('texts.new_quote'), - 'client' => $client, ); + $data = [ + 'entityType' => $invoice->getEntityType(), + 'invoice' => $invoice, + 'data' => Input::old('data'), + 'method' => 'POST', + 'url' => 'invoices', + 'title' => trans('texts.new_quote'), + ]; $data = array_merge($data, self::getViewModel()); - + return View::make('invoices.edit', $data); } @@ -156,8 +115,10 @@ class QuoteController extends BaseController 'currencies' => Cache::get('currencies'), 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), + 'languages' => Cache::get('languages'), 'industries' => Cache::get('industries'), 'invoiceDesigns' => InvoiceDesign::getDesigns(), + 'invoiceFonts' => Cache::get('fonts'), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), 'isRecurring' => false, ]; @@ -165,22 +126,21 @@ class QuoteController extends BaseController public function bulk() { - $action = Input::get('action'); + $action = Input::get('bulk_action') ?: Input::get('action');; + $ids = Input::get('bulk_public_id') ?: (Input::get('public_id') ?: Input::get('ids')); if ($action == 'convert') { - $invoice = Invoice::with('invoice_items')->scope(Input::get('id'))->firstOrFail(); - $clone = $this->invoiceRepo->cloneInvoice($invoice, $invoice->id); + $invoice = Invoice::with('invoice_items')->scope($ids)->firstOrFail(); + $clone = $this->invoiceService->convertQuote($invoice); Session::flash('message', trans('texts.converted_to_invoice')); return Redirect::to('invoices/'.$clone->public_id); } - - $statusId = Input::get('statusId'); - $ids = Input::get('id') ? Input::get('id') : Input::get('ids'); - $count = $this->invoiceRepo->bulk($ids, $action, $statusId); + + $count = $this->invoiceService->bulk($ids, $action); if ($count > 0) { - $key = $action == 'mark' ? "updated_quote" : "{$action}d_quote"; + $key = $action == 'markSent' ? "updated_quote" : "{$action}d_quote"; $message = Utils::pluralize($key, $count); Session::flash('message', $message); } @@ -197,19 +157,8 @@ class QuoteController extends BaseController $invitation = Invitation::with('invoice.invoice_items', 'invoice.invitations')->where('invitation_key', '=', $invitationKey)->firstOrFail(); $invoice = $invitation->invoice; - if ($invoice->is_quote && !$invoice->quote_invoice_id) { - Event::fire(new QuoteApproved($invoice)); - Activity::approveQuote($invitation); - - $invoice = $this->invoiceRepo->cloneInvoice($invoice, $invoice->id); - Session::flash('message', trans('texts.converted_to_invoice')); - - foreach ($invoice->invitations as $invitationClone) { - if ($invitation->contact_id == $invitationClone->contact_id) { - $invitationKey = $invitationClone->invitation_key; - } - } - } + $invitationKey = $this->invoiceService->approveQuote($invoice, $invitation); + Session::flash('message', trans('texts.quote_is_approved')); return Redirect::to("view/{$invitationKey}"); } diff --git a/app/Http/Controllers/RecurringInvoiceController.php b/app/Http/Controllers/RecurringInvoiceController.php new file mode 100644 index 000000000000..c59370647bb0 --- /dev/null +++ b/app/Http/Controllers/RecurringInvoiceController.php @@ -0,0 +1,36 @@ +invoiceRepo = $invoiceRepo; + } + + public function index() + { + $data = [ + 'title' => trans('texts.recurring_invoices'), + 'entityType' => ENTITY_RECURRING_INVOICE, + 'columns' => Utils::trans([ + 'checkbox', + 'frequency', + 'client', + 'start_date', + 'end_date', + 'invoice_total', + 'action' + ]) + ]; + + return response()->view('list', $data); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 25114f8d36aa..1757cb58501c 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -9,7 +9,6 @@ use DateInterval; use DatePeriod; use Session; use View; - use App\Models\Account; class ReportController extends BaseController @@ -17,7 +16,7 @@ class ReportController extends BaseController public function d3() { $message = ''; - $fileName = storage_path() . '/dataviz_sample.txt'; + $fileName = storage_path().'/dataviz_sample.txt'; if (Auth::user()->account->isPro()) { $account = Account::where('id', '=', Auth::user()->account->id) @@ -33,7 +32,6 @@ class ReportController extends BaseController } $data = [ - 'feature' => ACCOUNT_DATA_VISUALIZATIONS, 'clients' => $clients, 'message' => $message, ]; @@ -56,200 +54,13 @@ class ReportController extends BaseController } else { $groupBy = 'MONTH'; $chartType = 'Bar'; - $reportType = ''; + $reportType = ENTITY_INVOICE; $startDate = Utils::today(false)->modify('-3 month'); $endDate = Utils::today(false); $enableReport = true; $enableChart = true; } - $datasets = []; - $labels = []; - $maxTotals = 0; - $width = 10; - - $displayData = []; - $exportData = []; - $reportTotals = [ - 'amount' => [], - 'balance' => [], - 'paid' => [] - ]; - - if ($reportType) { - $columns = ['client', 'amount', 'paid', 'balance']; - } else { - $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; - } - - - if (Auth::user()->account->isPro()) { - - if ($enableReport) { - $query = DB::table('invoices') - ->join('clients', 'clients.id', '=', 'invoices.client_id') - ->join('contacts', 'contacts.client_id', '=', 'clients.id') - ->where('invoices.account_id', '=', Auth::user()->account_id) - ->where('invoices.is_deleted', '=', false) - ->where('clients.is_deleted', '=', false) - ->where('contacts.deleted_at', '=', null) - ->where('invoices.invoice_date', '>=', $startDate->format('Y-m-d')) - ->where('invoices.invoice_date', '<=', $endDate->format('Y-m-d')) - ->where('invoices.is_quote', '=', false) - ->where('invoices.is_recurring', '=', false) - ->where('contacts.is_primary', '=', true); - - $select = ['clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'clients.name as client_name', 'clients.public_id as client_public_id', 'invoices.public_id as invoice_public_id']; - - if ($reportType) { - $query->groupBy('clients.id'); - array_push($select, DB::raw('sum(invoices.amount) amount'), DB::raw('sum(invoices.balance) balance'), DB::raw('sum(invoices.amount - invoices.balance) paid')); - } else { - array_push($select, 'invoices.invoice_number', 'invoices.amount', 'invoices.balance', 'invoices.invoice_date', DB::raw('(invoices.amount - invoices.balance) paid')); - $query->orderBy('invoices.id'); - } - - $query->select($select); - $data = $query->get(); - - foreach ($data as $record) { - // web display data - $displayRow = [link_to('/clients/'.$record->client_public_id, Utils::getClientDisplayName($record))]; - if (!$reportType) { - array_push($displayRow, - link_to('/invoices/'.$record->invoice_public_id, $record->invoice_number), - Utils::fromSqlDate($record->invoice_date, true) - ); - } - array_push($displayRow, - Utils::formatMoney($record->amount, $record->currency_id), - Utils::formatMoney($record->paid, $record->currency_id), - Utils::formatMoney($record->balance, $record->currency_id) - ); - - // export data - $exportRow = [trans('texts.client') => Utils::getClientDisplayName($record)]; - if (!$reportType) { - $exportRow[trans('texts.invoice_number')] = $record->invoice_number; - $exportRow[trans('texts.invoice_date')] = Utils::fromSqlDate($record->invoice_date, true); - } - $exportRow[trans('texts.amount')] = Utils::formatMoney($record->amount, $record->currency_id); - $exportRow[trans('texts.paid')] = Utils::formatMoney($record->paid, $record->currency_id); - $exportRow[trans('texts.balance')] = Utils::formatMoney($record->balance, $record->currency_id); - - $displayData[] = $displayRow; - $exportData[] = $exportRow; - - $accountCurrencyId = Auth::user()->account->currency_id; - $currencyId = $record->currency_id ? $record->currency_id : ($accountCurrencyId ? $accountCurrencyId : DEFAULT_CURRENCY); - if (!isset($reportTotals['amount'][$currencyId])) { - $reportTotals['amount'][$currencyId] = 0; - $reportTotals['balance'][$currencyId] = 0; - $reportTotals['paid'][$currencyId] = 0; - } - $reportTotals['amount'][$currencyId] += $record->amount; - $reportTotals['paid'][$currencyId] += $record->paid; - $reportTotals['balance'][$currencyId] += $record->balance; - } - - if ($action == 'export') - { - self::export($exportData, $reportTotals); - } - } - - if ($enableChart) - { - foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) - { - // SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions. - // Let's see if SQLite is being used. - if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') - { - // Replace the unsupported function with it's date format counterpart - switch ($groupBy) - { - case 'MONTH': - $dateFormat = '%m'; // returns 01-12 - break; - case 'WEEK': - $dateFormat = '%W'; // returns 00-53 - break; - case 'DAYOFYEAR': - $dateFormat = '%j'; // returns 001-366 - break; - default: - $dateFormat = '%m'; // MONTH by default - break; - } - - // Concatenate the year and the chosen timeframe (Month, Week or Day) - $timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)'; - } - else - { - // Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL) - $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; - } - - $records = DB::table($entityType.'s') - ->select(DB::raw('sum(amount) as total, '.$timeframe.' as '.$groupBy)) - ->where('account_id', '=', Auth::user()->account_id) - ->where($entityType.'s.is_deleted', '=', false) - ->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d')) - ->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d')) - ->groupBy($groupBy); - - if ($entityType == ENTITY_INVOICE) - { - $records->where('is_quote', '=', false) - ->where('is_recurring', '=', false); - } - - $totals = $records->lists('total'); - $dates = $records->lists($groupBy); - $data = array_combine($dates, $totals); - - $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); - $endDate->modify('+1 '.$padding); - $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); - $period = new DatePeriod($startDate, $interval, $endDate); - $endDate->modify('-1 '.$padding); - - $totals = []; - - foreach ($period as $d) - { - $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); - // MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365 - $date = $groupBy == 'DAYOFYEAR' ? $d->format('Y') . ($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat); - $totals[] = isset($data[$date]) ? $data[$date] : 0; - - if ($entityType == ENTITY_INVOICE) - { - $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); - $label = $d->format($labelFormat); - $labels[] = $label; - } - } - - $max = max($totals); - - if ($max > 0) - { - $datasets[] = [ - 'totals' => $totals, - 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), - ]; - $maxTotals = max($max, $maxTotals); - } - } - - $width = (ceil($maxTotals / 100) * 100) / 10; - $width = max($width, 10); - } - } - $dateTypes = [ 'DAYOFYEAR' => 'Daily', 'WEEK' => 'Weekly', @@ -262,24 +73,18 @@ class ReportController extends BaseController ]; $reportTypes = [ - '' => '', - 'Client' => trans('texts.client') + ENTITY_CLIENT => trans('texts.client'), + ENTITY_INVOICE => trans('texts.invoice'), + ENTITY_PAYMENT => trans('texts.payment'), ]; $params = [ - 'labels' => $labels, - 'datasets' => $datasets, - 'scaleStepWidth' => $width, 'dateTypes' => $dateTypes, 'chartTypes' => $chartTypes, 'chartType' => $chartType, 'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), 'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), 'groupBy' => $groupBy, - 'feature' => ACCOUNT_CHART_BUILDER, - 'displayData' => $displayData, - 'columns' => $columns, - 'reportTotals' => $reportTotals, 'reportTypes' => $reportTypes, 'reportType' => $reportType, 'enableChart' => $enableChart, @@ -287,9 +92,273 @@ class ReportController extends BaseController 'title' => trans('texts.charts_and_reports'), ]; + if (Auth::user()->account->isPro()) { + if ($enableReport) { + $params = array_merge($params, self::generateReport($reportType, $groupBy, $startDate, $endDate)); + + if ($action == 'export') { + self::export($params['exportData'], $params['reportTotals']); + } + } + if ($enableChart) { + $params = array_merge($params, self::generateChart($groupBy, $startDate, $endDate)); + } + } else { + $params['columns'] = []; + $params['displayData'] = []; + $params['reportTotals'] = [ + 'amount' => [], + 'balance' => [], + 'paid' => [], + ]; + $params['labels'] = []; + $params['datasets'] = []; + $params['scaleStepWidth'] = 100; + } + return View::make('reports.chart_builder', $params); } + private function generateChart($groupBy, $startDate, $endDate) + { + $width = 10; + $datasets = []; + $labels = []; + $maxTotals = 0; + + foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) { + // SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions. + // Let's see if SQLite is being used. + if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') { + // Replace the unsupported function with it's date format counterpart + switch ($groupBy) { + case 'MONTH': + $dateFormat = '%m'; // returns 01-12 + break; + case 'WEEK': + $dateFormat = '%W'; // returns 00-53 + break; + case 'DAYOFYEAR': + $dateFormat = '%j'; // returns 001-366 + break; + default: + $dateFormat = '%m'; // MONTH by default + break; + } + + // Concatenate the year and the chosen timeframe (Month, Week or Day) + $timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)'; + } else { + // Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL) + $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; + } + + $records = DB::table($entityType.'s') + ->select(DB::raw('sum(amount) as total, '.$timeframe.' as '.$groupBy)) + ->where('account_id', '=', Auth::user()->account_id) + ->where($entityType.'s.is_deleted', '=', false) + ->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d')) + ->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d')) + ->groupBy($groupBy); + + if ($entityType == ENTITY_INVOICE) { + $records->where('is_quote', '=', false) + ->where('is_recurring', '=', false); + } + + $totals = $records->lists('total'); + $dates = $records->lists($groupBy); + $data = array_combine($dates, $totals); + + $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); + $endDate->modify('+1 '.$padding); + $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); + $period = new DatePeriod($startDate, $interval, $endDate); + $endDate->modify('-1 '.$padding); + + $totals = []; + + foreach ($period as $d) { + $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); + // MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365 + $date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat); + $totals[] = isset($data[$date]) ? $data[$date] : 0; + + if ($entityType == ENTITY_INVOICE) { + $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); + $label = $d->format($labelFormat); + $labels[] = $label; + } + } + + $max = max($totals); + + if ($max > 0) { + $datasets[] = [ + 'totals' => $totals, + 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), + ]; + $maxTotals = max($max, $maxTotals); + } + } + + $width = (ceil($maxTotals / 100) * 100) / 10; + $width = max($width, 10); + + return [ + 'datasets' => $datasets, + 'scaleStepWidth' => $width, + 'labels' => $labels, + ]; + } + + private function generateReport($reportType, $groupBy, $startDate, $endDate) + { + if ($reportType == ENTITY_CLIENT) { + $columns = ['client', 'amount', 'paid', 'balance']; + } elseif ($reportType == ENTITY_INVOICE) { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; + } else { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'payment_date', 'paid', 'method']; + } + + $query = DB::table('invoices') + ->join('accounts', 'accounts.id', '=', 'invoices.account_id') + ->join('clients', 'clients.id', '=', 'invoices.client_id') + ->join('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('invoices.account_id', '=', Auth::user()->account_id) + ->where('invoices.is_deleted', '=', false) + ->where('clients.is_deleted', '=', false) + ->where('contacts.deleted_at', '=', null) + ->where('invoices.invoice_date', '>=', $startDate->format('Y-m-d')) + ->where('invoices.invoice_date', '<=', $endDate->format('Y-m-d')) + ->where('invoices.is_quote', '=', false) + ->where('invoices.is_recurring', '=', false) + ->where('contacts.is_primary', '=', true); + + $select = [ + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + 'accounts.country_id', + 'contacts.first_name', + 'contacts.last_name', + 'contacts.email', + 'clients.name as client_name', + 'clients.public_id as client_public_id', + 'invoices.public_id as invoice_public_id' + ]; + + if ($reportType == ENTITY_CLIENT) { + $query->groupBy('clients.id'); + array_push($select, DB::raw('sum(invoices.amount) amount'), DB::raw('sum(invoices.balance) balance'), DB::raw('sum(invoices.amount - invoices.balance) paid')); + } else { + $query->orderBy('invoices.id'); + array_push($select, 'invoices.invoice_number', 'invoices.amount', 'invoices.balance', 'invoices.invoice_date'); + if ($reportType == ENTITY_INVOICE) { + array_push($select, DB::raw('(invoices.amount - invoices.balance) paid')); + } else { + $query->join('payments', 'payments.invoice_id', '=', 'invoices.id') + ->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id') + ->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id') + ->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id'); + array_push($select, 'payments.payment_date', 'payments.amount as paid', 'payment_types.name as payment_type', 'gateways.name as gateway'); + } + } + + $query->select($select); + $data = $query->get(); + + $lastInvoiceId = null; + $sameAsLast = false; + $displayData = []; + + $exportData = []; + $reportTotals = [ + 'amount' => [], + 'balance' => [], + 'paid' => [], + ]; + + foreach ($data as $record) { + $sameAsLast = ($lastInvoiceId == $record->invoice_public_id); + $lastInvoiceId = $record->invoice_public_id; + + $displayRow = []; + if ($sameAsLast) { + array_push($displayRow, '', '', '', ''); + } else { + array_push($displayRow, link_to('/clients/'.$record->client_public_id, Utils::getClientDisplayName($record))); + if ($reportType != ENTITY_CLIENT) { + array_push($displayRow, + link_to('/invoices/'.$record->invoice_public_id, $record->invoice_number), + Utils::fromSqlDate($record->invoice_date, true) + ); + } + array_push($displayRow, Utils::formatMoney($record->amount, $record->currency_id, $record->country_id)); + } + if ($reportType != ENTITY_PAYMENT) { + array_push($displayRow, Utils::formatMoney($record->paid, $record->currency_id, $record->country_id)); + } + if ($reportType == ENTITY_PAYMENT) { + array_push($displayRow, + Utils::fromSqlDate($record->payment_date, true), + Utils::formatMoney($record->paid, $record->currency_id, $record->country_id), + $record->gateway ?: $record->payment_type + ); + } else { + array_push($displayRow, Utils::formatMoney($record->balance, $record->currency_id, $record->country_id)); + } + + // export data + $exportRow = []; + if ($sameAsLast) { + $exportRow[trans('texts.client')] = ' '; + $exportRow[trans('texts.invoice_number')] = ' '; + $exportRow[trans('texts.invoice_date')] = ' '; + $exportRow[trans('texts.amount')] = ' '; + } else { + $exportRow[trans('texts.client')] = Utils::getClientDisplayName($record); + if ($reportType != ENTITY_CLIENT) { + $exportRow[trans('texts.invoice_number')] = $record->invoice_number; + $exportRow[trans('texts.invoice_date')] = Utils::fromSqlDate($record->invoice_date, true); + } + $exportRow[trans('texts.amount')] = Utils::formatMoney($record->amount, $record->currency_id, $record->country_id); + } + if ($reportType != ENTITY_PAYMENT) { + $exportRow[trans('texts.paid')] = Utils::formatMoney($record->paid, $record->currency_id, $record->country_id); + } + if ($reportType == ENTITY_PAYMENT) { + $exportRow[trans('texts.payment_date')] = Utils::fromSqlDate($record->payment_date, true); + $exportRow[trans('texts.payment_amount')] = Utils::formatMoney($record->paid, $record->currency_id, $record->country_id); + $exportRow[trans('texts.method')] = $record->gateway ?: $record->payment_type; + } else { + $exportRow[trans('texts.balance')] = Utils::formatMoney($record->balance, $record->currency_id, $record->country_id); + } + + $displayData[] = $displayRow; + $exportData[] = $exportRow; + + $accountCurrencyId = Auth::user()->account->currency_id; + $currencyId = $record->currency_id ? $record->currency_id : ($accountCurrencyId ? $accountCurrencyId : DEFAULT_CURRENCY); + if (!isset($reportTotals['amount'][$currencyId])) { + $reportTotals['amount'][$currencyId] = 0; + $reportTotals['balance'][$currencyId] = 0; + $reportTotals['paid'][$currencyId] = 0; + } + if (!$sameAsLast) { + $reportTotals['amount'][$currencyId] += $record->amount; + $reportTotals['balance'][$currencyId] += $record->balance; + } + $reportTotals['paid'][$currencyId] += $record->paid; + } + + return [ + 'columns' => $columns, + 'displayData' => $displayData, + 'reportTotals' => $reportTotals, + 'exportData' => $exportData + ]; + } + private function export($data, $totals) { $output = fopen('php://output', 'w') or Utils::fatalError(); @@ -299,11 +368,11 @@ class ReportController extends BaseController Utils::exportData($output, $data); foreach (['amount', 'paid', 'balance'] as $type) { - $csv = trans("texts.{$type}") . ','; + $csv = trans("texts.{$type}").','; foreach ($totals[$type] as $currencyId => $amount) { - $csv .= Utils::formatMoney($amount, $currencyId) . ','; + $csv .= Utils::formatMoney($amount, $currencyId).','; } - fwrite($output, $csv . "\n"); + fwrite($output, $csv."\n"); } fclose($output); diff --git a/app/Http/Controllers/TaskApiController.php b/app/Http/Controllers/TaskApiController.php new file mode 100644 index 000000000000..a302944d2b62 --- /dev/null +++ b/app/Http/Controllers/TaskApiController.php @@ -0,0 +1,101 @@ +taskRepo = $taskRepo; + } + + /** + * @SWG\Get( + * path="/tasks", + * tags={"task"}, + * summary="List of tasks", + * @SWG\Response( + * response=200, + * description="A list with tasks", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Task")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function index() + { + $paginator = Task::scope(); + $tasks = Task::scope() + ->with($this->getIncluded()); + + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $tasks->whereHas('client', $filter); + $paginator->whereHas('client', $filter); + } + + $tasks = $tasks->orderBy('created_at', 'desc')->paginate(); + $paginator = $paginator->paginate(); + $transformer = new TaskTransformer(\Auth::user()->account, Input::get('serializer')); + + $data = $this->createCollection($tasks, $transformer, 'tasks', $paginator); + + return $this->response($data); + } + + /** + * @SWG\Post( + * path="/tasks", + * tags={"task"}, + * summary="Create a task", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Task") + * ), + * @SWG\Response( + * response=200, + * description="New task", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Task")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store() + { + $data = Input::all(); + $taskId = isset($data['id']) ? $data['id'] : false; + + if (isset($data['client_id']) && $data['client_id']) { + $data['client'] = $data['client_id']; + } + + $task = $this->taskRepo->save($taskId, $data); + $task = Task::scope($task->public_id)->with('client')->first(); + + $transformer = new TaskTransformer(Auth::user()->account, Input::get('serializer')); + $data = $this->createItem($task, $transformer, 'task'); + + return $this->response($data); + } + +} diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index c9e65148a024..fc822df5d124 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -16,17 +16,20 @@ use App\Models\Client; use App\Models\Task; use App\Ninja\Repositories\TaskRepository; use App\Ninja\Repositories\InvoiceRepository; +use App\Services\TaskService; class TaskController extends BaseController { protected $taskRepo; + protected $taskService; - public function __construct(TaskRepository $taskRepo, InvoiceRepository $invoiceRepo) + public function __construct(TaskRepository $taskRepo, InvoiceRepository $invoiceRepo, TaskService $taskService) { parent::__construct(); $this->taskRepo = $taskRepo; $this->invoiceRepo = $invoiceRepo; + $this->taskService = $taskService; } /** @@ -36,81 +39,27 @@ class TaskController extends BaseController */ public function index() { - self::checkTimezone(); - return View::make('list', array( 'entityType' => ENTITY_TASK, 'title' => trans('texts.tasks'), 'sortCol' => '2', - 'columns' => Utils::trans(['checkbox', 'client', 'date', 'duration', 'description', 'status', 'action']), + 'columns' => Utils::trans([ + 'checkbox', + 'client', + 'date', + 'duration', + 'description', + 'status', + '' + ]), )); } public function getDatatable($clientPublicId = null) { - $tasks = $this->taskRepo->find($clientPublicId, Input::get('sSearch')); - - $table = Datatable::query($tasks); - - if (!$clientPublicId) { - $table->addColumn('checkbox', function ($model) { return ''; }) - ->addColumn('client_name', function ($model) { return $model->client_public_id ? link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)) : ''; }); - } - - return $table->addColumn('created_at', function($model) { return Task::calcStartTime($model); }) - ->addColumn('time_log', function($model) { return gmdate('H:i:s', Task::calcDuration($model)); }) - ->addColumn('description', function($model) { return $model->description; }) - ->addColumn('invoice_number', function($model) { return self::getStatusLabel($model); }) - ->addColumn('dropdown', function ($model) { - $str = ' - '; - }) - ->make(); + return $this->taskService->getDatatable($clientPublicId, Input::get('sSearch')); } - private function getStatusLabel($model) { - if ($model->invoice_number) { - $class = 'success'; - $label = trans('texts.invoiced'); - } elseif ($model->is_running) { - $class = 'primary'; - $label = trans('texts.running'); - } else { - $class = 'default'; - $label = trans('texts.logged'); - } - return " '; - } - - return $str . '" . trans('texts.email_signature') . "
\$account
" . trans('texts.email_signature') . "\n
\$account p>";
}
}
+ public function getReminderDate($reminder)
+ {
+ if ( ! $this->{"enable_reminder{$reminder}"}) {
+ return false;
+ }
+
+ $numDays = $this->{"num_days_reminder{$reminder}"};
+ $plusMinus = $this->{"direction_reminder{$reminder}"} == REMINDER_DIRECTION_AFTER ? '-' : '+';
+
+ return date('Y-m-d', strtotime("$plusMinus $numDays days"));
+ }
+
+ public function getInvoiceReminder($invoice)
+ {
+ for ($i=1; $i<=3; $i++) {
+ if ($date = $this->getReminderDate($i)) {
+ $field = $this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';
+ if ($invoice->$field == $date) {
+ return "reminder{$i}";
+ }
+ }
+ }
+
+ return false;
+ }
+
public function showTokenCheckbox()
{
if (!$this->isGatewayConfigured(GATEWAY_STRIPE)) {
@@ -431,6 +847,154 @@ class Account extends Eloquent
{
return $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT;
}
+
+ public function getSiteUrl()
+ {
+ $url = SITE_URL;
+ $iframe_url = $this->iframe_url;
+
+ if ($iframe_url) {
+ return "{$iframe_url}/?";
+ } else if ($this->subdomain) {
+ $url = Utils::replaceSubdomain($url, $this->subdomain);
+ }
+
+ return $url;
+ }
+
+ public function checkSubdomain($host)
+ {
+ if (!$this->subdomain) {
+ return true;
+ }
+
+ $server = explode('.', $host);
+ $subdomain = $server[0];
+
+ if (!in_array($subdomain, ['app', 'www']) && $subdomain != $this->subdomain) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function showCustomField($field, $entity)
+ {
+ if ($this->isPro()) {
+ return $this->$field ? true : false;
+ }
+
+ if (!$entity) {
+ return false;
+ }
+
+ // convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1'
+ $field = str_replace(['invoice_', 'label'], ['', 'value'], $field);
+
+ return Utils::isEmpty($entity->$field) ? false : true;
+ }
+
+ public function attatchPDF()
+ {
+ return $this->isPro() && $this->pdf_email_attachment;
+ }
+
+ public function clientViewCSS(){
+ $css = null;
+
+ if ($this->isPro()) {
+ $bodyFont = $this->getBodyFontCss();
+ $headerFont = $this->getHeaderFontCss();
+
+ $css = 'body{'.$bodyFont.'}';
+ if ($headerFont != $bodyFont) {
+ $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}';
+ }
+
+ if ((Utils::isNinja() && $this->isPro()) || $this->isWhiteLabel()) {
+ // For self-hosted users, a white-label license is required for custom CSS
+ $css .= $this->client_view_css;
+ }
+ }
+
+ return $css;
+ }
+
+ public function hasLargeFont()
+ {
+ return stripos($this->getBodyFontName(), 'chinese') || stripos($this->getHeaderFontName(), 'chinese');
+ }
+
+ public function getFontsUrl($protocol = ''){
+ $bodyFont = $this->getHeaderFontId();
+ $headerFont = $this->getBodyFontId();
+
+ $bodyFontSettings = Utils::getFromCache($bodyFont, 'fonts');
+ $google_fonts = array($bodyFontSettings['google_font']);
+
+ if($headerFont != $bodyFont){
+ $headerFontSettings = Utils::getFromCache($headerFont, 'fonts');
+ $google_fonts[] = $headerFontSettings['google_font'];
+ }
+
+ return ($protocol?$protocol.':':'').'//fonts.googleapis.com/css?family='.implode('|',$google_fonts);
+ }
+
+ public function getHeaderFontId() {
+ return ($this->isPro() && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT;
+ }
+
+ public function getBodyFontId() {
+ return ($this->isPro() && $this->body_font_id) ? $this->body_font_id : DEFAULT_BODY_FONT;
+ }
+
+ public function getHeaderFontName(){
+ return Utils::getFromCache($this->getHeaderFontId(), 'fonts')['name'];
+ }
+
+ public function getBodyFontName(){
+ return Utils::getFromCache($this->getBodyFontId(), 'fonts')['name'];
+ }
+
+ public function getHeaderFontCss($include_weight = true){
+ $font_data = Utils::getFromCache($this->getHeaderFontId(), 'fonts');
+ $css = 'font-family:'.$font_data['css_stack'].';';
+
+ if($include_weight){
+ $css .= 'font-weight:'.$font_data['css_weight'].';';
+ }
+
+ return $css;
+ }
+
+ public function getBodyFontCss($include_weight = true){
+ $font_data = Utils::getFromCache($this->getBodyFontId(), 'fonts');
+ $css = 'font-family:'.$font_data['css_stack'].';';
+
+ if($include_weight){
+ $css .= 'font-weight:'.$font_data['css_weight'].';';
+ }
+
+ return $css;
+ }
+
+ public function getFonts(){
+ return array_unique(array($this->getHeaderFontId(), $this->getBodyFontId()));
+ }
+
+ public function getFontsData(){
+ $data = array();
+
+ foreach($this->getFonts() as $font){
+ $data[] = Utils::getFromCache($font, 'fonts');
+ }
+
+ return $data;
+ }
+
+ public function getFontFolders(){
+ return array_map(function($item){return $item['folder'];}, $this->getFontsData());
+ }
}
Account::updated(function ($account) {
diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php
index 16d72076c394..a4027db6ef36 100644
--- a/app/Models/AccountGateway.php
+++ b/app/Models/AccountGateway.php
@@ -1,5 +1,6 @@
belongsTo('App\Models\Gateway');
@@ -27,16 +33,49 @@ class AccountGateway extends EntityModel
return $arrayOfImages;
}
- public function getPaymentType() {
+ public function getPaymentType()
+ {
return Gateway::getPaymentType($this->gateway_id);
}
- public function isPaymentType($type) {
+ public function isPaymentType($type)
+ {
return $this->getPaymentType() == $type;
}
- public function isGateway($gatewayId) {
+ public function isGateway($gatewayId)
+ {
return $this->gateway_id == $gatewayId;
}
+
+ public function setConfig($config)
+ {
+ $this->config = Crypt::encrypt(json_encode($config));
+ }
+
+ public function getConfig()
+ {
+ return json_decode(Crypt::decrypt($this->config));
+ }
+
+ public function getConfigField($field)
+ {
+ $config = $this->getConfig();
+
+ if (!$field || !property_exists($config, $field)) {
+ return false;
+ }
+
+ return $config->$field;
+ }
+
+ public function getPublishableStripeKey()
+ {
+ if ( ! $this->isGateway(GATEWAY_STRIPE)) {
+ return false;
+ }
+
+ return $this->getConfigField('publishableKey');
+ }
}
diff --git a/app/Models/AccountToken.php b/app/Models/AccountToken.php
index 909cfbfe1195..dd9a98800535 100644
--- a/app/Models/AccountToken.php
+++ b/app/Models/AccountToken.php
@@ -7,6 +7,11 @@ class AccountToken extends EntityModel
use SoftDeletes;
protected $dates = ['deleted_at'];
+ public function getEntityType()
+ {
+ return ENTITY_TOKEN;
+ }
+
public function account()
{
return $this->belongsTo('App\Models\Account');
diff --git a/app/Models/Activity.php b/app/Models/Activity.php
index 0dd5ec54bf37..921c037d5f69 100644
--- a/app/Models/Activity.php
+++ b/app/Models/Activity.php
@@ -23,471 +23,56 @@ class Activity extends Eloquent
public function user()
{
- return $this->belongsTo('App\Models\User');
+ return $this->belongsTo('App\Models\User')->withTrashed();
}
- private static function getBlank($entity = false)
+ public function contact()
{
- $activity = new Activity();
-
- if ($entity) {
- $activity->user_id = $entity instanceof User ? $entity->id : $entity->user_id;
- $activity->account_id = $entity->account_id;
- } elseif (Auth::check()) {
- $activity->user_id = Auth::user()->id;
- $activity->account_id = Auth::user()->account_id;
- } else {
- Utils::fatalError();
- }
-
- $activity->token_id = Session::get('token_id', null);
- $activity->ip = Request::getClientIp();
-
- return $activity;
+ return $this->belongsTo('App\Models\Contact')->withTrashed();
}
- public static function createClient($client, $notify = true)
+ public function client()
{
- $activity = Activity::getBlank();
- $activity->client_id = $client->id;
- $activity->activity_type_id = ACTIVITY_TYPE_CREATE_CLIENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'created', $client);
- $activity->save();
-
- if ($notify) {
- Activity::checkSubscriptions(EVENT_CREATE_CLIENT, $client);
- }
+ return $this->belongsTo('App\Models\Client')->withTrashed();
}
- public static function updateClient($client)
+ public function invoice()
{
- if ($client->is_deleted && !$client->getOriginal('is_deleted')) {
- $activity = Activity::getBlank();
- $activity->client_id = $client->id;
- $activity->activity_type_id = ACTIVITY_TYPE_DELETE_CLIENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'deleted', $client);
- $activity->balance = $client->balance;
- $activity->save();
- }
+ return $this->belongsTo('App\Models\Invoice')->withTrashed();
}
- public static function archiveClient($client)
+ public function credit()
{
- if (!$client->is_deleted) {
- $activity = Activity::getBlank();
- $activity->client_id = $client->id;
- $activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_CLIENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'archived', $client);
- $activity->balance = $client->balance;
- $activity->save();
- }
+ return $this->belongsTo('App\Models\Credit')->withTrashed();
}
- public static function restoreClient($client)
+ public function payment()
{
- $activity = Activity::getBlank();
- $activity->client_id = $client->id;
- $activity->activity_type_id = ACTIVITY_TYPE_RESTORE_CLIENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'restored', $client);
- $activity->balance = $client->balance;
- $activity->save();
+ return $this->belongsTo('App\Models\Payment')->withTrashed();
}
- public static function createInvoice($invoice)
+ public function getMessage()
{
- if (Auth::check()) {
- $message = Utils::encodeActivity(Auth::user(), 'created', $invoice);
- } else {
- $message = Utils::encodeActivity(null, 'created', $invoice);
- }
+ $activityTypeId = $this->activity_type_id;
+ $account = $this->account;
+ $client = $this->client;
+ $user = $this->user;
+ $invoice = $this->invoice;
+ $contactId = $this->contact_id;
+ $payment = $this->payment;
+ $credit = $this->credit;
+ $isSystem = $this->is_system;
- $adjustment = 0;
- $client = $invoice->client;
- if (!$invoice->is_quote && !$invoice->is_recurring) {
- $adjustment = $invoice->amount;
- $client->balance = $client->balance + $adjustment;
- $client->save();
- }
+ $data = [
+ 'client' => link_to($client->getRoute(), $client->getDisplayName()),
+ 'user' => $isSystem ? '' . trans('texts.system') . '' : $user->getDisplayName(),
+ 'invoice' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
+ 'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
+ 'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(),
+ 'payment' => $payment ? $payment->transaction_reference : null,
+ 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null,
+ ];
- $activity = Activity::getBlank($invoice);
- $activity->invoice_id = $invoice->id;
- $activity->client_id = $invoice->client_id;
- $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_CREATE_QUOTE : ACTIVITY_TYPE_CREATE_INVOICE;
- $activity->message = $message;
- $activity->balance = $client->balance;
- $activity->adjustment = $adjustment;
- $activity->save();
-
- Activity::checkSubscriptions($invoice->is_quote ? EVENT_CREATE_QUOTE : EVENT_CREATE_INVOICE, $invoice);
- }
-
- public static function archiveInvoice($invoice)
- {
- if (!$invoice->is_deleted) {
- $activity = Activity::getBlank();
- $activity->invoice_id = $invoice->id;
- $activity->client_id = $invoice->client_id;
- $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_ARCHIVE_QUOTE : ACTIVITY_TYPE_ARCHIVE_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::user(), 'archived', $invoice);
- $activity->balance = $invoice->client->balance;
-
- $activity->save();
- }
- }
-
- public static function restoreInvoice($invoice)
- {
- $activity = Activity::getBlank();
- $activity->invoice_id = $invoice->id;
- $activity->client_id = $invoice->client_id;
- $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_RESTORE_QUOTE : ACTIVITY_TYPE_RESTORE_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::user(), 'restored', $invoice);
- $activity->balance = $invoice->client->balance;
-
- $activity->save();
- }
-
- public static function emailInvoice($invitation)
- {
- $adjustment = 0;
- $client = $invitation->invoice->client;
-
- $activity = Activity::getBlank($invitation);
- $activity->client_id = $invitation->invoice->client_id;
- $activity->invoice_id = $invitation->invoice_id;
- $activity->contact_id = $invitation->contact_id;
- $activity->activity_type_id = $invitation->invoice ? ACTIVITY_TYPE_EMAIL_QUOTE : ACTIVITY_TYPE_EMAIL_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::check() ? Auth::user() : null, 'emailed', $invitation->invoice, $invitation->contact);
- $activity->balance = $client->balance;
- $activity->save();
- }
-
- public static function updateInvoice($invoice)
- {
- $client = $invoice->client;
-
- if ($invoice->is_deleted && !$invoice->getOriginal('is_deleted')) {
- $adjustment = 0;
- if (!$invoice->is_quote && !$invoice->is_recurring) {
- $adjustment = $invoice->balance * -1;
- $client->balance = $client->balance - $invoice->balance;
- $client->paid_to_date = $client->paid_to_date - ($invoice->amount - $invoice->balance);
- $client->save();
- }
-
- $activity = Activity::getBlank();
- $activity->client_id = $invoice->client_id;
- $activity->invoice_id = $invoice->id;
- $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_DELETE_QUOTE : ACTIVITY_TYPE_DELETE_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::user(), 'deleted', $invoice);
- $activity->balance = $invoice->client->balance;
- $activity->adjustment = $adjustment;
- $activity->save();
- } else {
- $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount'));
-
- $fieldChanged = false;
- foreach (['invoice_number', 'po_number', 'invoice_date', 'due_date', 'terms', 'public_notes', 'invoice_footer', 'partial'] as $field) {
- if ($invoice->$field != $invoice->getOriginal($field)) {
- $fieldChanged = true;
- break;
- }
- }
-
- if ($diff != 0 || $fieldChanged) {
- $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($invoice->id);
-
- if ($diff != 0 && !$invoice->is_quote && !$invoice->is_recurring) {
- $client->balance = $client->balance + $diff;
- $client->save();
- }
-
- $activity = Activity::getBlank($invoice);
- $activity->client_id = $invoice->client_id;
- $activity->invoice_id = $invoice->id;
- $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice);
- $activity->balance = $client->balance;
- $activity->adjustment = $invoice->is_quote || $invoice->is_recurring ? 0 : $diff;
- $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON();
- $activity->save();
-
- if ($invoice->isPaid() && $invoice->balance > 0) {
- $invoice->invoice_status_id = INVOICE_STATUS_PARTIAL;
- } elseif ($invoice->invoice_status_id && $invoice->balance == 0) {
- $invoice->invoice_status_id = INVOICE_STATUS_PAID;
- }
- }
- }
- }
-
- public static function viewInvoice($invitation)
- {
- if (Session::get($invitation->invitation_key)) {
- return;
- }
-
- Session::put($invitation->invitation_key, true);
- $invoice = $invitation->invoice;
-
- if (!$invoice->isViewed()) {
- $invoice->invoice_status_id = INVOICE_STATUS_VIEWED;
- $invoice->save();
- }
-
- $now = Carbon::now()->toDateTimeString();
-
- $invitation->viewed_date = $now;
- $invitation->save();
-
- $client = $invoice->client;
- $client->last_login = $now;
- $client->save();
-
- $activity = Activity::getBlank($invitation);
- $activity->client_id = $invitation->invoice->client_id;
- $activity->invitation_id = $invitation->id;
- $activity->contact_id = $invitation->contact_id;
- $activity->invoice_id = $invitation->invoice_id;
- $activity->activity_type_id = $invitation->invoice->is_quote ? ACTIVITY_TYPE_VIEW_QUOTE : ACTIVITY_TYPE_VIEW_INVOICE;
- $activity->message = Utils::encodeActivity($invitation->contact, 'viewed', $invitation->invoice);
- $activity->balance = $invitation->invoice->client->balance;
- $activity->save();
- }
-
- public static function approveQuote($invitation) {
-
- $activity = Activity::getBlank($invitation);
- $activity->client_id = $invitation->invoice->client_id;
- $activity->invitation_id = $invitation->id;
- $activity->contact_id = $invitation->contact_id;
- $activity->invoice_id = $invitation->invoice_id;
- $activity->activity_type_id = ACTIVITY_TYPE_APPROVE_QUOTE;
- $activity->message = Utils::encodeActivity($invitation->contact, 'approved', $invitation->invoice);
- $activity->balance = $invitation->invoice->client->balance;
- $activity->save();
- }
-
- public static function createPayment($payment)
- {
- $client = $payment->client;
- $client->balance = $client->balance - $payment->amount;
- $client->paid_to_date = $client->paid_to_date + $payment->amount;
- $client->save();
-
- if ($payment->contact_id) {
- $activity = Activity::getBlank($client);
- $activity->contact_id = $payment->contact_id;
- $activity->message = Utils::encodeActivity($payment->invitation->contact, 'entered '.$payment->getName().' for ', $payment->invoice);
- } else {
- $activity = Activity::getBlank($client);
- $message = $payment->payment_type_id == PAYMENT_TYPE_CREDIT ? 'applied credit for ' : 'entered '.$payment->getName().' for ';
- $activity->message = Utils::encodeActivity(Auth::user(), $message, $payment->invoice);
- }
-
- $activity->payment_id = $payment->id;
-
- if ($payment->invoice_id) {
- $activity->invoice_id = $payment->invoice_id;
-
- $invoice = $payment->invoice;
- $invoice->balance = $invoice->balance - $payment->amount;
- $invoice->invoice_status_id = ($invoice->balance > 0) ? INVOICE_STATUS_PARTIAL : INVOICE_STATUS_PAID;
- if ($invoice->partial > 0) {
- $invoice->partial = max(0, $invoice->partial - $payment->amount);
- }
- $invoice->save();
- }
-
- $activity->payment_id = $payment->id;
- $activity->client_id = $payment->client_id;
- $activity->activity_type_id = ACTIVITY_TYPE_CREATE_PAYMENT;
- $activity->balance = $client->balance;
- $activity->adjustment = $payment->amount * -1;
- $activity->save();
-
- Activity::checkSubscriptions(EVENT_CREATE_PAYMENT, $payment);
- }
-
- public static function updatePayment($payment)
- {
- if ($payment->is_deleted && !$payment->getOriginal('is_deleted')) {
- $client = $payment->client;
- $client->balance = $client->balance + $payment->amount;
- $client->paid_to_date = $client->paid_to_date - $payment->amount;
- $client->save();
-
- $invoice = $payment->invoice;
- $invoice->balance = $invoice->balance + $payment->amount;
- if ($invoice->isPaid() && $invoice->balance > 0) {
- $invoice->invoice_status_id = ($invoice->balance == $invoice->amount ? INVOICE_STATUS_DRAFT : INVOICE_STATUS_PARTIAL);
- }
- $invoice->save();
-
- // deleting a payment from credit creates a new credit
- if ($payment->payment_type_id == PAYMENT_TYPE_CREDIT) {
- $credit = Credit::createNew();
- $credit->client_id = $client->id;
- $credit->credit_date = Carbon::now()->toDateTimeString();
- $credit->balance = $credit->amount = $payment->amount;
- $credit->private_notes = $payment->transaction_reference;
- $credit->save();
- }
-
- $activity = Activity::getBlank();
- $activity->payment_id = $payment->id;
- $activity->client_id = $invoice->client_id;
- $activity->invoice_id = $invoice->id;
- $activity->activity_type_id = ACTIVITY_TYPE_DELETE_PAYMENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'deleted '.$payment->getName());
- $activity->balance = $client->balance;
- $activity->adjustment = $payment->amount;
- $activity->save();
- } else {
- /*
- $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount'));
-
- if ($diff == 0)
- {
- return;
- }
-
- $client = $invoice->client;
- $client->balance = $client->balance + $diff;
- $client->save();
-
- $activity = Activity::getBlank($invoice);
- $activity->client_id = $invoice->client_id;
- $activity->invoice_id = $invoice->id;
- $activity->activity_type_id = ACTIVITY_TYPE_UPDATE_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice);
- $activity->balance = $client->balance;
- $activity->adjustment = $diff;
- $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON();
- $activity->save();
- */
- }
- }
-
- public static function archivePayment($payment)
- {
- if ($payment->is_deleted) {
- return;
- }
-
- $client = $payment->client;
- $invoice = $payment->invoice;
-
- $activity = Activity::getBlank();
- $activity->payment_id = $payment->id;
- $activity->invoice_id = $invoice->id;
- $activity->client_id = $client->id;
- $activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_PAYMENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'archived '.$payment->getName());
- $activity->balance = $client->balance;
- $activity->adjustment = 0;
- $activity->save();
- }
-
- public static function restorePayment($payment)
- {
- $client = $payment->client;
- $invoice = $payment->invoice;
-
- $activity = Activity::getBlank();
- $activity->payment_id = $payment->id;
- $activity->invoice_id = $invoice->id;
- $activity->client_id = $client->id;
- $activity->activity_type_id = ACTIVITY_TYPE_RESTORE_PAYMENT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'restored '.$payment->getName());
- $activity->balance = $client->balance;
- $activity->adjustment = 0;
- $activity->save();
- }
-
- public static function createCredit($credit)
- {
- $activity = Activity::getBlank();
- $activity->message = Utils::encodeActivity(Auth::user(), 'entered '.Utils::formatMoney($credit->amount, $credit->client->getCurrencyId()).' credit');
- $activity->credit_id = $credit->id;
- $activity->client_id = $credit->client_id;
- $activity->activity_type_id = ACTIVITY_TYPE_CREATE_CREDIT;
- $activity->balance = $credit->client->balance;
- $activity->save();
- }
-
- public static function updateCredit($credit)
- {
- if ($credit->is_deleted && !$credit->getOriginal('is_deleted')) {
- $activity = Activity::getBlank();
- $activity->credit_id = $credit->id;
- $activity->client_id = $credit->client_id;
- $activity->activity_type_id = ACTIVITY_TYPE_DELETE_CREDIT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'deleted '.Utils::formatMoney($credit->balance, $credit->client->getCurrencyId()).' credit');
- $activity->balance = $credit->client->balance;
- $activity->save();
- } else {
- /*
- $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount'));
-
- if ($diff == 0)
- {
- return;
- }
-
- $client = $invoice->client;
- $client->balance = $client->balance + $diff;
- $client->save();
-
- $activity = Activity::getBlank($invoice);
- $activity->client_id = $invoice->client_id;
- $activity->invoice_id = $invoice->id;
- $activity->activity_type_id = ACTIVITY_TYPE_UPDATE_INVOICE;
- $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice);
- $activity->balance = $client->balance;
- $activity->adjustment = $diff;
- $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON();
- $activity->save();
- */
- }
- }
-
- public static function archiveCredit($credit)
- {
- if ($credit->is_deleted) {
- return;
- }
-
- $activity = Activity::getBlank();
- $activity->client_id = $credit->client_id;
- $activity->credit_id = $credit->id;
- $activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_CREDIT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'archived '.Utils::formatMoney($credit->balance, $credit->client->getCurrencyId()).' credit');
- $activity->balance = $credit->client->balance;
- $activity->save();
- }
-
- public static function restoreCredit($credit)
- {
- $activity = Activity::getBlank();
- $activity->client_id = $credit->client_id;
- $activity->credit_id = $credit->id;
- $activity->activity_type_id = ACTIVITY_TYPE_RESTORE_CREDIT;
- $activity->message = Utils::encodeActivity(Auth::user(), 'restored '.Utils::formatMoney($credit->balance, $credit->client->getCurrencyId()).' credit');
- $activity->balance = $credit->client->balance;
- $activity->save();
- }
-
- private static function checkSubscriptions($event, $data)
- {
- if (!Auth::check()) {
- return;
- }
-
- $subscription = Auth::user()->account->getSubscription($event);
-
- if ($subscription) {
- Utils::notifyZapier($subscription, $data);
- }
+ return trans("texts.activity_{$activityTypeId}", $data);
}
}
diff --git a/app/Models/BalanceAffecting.php b/app/Models/BalanceAffecting.php
new file mode 100644
index 000000000000..0ba99f73a284
--- /dev/null
+++ b/app/Models/BalanceAffecting.php
@@ -0,0 +1,6 @@
+config);
+
+ return new \App\Libraries\Bank($finance, $config->fid, $config->url, $config->org);
+ }
+}
diff --git a/app/Models/BankAccount.php b/app/Models/BankAccount.php
new file mode 100644
index 000000000000..01ae612dc839
--- /dev/null
+++ b/app/Models/BankAccount.php
@@ -0,0 +1,23 @@
+belongsTo('App\Models\Bank');
+ }
+
+}
+
diff --git a/app/Models/Client.php b/app/Models/Client.php
index 554f74556910..6655a9ef2e67 100644
--- a/app/Models/Client.php
+++ b/app/Models/Client.php
@@ -1,29 +1,101 @@
'first_name',
+ 'last' => 'last_name',
+ 'email' => 'email',
+ 'mobile|phone' => 'phone',
+ 'name|organization' => 'name',
+ 'street2|address2' => 'address2',
+ 'street|address|address1' => 'address1',
+ 'city' => 'city',
+ 'state|province' => 'state',
+ 'zip|postal|code' => 'postal_code',
+ 'country' => 'country',
+ 'note' => 'notes',
+ ];
+ }
public function account()
{
return $this->belongsTo('App\Models\Account');
}
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User')->withTrashed();
+ }
+
public function invoices()
{
return $this->hasMany('App\Models\Invoice');
@@ -39,11 +111,6 @@ class Client extends EntityModel
return $this->hasMany('App\Models\Contact');
}
- public function projects()
- {
- return $this->hasMany('App\Models\Project');
- }
-
public function country()
{
return $this->belongsTo('App\Models\Country');
@@ -54,6 +121,11 @@ class Client extends EntityModel
return $this->belongsTo('App\Models\Currency');
}
+ public function language()
+ {
+ return $this->belongsTo('App\Models\Language');
+ }
+
public function size()
{
return $this->belongsTo('App\Models\Size');
@@ -64,6 +136,40 @@ class Client extends EntityModel
return $this->belongsTo('App\Models\Industry');
}
+ public function addContact($data, $isPrimary = false)
+ {
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
+ if ($publicId && $publicId != '-1') {
+ $contact = Contact::scope($publicId)->firstOrFail();
+ } else {
+ $contact = Contact::createNew();
+ $contact->send_invoice = true;
+ }
+
+ $contact->fill($data);
+ $contact->is_primary = $isPrimary;
+
+ return $this->contacts()->save($contact);
+ }
+
+ public function updateBalances($balanceAdjustment, $paidToDateAdjustment)
+ {
+ if ($balanceAdjustment === 0 && $paidToDateAdjustment === 0) {
+ return;
+ }
+
+ $this->balance = $this->balance + $balanceAdjustment;
+ $this->paid_to_date = $this->paid_to_date + $paidToDateAdjustment;
+
+ $this->save();
+ }
+
+ public function getRoute()
+ {
+ return "/clients/{$this->public_id}";
+ }
+
public function getTotalCredit()
{
return DB::table('credits')
@@ -83,33 +189,43 @@ class Client extends EntityModel
return $this->name;
}
- $contact = $this->contacts()->first();
+ if ( ! count($this->contacts)) {
+ return '';
+ }
+ $contact = $this->contacts[0];
return $contact->getDisplayName();
}
+ public function getCityState()
+ {
+ $swap = $this->country && $this->country->swap_postal_code;
+ return Utils::cityStateZip($this->city, $this->state, $this->postal_code, $swap);
+ }
+
public function getEntityType()
{
return ENTITY_CLIENT;
}
- public function getWebsite()
+ public function hasAddress()
{
- if (!$this->website) {
- return '';
+ $fields = [
+ 'address1',
+ 'address2',
+ 'city',
+ 'state',
+ 'postal_code',
+ 'country_id',
+ ];
+
+ foreach ($fields as $field) {
+ if ($this->$field) {
+ return true;
+ }
}
- $link = $this->website;
- $title = $this->website;
- $prefix = 'http://';
-
- if (strlen($link) > 7 && substr($link, 0, 7) === $prefix) {
- $title = substr($title, 7);
- } else {
- $link = $prefix.$link;
- }
-
- return link_to($link, $title, array('target' => '_blank'));
+ return false;
}
public function getDateCreated()
@@ -160,24 +276,31 @@ class Client extends EntityModel
return $this->account->currency_id ?: DEFAULT_CURRENCY;
}
+
+ public function getCounter($isQuote)
+ {
+ return $isQuote ? $this->quote_number_counter : $this->invoice_number_counter;
+ }
+
+ public function markLoggedIn()
+ {
+ $this->last_login = Carbon::now()->toDateTimeString();
+ $this->save();
+ }
}
-/*
-Client::created(function($client)
-{
- Activity::createClient($client);
+Client::creating(function ($client) {
+ $client->setNullValues();
+});
+
+Client::created(function ($client) {
+ event(new ClientWasCreated($client));
});
-*/
Client::updating(function ($client) {
- Activity::updateClient($client);
+ $client->setNullValues();
});
-Client::deleting(function ($client) {
- Activity::archiveClient($client);
+Client::updated(function ($client) {
+ event(new ClientWasUpdated($client));
});
-
-/*Client::restoring(function ($client) {
- Activity::restoreClient($client);
-});
-*/
\ No newline at end of file
diff --git a/app/Models/Contact.php b/app/Models/Contact.php
index 0856a6d431d4..a95f40bab059 100644
--- a/app/Models/Contact.php
+++ b/app/Models/Contact.php
@@ -9,14 +9,32 @@ class Contact extends EntityModel
use SoftDeletes;
protected $dates = ['deleted_at'];
- public static $fieldFirstName = 'Contact - First Name';
- public static $fieldLastName = 'Contact - Last Name';
- public static $fieldEmail = 'Contact - Email';
- public static $fieldPhone = 'Contact - Phone';
+ protected $fillable = [
+ 'first_name',
+ 'last_name',
+ 'email',
+ 'phone',
+ 'send_invoice',
+ ];
+
+ public static $fieldFirstName = 'first_name';
+ public static $fieldLastName = 'last_name';
+ public static $fieldEmail = 'email';
+ public static $fieldPhone = 'phone';
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
public function client()
{
- return $this->belongsTo('App\Models\Client');
+ return $this->belongsTo('App\Models\Client')->withTrashed();
}
public function getPersonType()
@@ -24,20 +42,6 @@ class Contact extends EntityModel
return PERSON_CONTACT;
}
- /*
- public function getLastLogin()
- {
- if ($this->last_login == '0000-00-00 00:00:00')
- {
- return '---';
- }
- else
- {
- return $this->last_login->format('m/d/y h:i a');
- }
- }
- */
-
public function getName()
{
return $this->getDisplayName();
diff --git a/app/Models/Country.php b/app/Models/Country.php
index 219251a44cc4..8a87500e3299 100644
--- a/app/Models/Country.php
+++ b/app/Models/Country.php
@@ -6,7 +6,19 @@ class Country extends Eloquent
{
public $timestamps = false;
- protected $visible = ['id', 'name'];
+ protected $visible = [
+ 'id',
+ 'name',
+ 'swap_postal_code',
+ 'swap_currency_symbol',
+ 'thousand_separator',
+ 'decimal_separator'
+ ];
+
+ protected $casts = [
+ 'swap_postal_code' => 'boolean',
+ 'swap_currency_symbol' => 'boolean',
+ ];
public function getName()
{
diff --git a/app/Models/Credit.php b/app/Models/Credit.php
index 9a507bc6bb27..c46095e80ee3 100644
--- a/app/Models/Credit.php
+++ b/app/Models/Credit.php
@@ -1,11 +1,26 @@
belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
public function invoice()
{
@@ -43,18 +58,10 @@ class Credit extends EntityModel
}
}
+Credit::creating(function ($credit) {
+
+});
+
Credit::created(function ($credit) {
- Activity::createCredit($credit);
-});
-
-Credit::updating(function ($credit) {
- Activity::updateCredit($credit);
-});
-
-Credit::deleting(function ($credit) {
- Activity::archiveCredit($credit);
-});
-
-Credit::restoring(function ($credit) {
- Activity::restoreCredit($credit);
-});
+ event(new CreditWasCreated($credit));
+});
\ No newline at end of file
diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php
index 550de1d3cef0..b8e7d651ada4 100644
--- a/app/Models/EntityModel.php
+++ b/app/Models/EntityModel.php
@@ -9,14 +9,14 @@ class EntityModel extends Eloquent
public $timestamps = true;
protected $hidden = ['id'];
- public static function createNew($parent = false)
+ public static function createNew($context = null)
{
$className = get_called_class();
$entity = new $className();
- if ($parent) {
- $entity->user_id = $parent instanceof User ? $parent->id : $parent->user_id;
- $entity->account_id = $parent->account_id;
+ if ($context) {
+ $entity->user_id = $context instanceof User ? $context->id : $context->user_id;
+ $entity->account_id = $context->account_id;
} elseif (Auth::check()) {
$entity->user_id = Auth::user()->id;
$entity->account_id = Auth::user()->account_id;
@@ -24,7 +24,10 @@ class EntityModel extends Eloquent
Utils::fatalError();
}
- $lastEntity = $className::withTrashed()->scope(false, $entity->account_id)->orderBy('public_id', 'DESC')->first();
+ $lastEntity = $className::withTrashed()
+ ->scope(false, $entity->account_id)
+ ->orderBy('public_id', 'DESC')
+ ->first();
if ($lastEntity) {
$entity->public_id = $lastEntity->public_id + 1;
@@ -39,7 +42,7 @@ class EntityModel extends Eloquent
{
$className = get_called_class();
- return $className::scope($publicId)->pluck('id');
+ return $className::scope($publicId)->withTrashed()->pluck('id');
}
public function getActivityKey()
@@ -112,4 +115,21 @@ class EntityModel extends Eloquent
return $data;
}
+ public function setNullValues()
+ {
+ foreach ($this->fillable as $field) {
+ if (strstr($field, '_id') && !$this->$field) {
+ $this->$field = null;
+ }
+ }
+ }
+
+ // converts "App\Models\Client" to "client_id"
+ public function getKeyField()
+ {
+ $class = get_class($this);
+ $parts = explode('\\', $class);
+ $name = $parts[count($parts)-1];
+ return strtolower($name) . '_id';
+ }
}
diff --git a/app/Models/Expense.php b/app/Models/Expense.php
new file mode 100644
index 000000000000..ce1241b11b2a
--- /dev/null
+++ b/app/Models/Expense.php
@@ -0,0 +1,114 @@
+belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function client()
+ {
+ return $this->belongsTo('App\Models\Client')->withTrashed();
+ }
+
+ public function invoice()
+ {
+ return $this->belongsTo('App\Models\Invoice')->withTrashed();
+ }
+
+ public function getName()
+ {
+ if($this->expense_number)
+ return $this->expense_number;
+
+ return $this->public_id;
+ }
+
+ public function getDisplayName()
+ {
+ return $this->getName();
+ }
+
+ public function getRoute()
+ {
+ return "/expenses/{$this->public_id}";
+ }
+
+ public function getEntityType()
+ {
+ return ENTITY_EXPENSE;
+ }
+
+ public function apply($amount)
+ {
+ if ($amount > $this->balance) {
+ $applied = $this->balance;
+ $this->balance = 0;
+ } else {
+ $applied = $amount;
+ $this->balance = $this->balance - $amount;
+ }
+
+ $this->save();
+
+ return $applied;
+ }
+}
+
+Expense::creating(function ($expense) {
+ $expense->setNullValues();
+});
+
+Expense::created(function ($expense) {
+ event(new ExpenseWasCreated($expense));
+});
+
+Expense::updating(function ($expense) {
+ $expense->setNullValues();
+});
+
+Expense::updated(function ($expense) {
+ event(new ExpenseWasUpdated($expense));
+});
+
+Expense::deleting(function ($expense) {
+ $expense->setNullValues();
+});
+
+Expense::deleted(function ($expense) {
+ event(new ExpenseWasDeleted($expense));
+});
diff --git a/app/Models/Font.php b/app/Models/Font.php
new file mode 100644
index 000000000000..b9518a91ba4d
--- /dev/null
+++ b/app/Models/Font.php
@@ -0,0 +1,8 @@
+provider.'.png';
}
+ public function isGateway($gatewayId)
+ {
+ return $this->id == $gatewayId;
+ }
+
+ public static function getPaymentTypeName($type)
+ {
+ return Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type)));
+ }
+
+ /*
public static function getPaymentTypeLinks() {
$data = [];
foreach (self::$paymentTypes as $type) {
- $data[] = strtolower(str_replace('PAYMENT_TYPE_', '', $type));
+ $data[] = Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type)));
}
return $data;
}
+ */
public function getHelp()
{
@@ -81,6 +95,8 @@ class Gateway extends Eloquent
return PAYMENT_TYPE_BITCOIN;
} else if ($gatewayId == GATEWAY_DWOLLA) {
return PAYMENT_TYPE_DWOLLA;
+ }else if ($gatewayId == GATEWAY_GOCARDLESS) {
+ return PAYMENT_TYPE_DIRECT_DEBIT;
} else {
return PAYMENT_TYPE_CREDIT_CARD;
}
diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php
index 7941c81f4788..2cc71029578d 100644
--- a/app/Models/Invitation.php
+++ b/app/Models/Invitation.php
@@ -1,5 +1,7 @@
belongsTo('App\Models\Account');
}
- public function getLink()
+ public function getLink($type = 'view')
{
if (!$this->account) {
$this->load('account');
}
-
+
$url = SITE_URL;
-
- if ($this->account->subdomain) {
- $parsedUrl = parse_url($url);
- $host = explode('.', $parsedUrl['host']);
- $subdomain = $host[0];
- $url = str_replace("://{$subdomain}.", "://{$this->account->subdomain}.", $url);
+ $iframe_url = $this->account->iframe_url;
+
+ if ($this->account->isPro()) {
+ if ($iframe_url) {
+ return "{$iframe_url}/?{$this->invitation_key}";
+ } elseif ($this->account->subdomain) {
+ $url = Utils::replaceSubdomain($url, $this->account->subdomain);
+ }
+ }
+
+ return "{$url}/{$type}/{$this->invitation_key}";
+ }
+
+ public function getStatus()
+ {
+ $hasValue = false;
+ $parts = [];
+ $statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
+
+ foreach ($statuses as $status) {
+ $field = "{$status}_date";
+ $date = '';
+ if ($this->$field && $this->field != '0000-00-00 00:00:00') {
+ $date = Utils::dateToString($this->$field);
+ $hasValue = true;
+ }
+ $parts[] = trans('texts.invitation_status.' . $status) . ': ' . $date;
}
- return "{$url}/view/{$this->invitation_key}";
+ return $hasValue ? implode($parts, '
') : false;
}
public function getName()
{
return $this->invitation_key;
}
+
+ public function markSent($messageId = null)
+ {
+ $this->message_id = $messageId;
+ $this->email_error = null;
+ $this->sent_date = Carbon::now()->toDateTimeString();
+ $this->save();
+ }
+
+ public function markViewed()
+ {
+ $invoice = $this->invoice;
+ $client = $invoice->client;
+
+ $this->viewed_date = Carbon::now()->toDateTimeString();
+ $this->save();
+
+ $invoice->markViewed();
+ $client->markLoggedIn();
+ }
}
diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php
index aeebfaed6ab0..39fedd489731 100644
--- a/app/Models/Invoice.php
+++ b/app/Models/Invoice.php
@@ -1,18 +1,152 @@
'boolean',
'has_tasks' => 'boolean',
+ 'auto_bill' => 'boolean',
+ 'has_expenses' => 'boolean',
];
+ // used for custom invoice numbers
+ public static $patternFields = [
+ 'counter',
+ 'custom1',
+ 'custom2',
+ 'userId',
+ 'year',
+ 'date:',
+ ];
+
+ public static $fieldInvoiceNumber = 'invoice_number';
+ public static $fieldInvoiceDate = 'invoice_date';
+ public static $fieldDueDate = 'due_date';
+ public static $fieldAmount = 'amount';
+ public static $fieldPaid = 'paid';
+ public static $fieldNotes = 'notes';
+ public static $fieldTerms = 'terms';
+
+ public static function getImportColumns()
+ {
+ return [
+ Client::$fieldName,
+ Invoice::$fieldInvoiceNumber,
+ Invoice::$fieldInvoiceDate,
+ Invoice::$fieldDueDate,
+ Invoice::$fieldAmount,
+ Invoice::$fieldPaid,
+ Invoice::$fieldNotes,
+ Invoice::$fieldTerms,
+ ];
+ }
+
+ public static function getImportMap()
+ {
+ return [
+ 'number^po' => 'invoice_number',
+ 'amount' => 'amount',
+ 'organization' => 'name',
+ 'paid^date' => 'paid',
+ 'invoice_date|create_date' => 'invoice_date',
+ 'terms' => 'terms',
+ 'notes' => 'notes',
+ ];
+ }
+ public function getRoute()
+ {
+ $entityType = $this->getEntityType();
+ return "/{$entityType}s/{$this->public_id}/edit";
+ }
+
+ public function getDisplayName()
+ {
+ return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number;
+ }
+
+ public function affectsBalance()
+ {
+ return !$this->is_quote && !$this->is_recurring;
+ }
+
+ public function getAdjustment()
+ {
+ if (!$this->affectsBalance()) {
+ return 0;
+ }
+
+ return $this->getRawAdjustment();
+ }
+
+ private function getRawAdjustment()
+ {
+ return floatval($this->amount) - floatval($this->getOriginal('amount'));
+ }
+
+ public function isChanged()
+ {
+ if ($this->getRawAdjustment() != 0) {
+ return true;
+ }
+
+ foreach ([
+ 'invoice_number',
+ 'po_number',
+ 'invoice_date',
+ 'due_date',
+ 'terms',
+ 'public_notes',
+ 'invoice_footer',
+ 'partial',
+ ] as $field) {
+ if ($this->$field != $this->getOriginal($field)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function getAmountPaid()
+ {
+ if ($this->is_quote || $this->is_recurring) {
+ return 0;
+ }
+
+ return ($this->amount - $this->balance);
+ }
+
+ public function trashed()
+ {
+ if ($this->client && $this->client->trashed()) {
+ return true;
+ }
+
+ return self::parentTrashed();
+ }
+
public function account()
{
return $this->belongsTo('App\Models\Account');
@@ -20,7 +154,7 @@ class Invoice extends EntityModel
public function user()
{
- return $this->belongsTo('App\Models\User');
+ return $this->belongsTo('App\Models\User')->withTrashed();
}
public function client()
@@ -43,6 +177,11 @@ class Invoice extends EntityModel
return $this->belongsTo('App\Models\InvoiceDesign');
}
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Payment', 'invoice_id', 'id');
+ }
+
public function recurring_invoice()
{
return $this->belongsTo('App\Models\Invoice');
@@ -58,6 +197,85 @@ class Invoice extends EntityModel
return $this->hasMany('App\Models\Invitation')->orderBy('invitations.contact_id');
}
+ public function markInvitationsSent($notify = false)
+ {
+ foreach ($this->invitations as $invitation) {
+ $this->markInvitationSent($invitation, false, $notify);
+ }
+ }
+
+ public function markInvitationSent($invitation, $messageId = false, $notify = true)
+ {
+ if (!$this->isSent()) {
+ $this->invoice_status_id = INVOICE_STATUS_SENT;
+ $this->save();
+ }
+
+ $invitation->markSent($messageId);
+
+ // if the user marks it as sent rather than acually sending it
+ // then we won't track it in the activity log
+ if (!$notify) {
+ return;
+ }
+
+ if ($this->is_quote) {
+ event(new QuoteInvitationWasEmailed($invitation));
+ } else {
+ event(new InvoiceInvitationWasEmailed($invitation));
+ }
+ }
+
+ public function markViewed()
+ {
+ if (!$this->isViewed()) {
+ $this->invoice_status_id = INVOICE_STATUS_VIEWED;
+ $this->save();
+ }
+ }
+
+ public function updatePaidStatus($save = true)
+ {
+ $statusId = false;
+ if ($this->amount > 0 && $this->balance == 0) {
+ $statusId = INVOICE_STATUS_PAID;
+ } elseif ($this->balance > 0 && $this->balance < $this->amount) {
+ $statusId = INVOICE_STATUS_PARTIAL;
+ } elseif ($this->isPartial() && $this->balance > 0) {
+ $statusId = ($this->balance == $this->amount ? INVOICE_STATUS_SENT : INVOICE_STATUS_PARTIAL);
+ }
+
+ if ($statusId && $statusId != $this->invoice_status_id) {
+ $this->invoice_status_id = $statusId;
+ if ($save) {
+ $this->save();
+ }
+ }
+ }
+
+ public function markApproved()
+ {
+ if ($this->is_quote) {
+ $this->invoice_status_id = INVOICE_STATUS_APPROVED;
+ $this->save();
+ }
+ }
+
+ public function updateBalances($balanceAdjustment, $partial = 0)
+ {
+ if ($this->is_deleted) {
+ return;
+ }
+
+ $this->balance = $this->balance + $balanceAdjustment;
+
+ if ($this->partial > 0) {
+ $this->partial = $partial;
+ }
+
+ $this->save();
+ }
+
public function getName()
{
return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number;
@@ -99,16 +317,41 @@ class Invoice extends EntityModel
return $this->invoice_status_id >= INVOICE_STATUS_VIEWED;
}
+ public function isPartial()
+ {
+ return $this->invoice_status_id >= INVOICE_STATUS_PARTIAL;
+ }
+
public function isPaid()
{
return $this->invoice_status_id >= INVOICE_STATUS_PAID;
}
+ public function isOverdue()
+ {
+ if ( ! $this->due_date) {
+ return false;
+ }
+
+ return time() > strtotime($this->due_date);
+ }
+
public function getRequestedAmount()
{
return $this->partial > 0 ? $this->partial : $this->balance;
}
+ public function getCurrencyCode()
+ {
+ if ($this->client->currency) {
+ return $this->client->currency->code;
+ } elseif ($this->account->currency) {
+ return $this->account->currency->code;
+ } else {
+ return 'USD';
+ }
+ }
+
public function hidePrivateFields()
{
$this->setVisible([
@@ -130,6 +373,7 @@ class Invoice extends EntityModel
'account',
'invoice_design',
'invoice_design_id',
+ 'invoice_fonts',
'is_pro',
'is_quote',
'custom_value1',
@@ -138,6 +382,9 @@ class Invoice extends EntityModel
'custom_taxes2',
'partial',
'has_tasks',
+ 'custom_text_value1',
+ 'custom_text_value2',
+ 'has_expenses',
]);
$this->client->setVisible([
@@ -160,6 +407,7 @@ class Invoice extends EntityModel
$this->account->setVisible([
'name',
+ 'website',
'id_number',
'vat_number',
'address1',
@@ -184,6 +432,9 @@ class Invoice extends EntityModel
'custom_invoice_label1',
'custom_invoice_label2',
'pdf_email_attachment',
+ 'show_item_taxes',
+ 'custom_invoice_text_label1',
+ 'custom_invoice_text_label2',
]);
foreach ($this->invoice_items as $invoiceItem) {
@@ -209,6 +460,212 @@ class Invoice extends EntityModel
return $this;
}
+ public function getSchedule()
+ {
+ if (!$this->start_date || !$this->is_recurring || !$this->frequency_id) {
+ return false;
+ }
+
+ $startDate = $this->getOriginal('last_sent_date') ?: $this->getOriginal('start_date');
+ $startDate .= ' ' . $this->account->recurring_hour . ':00:00';
+ $startDate = $this->account->getDateTime($startDate);
+ $endDate = $this->end_date ? $this->account->getDateTime($this->getOriginal('end_date')) : null;
+ $timezone = $this->account->getTimezone();
+
+ $rule = $this->getRecurrenceRule();
+ $rule = new \Recurr\Rule("{$rule}", $startDate, $endDate, $timezone);
+
+ // Fix for months with less than 31 days
+ $transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig();
+ $transformerConfig->enableLastDayOfMonthFix();
+
+ $transformer = new \Recurr\Transformer\ArrayTransformer();
+ $transformer->setConfig($transformerConfig);
+ $dates = $transformer->transform($rule);
+
+ if (count($dates) < 2) {
+ return false;
+ }
+
+ return $dates;
+ }
+
+ public function getNextSendDate()
+ {
+ if ($this->start_date && !$this->last_sent_date) {
+ $startDate = $this->getOriginal('start_date') . ' ' . $this->account->recurring_hour . ':00:00';
+ return $this->account->getDateTime($startDate);
+ }
+
+ if (!$schedule = $this->getSchedule()) {
+ return null;
+ }
+
+ if (count($schedule) < 2) {
+ return null;
+ }
+
+ return $schedule[1]->getStart();
+ }
+
+ public function getDueDate($invoice_date = null){
+ if(!$this->is_recurring) {
+ return $this->due_date ? $this->due_date : null;
+ }
+ else{
+ $now = time();
+ if($invoice_date) {
+ // If $invoice_date is specified, all calculations are based on that date
+ if(is_numeric($invoice_date)) {
+ $now = $invoice_date;
+ }
+ else if(is_string($invoice_date)) {
+ $now = strtotime($invoice_date);
+ }
+ elseif ($invoice_date instanceof \DateTime) {
+ $now = $invoice_date->getTimestamp();
+ }
+ }
+
+ if($this->due_date && $this->due_date != '0000-00-00'){
+ // This is a recurring invoice; we're using a custom format here.
+ // The year is always 1998; January is 1st, 2nd, last day of the month.
+ // February is 1st Sunday after, 1st Monday after, ..., through 4th Saturday after.
+ $dueDateVal = strtotime($this->due_date);
+ $monthVal = (int)date('n', $dueDateVal);
+ $dayVal = (int)date('j', $dueDateVal);
+ $dueDate = false;
+
+ if($monthVal == 1) {// January; day of month
+ $currentDay = (int)date('j', $now);
+ $lastDayOfMonth = (int)date('t', $now);
+
+ $dueYear = (int)date('Y', $now);// This year
+ $dueMonth = (int)date('n', $now);// This month
+ $dueDay = $dayVal;// The day specified for the invoice
+
+ if($dueDay > $lastDayOfMonth) {
+ // No later than the end of the month
+ $dueDay = $lastDayOfMonth;
+ }
+
+ if($currentDay >= $dueDay) {
+ // Wait until next month
+ // We don't need to handle the December->January wraparaound, since PHP handles month 13 as January of next year
+ $dueMonth++;
+
+ // Reset the due day
+ $dueDay = $dayVal;
+ $lastDayOfMonth = (int)date('t', mktime(0, 0, 0, $dueMonth, 1, $dueYear));// The number of days in next month
+
+ // Check against the last day again
+ if($dueDay > $lastDayOfMonth){
+ // No later than the end of the month
+ $dueDay = $lastDayOfMonth;
+ }
+ }
+
+ $dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear);
+ }
+ else if($monthVal == 2) {// February; day of week
+ $ordinals = array('first', 'second', 'third', 'fourth');
+ $daysOfWeek = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday');
+
+ $ordinalIndex = ceil($dayVal / 7) - 1;// 1-7 are "first"; 8-14 are "second", etc.
+ $dayOfWeekIndex = ($dayVal - 1) % 7;// 1,8,15,22 are Sunday, 2,9,16,23 are Monday, etc.
+ $dayStr = $ordinals[$ordinalIndex] . ' ' . $daysOfWeek[$dayOfWeekIndex];// "first sunday", "first monday", etc.
+
+ $dueDate = strtotime($dayStr, $now);
+ }
+
+ if($dueDate) {
+ return date('Y-m-d', $dueDate);// SQL format
+ }
+ }
+ else if ($this->client->payment_terms != 0) {
+ // No custom due date set for this invoice; use the client's payment terms
+ $days = $this->client->payment_terms;
+ if ($days == -1) {
+ $days = 0;
+ }
+ return date('Y-m-d', strtotime('+'.$days.' day', $now));
+ }
+ }
+
+ // Couldn't calculate one
+ return null;
+ }
+
+ public function getPrettySchedule($min = 1, $max = 10)
+ {
+ if (!$schedule = $this->getSchedule($max)) {
+ return null;
+ }
+
+ $dates = [];
+
+ for ($i=$min; $i
', $dates);
+ }
+
+ private function getRecurrenceRule()
+ {
+ $rule = '';
+
+ switch ($this->frequency_id) {
+ case FREQUENCY_WEEKLY:
+ $rule = 'FREQ=WEEKLY;';
+ break;
+ case FREQUENCY_TWO_WEEKS:
+ $rule = 'FREQ=WEEKLY;INTERVAL=2;';
+ break;
+ case FREQUENCY_FOUR_WEEKS:
+ $rule = 'FREQ=WEEKLY;INTERVAL=4;';
+ break;
+ case FREQUENCY_MONTHLY:
+ $rule = 'FREQ=MONTHLY;';
+ break;
+ case FREQUENCY_THREE_MONTHS:
+ $rule = 'FREQ=MONTHLY;INTERVAL=3;';
+ break;
+ case FREQUENCY_SIX_MONTHS:
+ $rule = 'FREQ=MONTHLY;INTERVAL=6;';
+ break;
+ case FREQUENCY_ANNUALLY:
+ $rule = 'FREQ=YEARLY;';
+ break;
+ }
+
+ if ($this->end_date) {
+ $rule .= 'UNTIL=' . $this->getOriginal('end_date');
+ }
+
+ return $rule;
+ }
+
+ /*
+ public function shouldSendToday()
+ {
+ if (!$nextSendDate = $this->getNextSendDate()) {
+ return false;
+ }
+
+ return $this->account->getDateTime() >= $nextSendDate;
+ }
+ */
+
public function shouldSendToday()
{
if (!$this->start_date || strtotime($this->start_date) > strtotime('now')) {
@@ -260,26 +717,72 @@ class Invoice extends EntityModel
return false;
}
+
+ public function getPDFString()
+ {
+ if (!env('PHANTOMJS_CLOUD_KEY')) {
+ return false;
+ }
+
+ $invitation = $this->invitations[0];
+ $link = $invitation->getLink();
+ $curl = curl_init();
+
+ $jsonEncodedData = json_encode([
+ 'url' => "{$link}?phantomjs=true",
+ 'renderType' => 'html',
+ 'outputAsJson' => false,
+ 'renderSettings' => [
+ 'passThroughHeaders' => true,
+ ],
+ // 'delayTime' => 1000,
+ ]);
+
+ $opts = [
+ CURLOPT_URL => PHANTOMJS_CLOUD . env('PHANTOMJS_CLOUD_KEY') . '/',
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => $jsonEncodedData,
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ 'Content-Length: '.strlen($jsonEncodedData)
+ ],
+ ];
+
+ curl_setopt_array($curl, $opts);
+ $response = curl_exec($curl);
+ curl_close($curl);
+
+ $encodedString = strip_tags($response);
+ $pdfString = Utils::decodePDF($encodedString);
+
+ if ( ! $pdfString || strlen($pdfString) < 200) {
+ Utils::logError("PhantomJSCloud - failed to create pdf: {$encodedString}");
+ }
+
+ return $pdfString;
+ }
}
Invoice::creating(function ($invoice) {
if (!$invoice->is_recurring) {
- $invoice->account->incrementCounter($invoice->is_quote);
+ $invoice->account->incrementCounter($invoice);
}
});
Invoice::created(function ($invoice) {
- Activity::createInvoice($invoice);
+ if ($invoice->is_quote) {
+ event(new QuoteWasCreated($invoice));
+ } else {
+ event(new InvoiceWasCreated($invoice));
+ }
});
Invoice::updating(function ($invoice) {
- Activity::updateInvoice($invoice);
+ if ($invoice->is_quote) {
+ event(new QuoteWasUpdated($invoice));
+ } else {
+ event(new InvoiceWasUpdated($invoice));
+ }
});
-
-Invoice::deleting(function ($invoice) {
- Activity::archiveInvoice($invoice);
-});
-
-Invoice::restoring(function ($invoice) {
- Activity::restoreInvoice($invoice);
-});
\ No newline at end of file
diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php
index 396a1c713413..b7b3c8ffc8e9 100644
--- a/app/Models/InvoiceItem.php
+++ b/app/Models/InvoiceItem.php
@@ -16,4 +16,10 @@ class InvoiceItem extends EntityModel
{
return $this->belongsTo('App\Models\Product');
}
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
}
diff --git a/app/Models/Language.php b/app/Models/Language.php
index d1e757936808..084c2fe86da7 100644
--- a/app/Models/Language.php
+++ b/app/Models/Language.php
@@ -5,4 +5,9 @@ use Eloquent;
class Language extends Eloquent
{
public $timestamps = false;
+
+ public function getName()
+ {
+ return $this->name;
+ }
}
diff --git a/app/Models/OwnedByClientTrait.php b/app/Models/OwnedByClientTrait.php
new file mode 100644
index 000000000000..7f11448324b9
--- /dev/null
+++ b/app/Models/OwnedByClientTrait.php
@@ -0,0 +1,13 @@
+client) {
+ return false;
+ }
+
+ return $this->client->trashed();
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Payment.php b/app/Models/Payment.php
index ee382dece1b6..a2e8b2591fe3 100644
--- a/app/Models/Payment.php
+++ b/app/Models/Payment.php
@@ -1,11 +1,16 @@
belongsTo('App\Models\Contact');
}
+ public function account_gateway()
+ {
+ return $this->belongsTo('App\Models\AccountGateway');
+ }
+
+ public function payment_type()
+ {
+ return $this->belongsTo('App\Models\PaymentType');
+ }
+
+ public function getRoute()
+ {
+ return "/payments/{$this->public_id}/edit";
+ }
+
+ /*
public function getAmount()
{
return Utils::formatMoney($this->amount, $this->client->getCurrencyId());
}
+ */
public function getName()
{
@@ -53,18 +75,10 @@ class Payment extends EntityModel
}
}
+Payment::creating(function ($payment) {
+
+});
+
Payment::created(function ($payment) {
- Activity::createPayment($payment);
-});
-
-Payment::updating(function ($payment) {
- Activity::updatePayment($payment);
-});
-
-Payment::deleting(function ($payment) {
- Activity::archivePayment($payment);
-});
-
-Payment::restoring(function ($payment) {
- Activity::restorePayment($payment);
-});
+ event(new PaymentWasCreated($payment));
+});
\ No newline at end of file
diff --git a/app/Models/PaymentTerm.php b/app/Models/PaymentTerm.php
index de8cced5db72..dbb788aef1c2 100644
--- a/app/Models/PaymentTerm.php
+++ b/app/Models/PaymentTerm.php
@@ -1,8 +1,17 @@
where('product_key', '=', $key)->first();
}
+
+ public function default_tax_rate()
+ {
+ return $this->belongsTo('App\Models\TaxRate');
+ }
}
diff --git a/app/Models/Task.php b/app/Models/Task.php
index 4ccbf9688ab3..17b667558c49 100644
--- a/app/Models/Task.php
+++ b/app/Models/Task.php
@@ -3,10 +3,14 @@
use DB;
use Utils;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Laracasts\Presenter\PresentableTrait;
class Task extends EntityModel
{
use SoftDeletes;
+ use PresentableTrait;
+
+ protected $presenter = 'App\Ninja\Presenters\TaskPresenter';
public function account()
{
@@ -18,6 +22,11 @@ class Task extends EntityModel
return $this->belongsTo('App\Models\Invoice');
}
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
public function client()
{
return $this->belongsTo('App\Models\Client')->withTrashed();
@@ -82,20 +91,4 @@ class Task extends EntityModel
{
return round($this->getDuration() / (60 * 60), 2);
}
-}
-
-Task::created(function ($task) {
- //Activity::createTask($task);
-});
-
-Task::updating(function ($task) {
- //Activity::updateTask($task);
-});
-
-Task::deleting(function ($task) {
- //Activity::archiveTask($task);
-});
-
-Task::restoring(function ($task) {
- //Activity::restoreTask($task);
-});
+}
\ No newline at end of file
diff --git a/app/Models/TaxRate.php b/app/Models/TaxRate.php
index bb74c89f541c..751cdb3205b2 100644
--- a/app/Models/TaxRate.php
+++ b/app/Models/TaxRate.php
@@ -6,4 +6,9 @@ class TaxRate extends EntityModel
{
use SoftDeletes;
protected $dates = ['deleted_at'];
+
+ public function getEntityType()
+ {
+ return ENTITY_TAX_RATE;
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 93b263a55c63..129ddb0abecd 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -5,6 +5,7 @@ use Auth;
use Event;
use App\Libraries\Utils;
use App\Events\UserSettingsChanged;
+use App\Events\UserSignedUp;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
@@ -28,14 +29,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*
* @var array
*/
- protected $fillable = ['name', 'email', 'password'];
+ protected $fillable = ['first_name', 'last_name', 'email', 'password'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
- protected $hidden = ['password', 'remember_token'];
+ protected $hidden = ['password', 'remember_token', 'confirmation_code'];
use SoftDeletes;
protected $dates = ['deleted_at'];
@@ -95,11 +96,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->account->isPro();
}
- public function isDemo()
- {
- return $this->account->id == Utils::getDemoAccountId();
- }
-
public function maxInvoiceDesignId()
{
return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST);
@@ -134,27 +130,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
return Session::get(SESSION_COUNTER, 0);
}
-
- /*
- public function getPopOverText()
- {
- if (!Utils::isNinja() || !Auth::check() || Session::has('error')) {
- return false;
- }
-
- $count = self::getRequestsCount();
-
- if ($count == 1 || $count % 5 == 0) {
- if (!Utils::isRegistered()) {
- return trans('texts.sign_up_to_save');
- } elseif (!Auth::user()->account->name) {
- return trans('texts.set_name');
- }
- }
-
- return false;
- }
- */
public function afterSave($success = true, $forced = false)
{
@@ -167,9 +142,31 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
public function getMaxNumClients()
{
- return $this->isPro() ? MAX_NUM_CLIENTS_PRO : MAX_NUM_CLIENTS;
+ if ($this->isPro()) {
+ return MAX_NUM_CLIENTS_PRO;
+ }
+
+ if ($this->id < LEGACY_CUTOFF) {
+ return MAX_NUM_CLIENTS_LEGACY;
+ }
+
+ return MAX_NUM_CLIENTS;
}
+ public function getMaxNumVendors()
+ {
+ if ($this->isPro()) {
+ return MAX_NUM_VENDORS_PRO;
+ }
+
+ if ($this->id < LEGACY_CUTOFF) {
+ return MAX_NUM_VENDORS_LEGACY;
+ }
+
+ return MAX_NUM_VENDORS;
+ }
+
+
public function getRememberToken()
{
return $this->remember_token;
@@ -203,20 +200,44 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}
}
- public static function updateUser($user)
+ public static function onUpdatingUser($user)
{
- if ($user->password != !$user->getOriginal('password')) {
+ if ($user->password != $user->getOriginal('password')) {
$user->failed_logins = 0;
}
+
+ // if the user changes their email then they need to reconfirm it
+ if ($user->isEmailBeingChanged()) {
+ $user->confirmed = 0;
+ $user->confirmation_code = str_random(RANDOM_KEY_LENGTH);
+ }
+ }
+
+ public static function onUpdatedUser($user)
+ {
+ if (!$user->getOriginal('email')
+ || $user->getOriginal('email') == TEST_USERNAME
+ || $user->getOriginal('username') == TEST_USERNAME
+ || $user->getOriginal('email') == 'tests@bitrock.com') {
+ event(new UserSignedUp());
+ }
+
+ event(new UserSettingsChanged($user));
+ }
+
+ public function isEmailBeingChanged()
+ {
+ return Utils::isNinjaProd()
+ && $this->email != $this->getOriginal('email')
+ && $this->getOriginal('confirmed');
}
}
User::updating(function ($user) {
- User::updateUser($user);
+ User::onUpdatingUser($user);
});
User::updated(function ($user) {
- Event::fire(new UserSettingsChanged());
+ User::onUpdatedUser($user);
});
-
diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php
new file mode 100644
index 000000000000..bc34fbd2b1dc
--- /dev/null
+++ b/app/Models/Vendor.php
@@ -0,0 +1,242 @@
+ 'first_name',
+ 'last' => 'last_name',
+ 'email' => 'email',
+ 'mobile|phone' => 'phone',
+ 'name|organization' => 'name',
+ 'street2|address2' => 'address2',
+ 'street|address|address1' => 'address1',
+ 'city' => 'city',
+ 'state|province' => 'state',
+ 'zip|postal|code' => 'postal_code',
+ 'country' => 'country',
+ 'note' => 'notes',
+ ];
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Payment');
+ }
+
+ public function vendorContacts()
+ {
+ return $this->hasMany('App\Models\VendorContact');
+ }
+
+ public function country()
+ {
+ return $this->belongsTo('App\Models\Country');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Currency');
+ }
+
+ public function language()
+ {
+ return $this->belongsTo('App\Models\Language');
+ }
+
+ public function size()
+ {
+ return $this->belongsTo('App\Models\Size');
+ }
+
+ public function industry()
+ {
+ return $this->belongsTo('App\Models\Industry');
+ }
+
+ public function addVendorContact($data, $isPrimary = false)
+ {
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
+ if ($publicId && $publicId != '-1') {
+ $contact = VendorContact::scope($publicId)->firstOrFail();
+ } else {
+ $contact = VendorContact::createNew();
+ }
+
+ $contact->fill($data);
+ $contact->is_primary = $isPrimary;
+
+ return $this->vendorContacts()->save($contact);
+ }
+
+ public function getRoute()
+ {
+ return "/vendors/{$this->public_id}";
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getDisplayName()
+ {
+ return $this->getName();
+ }
+
+ public function getCityState()
+ {
+ $swap = $this->country && $this->country->swap_postal_code;
+ return Utils::cityStateZip($this->city, $this->state, $this->postal_code, $swap);
+ }
+
+ public function getEntityType()
+ {
+ return 'vendor';
+ }
+
+ public function hasAddress()
+ {
+ $fields = [
+ 'address1',
+ 'address2',
+ 'city',
+ 'state',
+ 'postal_code',
+ 'country_id',
+ ];
+
+ foreach ($fields as $field) {
+ if ($this->$field) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function getDateCreated()
+ {
+ if ($this->created_at == '0000-00-00 00:00:00') {
+ return '---';
+ } else {
+ return $this->created_at->format('m/d/y h:i a');
+ }
+ }
+
+ public function getCurrencyId()
+ {
+ if ($this->currency_id) {
+ return $this->currency_id;
+ }
+
+ if (!$this->account) {
+ $this->load('account');
+ }
+
+ return $this->account->currency_id ?: DEFAULT_CURRENCY;
+ }
+
+ public function getTotalExpense()
+ {
+ return DB::table('expenses')
+ ->where('vendor_id', '=', $this->id)
+ ->whereNull('deleted_at')
+ ->sum('amount');
+ }
+}
+
+Vendor::creating(function ($vendor) {
+ $vendor->setNullValues();
+});
+
+Vendor::created(function ($vendor) {
+ event(new VendorWasCreated($vendor));
+});
+
+Vendor::updating(function ($vendor) {
+ $vendor->setNullValues();
+});
+
+Vendor::updated(function ($vendor) {
+ event(new VendorWasUpdated($vendor));
+});
+
+
+Vendor::deleting(function ($vendor) {
+ $vendor->setNullValues();
+});
+
+Vendor::deleted(function ($vendor) {
+ event(new VendorWasDeleted($vendor));
+});
diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php
new file mode 100644
index 000000000000..5546b27d2adb
--- /dev/null
+++ b/app/Models/VendorContact.php
@@ -0,0 +1,68 @@
+belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function getPersonType()
+ {
+ return PERSON_VENDOR_CONTACT;
+ }
+
+ public function getName()
+ {
+ return $this->getDisplayName();
+ }
+
+ public function getDisplayName()
+ {
+ if ($this->getFullName()) {
+ return $this->getFullName();
+ } else {
+ return $this->email;
+ }
+ }
+
+ public function getFullName()
+ {
+ if ($this->first_name || $this->last_name) {
+ return $this->first_name.' '.$this->last_name;
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php
new file mode 100644
index 000000000000..8e17bfeec36f
--- /dev/null
+++ b/app/Ninja/Import/BaseTransformer.php
@@ -0,0 +1,97 @@
+maps = $maps;
+ }
+
+ protected function hasClient($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps[ENTITY_CLIENT][$name]);
+ }
+
+ protected function getString($data, $field)
+ {
+ return (isset($data->$field) && $data->$field) ? $data->$field : '';
+ }
+
+ protected function getClientId($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null;
+ }
+
+ protected function getCountryId($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps['countries'][$name]) ? $this->maps['countries'][$name] : null;
+ }
+
+ protected function getCountryIdBy2($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps['countries2'][$name]) ? $this->maps['countries2'][$name] : null;
+ }
+
+ protected function getFirstName($name)
+ {
+ $name = Utils::splitName($name);
+ return $name[0];
+ }
+
+ protected function getDate($date, $format = 'Y-m-d')
+ {
+ if ( ! $date instanceof DateTime) {
+ $date = DateTime::createFromFormat($format, $date);
+ }
+
+ return $date ? $date->format('Y-m-d') : null;
+ }
+
+ protected function getLastName($name)
+ {
+ $name = Utils::splitName($name);
+ return $name[1];
+ }
+
+ protected function getInvoiceNumber($number)
+ {
+ $number = strtolower($number);
+ return str_pad($number, 4, '0', STR_PAD_LEFT);
+ }
+
+ protected function getInvoiceId($invoiceNumber)
+ {
+ $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
+ return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE][$invoiceNumber] : null;
+ }
+
+ protected function hasInvoice($invoiceNumber)
+ {
+ $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
+ return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]);
+ }
+
+ protected function getInvoiceClientId($invoiceNumber)
+ {
+ $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
+ return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null;
+ }
+
+
+ protected function getVendorId($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/CSV/ClientTransformer.php b/app/Ninja/Import/CSV/ClientTransformer.php
new file mode 100644
index 000000000000..b480d5e88f4e
--- /dev/null
+++ b/app/Ninja/Import/CSV/ClientTransformer.php
@@ -0,0 +1,35 @@
+name) && $this->hasClient($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'name'),
+ 'work_phone' => $this->getString($data, 'work_phone'),
+ 'address1' => $this->getString($data, 'address1'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'state'),
+ 'postal_code' => $this->getString($data, 'postal_code'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'first_name'),
+ 'last_name' => $this->getString($data, 'last_name'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'phone'),
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/CSV/InvoiceTransformer.php b/app/Ninja/Import/CSV/InvoiceTransformer.php
new file mode 100644
index 000000000000..e58bfe335ed2
--- /dev/null
+++ b/app/Ninja/Import/CSV/InvoiceTransformer.php
@@ -0,0 +1,38 @@
+getClientId($data->name)) {
+ return false;
+ }
+
+ if (isset($data->invoice_number) && $this->hasInvoice($data->invoice_number)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->name),
+ 'invoice_number' => isset($data->invoice_number) ? $this->getInvoiceNumber($data->invoice_number) : null,
+ 'paid' => isset($data->paid) ? (float) $data->paid : null,
+ 'po_number' => $this->getString($data, 'po_number'),
+ 'terms' => $this->getString($data, 'terms'),
+ 'public_notes' => $this->getString($data, 'public_notes'),
+ 'invoice_date_sql' => isset($data->invoice_date) ? $data->invoice_date : null,
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'notes'),
+ 'cost' => isset($data->amount) ? (float) $data->amount : null,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/CSV/PaymentTransformer.php b/app/Ninja/Import/CSV/PaymentTransformer.php
new file mode 100644
index 000000000000..7acd3d88f839
--- /dev/null
+++ b/app/Ninja/Import/CSV/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ $data->paid,
+ 'payment_date_sql' => isset($data->invoice_date) ? $data->invoice_date : null,
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/CSV/VendorTransformer.php b/app/Ninja/Import/CSV/VendorTransformer.php
new file mode 100644
index 000000000000..464274e5a4fa
--- /dev/null
+++ b/app/Ninja/Import/CSV/VendorTransformer.php
@@ -0,0 +1,35 @@
+name) && $this->hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'name'),
+ 'work_phone' => $this->getString($data, 'work_phone'),
+ 'address1' => $this->getString($data, 'address1'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'state'),
+ 'postal_code' => $this->getString($data, 'postal_code'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'first_name'),
+ 'last_name' => $this->getString($data, 'last_name'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'phone'),
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/FreshBooks/ClientTransformer.php b/app/Ninja/Import/FreshBooks/ClientTransformer.php
new file mode 100644
index 000000000000..d71be4befdd9
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/ClientTransformer.php
@@ -0,0 +1,36 @@
+hasClient($data->organization)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'organization'),
+ 'work_phone' => $this->getString($data, 'busphone'),
+ 'address1' => $this->getString($data, 'street'),
+ 'address2' => $this->getString($data, 'street2'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'province'),
+ 'postal_code' => $this->getString($data, 'postalcode'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'firstname'),
+ 'last_name' => $this->getString($data, 'lastname'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'mobphone') ?: $this->getString($data, 'homephone'),
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/FreshBooks/InvoiceTransformer.php b/app/Ninja/Import/FreshBooks/InvoiceTransformer.php
new file mode 100644
index 000000000000..06c4af967491
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/InvoiceTransformer.php
@@ -0,0 +1,37 @@
+getClientId($data->organization)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->invoice_number)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->organization),
+ 'invoice_number' => $this->getInvoiceNumber($data->invoice_number),
+ 'paid' => (float) $data->paid,
+ 'po_number' => $this->getString($data, 'po_number'),
+ 'terms' => $this->getString($data, 'terms'),
+ 'invoice_date_sql' => $data->create_date,
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'notes'),
+ 'cost' => (float) $data->amount,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/FreshBooks/PaymentTransformer.php b/app/Ninja/Import/FreshBooks/PaymentTransformer.php
new file mode 100644
index 000000000000..1f69fdbacf41
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ $data->paid,
+ 'payment_date_sql' => $data->create_date,
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/FreshBooks/TaskTransformer.php b/app/Ninja/Import/FreshBooks/TaskTransformer.php
new file mode 100644
index 000000000000..8c1363edcfec
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/TaskTransformer.php
@@ -0,0 +1,29 @@
+hours * 3600);
+ $timeLogFinish = strtotime($data->date);
+ $timeLogStart = intval($timeLogFinish - $seconds);
+ $timeLog[] = [];
+ $timelog[] = $timeLogStart;
+ $timelog[] = $timeLogFinish;
+ $timeLog = json_encode(array($timelog));
+
+ return [
+ 'action' => 'stop',
+ 'time_log' => $timeLog,
+ 'description' => $data->task,
+ ];
+ }
+
+}
+*/
\ No newline at end of file
diff --git a/app/Ninja/Import/FreshBooks/VendorTransformer.php b/app/Ninja/Import/FreshBooks/VendorTransformer.php
new file mode 100644
index 000000000000..c083360aa305
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/VendorTransformer.php
@@ -0,0 +1,36 @@
+hasVendor($data->organization)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->organization,
+ 'work_phone' => $data->busphone,
+ 'address1' => $data->street,
+ 'address2' => $data->street2,
+ 'city' => $data->city,
+ 'state' => $data->province,
+ 'postal_code' => $data->postalcode,
+ 'private_notes' => $data->notes,
+ 'contacts' => [
+ [
+ 'first_name' => $data->firstname,
+ 'last_name' => $data->lastname,
+ 'email' => $data->email,
+ 'phone' => $data->mobphone ?: $data->homephone,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/ClientTransformer.php b/app/Ninja/Import/Harvest/ClientTransformer.php
new file mode 100644
index 000000000000..fb8200ec3ae0
--- /dev/null
+++ b/app/Ninja/Import/Harvest/ClientTransformer.php
@@ -0,0 +1,20 @@
+hasClient($data->client_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'client_name'),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/ContactTransformer.php b/app/Ninja/Import/Harvest/ContactTransformer.php
new file mode 100644
index 000000000000..6baf883c95c2
--- /dev/null
+++ b/app/Ninja/Import/Harvest/ContactTransformer.php
@@ -0,0 +1,24 @@
+hasClient($data->client)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->client),
+ 'first_name' => $this->getString($data, 'first_name'),
+ 'last_name' => $this->getString($data, 'last_name'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'office_phone') ?: $this->getString($data, 'mobile_phone'),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/InvoiceTransformer.php b/app/Ninja/Import/Harvest/InvoiceTransformer.php
new file mode 100644
index 000000000000..850eeede594c
--- /dev/null
+++ b/app/Ninja/Import/Harvest/InvoiceTransformer.php
@@ -0,0 +1,36 @@
+getClientId($data->client)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->id)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->client),
+ 'invoice_number' => $this->getInvoiceNumber($data->id),
+ 'paid' => (float) $data->paid_amount,
+ 'po_number' => $this->getString($data, 'po_number'),
+ 'invoice_date_sql' => $this->getDate($data->issue_date, 'm/d/Y'),
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'subject'),
+ 'cost' => (float) $data->invoice_amount,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Harvest/PaymentTransformer.php b/app/Ninja/Import/Harvest/PaymentTransformer.php
new file mode 100644
index 000000000000..0efd442886cc
--- /dev/null
+++ b/app/Ninja/Import/Harvest/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ $data->paid_amount,
+ 'payment_date_sql' => $this->getDate($data->last_payment_date, 'm/d/Y'),
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Harvest/VendorContactTransformer.php b/app/Ninja/Import/Harvest/VendorContactTransformer.php
new file mode 100644
index 000000000000..3aa0b0b36aa2
--- /dev/null
+++ b/app/Ninja/Import/Harvest/VendorContactTransformer.php
@@ -0,0 +1,24 @@
+hasVendor($data->vendor)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'vendor_id' => $this->getVendorId($data->vendor),
+ 'first_name' => $data->first_name,
+ 'last_name' => $data->last_name,
+ 'email' => $data->email,
+ 'phone' => $data->office_phone ?: $data->mobile_phone,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/VendorTransformer.php b/app/Ninja/Import/Harvest/VendorTransformer.php
new file mode 100644
index 000000000000..efab1e6b66ad
--- /dev/null
+++ b/app/Ninja/Import/Harvest/VendorTransformer.php
@@ -0,0 +1,20 @@
+hasVendor($data->vendor_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->vendor_name,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Hiveage/ClientTransformer.php b/app/Ninja/Import/Hiveage/ClientTransformer.php
new file mode 100644
index 000000000000..515eb8353562
--- /dev/null
+++ b/app/Ninja/Import/Hiveage/ClientTransformer.php
@@ -0,0 +1,35 @@
+hasClient($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'name'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->primary_contact),
+ 'last_name' => $this->getLastName($data->primary_contactk),
+ 'email' => $this->getString($data, 'business_email'),
+ ],
+ ],
+ 'address1' => $this->getString($data, 'address_1'),
+ 'address2' => $this->getString($data, 'address_2'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'state_name'),
+ 'postal_code' => $this->getString($data, 'zip_code'),
+ 'work_phone' => $this->getString($data, 'phone'),
+ 'website' => $this->getString($data, 'website'),
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Hiveage/InvoiceTransformer.php b/app/Ninja/Import/Hiveage/InvoiceTransformer.php
new file mode 100644
index 000000000000..e9054f1b8791
--- /dev/null
+++ b/app/Ninja/Import/Hiveage/InvoiceTransformer.php
@@ -0,0 +1,36 @@
+getClientId($data->client)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->statement_no)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->client),
+ 'invoice_number' => $this->getInvoiceNumber($data->statement_no),
+ 'paid' => (float) $data->paid_total,
+ 'invoice_date_sql' => $this->getDate($data->date),
+ 'due_date_sql' => $this->getDate($data->due_date),
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'summary'),
+ 'cost' => (float) $data->billed_total,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Hiveage/PaymentTransformer.php b/app/Ninja/Import/Hiveage/PaymentTransformer.php
new file mode 100644
index 000000000000..d6232d05bcc9
--- /dev/null
+++ b/app/Ninja/Import/Hiveage/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ $data->paid_total,
+ 'payment_date_sql' => $this->getDate($data->last_paid_on),
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Hiveage/VendorTransformer.php b/app/Ninja/Import/Hiveage/VendorTransformer.php
new file mode 100644
index 000000000000..dec1b62d1ccb
--- /dev/null
+++ b/app/Ninja/Import/Hiveage/VendorTransformer.php
@@ -0,0 +1,35 @@
+hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->name,
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->primary_contact),
+ 'last_name' => $this->getLastName($data->primary_contactk),
+ 'email' => $data->business_email,
+ ],
+ ],
+ 'address1' => $data->address_1,
+ 'address2' => $data->address_2,
+ 'city' => $data->city,
+ 'state' => $data->state_name,
+ 'postal_code' => $data->zip_code,
+ 'work_phone' => $data->phone,
+ 'website' => $data->website,
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Invoiceable/ClientTransformer.php b/app/Ninja/Import/Invoiceable/ClientTransformer.php
new file mode 100644
index 000000000000..7e462ceef9b0
--- /dev/null
+++ b/app/Ninja/Import/Invoiceable/ClientTransformer.php
@@ -0,0 +1,34 @@
+hasClient($data->client_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'client_name'),
+ 'work_phone' => $this->getString($data, 'tel'),
+ 'website' => $this->getString($data, 'website'),
+ 'address1' => $this->getString($data, 'address'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'state'),
+ 'postal_code' => $this->getString($data, 'postcode'),
+ 'country_id' => $this->getCountryIdBy2($data->country),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'contacts' => [
+ [
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'mobile'),
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Invoiceable/InvoiceTransformer.php b/app/Ninja/Import/Invoiceable/InvoiceTransformer.php
new file mode 100644
index 000000000000..f6697a7e90c2
--- /dev/null
+++ b/app/Ninja/Import/Invoiceable/InvoiceTransformer.php
@@ -0,0 +1,38 @@
+getClientId($data->client_name)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->ref)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->client_name),
+ 'invoice_number' => $this->getInvoiceNumber($data->ref),
+ 'po_number' => $this->getString($data, 'po_number'),
+ 'invoice_date_sql' => $data->date,
+ 'due_date_sql' => $data->due_date,
+ 'invoice_footer' => $this->getString($data, 'footer'),
+ 'paid' => (float) $data->paid,
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'description'),
+ 'cost' => (float) $data->total,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Invoiceable/PaymentTransformer.php b/app/Ninja/Import/Invoiceable/PaymentTransformer.php
new file mode 100644
index 000000000000..c52494cdc689
--- /dev/null
+++ b/app/Ninja/Import/Invoiceable/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ $data->paid,
+ 'payment_date_sql' => $data->date_paid,
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Invoiceable/VendorTransformer.php b/app/Ninja/Import/Invoiceable/VendorTransformer.php
new file mode 100644
index 000000000000..1ec4a2876884
--- /dev/null
+++ b/app/Ninja/Import/Invoiceable/VendorTransformer.php
@@ -0,0 +1,34 @@
+hasVendor($data->vendor_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->vendor_name,
+ 'work_phone' => $data->tel,
+ 'website' => $data->website,
+ 'address1' => $data->address,
+ 'city' => $data->city,
+ 'state' => $data->state,
+ 'postal_code' => $data->postcode,
+ 'country_id' => $this->getCountryIdBy2($data->country),
+ 'private_notes' => $data->notes,
+ 'contacts' => [
+ [
+ 'email' => $data->email,
+ 'phone' => $data->mobile,
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Nutcache/ClientTransformer.php b/app/Ninja/Import/Nutcache/ClientTransformer.php
new file mode 100644
index 000000000000..74705a597a24
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/ClientTransformer.php
@@ -0,0 +1,35 @@
+hasClient($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'name'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'stateprovince'),
+ 'id_number' => $this->getString($data, 'registration_number'),
+ 'postal_code' => $this->getString($data, 'postalzip_code'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'work_phone' => $this->getString($data, 'phone'),
+ 'contacts' => [
+ [
+ 'first_name' => isset($data->contact_name) ? $this->getFirstName($data->contact_name) : '',
+ 'last_name' => isset($data->contact_name) ? $this->getLastName($data->contact_name) : '',
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'mobile'),
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Nutcache/InvoiceTransformer.php b/app/Ninja/Import/Nutcache/InvoiceTransformer.php
new file mode 100644
index 000000000000..a3e3bc91372d
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/InvoiceTransformer.php
@@ -0,0 +1,39 @@
+getClientId($data->client)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->document_no)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->client),
+ 'invoice_number' => $this->getInvoiceNumber($data->document_no),
+ 'paid' => (float) $data->paid_to_date,
+ 'po_number' => $this->getString($data, 'purchase_order'),
+ 'terms' => $this->getString($data, 'terms'),
+ 'public_notes' => $this->getString($data, 'notes'),
+ 'invoice_date_sql' => $this->getDate($data->date),
+ 'due_date_sql' => $this->getDate($data->due_date),
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'description'),
+ 'cost' => (float) $data->total,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Nutcache/PaymentTransformer.php b/app/Ninja/Import/Nutcache/PaymentTransformer.php
new file mode 100644
index 000000000000..04e783361f80
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ (float) $data->paid_to_date,
+ 'payment_date_sql' => $this->getDate($data->date),
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Nutcache/TaskTransformer.php b/app/Ninja/Import/Nutcache/TaskTransformer.php
new file mode 100644
index 000000000000..8c1363edcfec
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/TaskTransformer.php
@@ -0,0 +1,29 @@
+hours * 3600);
+ $timeLogFinish = strtotime($data->date);
+ $timeLogStart = intval($timeLogFinish - $seconds);
+ $timeLog[] = [];
+ $timelog[] = $timeLogStart;
+ $timelog[] = $timeLogFinish;
+ $timeLog = json_encode(array($timelog));
+
+ return [
+ 'action' => 'stop',
+ 'time_log' => $timeLog,
+ 'description' => $data->task,
+ ];
+ }
+
+}
+*/
\ No newline at end of file
diff --git a/app/Ninja/Import/Nutcache/VendorTransformer.php b/app/Ninja/Import/Nutcache/VendorTransformer.php
new file mode 100644
index 000000000000..b97f0811906e
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/VendorTransformer.php
@@ -0,0 +1,35 @@
+hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->name,
+ 'city' => isset($data->city) ? $data->city : '',
+ 'state' => isset($data->city) ? $data->stateprovince : '',
+ 'id_number' => isset($data->registration_number) ? $data->registration_number : '',
+ 'postal_code' => isset($data->postalzip_code) ? $data->postalzip_code : '',
+ 'private_notes' => isset($data->notes) ? $data->notes : '',
+ 'work_phone' => isset($data->phone) ? $data->phone : '',
+ 'contacts' => [
+ [
+ 'first_name' => isset($data->contact_name) ? $this->getFirstName($data->contact_name) : '',
+ 'last_name' => isset($data->contact_name) ? $this->getLastName($data->contact_name) : '',
+ 'email' => $data->email,
+ 'phone' => isset($data->mobile) ? $data->mobile : '',
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Ronin/ClientTransformer.php b/app/Ninja/Import/Ronin/ClientTransformer.php
new file mode 100644
index 000000000000..f79523830e99
--- /dev/null
+++ b/app/Ninja/Import/Ronin/ClientTransformer.php
@@ -0,0 +1,28 @@
+hasClient($data->company)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'company'),
+ 'work_phone' => $this->getString($data, 'phone'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->name),
+ 'last_name' => $this->getLastName($data->name),
+ 'email' => $this->getString($data, 'email'),
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Ronin/InvoiceTransformer.php b/app/Ninja/Import/Ronin/InvoiceTransformer.php
new file mode 100644
index 000000000000..5a4ff6ce2aba
--- /dev/null
+++ b/app/Ninja/Import/Ronin/InvoiceTransformer.php
@@ -0,0 +1,37 @@
+getClientId($data->client)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->number)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->client),
+ 'invoice_number' => $this->getInvoiceNumber($data->number),
+ 'paid' => (float) $data->total - (float) $data->balance,
+ 'public_notes' => $this->getString($data, 'subject'),
+ 'invoice_date_sql' => $data->date_sent,
+ 'due_date_sql' => $data->date_due,
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'line_item'),
+ 'cost' => (float) $data->total,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Ronin/PaymentTransformer.php b/app/Ninja/Import/Ronin/PaymentTransformer.php
new file mode 100644
index 000000000000..c04101456200
--- /dev/null
+++ b/app/Ninja/Import/Ronin/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ (float) $data->total - (float) $data->balance,
+ 'payment_date_sql' => $data->date_paid,
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Ronin/VendorTransformer.php b/app/Ninja/Import/Ronin/VendorTransformer.php
new file mode 100644
index 000000000000..817de03d6647
--- /dev/null
+++ b/app/Ninja/Import/Ronin/VendorTransformer.php
@@ -0,0 +1,28 @@
+hasVendor($data->company)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->company,
+ 'work_phone' => $data->phone,
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->name),
+ 'last_name' => $this->getLastName($data->name),
+ 'email' => $data->email,
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Wave/ClientTransformer.php b/app/Ninja/Import/Wave/ClientTransformer.php
new file mode 100644
index 000000000000..f76ba9c48a26
--- /dev/null
+++ b/app/Ninja/Import/Wave/ClientTransformer.php
@@ -0,0 +1,38 @@
+hasClient($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'customer_name'),
+ 'id_number' => $this->getString($data, 'account_number'),
+ 'work_phone' => $this->getString($data, 'phone'),
+ 'website' => $this->getString($data, 'website'),
+ 'address1' => $this->getString($data, 'address_line_1'),
+ 'address2' => $this->getString($data, 'address_line_2'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'provincestate'),
+ 'postal_code' => $this->getString($data, 'postal_codezip_code'),
+ 'private_notes' => $this->getString($data, 'delivery_instructions'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'contact_first_name'),
+ 'last_name' => $this->getString($data, 'contact_last_name'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'mobile'),
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Wave/InvoiceTransformer.php b/app/Ninja/Import/Wave/InvoiceTransformer.php
new file mode 100644
index 000000000000..b10585aa72bd
--- /dev/null
+++ b/app/Ninja/Import/Wave/InvoiceTransformer.php
@@ -0,0 +1,37 @@
+getClientId($data->customer)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->invoice_num)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->customer),
+ 'invoice_number' => $this->getInvoiceNumber($data->invoice_num),
+ 'po_number' => $this->getString($data, 'po_so'),
+ 'invoice_date_sql' => $this->getDate($data->invoice_date),
+ 'due_date_sql' => $this->getDate($data->due_date),
+ 'paid' => 0,
+ 'invoice_items' => [
+ [
+ 'product_key' => $this->getString($data, 'product'),
+ 'notes' => $this->getString($data, 'description'),
+ 'cost' => (float) $data->amount,
+ 'qty' => (float) $data->quantity,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Wave/PaymentTransformer.php b/app/Ninja/Import/Wave/PaymentTransformer.php
new file mode 100644
index 000000000000..522fe8ff9238
--- /dev/null
+++ b/app/Ninja/Import/Wave/PaymentTransformer.php
@@ -0,0 +1,23 @@
+getInvoiceClientId($data->invoice_num)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) use ($maps) {
+ return [
+ 'amount' => (float) $data->amount,
+ 'payment_date_sql' => $this->getDate($data->payment_date),
+ 'client_id' => $this->getInvoiceClientId($data->invoice_num),
+ 'invoice_id' => $this->getInvoiceId($data->invoice_num),
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Wave/VendorTransformer.php b/app/Ninja/Import/Wave/VendorTransformer.php
new file mode 100644
index 000000000000..f2fe2f43e375
--- /dev/null
+++ b/app/Ninja/Import/Wave/VendorTransformer.php
@@ -0,0 +1,38 @@
+hasVendor($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->customer_name,
+ 'id_number' => $data->account_number,
+ 'work_phone' => $data->phone,
+ 'website' => $data->website,
+ 'address1' => $data->address_line_1,
+ 'address2' => $data->address_line_2,
+ 'city' => $data->city,
+ 'state' => $data->provincestate,
+ 'postal_code' => $data->postal_codezip_code,
+ 'private_notes' => $data->delivery_instructions,
+ 'contacts' => [
+ [
+ 'first_name' => $data->contact_first_name,
+ 'last_name' => $data->contact_last_name,
+ 'email' => $data->email,
+ 'phone' => $data->mobile,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Zoho/ClientTransformer.php b/app/Ninja/Import/Zoho/ClientTransformer.php
new file mode 100644
index 000000000000..689bd1cf1a26
--- /dev/null
+++ b/app/Ninja/Import/Zoho/ClientTransformer.php
@@ -0,0 +1,37 @@
+hasClient($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'customer_name'),
+ 'id_number' => $this->getString($data, 'customer_id'),
+ 'work_phone' => $this->getString($data, 'phone'),
+ 'address1' => $this->getString($data, 'billing_address'),
+ 'city' => $this->getString($data, 'billing_city'),
+ 'state' => $this->getString($data, 'billing_state'),
+ 'postal_code' => $this->getString($data, 'billing_code'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'website' => $this->getString($data, 'website'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'first_name'),
+ 'last_name' => $this->getString($data, 'last_name'),
+ 'email' => $this->getString($data, 'emailid'),
+ 'phone' => $this->getString($data, 'mobilephone'),
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->billing_country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Zoho/InvoiceTransformer.php b/app/Ninja/Import/Zoho/InvoiceTransformer.php
new file mode 100644
index 000000000000..f6fc3c44a7a1
--- /dev/null
+++ b/app/Ninja/Import/Zoho/InvoiceTransformer.php
@@ -0,0 +1,37 @@
+getClientId($data->customer_name)) {
+ return false;
+ }
+
+ if ($this->hasInvoice($data->invoice_number)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'client_id' => $this->getClientId($data->customer_name),
+ 'invoice_number' => $this->getInvoiceNumber($data->invoice_number),
+ 'paid' => (float) $data->total - (float) $data->balance,
+ 'po_number' => $this->getString($data, 'purchaseorder'),
+ 'due_date_sql' => $data->due_date,
+ 'invoice_date_sql' => $data->invoice_date,
+ 'invoice_items' => [
+ [
+ 'product_key' => '',
+ 'notes' => $this->getString($data, 'item_desc'),
+ 'cost' => (float) $data->total,
+ 'qty' => 1,
+ ]
+ ],
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Zoho/PaymentTransformer.php b/app/Ninja/Import/Zoho/PaymentTransformer.php
new file mode 100644
index 000000000000..a8fc74962321
--- /dev/null
+++ b/app/Ninja/Import/Zoho/PaymentTransformer.php
@@ -0,0 +1,19 @@
+ (float) $data->total - (float) $data->balance,
+ 'payment_date_sql' => $data->last_payment_date,
+ 'client_id' => $data->client_id,
+ 'invoice_id' => $data->invoice_id,
+ ];
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Import/Zoho/VendorTransformer.php b/app/Ninja/Import/Zoho/VendorTransformer.php
new file mode 100644
index 000000000000..811a9f7ff2d9
--- /dev/null
+++ b/app/Ninja/Import/Zoho/VendorTransformer.php
@@ -0,0 +1,37 @@
+hasVendor($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->customer_name,
+ 'id_number' => $data->customer_id,
+ 'work_phone' => $data->phonek,
+ 'address1' => $data->billing_address,
+ 'city' => $data->billing_city,
+ 'state' => $data->billing_state,
+ 'postal_code' => $data->billing_code,
+ 'private_notes' => $data->notes,
+ 'website' => $data->website,
+ 'contacts' => [
+ [
+ 'first_name' => $data->first_name,
+ 'last_name' => $data->last_name,
+ 'email' => $data->emailid,
+ 'phone' => $data->mobilephone,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->billing_country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php
index f68633e83414..5e75a81120ad 100644
--- a/app/Ninja/Mailers/ContactMailer.php
+++ b/app/Ninja/Mailers/ContactMailer.php
@@ -1,110 +1,204 @@
load('invitations', 'client', 'account');
+ $invoice->load('invitations', 'client.language', 'account');
$entityType = $invoice->getEntityType();
- $view = 'invoice';
- $subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]);
- $accountName = $invoice->account->getDisplayName();
- $emailTemplate = $invoice->account->getEmailTemplate($entityType);
- $invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId());
+ $client = $invoice->client;
+ $account = $invoice->account;
- $this->initClosure($invoice);
+ if ($client->trashed()) {
+ return trans('texts.email_errors.inactive_client');
+ } elseif ($invoice->trashed()) {
+ return trans('texts.email_errors.inactive_invoice');
+ }
+
+ $account->loadLocalizationSettings($client);
+ $emailTemplate = $account->getEmailTemplate($reminder ?: $entityType);
+ $emailSubject = $account->getEmailSubject($reminder ?: $entityType);
+
+ $sent = false;
+
+ if ($account->attatchPDF() && !$pdfString) {
+ $pdfString = $invoice->getPDFString();
+ }
foreach ($invoice->invitations as $invitation) {
- if (!$invitation->user || !$invitation->user->email || $invitation->user->trashed()) {
- return false;
+ $response = $this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString);
+ if ($response === true) {
+ $sent = true;
}
- if (!$invitation->contact || !$invitation->contact->email || $invitation->contact->trashed()) {
- return false;
- }
-
- $invitation->sent_date = \Carbon::now()->toDateTimeString();
- $invitation->save();
-
- $variables = [
- '$footer' => $invoice->account->getEmailFooter(),
- '$link' => $invitation->getLink(),
- '$client' => $invoice->client->getDisplayName(),
- '$account' => $accountName,
- '$contact' => $invitation->contact->getDisplayName(),
- '$amount' => $invoiceAmount,
- '$advancedRawInvoice->' => '$'
- ];
-
- // Add variables for available payment types
- foreach (Gateway::getPaymentTypeLinks() as $type) {
- $variables["\${$type}_link"] = URL::to("/payment/{$invitation->invitation_key}/{$type}");
- }
-
- $data['body'] = str_replace(array_keys($variables), array_values($variables), $emailTemplate);
- $data['body'] = preg_replace_callback('/\{\{\$?(.*)\}\}/', $this->advancedTemplateHandler, $data['body']);
- $data['link'] = $invitation->getLink();
- $data['entityType'] = $entityType;
- $data['invoice_id'] = $invoice->id;
-
- $fromEmail = $invitation->user->email;
- $response = $this->sendTo($invitation->contact->email, $fromEmail, $accountName, $subject, $view, $data);
-
- if ($response !== true) {
- return $response;
- }
-
- Activity::emailInvoice($invitation);
}
+
+ $account->loadLocalizationSettings();
- if (!$invoice->isSent()) {
- $invoice->invoice_status_id = INVOICE_STATUS_SENT;
- $invoice->save();
+ if ($sent === true) {
+ if ($invoice->is_quote) {
+ event(new QuoteWasEmailed($invoice));
+ } else {
+ event(new InvoiceWasEmailed($invoice));
+ }
}
- Event::fire(new InvoiceSent($invoice));
-
return $response;
}
- public function sendPaymentConfirmation(Payment $payment)
+ private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString)
{
- $invoice = $payment->invoice;
- $view = 'payment_confirmation';
- $subject = trans('texts.payment_subject', ['invoice' => $invoice->invoice_number]);
- $accountName = $payment->account->getDisplayName();
- $emailTemplate = $invoice->account->getEmailTemplate(ENTITY_PAYMENT);
+ $client = $invoice->client;
+ $account = $invoice->account;
+
+ if (Auth::check()) {
+ $user = Auth::user();
+ } else {
+ $user = $invitation->user;
+ if ($invitation->user->trashed()) {
+ $user = $account->users()->orderBy('id')->first();
+ }
+ }
+
+ if (!$user->email || !$user->registered) {
+ return trans('texts.email_errors.user_unregistered');
+ } elseif (!$user->confirmed) {
+ return trans('texts.email_errors.user_unconfirmed');
+ } elseif (!$invitation->contact->email) {
+ return trans('texts.email_errors.invalid_contact_email');
+ } elseif ($invitation->contact->trashed()) {
+ return trans('texts.email_errors.inactive_contact');
+ }
$variables = [
- '$footer' => $payment->account->getEmailFooter(),
- '$client' => $payment->client->getDisplayName(),
- '$account' => $accountName,
- '$amount' => Utils::formatMoney($payment->amount, $payment->client->getCurrencyId())
+ 'account' => $account,
+ 'client' => $client,
+ 'invitation' => $invitation,
+ 'amount' => $invoice->getRequestedAmount()
];
- $data = ['body' => str_replace(array_keys($variables), array_values($variables), $emailTemplate)];
+ $data = [
+ 'body' => $this->processVariables($body, $variables),
+ 'link' => $invitation->getLink(),
+ 'entityType' => $invoice->getEntityType(),
+ 'invoiceId' => $invoice->id,
+ 'invitation' => $invitation,
+ 'account' => $account,
+ 'client' => $client,
+ 'invoice' => $invoice,
+ ];
+
+ if ($account->attatchPDF()) {
+ $data['pdfString'] = $pdfString;
+ $data['pdfFileName'] = $invoice->getFileName();
+ }
+
+ $subject = $this->processVariables($subject, $variables);
+ $fromEmail = $user->email;
+
+ if ($account->email_design_id == EMAIL_DESIGN_PLAIN) {
+ $view = ENTITY_INVOICE;
+ } else {
+ $view = 'design' . ($account->email_design_id - 1);
+ }
+
+ $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data);
+
+ if ($response === true) {
+ return true;
+ } else {
+ return $response;
+ }
+ }
+
+ public function sendPaymentConfirmation(Payment $payment)
+ {
+ $account = $payment->account;
+ $client = $payment->client;
+
+ $account->loadLocalizationSettings($client);
+
+ $invoice = $payment->invoice;
+ $accountName = $account->getDisplayName();
+ $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT);
+ $emailSubject = $invoice->account->getEmailSubject(ENTITY_PAYMENT);
if ($payment->invitation) {
$user = $payment->invitation->user;
$contact = $payment->contact;
+ $invitation = $payment->invitation;
} else {
$user = $payment->user;
- $contact = $payment->client->contacts[0];
+ $contact = $client->contacts[0];
+ $invitation = $payment->invoice->invitations[0];
+ }
+
+ $variables = [
+ 'account' => $account,
+ 'client' => $client,
+ 'invitation' => $invitation,
+ 'amount' => $payment->amount,
+ ];
+
+ $data = [
+ 'body' => $this->processVariables($emailTemplate, $variables),
+ 'link' => $invitation->getLink(),
+ 'invoice' => $invoice,
+ 'client' => $client,
+ 'account' => $account,
+ 'payment' => $payment,
+ 'entityType' => ENTITY_INVOICE,
+ ];
+
+ if ($account->attatchPDF()) {
+ $data['pdfString'] = $invoice->getPDFString();
+ $data['pdfFileName'] = $invoice->getFileName();
+ }
+
+ $subject = $this->processVariables($emailSubject, $variables);
+ $data['invoice_id'] = $payment->invoice->id;
+
+ if ($account->email_design_id == EMAIL_DESIGN_PLAIN) {
+ $view = 'payment_confirmation';
+ } else {
+ $view = 'design' . ($account->email_design_id - 1);
}
if ($user->email && $contact->email) {
$this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data);
}
+
+ $account->loadLocalizationSettings();
}
public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId)
@@ -121,30 +215,53 @@ class ContactMailer extends Mailer
}
$data = [
- 'account' => trans('texts.email_from'),
'client' => $name,
- 'amount' => Utils::formatMoney($amount, 1),
+ 'amount' => Utils::formatMoney($amount, DEFAULT_CURRENCY, DEFAULT_COUNTRY),
'license' => $license
];
$this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
- private function initClosure($object)
+ private function processVariables($template, $data)
{
- $this->advancedTemplateHandler = function($match) use ($object) {
- for ($i = 1; $i < count($match); $i++) {
- $blobConversion = $match[$i];
+ $account = $data['account'];
+ $client = $data['client'];
+ $invitation = $data['invitation'];
+ $invoice = $invitation->invoice;
- if (isset($$blobConversion)) {
- return $$blobConversion;
- } else if (preg_match('/trans\(([\w\.]+)\)/', $blobConversion, $regexTranslation)) {
- return trans($regexTranslation[1]);
- } else if (strpos($blobConversion, '->') !== false) {
- return Utils::stringToObjectResolution($object, $blobConversion);
- }
+ $variables = [
+ '$footer' => $account->getEmailFooter(),
+ '$client' => $client->getDisplayName(),
+ '$account' => $account->getDisplayName(),
+ '$contact' => $invitation->contact->getDisplayName(),
+ '$firstName' => $invitation->contact->first_name,
+ '$amount' => $account->formatMoney($data['amount'], $client),
+ '$invoice' => $invoice->invoice_number,
+ '$quote' => $invoice->invoice_number,
+ '$link' => $invitation->getLink(),
+ '$dueDate' => $account->formatDate($invoice->due_date),
+ '$viewLink' => $invitation->getLink(),
+ '$viewButton' => HTML::emailViewButton($invitation->getLink(), $invoice->getEntityType()),
+ '$paymentLink' => $invitation->getLink('payment'),
+ '$paymentButton' => HTML::emailPaymentButton($invitation->getLink('payment')),
+ '$customClient1' => $account->custom_client_label1,
+ '$customClient2' => $account->custom_client_label2,
+ '$customInvoice1' => $account->custom_invoice_text_label1,
+ '$customInvoice2' => $account->custom_invoice_text_label2,
+ ];
- }
- };
+ // Add variables for available payment types
+ foreach (Gateway::$paymentTypes as $type) {
+ $camelType = Gateway::getPaymentTypeName($type);
+ $type = Utils::toSnakeCase($camelType);
+ $variables["\${$camelType}Link"] = $invitation->getLink() . "/{$type}";
+ $variables["\${$camelType}Button"] = HTML::emailPaymentButton($invitation->getLink('payment') . "/{$type}");
+ }
+
+ $str = str_replace(array_keys($variables), array_values($variables), $template);
+ $str = autolink($str, 100);
+
+ return $str;
}
}
diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php
index 9995a27c4df5..c30c9a10d74c 100644
--- a/app/Ninja/Mailers/Mailer.php
+++ b/app/Ninja/Mailers/Mailer.php
@@ -9,44 +9,86 @@ class Mailer
{
public function sendTo($toEmail, $fromEmail, $fromName, $subject, $view, $data = [])
{
- $views = [
- 'emails.'.$view.'_html',
- 'emails.'.$view.'_text',
- ];
+ // check the username is set
+ if ( ! env('POSTMARK_API_TOKEN') && ! env('MAIL_USERNAME')) {
+ return trans('texts.invalid_mail_config');
+ }
+
+ // don't send emails to dummy addresses
+ if (stristr($toEmail, '@example.com')) {
+ return true;
+ }
+
+ if (isset($_ENV['POSTMARK_API_TOKEN'])) {
+ $views = 'emails.'.$view.'_html';
+ } else {
+ $views = [
+ 'emails.'.$view.'_html',
+ 'emails.'.$view.'_text',
+ ];
+ }
try {
- Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) {
+ $response = Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) {
$toEmail = strtolower($toEmail);
$replyEmail = $fromEmail;
$fromEmail = CONTACT_EMAIL;
- if (isset($data['invoice_id'])) {
- $invoice = Invoice::with('account')->where('id', '=', $data['invoice_id'])->get()->first();
- if($invoice->account->pdf_email_attachment && file_exists($invoice->getPDFPath())) {
- $message->attach(
- $invoice->getPDFPath(),
- array('as' => $invoice->getFileName(), 'mime' => 'application/pdf')
- );
- }
- }
-
$message->to($toEmail)
->from($fromEmail, $fromName)
->replyTo($replyEmail, $fromName)
->subject($subject);
+ // Attach the PDF to the email
+ if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) {
+ $message->attachData($data['pdfString'], $data['pdfFileName']);
+ }
});
-
- return true;
+
+ return $this->handleSuccess($response, $data);
} catch (Exception $exception) {
- if (isset($_ENV['POSTMARK_API_TOKEN'])) {
- $response = $exception->getResponse()->getBody()->getContents();
- $response = json_decode($response);
- return nl2br($response->Message);
- } else {
- return $exception->getMessage();
- }
+ return $this->handleFailure($exception);
}
}
+
+ private function handleSuccess($response, $data)
+ {
+ if (isset($data['invitation'])) {
+ $invitation = $data['invitation'];
+ $invoice = $invitation->invoice;
+ $messageId = false;
+
+ // Track the Postmark message id
+ if (isset($_ENV['POSTMARK_API_TOKEN']) && $response) {
+ $json = json_decode((string) $response->getBody());
+ $messageId = $json->MessageID;
+ }
+
+ $invoice->markInvitationSent($invitation, $messageId);
+ }
+
+ return true;
+ }
+
+ private function handleFailure($exception)
+ {
+ if (isset($_ENV['POSTMARK_API_TOKEN']) && method_exists($exception, 'getResponse')) {
+ $response = $exception->getResponse()->getBody()->getContents();
+ $response = json_decode($response);
+ $emailError = nl2br($response->Message);
+ } else {
+ $emailError = $exception->getMessage();
+ }
+
+ Utils::logError("Email Error: $emailError");
+
+ if (isset($data['invitation'])) {
+ $invitation = $data['invitation'];
+ $invitation->email_error = $emailError;
+ $invitation->save();
+ }
+
+ return $emailError;
+ }
}
diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php
index 3f2b4290a7eb..ce0c5383410d 100644
--- a/app/Ninja/Mailers/UserMailer.php
+++ b/app/Ninja/Mailers/UserMailer.php
@@ -2,6 +2,7 @@
use Utils;
+use App\Models\Invitation;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\User;
@@ -41,22 +42,52 @@ class UserMailer extends Mailer
$entityType = $notificationType == 'approved' ? ENTITY_QUOTE : ENTITY_INVOICE;
$view = "{$entityType}_{$notificationType}";
+ $account = $user->account;
+ $client = $invoice->client;
$data = [
'entityType' => $entityType,
- 'clientName' => $invoice->client->getDisplayName(),
- 'accountName' => $invoice->account->getDisplayName(),
+ 'clientName' => $client->getDisplayName(),
+ 'accountName' => $account->getDisplayName(),
'userName' => $user->getDisplayName(),
- 'invoiceAmount' => Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()),
+ 'invoiceAmount' => $account->formatMoney($invoice->getRequestedAmount(), $client),
'invoiceNumber' => $invoice->invoice_number,
'invoiceLink' => SITE_URL."/{$entityType}s/{$invoice->public_id}",
+ 'account' => $account,
];
if ($payment) {
- $data['paymentAmount'] = Utils::formatMoney($payment->amount, $invoice->client->getCurrencyId());
+ $data['paymentAmount'] = $account->formatMoney($payment->amount, $client);
}
- $subject = trans("texts.notification_{$entityType}_{$notificationType}_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->getDisplayName()]);
+ $subject = trans("texts.notification_{$entityType}_{$notificationType}_subject", [
+ 'invoice' => $invoice->invoice_number,
+ 'client' => $client->getDisplayName()
+ ]);
+
+ $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
+ }
+
+ public function sendEmailBounced(Invitation $invitation)
+ {
+ $user = $invitation->user;
+ $account = $user->account;
+ $invoice = $invitation->invoice;
+ $entityType = $invoice->getEntityType();
+
+ if (!$user->email) {
+ return;
+ }
+
+ $subject = trans("texts.notification_{$entityType}_bounced_subject", ['invoice' => $invoice->invoice_number]);
+ $view = 'email_bounced';
+ $data = [
+ 'userName' => $user->getDisplayName(),
+ 'emailError' => $invitation->email_error,
+ 'entityType' => $entityType,
+ 'contactName' => $invitation->contact->getDisplayName(),
+ 'invoiceNumber' => $invoice->invoice_number,
+ ];
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
diff --git a/app/Ninja/Presenters/AccountPresenter.php b/app/Ninja/Presenters/AccountPresenter.php
new file mode 100644
index 000000000000..dc9cacbb8aa7
--- /dev/null
+++ b/app/Ninja/Presenters/AccountPresenter.php
@@ -0,0 +1,24 @@
+entity->name ?: trans('texts.untitled_account');
+ }
+
+ public function website()
+ {
+ return Utils::addHttp($this->entity->website);
+ }
+
+ public function currencyCode()
+ {
+ $currencyId = $this->entity->getCurrencyId();
+ $currency = Utils::getFromCache($currencyId, 'currencies');
+ return $currency->code;
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/ClientPresenter.php b/app/Ninja/Presenters/ClientPresenter.php
new file mode 100644
index 000000000000..bb6e7db0657b
--- /dev/null
+++ b/app/Ninja/Presenters/ClientPresenter.php
@@ -0,0 +1,12 @@
+entity->country ? $this->entity->country->name : '';
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/CreditPresenter.php b/app/Ninja/Presenters/CreditPresenter.php
new file mode 100644
index 000000000000..7e38205b1067
--- /dev/null
+++ b/app/Ninja/Presenters/CreditPresenter.php
@@ -0,0 +1,17 @@
+entity->client ? $this->entity->client->getDisplayName() : '';
+ }
+
+ public function credit_date()
+ {
+ return Utils::fromSqlDate($this->entity->credit_date);
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php
new file mode 100644
index 000000000000..9cede24d039e
--- /dev/null
+++ b/app/Ninja/Presenters/ExpensePresenter.php
@@ -0,0 +1,23 @@
+entity->vendor ? $this->entity->vendor->getDisplayName() : '';
+ }
+
+ public function expense_date()
+ {
+ return Utils::fromSqlDate($this->entity->expense_date);
+ }
+
+ public function converted_amount()
+ {
+ return round($this->entity->amount * $this->entity->exchange_rate, 2);
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php
new file mode 100644
index 000000000000..fb292a8859b9
--- /dev/null
+++ b/app/Ninja/Presenters/InvoicePresenter.php
@@ -0,0 +1,58 @@
+entity->client ? $this->entity->client->getDisplayName() : '';
+ }
+
+ public function user()
+ {
+ return $this->entity->user->getDisplayName();
+ }
+
+ public function balanceDueLabel()
+ {
+ if ($this->entity->partial) {
+ return 'amount_due';
+ } elseif ($this->entity->is_quote) {
+ return 'total';
+ } else {
+ return 'balance_due';
+ }
+ }
+
+ // https://schema.org/PaymentStatusType
+ public function paymentStatus()
+ {
+ if ( ! $this->entity->balance) {
+ return 'PaymentComplete';
+ } elseif ($this->entity->isOverdue()) {
+ return 'PaymentPastDue';
+ } else {
+ return 'PaymentDue';
+ }
+ }
+
+ public function status()
+ {
+ $status = $this->entity->invoice_status ? $this->entity->invoice_status->name : 'draft';
+ $status = strtolower($status);
+ return trans("texts.status_{$status}");
+ }
+
+ public function invoice_date()
+ {
+ return Utils::fromSqlDate($this->entity->invoice_date);
+ }
+
+ public function due_date()
+ {
+ return Utils::fromSqlDate($this->entity->due_date);
+ }
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/PaymentPresenter.php b/app/Ninja/Presenters/PaymentPresenter.php
new file mode 100644
index 000000000000..a0a58663e5a7
--- /dev/null
+++ b/app/Ninja/Presenters/PaymentPresenter.php
@@ -0,0 +1,27 @@
+entity->client ? $this->entity->client->getDisplayName() : '';
+ }
+
+ public function payment_date()
+ {
+ return Utils::fromSqlDate($this->entity->payment_date);
+ }
+
+ public function method()
+ {
+ if ($this->entity->account_gateway) {
+ return $this->entity->account_gateway->gateway->name;
+ } elseif ($this->entity->payment_type) {
+ return $this->entity->payment_type->name;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/TaskPresenter.php b/app/Ninja/Presenters/TaskPresenter.php
new file mode 100644
index 000000000000..09b860a1a2bc
--- /dev/null
+++ b/app/Ninja/Presenters/TaskPresenter.php
@@ -0,0 +1,39 @@
+entity->client ? $this->entity->client->getDisplayName() : '';
+ }
+
+ public function user()
+ {
+ return $this->entity->user->getDisplayName();
+ }
+
+ public function times($account)
+ {
+ $parts = json_decode($this->entity->time_log) ?: [];
+ $times = [];
+
+ foreach ($parts as $part) {
+ $start = $part[0];
+ if (count($part) == 1 || !$part[1]) {
+ $end = time();
+ } else {
+ $end = $part[1];
+ }
+
+ $start = $account->formatDateTime("@{$start}");
+ $end = $account->formatTime("@{$end}");
+
+ $times[] = "### {$start} - {$end}";
+ }
+
+ return implode("\n", $times);
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/VendorPresenter.php b/app/Ninja/Presenters/VendorPresenter.php
new file mode 100644
index 000000000000..b3da402bec40
--- /dev/null
+++ b/app/Ninja/Presenters/VendorPresenter.php
@@ -0,0 +1,12 @@
+entity->country ? $this->entity->country->name : '';
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/AccountGatewayRepository.php b/app/Ninja/Repositories/AccountGatewayRepository.php
new file mode 100644
index 000000000000..b61f854f2dc5
--- /dev/null
+++ b/app/Ninja/Repositories/AccountGatewayRepository.php
@@ -0,0 +1,24 @@
+join('gateways', 'gateways.id', '=', 'account_gateways.gateway_id')
+ ->where('account_gateways.deleted_at', '=', null)
+ ->where('account_gateways.account_id', '=', $accountId)
+ ->select('account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id');
+ }
+}
diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php
index 593d113072c7..1fd6c7117dcc 100644
--- a/app/Ninja/Repositories/AccountRepository.php
+++ b/app/Ninja/Repositories/AccountRepository.php
@@ -6,6 +6,7 @@ use Session;
use Utils;
use DB;
use stdClass;
+use Validator;
use Schema;
use App\Models\AccountGateway;
use App\Models\Invitation;
@@ -17,6 +18,7 @@ use App\Models\Contact;
use App\Models\Account;
use App\Models\User;
use App\Models\UserAccount;
+use App\Models\AccountToken;
class AccountRepository
{
@@ -26,8 +28,14 @@ class AccountRepository
$account->ip = Request::getClientIp();
$account->account_key = str_random(RANDOM_KEY_LENGTH);
- if (Session::has(SESSION_LOCALE)) {
- $locale = Session::get(SESSION_LOCALE);
+ // Track referal code
+ if ($referralCode = Session::get(SESSION_REFERRAL_CODE)) {
+ if ($user = User::whereReferralCode($referralCode)->first()) {
+ $account->referral_user_id = $user->id;
+ }
+ }
+
+ if ($locale = Session::get(SESSION_LOCALE)) {
if ($language = Language::whereLocale($locale)->first()) {
$account->language_id = $language->id;
}
@@ -43,6 +51,9 @@ class AccountRepository
$user->first_name = $firstName;
$user->last_name = $lastName;
$user->email = $user->username = $email;
+ if (!$password) {
+ $password = str_random(RANDOM_KEY_LENGTH);
+ }
$user->password = bcrypt($password);
}
@@ -129,7 +140,7 @@ class AccountRepository
$invoice->user_id = $account->users()->first()->id;
$invoice->public_id = $publicId;
$invoice->client_id = $client->id;
- $invoice->invoice_number = $account->getNextInvoiceNumber();
+ $invoice->invoice_number = $account->getNextInvoiceNumber($invoice);
$invoice->invoice_date = date_create()->format('Y-m-d');
$invoice->amount = PRO_PLAN_PRICE;
$invoice->balance = PRO_PLAN_PRICE;
@@ -188,7 +199,7 @@ class AccountRepository
$accountGateway->user_id = $user->id;
$accountGateway->gateway_id = NINJA_GATEWAY_ID;
$accountGateway->public_id = 1;
- $accountGateway->config = NINJA_GATEWAY_CONFIG;
+ $accountGateway->setConfig(json_decode(env(NINJA_GATEWAY_CONFIG)));
$account->account_gateways()->save($accountGateway);
}
@@ -206,7 +217,7 @@ class AccountRepository
$client->public_id = $account->id;
$client->user_id = $ninjaAccount->users()->first()->id;
$client->currency_id = 1;
- foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone'] as $field) {
+ foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id'] as $field) {
$client->$field = $account->$field;
}
$ninjaAccount->clients()->save($client);
@@ -225,9 +236,52 @@ class AccountRepository
return $client;
}
- public function registerUser($user)
+ public function findByKey($key)
{
- $url = (Utils::isNinjaDev() ? '' : NINJA_APP_URL) . '/signup/register';
+ $account = Account::whereAccountKey($key)
+ ->with('clients.invoices.invoice_items', 'clients.contacts')
+ ->firstOrFail();
+
+ return $account;
+ }
+
+ public function unlinkUserFromOauth($user)
+ {
+ $user->oauth_provider_id = null;
+ $user->oauth_user_id = null;
+ $user->save();
+ }
+
+ public function updateUserFromOauth($user, $firstName, $lastName, $email, $providerId, $oauthUserId)
+ {
+ if (!$user->registered) {
+ $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
+ $validator = Validator::make(['email' => $email], $rules);
+ if ($validator->fails()) {
+ $messages = $validator->messages();
+ return $messages->first('email');
+ }
+
+ $user->email = $email;
+ $user->first_name = $firstName;
+ $user->last_name = $lastName;
+ $user->registered = true;
+ }
+
+ $user->oauth_provider_id = $providerId;
+ $user->oauth_user_id = $oauthUserId;
+ $user->save();
+
+ return true;
+ }
+
+ public function registerNinjaUser($user)
+ {
+ if ($user->email == TEST_USERNAME) {
+ return false;
+ }
+
+ $url = (Utils::isNinjaDev() ? SITE_URL : NINJA_APP_URL) . '/signup/register';
$data = '';
$fields = [
'first_name' => urlencode($user->first_name),
@@ -244,10 +298,29 @@ class AccountRepository
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, count($fields));
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
}
+ public function findUserByOauth($providerId, $oauthUserId)
+ {
+ return User::where('oauth_user_id', $oauthUserId)
+ ->where('oauth_provider_id', $providerId)
+ ->first();
+ }
+
+ public function findUsers($user, $with = null)
+ {
+ $accounts = $this->findUserAccounts($user->id);
+
+ if ($accounts) {
+ return $this->getUserAccounts($accounts, $with);
+ } else {
+ return [$user];
+ }
+ }
+
public function findUserAccounts($userId1, $userId2 = false)
{
if (!Schema::hasTable('user_accounts')) {
@@ -271,7 +344,8 @@ class AccountRepository
return $query->first(['id', 'user_id1', 'user_id2', 'user_id3', 'user_id4', 'user_id5']);
}
- public function prepareUsersData($record) {
+ public function getUserAccounts($record, $with = null)
+ {
if (!$record) {
return false;
}
@@ -285,8 +359,22 @@ class AccountRepository
}
$users = User::with('account')
- ->whereIn('id', $userIds)
- ->get();
+ ->whereIn('id', $userIds);
+
+ if ($with) {
+ $users->with($with);
+ }
+
+ return $users->get();
+ }
+
+ public function prepareUsersData($record)
+ {
+ if (!$record) {
+ return false;
+ }
+
+ $users = $this->getUserAccounts($record);
$data = [];
foreach ($users as $user) {
@@ -297,7 +385,7 @@ class AccountRepository
$item->account_id = $user->account->id;
$item->account_name = $user->account->getDisplayName();
$item->pro_plan_paid = $user->account->pro_plan_paid;
- $item->logo_path = file_exists($user->account->getLogoPath()) ? $user->account->getLogoPath() : null;
+ $item->logo_path = $user->account->hasLogo() ? $user->account->getLogoPath() : null;
$data[] = $item;
}
@@ -386,4 +474,46 @@ class AccountRepository
$userAccount->save();
}
}
+
+ public function findWithReminders()
+ {
+ return Account::whereRaw('enable_reminder1 = 1 OR enable_reminder2 = 1 OR enable_reminder3 = 1')->get();
+ }
+
+ public function getReferralCode()
+ {
+ do {
+ $code = strtoupper(str_random(8));
+ $match = User::whereReferralCode($code)
+ ->withTrashed()
+ ->first();
+ } while ($match);
+
+ return $code;
+ }
+
+ public function createTokens($user, $name)
+ {
+ $name = trim($name) ?: 'TOKEN';
+ $users = $this->findUsers($user);
+
+ foreach ($users as $user) {
+ if ($token = AccountToken::whereUserId($user->id)->whereName($name)->first()) {
+ continue;
+ }
+
+ $token = AccountToken::createNew($user);
+ $token->name = $name;
+ $token->token = str_random(RANDOM_KEY_LENGTH);
+ $token->save();
+ }
+ }
+
+ public function getUserAccountId($account)
+ {
+ $user = $account->users()->first();
+ $userAccount = $this->findUserAccounts($user->id);
+
+ return $userAccount ? $userAccount->id : false;
+ }
}
diff --git a/app/Ninja/Repositories/ActivityRepository.php b/app/Ninja/Repositories/ActivityRepository.php
new file mode 100644
index 000000000000..b51c8bbf2997
--- /dev/null
+++ b/app/Ninja/Repositories/ActivityRepository.php
@@ -0,0 +1,104 @@
+invoice->client;
+ } else {
+ $client = $entity->client;
+ }
+
+ // init activity and copy over context
+ $activity = self::getBlank($altEntity ?: $client);
+ $activity = Utils::copyContext($activity, $entity);
+ $activity = Utils::copyContext($activity, $altEntity);
+
+ $activity->client_id = $client->id;
+ $activity->activity_type_id = $activityTypeId;
+ $activity->adjustment = $balanceChange;
+ $activity->balance = $client->balance + $balanceChange;
+
+ $keyField = $entity->getKeyField();
+ $activity->$keyField = $entity->id;
+
+ $activity->ip = Request::getClientIp();
+ $activity->save();
+
+ $client->updateBalances($balanceChange, $paidToDateChange);
+
+ return $activity;
+ }
+
+ private function getBlank($entity)
+ {
+ $activity = new Activity();
+
+ if (Auth::check() && Auth::user()->account_id == $entity->account_id) {
+ $activity->user_id = Auth::user()->id;
+ $activity->account_id = Auth::user()->account_id;
+ } else {
+ $activity->user_id = $entity->user_id;
+ $activity->account_id = $entity->account_id;
+
+ if ( ! $entity instanceof Invitation) {
+ $activity->is_system = true;
+ }
+ }
+
+ $activity->token_id = session('token_id');
+
+ return $activity;
+ }
+
+ public function findByClientId($clientId)
+ {
+ return DB::table('activities')
+ ->join('accounts', 'accounts.id', '=', 'activities.account_id')
+ ->join('users', 'users.id', '=', 'activities.user_id')
+ ->join('clients', 'clients.id', '=', 'activities.client_id')
+ ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
+ ->leftJoin('invoices', 'invoices.id', '=', 'activities.invoice_id')
+ ->leftJoin('payments', 'payments.id', '=', 'activities.payment_id')
+ ->leftJoin('credits', 'credits.id', '=', 'activities.credit_id')
+ ->where('clients.id', '=', $clientId)
+ ->where('contacts.is_primary', '=', 1)
+ ->whereNull('contacts.deleted_at')
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'activities.id',
+ 'activities.created_at',
+ 'activities.contact_id',
+ 'activities.activity_type_id',
+ 'activities.is_system',
+ 'activities.balance',
+ 'activities.adjustment',
+ 'users.first_name as user_first_name',
+ 'users.last_name as user_last_name',
+ 'users.email as user_email',
+ 'invoices.invoice_number as invoice',
+ 'invoices.public_id as invoice_public_id',
+ 'invoices.is_recurring',
+ 'clients.name as client_name',
+ 'clients.public_id as client_public_id',
+ 'contacts.id as contact',
+ 'contacts.first_name as first_name',
+ 'contacts.last_name as last_name',
+ 'contacts.email as email',
+ 'payments.transaction_reference as payment',
+ 'credits.amount as credit'
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/BankAccountRepository.php b/app/Ninja/Repositories/BankAccountRepository.php
new file mode 100644
index 000000000000..5ab3148e381b
--- /dev/null
+++ b/app/Ninja/Repositories/BankAccountRepository.php
@@ -0,0 +1,24 @@
+join('banks', 'banks.id', '=', 'bank_accounts.bank_id')
+ ->where('bank_accounts.deleted_at', '=', null)
+ ->where('bank_accounts.account_id', '=', $accountId)
+ ->select('bank_accounts.public_id', 'banks.name as bank_name', 'bank_accounts.deleted_at', 'banks.bank_library_id');
+ }
+}
diff --git a/app/Ninja/Repositories/BaseRepository.php b/app/Ninja/Repositories/BaseRepository.php
new file mode 100644
index 000000000000..bec95fb96921
--- /dev/null
+++ b/app/Ninja/Repositories/BaseRepository.php
@@ -0,0 +1,73 @@
+getClassName();
+ return new $className();
+ }
+
+ private function getEventClass($entity, $type)
+ {
+ return 'App\Events\\' . ucfirst($entity->getEntityType()) . 'Was' . $type;
+ }
+
+ public function archive($entity)
+ {
+ $entity->delete();
+
+ $className = $this->getEventClass($entity, 'Archived');
+
+ if (class_exists($className)) {
+ event(new $className($entity));
+ }
+ }
+
+ public function restore($entity)
+ {
+ $fromDeleted = false;
+ $entity->restore();
+
+ if ($entity->is_deleted) {
+ $fromDeleted = true;
+ $entity->is_deleted = false;
+ $entity->save();
+ }
+
+ $className = $this->getEventClass($entity, 'Restored');
+
+ if (class_exists($className)) {
+ event(new $className($entity, $fromDeleted));
+ }
+ }
+
+ public function delete($entity)
+ {
+ $entity->is_deleted = true;
+ $entity->save();
+
+ $entity->delete();
+
+ $className = $this->getEventClass($entity, 'Deleted');
+
+ if (class_exists($className)) {
+ event(new $className($entity));
+ }
+ }
+
+ public function findByPublicIds($ids)
+ {
+ return $this->getInstance()->scope($ids)->get();
+ }
+
+ public function findByPublicIdsWithTrashed($ids)
+ {
+ return $this->getInstance()->scope($ids)->withTrashed()->get();
+ }
+}
diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php
index ab0baacda59f..3e43d8f34f5f 100644
--- a/app/Ninja/Repositories/ClientRepository.php
+++ b/app/Ninja/Repositories/ClientRepository.php
@@ -1,19 +1,50 @@
with('user', 'contacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
public function find($filter = null)
{
- $query = \DB::table('clients')
+ $query = DB::table('clients')
+ ->join('accounts', 'accounts.id', '=', 'clients.account_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('clients.account_id', '=', \Auth::user()->account_id)
->where('contacts.is_primary', '=', true)
->where('contacts.deleted_at', '=', null)
- ->select('clients.public_id', 'clients.name', 'contacts.first_name', 'contacts.last_name', 'clients.balance', 'clients.last_login', 'clients.created_at', 'clients.work_phone', 'contacts.email', 'clients.currency_id', 'clients.deleted_at', 'clients.is_deleted');
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'clients.public_id',
+ 'clients.name',
+ 'contacts.first_name',
+ 'contacts.last_name',
+ 'clients.balance',
+ 'clients.last_login',
+ 'clients.created_at',
+ 'clients.work_phone',
+ 'contacts.email',
+ 'clients.deleted_at',
+ 'clients.is_deleted'
+ );
if (!\Session::get('show_trash:client')) {
$query->where('clients.deleted_at', '=', null);
@@ -30,176 +61,42 @@ class ClientRepository
return $query;
}
-
- public function getErrors($data)
+
+ public function save($data)
{
- $contact = isset($data['contacts']) ? (array) $data['contacts'][0] : (isset($data['contact']) ? $data['contact'] : []);
- $validator = \Validator::make($contact, [
- 'email' => 'email|required_without:first_name',
- 'first_name' => 'required_without:email',
- ]);
- if ($validator->fails()) {
- return $validator->messages();
- }
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
- return false;
- }
-
- public function save($publicId, $data, $notify = true)
- {
- if (!$publicId || $publicId == "-1") {
+ if (!$publicId || $publicId == '-1') {
$client = Client::createNew();
- $contact = Contact::createNew();
- $contact->is_primary = true;
- $contact->send_invoice = true;
} else {
$client = Client::scope($publicId)->with('contacts')->firstOrFail();
- $contact = $client->contacts()->where('is_primary', '=', true)->firstOrFail();
- }
-
- if (isset($data['name'])) {
- $client->name = trim($data['name']);
- }
- if (isset($data['id_number'])) {
- $client->id_number = trim($data['id_number']);
- }
- if (isset($data['vat_number'])) {
- $client->vat_number = trim($data['vat_number']);
- }
- if (isset($data['work_phone'])) {
- $client->work_phone = trim($data['work_phone']);
- }
- if (isset($data['custom_value1'])) {
- $client->custom_value1 = trim($data['custom_value1']);
- }
- if (isset($data['custom_value2'])) {
- $client->custom_value2 = trim($data['custom_value2']);
- }
- if (isset($data['address1'])) {
- $client->address1 = trim($data['address1']);
- }
- if (isset($data['address2'])) {
- $client->address2 = trim($data['address2']);
- }
- if (isset($data['city'])) {
- $client->city = trim($data['city']);
- }
- if (isset($data['state'])) {
- $client->state = trim($data['state']);
- }
- if (isset($data['postal_code'])) {
- $client->postal_code = trim($data['postal_code']);
- }
- if (isset($data['country_id'])) {
- $client->country_id = $data['country_id'] ? $data['country_id'] : null;
- }
- if (isset($data['private_notes'])) {
- $client->private_notes = trim($data['private_notes']);
- }
- if (isset($data['size_id'])) {
- $client->size_id = $data['size_id'] ? $data['size_id'] : null;
- }
- if (isset($data['industry_id'])) {
- $client->industry_id = $data['industry_id'] ? $data['industry_id'] : null;
- }
- if (isset($data['currency_id'])) {
- $client->currency_id = $data['currency_id'] ? $data['currency_id'] : null;
- }
- if (isset($data['payment_terms'])) {
- $client->payment_terms = $data['payment_terms'];
- }
- if (isset($data['website'])) {
- $client->website = trim($data['website']);
}
+ $client->fill($data);
$client->save();
- $isPrimary = true;
+ /*
+ if ( ! isset($data['contact']) && ! isset($data['contacts'])) {
+ return $client;
+ }
+ */
+
+ $first = true;
+ $contacts = isset($data['contact']) ? [$data['contact']] : $data['contacts'];
$contactIds = [];
- if (isset($data['contact'])) {
- $info = $data['contact'];
- if (isset($info['email'])) {
- $contact->email = trim($info['email']);
- }
- if (isset($info['first_name'])) {
- $contact->first_name = trim($info['first_name']);
- }
- if (isset($info['last_name'])) {
- $contact->last_name = trim($info['last_name']);
- }
- if (isset($info['phone'])) {
- $contact->phone = trim($info['phone']);
- }
- $contact->is_primary = true;
- $contact->send_invoice = true;
- $client->contacts()->save($contact);
- } else {
- foreach ($data['contacts'] as $record) {
- $record = (array) $record;
-
- if ($publicId != "-1" && isset($record['public_id']) && $record['public_id']) {
- $contact = Contact::scope($record['public_id'])->firstOrFail();
- } else {
- $contact = Contact::createNew();
- }
-
- if (isset($record['email'])) {
- $contact->email = trim($record['email']);
- }
- if (isset($record['first_name'])) {
- $contact->first_name = trim($record['first_name']);
- }
- if (isset($record['last_name'])) {
- $contact->last_name = trim($record['last_name']);
- }
- if (isset($record['phone'])) {
- $contact->phone = trim($record['phone']);
- }
- $contact->is_primary = $isPrimary;
- $contact->send_invoice = isset($record['send_invoice']) ? $record['send_invoice'] : true;
- $isPrimary = false;
-
- $client->contacts()->save($contact);
- $contactIds[] = $contact->public_id;
- }
-
- foreach ($client->contacts as $contact) {
- if (!in_array($contact->public_id, $contactIds)) {
- $contact->delete();
- }
- }
+ foreach ($contacts as $contact) {
+ $contact = $client->addContact($contact, $first);
+ $contactIds[] = $contact->public_id;
+ $first = false;
}
- $client->save();
-
- if (!$publicId || $publicId == "-1") {
- Activity::createClient($client, $notify);
+ foreach ($client->contacts as $contact) {
+ if (!in_array($contact->public_id, $contactIds)) {
+ $contact->delete();
+ }
}
return $client;
}
-
- public function bulk($ids, $action)
- {
- $clients = Client::withTrashed()->scope($ids)->get();
-
- foreach ($clients as $client) {
- if ($action == 'restore') {
- $client->restore();
-
- $client->is_deleted = false;
- $client->save();
- } else {
- if ($action == 'delete') {
- $client->is_deleted = true;
- $client->save();
- }
-
- $client->delete();
- }
- }
-
- return count($clients);
- }
}
diff --git a/app/Ninja/Repositories/ContactRepository.php b/app/Ninja/Repositories/ContactRepository.php
new file mode 100644
index 000000000000..49b73e91a664
--- /dev/null
+++ b/app/Ninja/Repositories/ContactRepository.php
@@ -0,0 +1,26 @@
+send_invoice = true;
+ $contact->client_id = $data['client_id'];
+ $contact->is_primary = Contact::scope()->where('client_id', '=', $contact->client_id)->count() == 0;
+ } else {
+ $contact = Contact::scope($publicId)->firstOrFail();
+ }
+
+ $contact->fill($data);
+ $contact->save();
+
+ return $contact;
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/CreditRepository.php b/app/Ninja/Repositories/CreditRepository.php
index 700467620294..1c33cb19e41d 100644
--- a/app/Ninja/Repositories/CreditRepository.php
+++ b/app/Ninja/Repositories/CreditRepository.php
@@ -1,20 +1,44 @@
join('accounts', 'accounts.id', '=', 'credits.account_id')
->join('clients', 'clients.id', '=', 'credits.client_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('clients.account_id', '=', \Auth::user()->account_id)
->where('clients.deleted_at', '=', null)
+ ->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
- ->select('credits.public_id', 'clients.name as client_name', 'clients.public_id as client_public_id', 'credits.amount', 'credits.balance', 'credits.credit_date', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'credits.private_notes', 'credits.deleted_at', 'credits.is_deleted');
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'credits.public_id',
+ 'clients.name as client_name',
+ 'clients.public_id as client_public_id',
+ 'credits.amount',
+ 'credits.balance',
+ 'credits.credit_date',
+ 'contacts.first_name',
+ 'contacts.last_name',
+ 'contacts.email',
+ 'credits.private_notes',
+ 'credits.deleted_at',
+ 'credits.is_deleted'
+ );
if ($clientPublicId) {
$query->where('clients.public_id', '=', $clientPublicId);
@@ -33,8 +57,10 @@ class CreditRepository
return $query;
}
- public function save($publicId = null, $input)
+ public function save($input)
{
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
if ($publicId) {
$credit = Credit::scope($publicId)->firstOrFail();
} else {
@@ -50,28 +76,4 @@ class CreditRepository
return $credit;
}
-
- public function bulk($ids, $action)
- {
- if (!$ids) {
- return 0;
- }
-
- $credits = Credit::withTrashed()->scope($ids)->get();
-
- foreach ($credits as $credit) {
- if ($action == 'restore') {
- $credit->restore();
- } else {
- if ($action == 'delete') {
- $credit->is_deleted = true;
- $credit->save();
- }
-
- $credit->delete();
- }
- }
-
- return count($credits);
- }
}
diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php
new file mode 100644
index 000000000000..3c65f1c2520b
--- /dev/null
+++ b/app/Ninja/Repositories/ExpenseRepository.php
@@ -0,0 +1,161 @@
+with('user')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
+ public function findVendor($vendorPublicId)
+ {
+ $accountid = \Auth::user()->account_id;
+ $query = DB::table('expenses')
+ ->join('accounts', 'accounts.id', '=', 'expenses.account_id')
+ ->where('expenses.account_id', '=', $accountid)
+ ->where('expenses.vendor_id', '=', $vendorPublicId)
+ ->select(
+ 'expenses.id',
+ 'expenses.expense_date',
+ 'expenses.amount',
+ 'expenses.public_notes',
+ 'expenses.public_id',
+ 'expenses.deleted_at',
+ 'expenses.should_be_invoiced',
+ 'expenses.created_at'
+ );
+
+ return $query;
+ }
+
+ public function find($filter = null)
+ {
+ $accountid = \Auth::user()->account_id;
+ $query = DB::table('expenses')
+ ->join('accounts', 'accounts.id', '=', 'expenses.account_id')
+ ->leftjoin('clients', 'clients.id', '=', 'expenses.client_id')
+ ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
+ ->leftjoin('vendors', 'vendors.id', '=', 'expenses.vendor_id')
+ ->leftJoin('invoices', 'invoices.id', '=', 'expenses.invoice_id')
+ ->where('expenses.account_id', '=', $accountid)
+ ->where('contacts.deleted_at', '=', null)
+ ->where('vendors.deleted_at', '=', null)
+ ->where('clients.deleted_at', '=', null)
+ ->where(function ($query) {
+ $query->where('contacts.is_primary', '=', true)
+ ->orWhere('contacts.is_primary', '=', null);
+ })
+ ->select(
+ 'expenses.account_id',
+ 'expenses.amount',
+ 'expenses.currency_id',
+ 'expenses.deleted_at',
+ 'expenses.exchange_rate',
+ 'expenses.expense_date',
+ 'expenses.id',
+ 'expenses.is_deleted',
+ 'expenses.private_notes',
+ 'expenses.public_id',
+ 'expenses.invoice_id',
+ 'expenses.public_notes',
+ 'expenses.should_be_invoiced',
+ 'expenses.vendor_id',
+ 'invoices.public_id as invoice_public_id',
+ 'accounts.country_id as account_country_id',
+ 'accounts.currency_id as account_currency_id',
+ 'vendors.name as vendor_name',
+ 'vendors.public_id as vendor_public_id',
+ 'clients.name as client_name',
+ 'clients.public_id as client_public_id',
+ 'contacts.first_name',
+ 'contacts.email',
+ 'contacts.last_name',
+ 'clients.country_id as client_country_id'
+ );
+
+ $showTrashed = \Session::get('show_trash:expense');
+
+ if (!$showTrashed) {
+ $query->where('expenses.deleted_at', '=', null);
+ }
+
+ if ($filter) {
+ $query->where(function ($query) use ($filter) {
+ $query->where('expenses.public_notes', 'like', '%'.$filter.'%');
+ });
+ }
+
+ return $query;
+ }
+
+ public function save($input)
+ {
+ $publicId = isset($input['public_id']) ? $input['public_id'] : false;
+
+ if ($publicId) {
+ $expense = Expense::scope($publicId)->firstOrFail();
+ } else {
+ $expense = Expense::createNew();
+ }
+
+ // First auto fill
+ $expense->fill($input);
+
+ $expense->expense_date = Utils::toSqlDate($input['expense_date']);
+ $expense->private_notes = trim($input['private_notes']);
+ $expense->public_notes = trim($input['public_notes']);
+ $expense->should_be_invoiced = isset($input['should_be_invoiced']) || $expense->client_id ? true : false;
+
+ if (! $expense->currency_id) {
+ $expense->currency_id = \Auth::user()->account->getCurrencyId();
+ }
+
+ $rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1;
+ $expense->exchange_rate = round($rate, 4);
+ $expense->amount = round(Utils::parseFloat($input['amount']), 2);
+
+ $expense->save();
+
+ return $expense;
+ }
+
+ public function bulk($ids, $action)
+ {
+ $expenses = Expense::withTrashed()->scope($ids)->get();
+
+ foreach ($expenses as $expense) {
+ if ($action == 'restore') {
+ $expense->restore();
+
+ $expense->is_deleted = false;
+ $expense->save();
+ } else {
+ if ($action == 'delete') {
+ $expense->is_deleted = true;
+ $expense->save();
+ }
+
+ $expense->delete();
+ }
+ }
+
+ return count($tasks);
+ }
+}
diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php
index df73c6f6a1e6..9c1a6dcc0356 100644
--- a/app/Ninja/Repositories/InvoiceRepository.php
+++ b/app/Ninja/Repositories/InvoiceRepository.php
@@ -1,18 +1,42 @@
-paymentService = $paymentService;
+ }
+
+ public function all()
+ {
+ return Invoice::scope()
+ ->with('user', 'client.contacts', 'invoice_status')
+ ->withTrashed()
+ ->where('is_quote', '=', false)
+ ->where('is_recurring', '=', false)
+ ->get();
+ }
+
public function getInvoices($accountId, $clientPublicId = false, $entityType = ENTITY_INVOICE, $filter = false)
{
- $query = \DB::table('invoices')
+ $query = DB::table('invoices')
+ ->join('accounts', 'accounts.id', '=', 'invoices.account_id')
->join('clients', 'clients.id', '=', 'invoices.client_id')
->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
@@ -21,7 +45,28 @@ class InvoiceRepository
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
->where('contacts.is_primary', '=', true)
- ->select('clients.public_id as client_public_id', 'invoice_number', 'invoice_status_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'quote_id', 'quote_invoice_id', 'invoices.deleted_at', 'invoices.is_deleted', 'invoices.partial');
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'clients.public_id as client_public_id',
+ 'invoice_number',
+ 'invoice_status_id',
+ 'clients.name as client_name',
+ 'invoices.public_id',
+ 'invoices.amount',
+ 'invoices.balance',
+ 'invoices.invoice_date',
+ 'invoices.due_date',
+ 'invoice_statuses.name as invoice_status_name',
+ 'contacts.first_name',
+ 'contacts.last_name',
+ 'contacts.email',
+ 'invoices.quote_id',
+ 'invoices.quote_invoice_id',
+ 'invoices.deleted_at',
+ 'invoices.is_deleted',
+ 'invoices.partial'
+ );
if (!\Session::get('show_trash:'.$entityType)) {
$query->where('invoices.deleted_at', '=', null);
@@ -47,7 +92,8 @@ class InvoiceRepository
public function getRecurringInvoices($accountId, $clientPublicId = false, $filter = false)
{
- $query = \DB::table('invoices')
+ $query = DB::table('invoices')
+ ->join('accounts', 'accounts.id', '=', 'invoices.account_id')
->join('clients', 'clients.id', '=', 'invoices.client_id')
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
@@ -57,13 +103,28 @@ class InvoiceRepository
->where('invoices.is_recurring', '=', true)
->where('contacts.is_primary', '=', true)
->where('clients.deleted_at', '=', null)
- ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'frequencies.name as frequency', 'start_date', 'end_date', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'invoices.deleted_at', 'invoices.is_deleted');
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'clients.public_id as client_public_id',
+ 'clients.name as client_name',
+ 'invoices.public_id',
+ 'invoices.amount',
+ 'frequencies.name as frequency',
+ 'invoices.start_date',
+ 'invoices.end_date',
+ 'contacts.first_name',
+ 'contacts.last_name',
+ 'contacts.email',
+ 'invoices.deleted_at',
+ 'invoices.is_deleted'
+ );
if ($clientPublicId) {
$query->where('clients.public_id', '=', $clientPublicId);
}
- if (!\Session::get('show_trash:invoice')) {
+ if (!\Session::get('show_trash:recurring_invoice')) {
$query->where('invoices.deleted_at', '=', null);
}
@@ -79,7 +140,8 @@ class InvoiceRepository
public function getClientDatatable($contactId, $entityType, $search)
{
- $query = \DB::table('invitations')
+ $query = DB::table('invitations')
+ ->join('accounts', 'accounts.id', '=', 'invitations.account_id')
->join('invoices', 'invoices.id', '=', 'invitations.invoice_id')
->join('clients', 'clients.id', '=', 'invoices.client_id')
->where('invitations.contact_id', '=', $contactId)
@@ -88,18 +150,38 @@ class InvoiceRepository
->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
- ->select('invitation_key', 'invoice_number', 'invoice_date', 'invoices.balance as balance', 'due_date', 'clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'start_date', 'end_date', 'clients.currency_id', 'invoices.partial');
+ // This needs to be a setting to also hide the activity on the dashboard page
+ //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT)
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'invitations.invitation_key',
+ 'invoices.invoice_number',
+ 'invoices.invoice_date',
+ 'invoices.balance as balance',
+ 'invoices.due_date',
+ 'clients.public_id as client_public_id',
+ 'clients.name as client_name',
+ 'invoices.public_id',
+ 'invoices.amount',
+ 'invoices.start_date',
+ 'invoices.end_date',
+ 'invoices.partial'
+ );
$table = \Datatable::query($query)
->addColumn('invoice_number', function ($model) use ($entityType) { return link_to('/view/'.$model->invitation_key, $model->invoice_number); })
->addColumn('invoice_date', function ($model) { return Utils::fromSqlDate($model->invoice_date); })
- ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); });
+ ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); });
if ($entityType == ENTITY_INVOICE) {
$table->addColumn('balance', function ($model) {
return $model->partial > 0 ?
- trans('texts.partial_remaining', ['partial' => Utils::formatMoney($model->partial, $model->currency_id), 'balance' => Utils::formatMoney($model->balance, $model->currency_id)]) :
- Utils::formatMoney($model->balance, $model->currency_id);
+ trans('texts.partial_remaining', [
+ 'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id),
+ 'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id)
+ ]) :
+ Utils::formatMoney($model->balance, $model->currency_id, $model->country_id);
});
}
@@ -107,204 +189,99 @@ class InvoiceRepository
->make();
}
- public function getDatatable($accountId, $clientPublicId = null, $entityType, $search)
+ public function save($data)
{
- $query = $this->getInvoices($accountId, $clientPublicId, $entityType, $search)
- ->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE ? true : false);
-
- $table = \Datatable::query($query);
-
- if (!$clientPublicId) {
- $table->addColumn('checkbox', function ($model) { return ''; });
- }
-
- $table->addColumn("invoice_number", function ($model) use ($entityType) { return link_to("{$entityType}s/".$model->public_id.'/edit', $model->invoice_number, ['class' => Utils::getEntityRowClass($model)]); });
-
- if (!$clientPublicId) {
- $table->addColumn('client_name', function ($model) { return link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)); });
- }
-
- $table->addColumn("invoice_date", function ($model) { return Utils::fromSqlDate($model->invoice_date); })
- ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); });
-
- if ($entityType == ENTITY_INVOICE) {
- $table->addColumn('balance', function ($model) {
- return $model->partial > 0 ?
- trans('texts.partial_remaining', ['partial' => Utils::formatMoney($model->partial, $model->currency_id), 'balance' => Utils::formatMoney($model->balance, $model->currency_id)]) :
- Utils::formatMoney($model->balance, $model->currency_id);
- });
- }
-
- return $table->addColumn('due_date', function ($model) { return Utils::fromSqlDate($model->due_date); })
- ->addColumn('invoice_status_name', function ($model) { return $model->quote_invoice_id ? link_to("invoices/{$model->quote_invoice_id}/edit", trans('texts.converted')) : self::getStatusLabel($model->invoice_status_id, $model->invoice_status_name); })
- ->addColumn('dropdown', function ($model) use ($entityType) {
-
- if ($model->is_deleted) {
- return '';
- }
-
- $str = ' ';
- })
- ->make();
- }
-
- private function getStatusLabel($statusId, $statusName) {
- $label = trans("texts.{$statusName}");
- $class = 'default';
- switch ($statusId) {
- case INVOICE_STATUS_SENT:
- $class = 'info';
- break;
- case INVOICE_STATUS_VIEWED:
- $class = 'warning';
- break;
- case INVOICE_STATUS_PARTIAL:
- $class = 'primary';
- break;
- case INVOICE_STATUS_PAID:
- $class = 'success';
- break;
- }
- return "
-
";
- }
-
- public function getErrors($input)
- {
- $contact = (array) $input->client->contacts[0];
- $rules = [
- 'email' => 'email|required_without:first_name',
- 'first_name' => 'required_without:email',
- ];
- $validator = \Validator::make($contact, $rules);
-
- if ($validator->fails()) {
- return $validator;
- }
-
- $invoice = (array) $input;
- $invoiceId = isset($invoice['public_id']) && $invoice['public_id'] ? Invoice::getPrivateId($invoice['public_id']) : null;
- $rules = [
- 'invoice_number' => 'required|unique:invoices,invoice_number,'.$invoiceId.',id,account_id,'.\Auth::user()->account_id,
- 'discount' => 'positive',
- ];
-
- if ($invoice['is_recurring'] && $invoice['start_date'] && $invoice['end_date']) {
- $rules['end_date'] = 'after:'.$invoice['start_date'];
- }
-
- $validator = \Validator::make($invoice, $rules);
-
- if ($validator->fails()) {
- return $validator;
- }
-
- return false;
- }
-
- public function save($publicId, $data, $entityType)
- {
- if ($publicId) {
- $invoice = Invoice::scope($publicId)->firstOrFail();
- } else {
- $invoice = Invoice::createNew();
-
- if ($entityType == ENTITY_QUOTE) {
- $invoice->is_quote = true;
- }
- }
-
$account = \Auth::user()->account;
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
+ $isNew = !$publicId || $publicId == '-1';
+
+ if ($isNew) {
+ $entityType = ENTITY_INVOICE;
+ if (isset($data['is_recurring']) && filter_var($data['is_recurring'], FILTER_VALIDATE_BOOLEAN)) {
+ $entityType = ENTITY_RECURRING_INVOICE;
+ } elseif (isset($data['is_quote']) && filter_var($data['is_quote'], FILTER_VALIDATE_BOOLEAN)) {
+ $entityType = ENTITY_QUOTE;
+ }
+ $invoice = $account->createInvoice($entityType, $data['client_id']);
+ if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
+ $invoice->has_tasks = true;
+ }
+ if (isset($data['has_expenses']) && filter_var($data['has_expenses'], FILTER_VALIDATE_BOOLEAN)) {
+ $invoice->has_expenses = true;
+ }
+ } else {
+ $invoice = Invoice::scope($publicId)->firstOrFail();
+ }
if ((isset($data['set_default_terms']) && $data['set_default_terms'])
|| (isset($data['set_default_footer']) && $data['set_default_footer'])) {
if (isset($data['set_default_terms']) && $data['set_default_terms']) {
- $account->invoice_terms = trim($data['terms']);
+ $account->{"{$invoice->getEntityType()}_terms"} = trim($data['terms']);
}
if (isset($data['set_default_footer']) && $data['set_default_footer']) {
$account->invoice_footer = trim($data['invoice_footer']);
}
$account->save();
- }
+ }
- if (isset($data['invoice_number'])) {
+ if (isset($data['invoice_number']) && !$invoice->is_recurring) {
$invoice->invoice_number = trim($data['invoice_number']);
}
- $invoice->discount = round(Utils::parseFloat($data['discount']), 2);
- $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
- $invoice->partial = round(Utils::parseFloat($data['partial']), 2);
- $invoice->invoice_date = isset($data['invoice_date_sql']) ? $data['invoice_date_sql'] : Utils::toSqlDate($data['invoice_date']);
- $invoice->has_tasks = isset($data['has_tasks']) ? $data['has_tasks'] : false;
-
- if (!$publicId) {
- $invoice->client_id = $data['client_id'];
- $invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false;
+ if (isset($data['discount'])) {
+ $invoice->discount = round(Utils::parseFloat($data['discount']), 2);
}
-
+ if (isset($data['is_amount_discount'])) {
+ $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
+ }
+ if (isset($data['partial'])) {
+ $invoice->partial = round(Utils::parseFloat($data['partial']), 2);
+ }
+ if (isset($data['invoice_date_sql'])) {
+ $invoice->invoice_date = $data['invoice_date_sql'];
+ } elseif (isset($data['invoice_date'])) {
+ $invoice->invoice_date = Utils::toSqlDate($data['invoice_date']);
+ }
+
if ($invoice->is_recurring) {
+ if ($invoice->start_date && $invoice->start_date != Utils::toSqlDate($data['start_date'])) {
+ $invoice->last_sent_date = null;
+ }
+
$invoice->frequency_id = $data['frequency_id'] ? $data['frequency_id'] : 0;
$invoice->start_date = Utils::toSqlDate($data['start_date']);
$invoice->end_date = Utils::toSqlDate($data['end_date']);
- $invoice->due_date = null;
+ $invoice->auto_bill = isset($data['auto_bill']) && $data['auto_bill'] ? true : false;
+
+ if (isset($data['recurring_due_date'])) {
+ $invoice->due_date = $data['recurring_due_date'];
+ } elseif (isset($data['due_date'])) {
+ $invoice->due_date = $data['due_date'];
+ }
} else {
- $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']);
+ if (isset($data['due_date']) || isset($data['due_date_sql'])) {
+ $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']);
+ }
$invoice->frequency_id = 0;
$invoice->start_date = null;
$invoice->end_date = null;
}
- $invoice->terms = trim($data['terms']) ? trim($data['terms']) : (!$publicId && $account->invoice_terms ? $account->invoice_terms : '');
- $invoice->invoice_footer = trim($data['invoice_footer']) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : '');
- $invoice->public_notes = trim($data['public_notes']);
+ $invoice->terms = (isset($data['terms']) && trim($data['terms'])) ? trim($data['terms']) : (!$publicId && $account->invoice_terms ? $account->invoice_terms : '');
+ $invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : '');
+ $invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null;
// process date variables
$invoice->terms = Utils::processVariables($invoice->terms);
$invoice->invoice_footer = Utils::processVariables($invoice->invoice_footer);
$invoice->public_notes = Utils::processVariables($invoice->public_notes);
- $invoice->po_number = trim($data['po_number']);
- $invoice->invoice_design_id = $data['invoice_design_id'];
+ if (isset($data['po_number'])) {
+ $invoice->po_number = trim($data['po_number']);
+ }
+
+ $invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id;
if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) {
$invoice->tax_rate = Utils::parseFloat($data['tax_rate']);
@@ -355,13 +332,29 @@ class InvoiceRepository
$total -= $invoice->discount;
} else {
$total *= (100 - $invoice->discount) / 100;
+ $total = round($total, 2);
}
}
- $invoice->custom_value1 = round($data['custom_value1'], 2);
- $invoice->custom_value2 = round($data['custom_value2'], 2);
- $invoice->custom_taxes1 = $data['custom_taxes1'] ? true : false;
- $invoice->custom_taxes2 = $data['custom_taxes2'] ? true : false;
+ if (isset($data['custom_value1'])) {
+ $invoice->custom_value1 = round($data['custom_value1'], 2);
+ if ($isNew) {
+ $invoice->custom_taxes1 = $account->custom_invoice_taxes1 ?: false;
+ }
+ }
+ if (isset($data['custom_value2'])) {
+ $invoice->custom_value2 = round($data['custom_value2'], 2);
+ if ($isNew) {
+ $invoice->custom_taxes2 = $account->custom_invoice_taxes2 ?: false;
+ }
+ }
+
+ if (isset($data['custom_text_value1'])) {
+ $invoice->custom_text_value1 = trim($data['custom_text_value1']);
+ }
+ if (isset($data['custom_text_value2'])) {
+ $invoice->custom_text_value2 = trim($data['custom_text_value2']);
+ }
// custom fields charged taxes
if ($invoice->custom_value1 && $invoice->custom_taxes1) {
@@ -407,16 +400,27 @@ class InvoiceRepository
$task->invoice_id = $invoice->id;
$task->client_id = $invoice->client_id;
$task->save();
- } else if ($item['product_key'] && !$invoice->has_tasks) {
- $product = Product::findProductByKey(trim($item['product_key']));
+ }
- if (\Auth::user()->account->update_products) {
+ if (isset($item['expense_public_id']) && $item['expense_public_id']) {
+ $expense = Expense::scope($item['expense_public_id'])->where('invoice_id', '=', null)->firstOrFail();
+ $expense->invoice_id = $invoice->id;
+ $expense->client_id = $invoice->client_id;
+ $expense->save();
+ }
+
+ if ($item['product_key']) {
+ $productKey = trim($item['product_key']);
+ if (\Auth::user()->account->update_products && ! strtotime($productKey)) {
+ $product = Product::findProductByKey($productKey);
if (!$product) {
$product = Product::createNew();
$product->product_key = trim($item['product_key']);
}
- $product->notes = $item['notes'];
+ $product->notes = $invoice->has_tasks ? '' : $item['notes'];
+ $product->notes = $invoice->has_expenses ? '' : $item['notes'];
+
$product->cost = $item['cost'];
$product->save();
}
@@ -424,7 +428,7 @@ class InvoiceRepository
$invoiceItem = InvoiceItem::createNew();
$invoiceItem->product_id = isset($product) ? $product->id : null;
- $invoiceItem->product_key = trim($invoice->is_recurring ? $item['product_key'] : Utils::processVariables($item['product_key']));
+ $invoiceItem->product_key = isset($item['product_key']) ? (trim($invoice->is_recurring ? $item['product_key'] : Utils::processVariables($item['product_key']))) : '';
$invoiceItem->notes = trim($invoice->is_recurring ? $item['notes'] : Utils::processVariables($item['notes']));
$invoiceItem->cost = Utils::parseFloat($item['cost']);
$invoiceItem->qty = Utils::parseFloat($item['qty']);
@@ -449,19 +453,22 @@ class InvoiceRepository
$clone = Invoice::createNew($invoice);
$clone->balance = $invoice->amount;
- // if the invoice prefix is diff than quote prefix, use the same number for the invoice
- if (($account->invoice_number_prefix || $account->quote_number_prefix)
- && $account->invoice_number_prefix != $account->quote_number_prefix
- && $account->share_counter) {
-
+ // if the invoice prefix is diff than quote prefix, use the same number for the invoice (if it's available)
+ $invoiceNumber = false;
+ if ($account->hasInvoicePrefix() && $account->share_counter) {
$invoiceNumber = $invoice->invoice_number;
if ($account->quote_number_prefix && strpos($invoiceNumber, $account->quote_number_prefix) === 0) {
$invoiceNumber = substr($invoiceNumber, strlen($account->quote_number_prefix));
}
- $clone->invoice_number = $account->invoice_number_prefix.$invoiceNumber;
- } else {
- $clone->invoice_number = $account->getNextInvoiceNumber();
+ $invoiceNumber = $account->invoice_number_prefix.$invoiceNumber;
+ if (Invoice::scope(false, $account->id)
+ ->withTrashed()
+ ->whereInvoiceNumber($invoiceNumber)
+ ->first()) {
+ $invoiceNumber = false;
+ }
}
+ $clone->invoice_number = $invoiceNumber ?: $account->getNextInvoiceNumber($clone);
foreach ([
'client_id',
@@ -486,7 +493,9 @@ class InvoiceRepository
'custom_value2',
'custom_taxes1',
'custom_taxes2',
- 'partial'] as $field) {
+ 'partial',
+ 'custom_text_value1',
+ 'custom_text_value2', ] as $field) {
$clone->$field = $invoice->$field;
}
@@ -529,31 +538,32 @@ class InvoiceRepository
return $clone;
}
- public function bulk($ids, $action, $statusId = false)
+ public function markSent($invoice)
{
- if (!$ids) {
- return 0;
+ $invoice->markInvitationsSent();
+ }
+
+ public function findInvoiceByInvitation($invitationKey)
+ {
+ $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first();
+
+ if (!$invitation) {
+ return false;
}
- $invoices = Invoice::withTrashed()->scope($ids)->get();
-
- foreach ($invoices as $invoice) {
- if ($action == 'mark') {
- $invoice->invoice_status_id = $statusId;
- $invoice->save();
- } elseif ($action == 'restore') {
- $invoice->restore();
- } else {
- if ($action == 'delete') {
- $invoice->is_deleted = true;
- $invoice->save();
- }
-
- $invoice->delete();
- }
+ $invoice = $invitation->invoice;
+ if (!$invoice || $invoice->is_deleted) {
+ return false;
}
- return count($invoices);
+ $invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country');
+ $client = $invoice->client;
+
+ if (!$client || $client->is_deleted) {
+ return false;
+ }
+
+ return $invitation;
}
public function findOpenInvoices($clientId)
@@ -588,7 +598,7 @@ class InvoiceRepository
$invoice = Invoice::createNew($recurInvoice);
$invoice->client_id = $recurInvoice->client_id;
$invoice->recurring_invoice_id = $recurInvoice->id;
- $invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber(false, 'R');
+ $invoice->invoice_number = 'R'.$recurInvoice->account->getNextInvoiceNumber($recurInvoice);
$invoice->amount = $recurInvoice->amount;
$invoice->balance = $recurInvoice->amount;
$invoice->invoice_date = date_create()->format('Y-m-d');
@@ -600,20 +610,14 @@ class InvoiceRepository
$invoice->tax_name = $recurInvoice->tax_name;
$invoice->tax_rate = $recurInvoice->tax_rate;
$invoice->invoice_design_id = $recurInvoice->invoice_design_id;
- $invoice->custom_value1 = $recurInvoice->custom_value1;
- $invoice->custom_value2 = $recurInvoice->custom_value2;
- $invoice->custom_taxes1 = $recurInvoice->custom_taxes1;
- $invoice->custom_taxes2 = $recurInvoice->custom_taxes2;
+ $invoice->custom_value1 = $recurInvoice->custom_value1 ?: 0;
+ $invoice->custom_value2 = $recurInvoice->custom_value2 ?: 0;
+ $invoice->custom_taxes1 = $recurInvoice->custom_taxes1 ?: 0;
+ $invoice->custom_taxes2 = $recurInvoice->custom_taxes2 ?: 0;
+ $invoice->custom_text_value1 = $recurInvoice->custom_text_value1;
+ $invoice->custom_text_value2 = $recurInvoice->custom_text_value2;
$invoice->is_amount_discount = $recurInvoice->is_amount_discount;
-
- if ($invoice->client->payment_terms != 0) {
- $days = $invoice->client->payment_terms;
- if ($days == -1) {
- $days = 0;
- }
- $invoice->due_date = date_create()->modify($days.' day')->format('Y-m-d');
- }
-
+ $invoice->due_date = $recurInvoice->getDueDate();
$invoice->save();
foreach ($recurInvoice->invoice_items as $recurItem) {
@@ -635,9 +639,37 @@ class InvoiceRepository
$invoice->invitations()->save($invitation);
}
- $recurInvoice->last_sent_date = Carbon::now()->toDateTimeString();
+ $recurInvoice->last_sent_date = date('Y-m-d');
$recurInvoice->save();
+ if ($recurInvoice->auto_bill) {
+ if ($this->paymentService->autoBillInvoice($invoice)) {
+ // update the invoice reference to match its actual state
+ // this is to ensure a 'payment received' email is sent
+ $invoice->invoice_status_id = INVOICE_STATUS_PAID;
+ }
+ }
+
return $invoice;
}
+
+ public function findNeedingReminding($account)
+ {
+ $dates = [];
+
+ for ($i=1; $i<=3; $i++) {
+ if ($date = $account->getReminderDate($i)) {
+ $field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';
+ $dates[] = "$field = '$date'";
+ }
+ }
+
+ $sql = implode(' OR ', $dates);
+ $invoices = Invoice::whereAccountId($account->id)
+ ->where('balance', '>', 0)
+ ->whereRaw('('.$sql.')')
+ ->get();
+
+ return $invoices;
+ }
}
diff --git a/app/Ninja/Repositories/NinjaRepository.php b/app/Ninja/Repositories/NinjaRepository.php
new file mode 100644
index 000000000000..3f9c1fa4f19e
--- /dev/null
+++ b/app/Ninja/Repositories/NinjaRepository.php
@@ -0,0 +1,18 @@
+first();
+
+ if (!$account) {
+ return;
+ }
+
+ $account->pro_plan_paid = $proPlanPaid;
+ $account->save();
+ }
+}
diff --git a/app/Ninja/Repositories/PaymentRepository.php b/app/Ninja/Repositories/PaymentRepository.php
index c8f69325c813..a080dd89c3d9 100644
--- a/app/Ninja/Repositories/PaymentRepository.php
+++ b/app/Ninja/Repositories/PaymentRepository.php
@@ -1,16 +1,24 @@
join('accounts', 'accounts.id', '=', 'payments.account_id')
->join('clients', 'clients.id', '=', 'payments.client_id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
@@ -21,11 +29,30 @@ class PaymentRepository
->where('clients.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->where('contacts.deleted_at', '=', null)
- ->select('payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id', 'payments.deleted_at', 'payments.is_deleted', 'invoices.is_deleted as invoice_is_deleted', 'gateways.name as gateway_name');
+ ->where('invoices.is_deleted', '=', false)
+ ->select('payments.public_id',
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'payments.transaction_reference',
+ 'clients.name as client_name',
+ 'clients.public_id as client_public_id',
+ 'payments.amount',
+ 'payments.payment_date',
+ 'invoices.public_id as invoice_public_id',
+ 'invoices.invoice_number',
+ 'contacts.first_name',
+ 'contacts.last_name',
+ 'contacts.email',
+ 'payment_types.name as payment_type',
+ 'payments.account_gateway_id',
+ 'payments.deleted_at',
+ 'payments.is_deleted',
+ 'invoices.is_deleted as invoice_is_deleted',
+ 'gateways.name as gateway_name'
+ );
if (!\Session::get('show_trash:payment')) {
- $query->where('payments.deleted_at', '=', null)
- ->where('invoices.deleted_at', '=', null);
+ $query->where('payments.deleted_at', '=', null);
}
if ($clientPublicId) {
@@ -43,7 +70,8 @@ class PaymentRepository
public function findForContact($contactId = null, $filter = null)
{
- $query = \DB::table('payments')
+ $query = DB::table('payments')
+ ->join('accounts', 'accounts.id', '=', 'payments.account_id')
->join('clients', 'clients.id', '=', 'payments.client_id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
@@ -57,7 +85,24 @@ class PaymentRepository
->where('invitations.deleted_at', '=', null)
->where('invoices.deleted_at', '=', null)
->where('invitations.contact_id', '=', $contactId)
- ->select('invitations.invitation_key', 'payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id');
+ ->select(
+ DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
+ 'invitations.invitation_key',
+ 'payments.public_id',
+ 'payments.transaction_reference',
+ 'clients.name as client_name',
+ 'clients.public_id as client_public_id',
+ 'payments.amount',
+ 'payments.payment_date',
+ 'invoices.public_id as invoice_public_id',
+ 'invoices.invoice_number',
+ 'contacts.first_name',
+ 'contacts.last_name',
+ 'contacts.email',
+ 'payment_types.name as payment_type',
+ 'payments.account_gateway_id'
+ );
if ($filter) {
$query->where(function ($query) use ($filter) {
@@ -68,34 +113,10 @@ class PaymentRepository
return $query;
}
- public function getErrors($input)
- {
- $rules = array(
- 'client' => 'required',
- 'invoice' => 'required',
- 'amount' => 'required',
- );
-
- if ($input['payment_type_id'] == PAYMENT_TYPE_CREDIT) {
- $rules['payment_type_id'] = 'has_credit:'.$input['client'].','.$input['amount'];
- }
-
- if (isset($input['invoice']) && $input['invoice']) {
- $invoice = Invoice::scope($input['invoice'])->firstOrFail();
- $rules['amount'] .= "|less_than:{$invoice->balance}";
- }
-
- $validator = \Validator::make($input, $rules);
-
- if ($validator->fails()) {
- return $validator;
- }
-
- return false;
- }
-
- public function save($publicId = null, $input)
+ public function save($input)
{
+ $publicId = isset($input['public_id']) ? $input['public_id'] : false;
+
if ($publicId) {
$payment = Payment::scope($publicId)->firstOrFail();
} else {
@@ -116,10 +137,12 @@ class PaymentRepository
$payment->payment_date = date('Y-m-d');
}
- $payment->transaction_reference = trim($input['transaction_reference']);
+ if (isset($input['transaction_reference'])) {
+ $payment->transaction_reference = trim($input['transaction_reference']);
+ }
if (!$publicId) {
- $clientId = Client::getPrivateId($input['client']);
+ $clientId = $input['client_id'];
$amount = Utils::parseFloat($input['amount']);
if ($paymentTypeId == PAYMENT_TYPE_CREDIT) {
@@ -136,8 +159,8 @@ class PaymentRepository
}
}
+ $payment->invoice_id = $input['invoice_id'];
$payment->client_id = $clientId;
- $payment->invoice_id = isset($input['invoice']) && $input['invoice'] != "-1" ? Invoice::getPrivateId($input['invoice']) : null;
$payment->amount = $amount;
}
@@ -146,27 +169,23 @@ class PaymentRepository
return $payment;
}
- public function bulk($ids, $action)
+ public function delete($payment)
{
- if (!$ids) {
- return 0;
+ if ($payment->invoice->is_deleted) {
+ return false;
}
- $payments = Payment::withTrashed()->scope($ids)->get();
-
- foreach ($payments as $payment) {
- if ($action == 'restore') {
- $payment->restore();
- } else {
- if ($action == 'delete') {
- $payment->is_deleted = true;
- $payment->save();
- }
-
- $payment->delete();
- }
- }
-
- return count($payments);
+ parent::delete($payment);
}
+
+ public function restore($payment)
+ {
+ if ($payment->invoice->is_deleted) {
+ return false;
+ }
+
+ parent::restore($payment);
+ }
+
+
}
diff --git a/app/Ninja/Repositories/PaymentTermRepository.php b/app/Ninja/Repositories/PaymentTermRepository.php
new file mode 100644
index 000000000000..e631e9f1627a
--- /dev/null
+++ b/app/Ninja/Repositories/PaymentTermRepository.php
@@ -0,0 +1,22 @@
+where('payment_terms.account_id', '=', $accountId)
+ ->where('payment_terms.deleted_at', '=', null)
+ ->select('payment_terms.public_id', 'payment_terms.name', 'payment_terms.num_days', 'payment_terms.deleted_at');
+ }
+}
diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php
new file mode 100644
index 000000000000..417b49f23640
--- /dev/null
+++ b/app/Ninja/Repositories/ProductRepository.php
@@ -0,0 +1,32 @@
+leftJoin('tax_rates', function($join) {
+ $join->on('tax_rates.id', '=', 'products.default_tax_rate_id')
+ ->whereNull('tax_rates.deleted_at');
+ })
+ ->where('products.account_id', '=', $accountId)
+ ->where('products.deleted_at', '=', null)
+ ->select(
+ 'products.public_id',
+ 'products.product_key',
+ 'products.notes',
+ 'products.cost',
+ 'tax_rates.name as tax_name',
+ 'tax_rates.rate as tax_rate',
+ 'products.deleted_at'
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/ReferralRepository.php b/app/Ninja/Repositories/ReferralRepository.php
new file mode 100644
index 000000000000..c847f3386b39
--- /dev/null
+++ b/app/Ninja/Repositories/ReferralRepository.php
@@ -0,0 +1,31 @@
+where('referral_user_id', $userId)
+ ->get(['id', 'pro_plan_paid']);
+
+ $counts = [
+ 'free' => 0,
+ 'pro' => 0
+ ];
+
+ foreach ($accounts as $account) {
+ $counts['free']++;
+ if (Utils::withinPastYear($account->pro_plan_paid)) {
+ $counts['pro']++;
+ }
+ }
+
+ return $counts;
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php
index 47761900e1e6..47a052378ff1 100644
--- a/app/Ninja/Repositories/TaskRepository.php
+++ b/app/Ninja/Repositories/TaskRepository.php
@@ -23,7 +23,23 @@ class TaskRepository
})
->where('contacts.deleted_at', '=', null)
->where('clients.deleted_at', '=', null)
- ->select('tasks.public_id', 'clients.name as client_name', 'clients.public_id as client_public_id', 'contacts.first_name', 'contacts.email', 'contacts.last_name', 'invoices.invoice_status_id', 'tasks.description', 'tasks.is_deleted', 'tasks.deleted_at', 'invoices.invoice_number', 'invoices.public_id as invoice_public_id', 'tasks.is_running', 'tasks.time_log', 'tasks.created_at');
+ ->select(
+ 'tasks.public_id',
+ 'clients.name as client_name',
+ 'clients.public_id as client_public_id',
+ 'contacts.first_name',
+ 'contacts.email',
+ 'contacts.last_name',
+ 'invoices.invoice_status_id',
+ 'tasks.description',
+ 'tasks.is_deleted',
+ 'tasks.deleted_at',
+ 'invoices.invoice_number',
+ 'invoices.public_id as invoice_public_id',
+ 'tasks.is_running',
+ 'tasks.time_log',
+ 'tasks.created_at'
+ );
if ($clientPublicId) {
$query->where('clients.public_id', '=', $clientPublicId);
@@ -45,8 +61,22 @@ class TaskRepository
return $query;
}
+ public function getErrors($input)
+ {
+ $rules = [
+ 'time_log' => 'time_log',
+ ];
+ $validator = \Validator::make($input, $rules);
+
+ if ($validator->fails()) {
+ return $validator;
+ }
+
+ return false;
+ }
+
public function save($publicId, $data)
- {
+ {
if ($publicId) {
$task = Task::scope($publicId)->firstOrFail();
} else {
@@ -67,16 +97,20 @@ class TaskRepository
} else {
$timeLog = [];
}
+
+ array_multisort($timeLog);
- if ($data['action'] == 'start') {
- $task->is_running = true;
- $timeLog[] = [strtotime('now'), false];
- } else if ($data['action'] == 'resume') {
- $task->is_running = true;
- $timeLog[] = [strtotime('now'), false];
- } else if ($data['action'] == 'stop' && $task->is_running) {
- $timeLog[count($timeLog)-1][1] = time();
- $task->is_running = false;
+ if (isset($data['action'])) {
+ if ($data['action'] == 'start') {
+ $task->is_running = true;
+ $timeLog[] = [strtotime('now'), false];
+ } else if ($data['action'] == 'resume') {
+ $task->is_running = true;
+ $timeLog[] = [strtotime('now'), false];
+ } else if ($data['action'] == 'stop' && $task->is_running) {
+ $timeLog[count($timeLog)-1][1] = time();
+ $task->is_running = false;
+ }
}
$task->time_log = json_encode($timeLog);
diff --git a/app/Ninja/Repositories/TaxRateRepository.php b/app/Ninja/Repositories/TaxRateRepository.php
index d5bd4dc24287..1b9fa8df773b 100644
--- a/app/Ninja/Repositories/TaxRateRepository.php
+++ b/app/Ninja/Repositories/TaxRateRepository.php
@@ -1,10 +1,26 @@
where('tax_rates.account_id', '=', $accountId)
+ ->where('tax_rates.deleted_at', '=', null)
+ ->select('tax_rates.public_id', 'tax_rates.name', 'tax_rates.rate', 'tax_rates.deleted_at');
+ }
+
+ /*
public function save($taxRates)
{
$taxRateIds = [];
@@ -39,4 +55,5 @@ class TaxRateRepository
}
}
}
+ */
}
diff --git a/app/Ninja/Repositories/TokenRepository.php b/app/Ninja/Repositories/TokenRepository.php
new file mode 100644
index 000000000000..5237eb7a0369
--- /dev/null
+++ b/app/Ninja/Repositories/TokenRepository.php
@@ -0,0 +1,27 @@
+where('account_tokens.account_id', '=', $accountId);
+
+ if (!Session::get('show_trash:token')) {
+ $query->where('account_tokens.deleted_at', '=', null);
+ }
+
+ return $query->select('account_tokens.public_id', 'account_tokens.name', 'account_tokens.token', 'account_tokens.public_id', 'account_tokens.deleted_at');
+ }
+}
diff --git a/app/Ninja/Repositories/UserRepository.php b/app/Ninja/Repositories/UserRepository.php
new file mode 100644
index 000000000000..01b7017fa0a0
--- /dev/null
+++ b/app/Ninja/Repositories/UserRepository.php
@@ -0,0 +1,29 @@
+where('users.account_id', '=', $accountId);
+
+ if (!Session::get('show_trash:user')) {
+ $query->where('users.deleted_at', '=', null);
+ }
+
+ $query->select('users.public_id', 'users.first_name', 'users.last_name', 'users.email', 'users.confirmed', 'users.public_id', 'users.deleted_at');
+
+ return $query;
+ }
+}
diff --git a/app/Ninja/Repositories/VendorContactRepository.php b/app/Ninja/Repositories/VendorContactRepository.php
new file mode 100644
index 000000000000..242b1b9d0c54
--- /dev/null
+++ b/app/Ninja/Repositories/VendorContactRepository.php
@@ -0,0 +1,26 @@
+send_invoice = true;
+ $contact->vendor_id = $data['vendor_id'];
+ $contact->is_primary = VendorContact::scope()->where('vendor_id', '=', $contact->vendor_id)->count() == 0;
+ } else {
+ $contact = VendorContact::scope($publicId)->firstOrFail();
+ }
+
+ $contact->fill($data);
+ $contact->save();
+
+ return $contact;
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/VendorRepository.php b/app/Ninja/Repositories/VendorRepository.php
new file mode 100644
index 000000000000..c7fc5bb90b5f
--- /dev/null
+++ b/app/Ninja/Repositories/VendorRepository.php
@@ -0,0 +1,90 @@
+with('user', 'vendorcontacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
+ public function find($filter = null)
+ {
+ $query = DB::table('vendors')
+ ->join('accounts', 'accounts.id', '=', 'vendors.account_id')
+ ->join('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id')
+ ->where('vendors.account_id', '=', \Auth::user()->account_id)
+ ->where('vendor_contacts.is_primary', '=', true)
+ ->where('vendor_contacts.deleted_at', '=', null)
+ ->select(
+ DB::raw('COALESCE(vendors.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(vendors.country_id, accounts.country_id) country_id'),
+ 'vendors.public_id',
+ 'vendors.name',
+ 'vendor_contacts.first_name',
+ 'vendor_contacts.last_name',
+ 'vendors.created_at',
+ 'vendors.work_phone',
+ 'vendor_contacts.email',
+ 'vendors.deleted_at',
+ 'vendors.is_deleted'
+ );
+
+ if (!\Session::get('show_trash:vendor')) {
+ $query->where('vendors.deleted_at', '=', null);
+ }
+
+ if ($filter) {
+ $query->where(function ($query) use ($filter) {
+ $query->where('vendors.name', 'like', '%'.$filter.'%')
+ ->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%')
+ ->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%')
+ ->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%');
+ });
+ }
+
+ return $query;
+ }
+
+ public function save($data)
+ {
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
+ if (!$publicId || $publicId == '-1') {
+ $vendor = Vendor::createNew();
+ } else {
+ $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail();
+ }
+
+ $vendor->fill($data);
+ $vendor->save();
+
+ if ( ! isset($data['vendorcontact']) && ! isset($data['vendorcontacts'])) {
+ return $vendor;
+ }
+
+ $first = true;
+ $vendorcontacts = isset($data['vendorcontact']) ? [$data['vendorcontact']] : $data['vendorcontacts'];
+
+ foreach ($vendorcontacts as $vendorcontact) {
+ $vendorcontact = $vendor->addVendorContact($vendorcontact, $first);
+ $first = false;
+ }
+
+ return $vendor;
+ }
+}
diff --git a/app/Ninja/Serializers/ArraySerializer.php b/app/Ninja/Serializers/ArraySerializer.php
new file mode 100644
index 000000000000..bdbbbfe99981
--- /dev/null
+++ b/app/Ninja/Serializers/ArraySerializer.php
@@ -0,0 +1,18 @@
+ $data) : $data;
+ }
+
+ public function item($resourceKey, array $data)
+ {
+ return $data;
+ //return ($resourceKey && $resourceKey !== 'data') ? array($resourceKey => $data) : $data;
+ }
+}
diff --git a/app/Ninja/Transformers/AccountTokenTransformer.php b/app/Ninja/Transformers/AccountTokenTransformer.php
new file mode 100644
index 000000000000..e6ca7e8bfda7
--- /dev/null
+++ b/app/Ninja/Transformers/AccountTokenTransformer.php
@@ -0,0 +1,17 @@
+ $account_token->name,
+ 'token' => $account_token->token
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/AccountTransformer.php b/app/Ninja/Transformers/AccountTransformer.php
new file mode 100644
index 000000000000..eed474346550
--- /dev/null
+++ b/app/Ninja/Transformers/AccountTransformer.php
@@ -0,0 +1,92 @@
+serializer);
+ return $this->includeCollection($account->users, $transformer, 'users');
+ }
+
+ public function includeClients(Account $account)
+ {
+ $transformer = new ClientTransformer($account, $this->serializer);
+ return $this->includeCollection($account->clients, $transformer, 'clients');
+ }
+
+ public function includeInvoices(Account $account)
+ {
+ $transformer = new InvoiceTransformer($account, $this->serializer);
+ return $this->includeCollection($account->invoices, $transformer, 'invoices');
+ }
+
+ public function includeProducts(Account $account)
+ {
+ $transformer = new ProductTransformer($account, $this->serializer);
+ return $this->includeCollection($account->products, $transformer, 'products');
+ }
+
+ public function includeTaxRates(Account $account)
+ {
+ $transformer = new TaxRateTransformer($account, $this->serializer);
+ return $this->includeCollection($account->tax_rates, $transformer, 'taxRates');
+ }
+
+ public function transform(Account $account)
+ {
+ return [
+ 'account_key' => $account->account_key,
+ 'name' => $account->present()->name,
+ 'currency_id' => (int) $account->currency_id,
+ 'timezone_id' => (int) $account->timezone_id,
+ 'date_format_id' => (int) $account->date_format_id,
+ 'datetime_format_id' => (int) $account->datetime_format_id,
+ 'updated_at' => $this->getTimestamp($account->updated_at),
+ 'archived_at' => $this->getTimestamp($account->deleted_at),
+ 'address1' => $account->address1,
+ 'address2' => $account->address2,
+ 'city' => $account->city,
+ 'state' => $account->state,
+ 'postal_code' => $account->postal_code,
+ 'country_id' => (int) $account->country_id,
+ 'invoice_terms' => $account->invoice_terms,
+ 'email_footer' => $account->email_footer,
+ 'industry_id' => (int) $account->industry_id,
+ 'size_id' => (int) $account->size_id,
+ 'invoice_taxes' => (bool) $account->invoice_taxes,
+ 'invoice_item_taxes' => (bool) $account->invoice_item_taxes,
+ 'invoice_design_id' => (int) $account->invoice_design_id,
+ 'client_view_css' => (string) $account->client_view_css,
+ 'work_phone' => $account->work_phone,
+ 'work_email' => $account->work_email,
+ 'language_id' => (int) $account->language_id,
+ 'fill_products' => (bool) $account->fill_products,
+ 'update_products' => (bool) $account->update_products,
+ 'vat_number' => $account->vat_number,
+ 'custom_invoice_label1' => $account->custom_invoice_label1,
+ 'custom_invoice_label2' => $account->custom_invoice_label2,
+ 'custom_invoice_taxes1' => $account->custom_invoice_taxes1,
+ 'custom_invoice_taxes2' => $account->custom_invoice_taxes1,
+ 'custom_label1' => $account->custom_label1,
+ 'custom_label2' => $account->custom_label2,
+ 'custom_value1' => $account->custom_value1,
+ 'custom_value2' => $account->custom_value2
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/ClientTransformer.php b/app/Ninja/Transformers/ClientTransformer.php
new file mode 100644
index 000000000000..4289545673c7
--- /dev/null
+++ b/app/Ninja/Transformers/ClientTransformer.php
@@ -0,0 +1,91 @@
+account, $this->serializer);
+ return $this->includeCollection($client->contacts, $transformer, ENTITY_CONTACT);
+ }
+
+ public function includeInvoices(Client $client)
+ {
+ $transformer = new InvoiceTransformer($this->account, $this->serializer);
+ return $this->includeCollection($client->invoices, $transformer, ENTITY_INVOICE);
+ }
+
+ public function transform(Client $client)
+ {
+ return [
+ 'id' => (int) $client->public_id,
+ 'name' => $client->name,
+ 'balance' => (float) $client->balance,
+ 'paid_to_date' => (float) $client->paid_to_date,
+ 'user_id' => (int) $client->user->public_id + 1,
+ 'account_key' => $this->account->account_key,
+ 'updated_at' => $this->getTimestamp($client->updated_at),
+ 'archived_at' => $this->getTimestamp($client->deleted_at),
+ 'address1' => $client->address1,
+ 'address2' => $client->address2,
+ 'city' => $client->city,
+ 'state' => $client->state,
+ 'postal_code' => $client->postal_code,
+ 'country_id' => (int) $client->country_id,
+ 'work_phone' => $client->work_phone,
+ 'private_notes' => $client->private_notes,
+ 'last_login' => $client->last_login,
+ 'website' => $client->website,
+ 'industry_id' => (int) $client->industry_id,
+ 'size_id' => (int) $client->size_id,
+ 'is_deleted' => (bool) $client->is_deleted,
+ 'payment_terms' => (int) $client->payment_terms,
+ 'vat_number' => $client->vat_number,
+ 'id_number' => $client->id_number,
+ 'language_id' => (int) $client->language_id,
+ 'currency_id' => (int) $client->currency_id
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/ContactTransformer.php b/app/Ninja/Transformers/ContactTransformer.php
new file mode 100644
index 000000000000..75e3620308f6
--- /dev/null
+++ b/app/Ninja/Transformers/ContactTransformer.php
@@ -0,0 +1,24 @@
+ (int) $contact->public_id,
+ 'first_name' => $contact->first_name,
+ 'last_name' => $contact->last_name,
+ 'email' => $contact->email,
+ 'updated_at' => $this->getTimestamp($contact->updated_at),
+ 'archived_at' => $this->getTimestamp($contact->deleted_at),
+ 'is_primary' => (bool) $contact->is_primary,
+ 'phone' => $contact->phone,
+ 'last_login' => $contact->last_login,
+ 'account_key' => $this->account->account_key,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/EntityTransformer.php b/app/Ninja/Transformers/EntityTransformer.php
new file mode 100644
index 000000000000..ea0dba263d97
--- /dev/null
+++ b/app/Ninja/Transformers/EntityTransformer.php
@@ -0,0 +1,40 @@
+account = $account;
+ $this->serializer = $serializer;
+ }
+
+ protected function includeCollection($data, $transformer, $entityType)
+ {
+ if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) {
+ $entityType = null;
+ }
+
+ return $this->collection($data, $transformer, $entityType);
+ }
+
+ protected function includeItem($data, $transformer, $entityType)
+ {
+ if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) {
+ $entityType = null;
+ }
+
+ return $this->item($data, $transformer, $entityType);
+ }
+
+ protected function getTimestamp($date)
+ {
+ return $date ? $date->getTimestamp() : null;
+ }
+}
diff --git a/app/Ninja/Transformers/InvoiceItemTransformer.php b/app/Ninja/Transformers/InvoiceItemTransformer.php
new file mode 100644
index 000000000000..66d9fe137dd8
--- /dev/null
+++ b/app/Ninja/Transformers/InvoiceItemTransformer.php
@@ -0,0 +1,26 @@
+ (int) $item->public_id,
+ 'product_key' => $item->product_key,
+ 'account_key' => $this->account->account_key,
+ 'user_id' => (int) $item->user_id,
+ 'updated_at' => $this->getTimestamp($item->updated_at),
+ 'archived_at' => $this->getTimestamp($item->deleted_at),
+ 'product_key' => $item->product_key,
+ 'notes' => $item->notes,
+ 'cost' => (float) $item->cost,
+ 'qty' => (float) $item->qty,
+ 'tax_name' => $item->tax_name,
+ 'tax_rate' => (float) $item->tax_rate
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php
new file mode 100644
index 000000000000..8108115b2f01
--- /dev/null
+++ b/app/Ninja/Transformers/InvoiceTransformer.php
@@ -0,0 +1,83 @@
+account, $this->serializer);
+ return $this->includeCollection($invoice->invoice_items, $transformer, ENTITY_INVOICE_ITEMS);
+ }
+
+ public function includePayments(Invoice $invoice)
+ {
+ $transformer = new PaymentTransformer($this->account, $this->serializer);
+ return $this->includeCollection($invoice->payments, $transformer, ENTITY_PAYMENT);
+ }
+
+ public function transform(Invoice $invoice)
+ {
+ return [
+ 'id' => (int) $invoice->public_id,
+ 'amount' => (float) $invoice->amount,
+ 'balance' => (float) $invoice->balance,
+ 'client_id' => (int) $invoice->client->public_id,
+ 'invoice_status_id' => (int) $invoice->invoice_status_id,
+ 'updated_at' => $this->getTimestamp($invoice->updated_at),
+ 'archived_at' => $this->getTimestamp($invoice->deleted_at),
+ 'invoice_number' => $invoice->invoice_number,
+ 'discount' => (double) $invoice->discount,
+ 'po_number' => $invoice->po_number,
+ 'invoice_date' => $invoice->invoice_date,
+ 'due_date' => $invoice->due_date,
+ 'terms' => $invoice->terms,
+ 'public_notes' => $invoice->public_notes,
+ 'is_deleted' => (bool) $invoice->is_deleted,
+ 'is_quote' => (bool) $invoice->is_quote,
+ 'is_recurring' => (bool) $invoice->is_recurring,
+ 'frequency_id' => (int) $invoice->frequency_id,
+ 'start_date' => $invoice->start_date,
+ 'end_date' => $invoice->end_date,
+ 'last_sent_date' => $invoice->last_sent_date,
+ 'recurring_invoice_id' => (int) $invoice->recurring_invoice_id,
+ 'tax_name' => $invoice->tax_name,
+ 'tax_rate' => (float) $invoice->tax_rate,
+ 'amount' => (float) $invoice->amount,
+ 'balance' => (float) $invoice->balance,
+ 'is_amount_discount' => (bool) $invoice->is_amount_discount,
+ 'invoice_footer' => $invoice->invoice_footer,
+ 'partial' => (float) $invoice->partial,
+ 'has_tasks' => (bool) $invoice->has_tasks,
+ 'auto_bill' => (bool) $invoice->auto_bill,
+ 'account_key' => $this->account->account_key,
+ 'user_id' => (int) $invoice->user->public_id + 1,
+ 'custom_value1' => (float) $invoice->custom_value1,
+ 'custom_value2' => (float) $invoice->custom_value2,
+ 'custom_taxes1' => (bool) $invoice->custom_taxes1,
+ 'custom_taxes2' => (bool) $invoice->custom_taxes2,
+ 'has_expenses' => (bool) $invoice->has_expenses,
+ ];
+ }
+}
diff --git a/app/Ninja/Transformers/PaymentTransformer.php b/app/Ninja/Transformers/PaymentTransformer.php
new file mode 100644
index 000000000000..a2750eaad8af
--- /dev/null
+++ b/app/Ninja/Transformers/PaymentTransformer.php
@@ -0,0 +1,57 @@
+account, $this->serializer);
+ return $this->includeItem($payment->invoice, $transformer, 'invoice');
+ }
+
+ public function includeClient(Payment $payment)
+ {
+ $transformer = new ClientTransformer($this->account, $this->serializer);
+ return $this->includeItem($payment->client, $transformer, 'client');
+ }
+
+ public function transform(Payment $payment)
+ {
+ return [
+ 'id' => (int) $payment->public_id,
+ 'amount' => (float) $payment->amount,
+ 'account_key' => $this->account->account_key,
+ 'user_id' => (int) $payment->user->public_id + 1,
+ 'transaction_reference' => $payment->transaction_reference,
+ 'payment_date' => $payment->payment_date,
+ 'updated_at' => $this->getTimestamp($payment->updated_at),
+ 'archived_at' => $this->getTimestamp($payment->deleted_at),
+ 'is_deleted' => (bool) $payment->is_deleted,
+ 'payment_type_id' => (int) $payment->payment_type_id,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/ProductTransformer.php b/app/Ninja/Transformers/ProductTransformer.php
new file mode 100644
index 000000000000..76bf436066aa
--- /dev/null
+++ b/app/Ninja/Transformers/ProductTransformer.php
@@ -0,0 +1,21 @@
+ (int) $product->public_id,
+ 'product_key' => $product->product_key,
+ 'notes' => $product->notes,
+ 'cost' => $product->cost,
+ 'qty' => $product->qty,
+ 'account_key' =>$this->account->account_key,
+ 'default_tax_rate_id' =>$product->default_tax_rate_id,
+ 'updated_at' =>$this->getTimestamp($product->updated_at),
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/QuoteTransformer.php b/app/Ninja/Transformers/QuoteTransformer.php
new file mode 100644
index 000000000000..2c92640e805a
--- /dev/null
+++ b/app/Ninja/Transformers/QuoteTransformer.php
@@ -0,0 +1,27 @@
+account, $this->serializer);
+ return $this->includeCollection($invoice->invoice_items, $transformer, 'invoice_items');
+ }
+
+ public function transform(Invoice $invoice)
+ {
+ return [
+ 'id' => (int) $invoice->public_id,
+ 'quote_number' => $invoice->invoice_number,
+ 'amount' => (float) $invoice->amount,
+ 'quote_terms' => $invoice->terms,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/TaskTransformer.php b/app/Ninja/Transformers/TaskTransformer.php
new file mode 100644
index 000000000000..908a8118aaea
--- /dev/null
+++ b/app/Ninja/Transformers/TaskTransformer.php
@@ -0,0 +1,50 @@
+client) {
+ $transformer = new ClientTransformer($this->account, $this->serializer);
+ return $this->includeItem($task->client, $transformer, 'client');
+ } else {
+ return null;
+ }
+ }
+
+ public function transform(Task $task)
+ {
+ return [
+ 'id' => (int) $task->public_id,
+ 'account_key' => $this->account->account_key,
+ 'user_id' => (int) $task->user->public_id + 1,
+ 'description' => $task->description,
+ 'duration' => $task->getDuration()
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/TaxRateTransformer.php b/app/Ninja/Transformers/TaxRateTransformer.php
new file mode 100644
index 000000000000..8f4a375c7a72
--- /dev/null
+++ b/app/Ninja/Transformers/TaxRateTransformer.php
@@ -0,0 +1,33 @@
+ (int) $taxRate->public_id,
+ 'name' => $taxRate->name,
+ 'rate' => (float) $taxRate->rate,
+ 'updated_at' => $this->getTimestamp($taxRate->updated_at),
+ 'archived_at' => $this->getTimestamp($taxRate->deleted_at),
+ 'account_key' => $this->account->account_key,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/UserAccountTransformer.php b/app/Ninja/Transformers/UserAccountTransformer.php
new file mode 100644
index 000000000000..bc49a96c546a
--- /dev/null
+++ b/app/Ninja/Transformers/UserAccountTransformer.php
@@ -0,0 +1,39 @@
+tokenName = $tokenName;
+ }
+
+ public function includeUser(User $user)
+ {
+ $transformer = new UserTransformer($this->account, $this->serializer);
+ return $this->includeItem($user, $transformer, 'user');
+ }
+
+ public function transform(User $user)
+ {
+ return [
+ 'account_key' => $user->account->account_key,
+ 'name' => $user->account->present()->name,
+ 'token' => $user->account->getToken($this->tokenName),
+ 'default_url' => SITE_URL
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/UserTransformer.php b/app/Ninja/Transformers/UserTransformer.php
new file mode 100644
index 000000000000..dd3c6775dcb1
--- /dev/null
+++ b/app/Ninja/Transformers/UserTransformer.php
@@ -0,0 +1,27 @@
+ (int) ($user->public_id + 1),
+ 'first_name' => $user->first_name,
+ 'last_name' => $user->last_name,
+ 'email' => $user->email,
+ 'account_key' => $user->account->account_key,
+ 'updated_at' => $this->getTimestamp($user->updated_at),
+ 'deleted_at' => $this->getTimestamp($user->deleted_at),
+ 'phone' => $user->phone,
+ 'username' => $user->username,
+ 'registered' => (bool) $user->registered,
+ 'confirmed' => (bool) $user->confirmed,
+ 'oauth_user_id' => $user->oauth_user_id,
+ 'oauth_provider_id' => $user->oauth_provider_id
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/VendorContactTransformer.php b/app/Ninja/Transformers/VendorContactTransformer.php
new file mode 100644
index 000000000000..0166883aba4d
--- /dev/null
+++ b/app/Ninja/Transformers/VendorContactTransformer.php
@@ -0,0 +1,24 @@
+ (int) $contact->public_id,
+ 'first_name' => $contact->first_name,
+ 'last_name' => $contact->last_name,
+ 'email' => $contact->email,
+ 'updated_at' => $this->getTimestamp($contact->updated_at),
+ 'archived_at' => $this->getTimestamp($contact->deleted_at),
+ 'is_primary' => (bool) $contact->is_primary,
+ 'phone' => $contact->phone,
+ 'last_login' => $contact->last_login,
+ 'account_key' => $this->account->account_key,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/VendorTransformer.php b/app/Ninja/Transformers/VendorTransformer.php
new file mode 100644
index 000000000000..c1714b27a120
--- /dev/null
+++ b/app/Ninja/Transformers/VendorTransformer.php
@@ -0,0 +1,82 @@
+account, $this->serializer);
+ return $this->includeCollection($vendor->contacts, $transformer, ENTITY_CONTACT);
+ }
+
+ public function includeInvoices(Vendor $vendor)
+ {
+ $transformer = new InvoiceTransformer($this->account, $this->serializer);
+ return $this->includeCollection($vendor->invoices, $transformer, ENTITY_INVOICE);
+ }
+
+ public function transform(Vendor $vendor)
+ {
+ return [
+ 'id' => (int) $vendor->public_id,
+ 'name' => $vendor->name,
+ 'balance' => (float) $vendor->balance,
+ 'paid_to_date' => (float) $vendor->paid_to_date,
+ 'user_id' => (int) $vendor->user->public_id + 1,
+ 'account_key' => $this->account->account_key,
+ 'updated_at' => $this->getTimestamp($vendor->updated_at),
+ 'archived_at' => $this->getTimestamp($vendor->deleted_at),
+ 'address1' => $vendor->address1,
+ 'address2' => $vendor->address2,
+ 'city' => $vendor->city,
+ 'state' => $vendor->state,
+ 'postal_code' => $vendor->postal_code,
+ 'country_id' => (int) $vendor->country_id,
+ 'work_phone' => $vendor->work_phone,
+ 'private_notes' => $vendor->private_notes,
+ 'last_login' => $vendor->last_login,
+ 'website' => $vendor->website,
+ 'is_deleted' => (bool) $vendor->is_deleted,
+ 'vat_number' => $vendor->vat_number,
+ 'id_number' => $vendor->id_number,
+ 'currency_id' => (int) $vendor->currency_id
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 56a8e8bca115..a094b01a6079 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -33,28 +33,31 @@ class AppServiceProvider extends ServiceProvider {
$types = $type.'s';
$Type = ucfirst($type);
$Types = ucfirst($types);
- $class = ( Request::is($types) || Request::is('*'.$type.'*')) && !Request::is('*advanced_settings*') ? ' active' : '';
+ $class = ( Request::is($types) || Request::is('*'.$type.'*')) && !Request::is('*settings*') ? ' active' : '';
$str = '