diff --git a/.env.example b/.env.example index abc9da3a52..e8a53eaa41 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ APP_ENV=production APP_DEBUG=false APP_URL=http://ninja.dev APP_CIPHER=rijndael-128 -APP_KEY +APP_KEY=SomeRandomString APP_TIMEZONE DB_TYPE=mysql @@ -19,5 +19,3 @@ MAIL_USERNAME MAIL_FROM_ADDRESS MAIL_FROM_NAME MAIL_PASSWORD - -ALLOW_NEW_ACCOUNTS \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7055b9135f..fe7a9dd0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,18 +13,20 @@ /bootstrap/environment.php /vendor /node_modules -.env /.DS_Store /Thumbs.db -.env.development.php -.env.php -.idea -.project -error_log -public/error_log +/.env +/.env.development.php +/.env.php + +/error_log +/auth.json +/public/error_log + /ninja.sublime-project /ninja.sublime-workspace -auth.json - -.phpstorm.meta.php -_ide_helper.php \ No newline at end of file +/.phpstorm.meta.php +/_ide_helper.php +/.idea +/.project +tests/_output/ \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 3e358a158c..4de932ca45 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -51,7 +51,17 @@ module.exports = function(grunt) { 'public/vendor/knockout-mapping/build/output/knockout.mapping-latest.js', 'public/vendor/knockout-sortable/build/knockout-sortable.min.js', 'public/vendor/underscore/underscore.js', - 'public/vendor/bootstrap-datepicker/dist/js/bootstrap-datepicker.js', + 'public/vendor/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.de.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.da.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.pt-BR.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.nl.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.fr.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.it.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.lt.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.no.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.es.min.js', + 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.sv.min.js', 'public/vendor/typeahead.js/dist/typeahead.min.js', 'public/vendor/accounting/accounting.min.js', 'public/vendor/spectrum/spectrum.js', diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 8f65214f7f..6acd87aa0c 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -7,6 +7,7 @@ use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use App\Ninja\Mailers\ContactMailer as Mailer; +use App\Ninja\Repositories\InvoiceRepository; use App\Models\Invoice; use App\Models\InvoiceItem; use App\Models\Invitation; @@ -16,12 +17,14 @@ class SendRecurringInvoices extends Command protected $name = 'ninja:send-invoices'; protected $description = 'Send recurring invoices'; protected $mailer; + protected $invoiceRepo; - public function __construct(Mailer $mailer) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo) { parent::__construct(); $this->mailer = $mailer; + $this->invoiceRepo = $invoiceRepo; } public function fire() @@ -34,74 +37,14 @@ class SendRecurringInvoices extends Command $this->info(count($invoices).' recurring invoice(s) found'); foreach ($invoices as $recurInvoice) { - if ($recurInvoice->client->deleted_at) { - continue; + $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); + + $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); + + if ($invoice) { + $recurInvoice->account->loadLocalizationSettings(); + $this->mailer->sendInvoice($invoice); } - - if (!$recurInvoice->user->confirmed) { - continue; - } - - $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); - - if (!$recurInvoice->shouldSendToday()) { - continue; - } - - $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->amount = $recurInvoice->amount; - $invoice->balance = $recurInvoice->amount; - $invoice->invoice_date = date_create()->format('Y-m-d'); - $invoice->discount = $recurInvoice->discount; - $invoice->po_number = $recurInvoice->po_number; - $invoice->public_notes = Utils::processVariables($recurInvoice->public_notes); - $invoice->terms = Utils::processVariables($recurInvoice->terms); - $invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer); - $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->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->save(); - - foreach ($recurInvoice->invoice_items as $recurItem) { - $item = InvoiceItem::createNew($recurItem); - $item->product_id = $recurItem->product_id; - $item->qty = $recurItem->qty; - $item->cost = $recurItem->cost; - $item->notes = Utils::processVariables($recurItem->notes); - $item->product_key = Utils::processVariables($recurItem->product_key); - $item->tax_name = $recurItem->tax_name; - $item->tax_rate = $recurItem->tax_rate; - $invoice->invoice_items()->save($item); - } - - foreach ($recurInvoice->invitations as $recurInvitation) { - $invitation = Invitation::createNew($recurInvitation); - $invitation->contact_id = $recurInvitation->contact_id; - $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); - $invoice->invitations()->save($invitation); - } - - $this->mailer->sendInvoice($invoice); - - $recurInvoice->last_sent_date = Carbon::now()->toDateTimeString(); - $recurInvoice->save(); } $this->info('Done'); diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 540567d9b3..cf647dcaa8 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -40,7 +40,6 @@ use App\Ninja\Repositories\AccountRepository; use App\Ninja\Mailers\UserMailer; use App\Ninja\Mailers\ContactMailer; use App\Events\UserLoggedIn; -use App\Events\UserSettingsChanged; class AccountController extends BaseController { @@ -75,18 +74,18 @@ class AccountController extends BaseController public function getStarted() { - if (Auth::check()) { - return Redirect::to('invoices/create'); - } - - if (!Utils::isNinja() && !Utils::allowNewAccounts() && Account::count() > 0) { - return Redirect::to('/login'); - } - $user = false; $guestKey = Input::get('guest_key'); // local storage key to login until registered $prevUserId = Session::pull(PREV_USER_ID); // last user id used to link to new account + if (Auth::check()) { + return Redirect::to('invoices/create'); + } + + if (!Utils::isNinja() && (Account::count() > 0 && !$prevUserId)) { + return Redirect::to('/login'); + } + if ($guestKey && !$prevUserId) { $user = User::where('password', '=', $guestKey)->first(); @@ -110,7 +109,8 @@ class AccountController extends BaseController Auth::login($user, true); Event::fire(new UserLoggedIn()); - return Redirect::to('invoices/create')->with('sign_up', Input::get('sign_up')); + $redirectTo = Input::get('redirect_to', 'invoices/create'); + return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up')); } public function enableProPlan() @@ -149,6 +149,7 @@ class AccountController extends BaseController public function showSection($section = ACCOUNT_DETAILS, $subSection = 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'), @@ -159,8 +160,9 @@ class AccountController extends BaseController 'datetimeFormats' => Cache::get('datetimeFormats'), 'currencies' => Cache::get('currencies'), 'languages' => Cache::get('languages'), - 'showUser' => Auth::user()->id === Auth::user()->account->users()->first()->id, + 'showUser' => Auth::user()->id === $primaryUser->id, 'title' => trans('texts.company_details'), + 'primaryUser' => $primaryUser, ]; return View::make('accounts.details', $data); @@ -188,16 +190,18 @@ class AccountController extends BaseController } 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; + $account = Auth::user()->account->load('country'); $data = [ 'account' => $account, 'feature' => $subSection, 'title' => trans('texts.invoice_settings'), ]; - if ($subSection == ACCOUNT_INVOICE_DESIGN) { + 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'; @@ -208,11 +212,17 @@ class AccountController extends BaseController $client->work_phone = ''; $client->work_email = ''; - $invoice->invoice_number = Auth::user()->account->getNextInvoiceNumber(); - $invoice->invoice_date = date_create()->format('Y-m-d'); - $invoice->account = json_decode(Auth::user()->account->toJson()); + $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'; @@ -221,15 +231,28 @@ class AccountController extends BaseController $invoice->client = $client; $invoice->invoice_items = [$invoiceItem]; + $data['account'] = $account; $data['invoice'] = $invoice; - $data['invoiceDesigns'] = InvoiceDesign::availableDesigns(); $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['emailFooter'] = $account->getEmailFooter(); $data['title'] = trans('texts.email_templates'); } else if ($subSection == ACCOUNT_USER_MANAGEMENT) { $data['title'] = trans('texts.users_and_tokens'); @@ -263,6 +286,8 @@ class AccountController extends BaseController 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(); } @@ -271,6 +296,19 @@ class AccountController extends BaseController } } + 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'); + } + private function saveEmailTemplates() { if (Auth::user()->account->isPro()) { @@ -322,7 +360,6 @@ class AccountController extends BaseController $account->share_counter = Input::get('share_counter') ? true : false; $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; - $account->utf8_invoices = Input::get('utf8_invoices') ? true : false; $account->auto_wrap = Input::get('auto_wrap') ? true : false; if (!$account->share_counter) { @@ -596,9 +633,10 @@ class AccountController extends BaseController { $rules = array( 'name' => 'required', + 'logo' => 'sometimes|max:1024|mimes:jpeg,gif,png', ); - $user = Auth::user()->account->users()->first(); + $user = Auth::user()->account->users()->orderBy('id')->first(); if (Auth::user()->id === $user->id) { $rules['email'] = 'email|required|unique:users,email,'.$user->id.',id'; @@ -647,6 +685,9 @@ class AccountController extends BaseController $user->username = trim(Input::get('email')); $user->email = trim(strtolower(Input::get('email'))); $user->phone = trim(Input::get('phone')); + if (Utils::isNinjaDev()) { + $user->dark_mode = Input::get('dark_mode') ? true : false; + } $user->save(); } @@ -654,21 +695,28 @@ class AccountController extends BaseController if ($file = Input::file('logo')) { $path = Input::file('logo')->getRealPath(); File::delete('logo/'.$account->account_key.'.jpg'); + File::delete('logo/'.$account->account_key.'.png'); - $image = Image::make($path); $mimeType = $file->getMimeType(); - if ($image->width() == 200 && $mimeType == 'image/jpeg') { + if ($mimeType == 'image/jpeg') { $file->move('logo/', $account->account_key . '.jpg'); + } else if ($mimeType == 'image/png') { + $file->move('logo/', $account->account_key . '.png'); } else { - $image->resize(200, 120, function ($constraint) { - $constraint->aspectRatio(); - }); - Image::canvas($image->width(), $image->height(), '#FFFFFF')->insert($image)->save($account->getLogoPath()); + if (extension_loaded('fileinfo')) { + $image = Image::make($path); + $image->resize(200, 120, function ($constraint) { + $constraint->aspectRatio(); + }); + Image::canvas($image->width(), $image->height(), '#FFFFFF') + ->insert($image)->save('logo/'.$account->account_key.'.jpg'); + } else { + Session::flash('warning', 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.'); + } } } - Event::fire(new UserSettingsChanged()); Session::flash('message', trans('texts.updated_settings')); return Redirect::to('company/details'); @@ -678,6 +726,7 @@ class AccountController extends BaseController public function removeLogo() { File::delete('logo/'.Auth::user()->account->account_key.'.jpg'); + File::delete('logo/'.Auth::user()->account->account_key.'.png'); Session::flash('message', trans('texts.removed_logo')); @@ -717,9 +766,9 @@ class AccountController extends BaseController $user->username = $user->email; $user->password = bcrypt(trim(Input::get('new_password'))); $user->registered = true; - $user->save(); + $user->save(); - if (Utils::isNinja()) { + if (Utils::isNinjaProd()) { $this->userMailer->sendConfirmation($user); } diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index b6f3c5df22..ff4c84c649 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -257,6 +257,8 @@ class AccountGatewayController extends BaseController } $accountGateway->accepted_credit_cards = $cardCount; + $accountGateway->show_address = Input::get('show_address') ? true : false; + $accountGateway->update_address = Input::get('update_address') ? true : false; $accountGateway->config = json_encode($config); if ($accountGatewayPublicId) { @@ -278,7 +280,7 @@ class AccountGatewayController extends BaseController Session::flash('message', $message); - return Redirect::to('company/payments'); + return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } } diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index dac4b85ccb..703bd5fdd5 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -34,7 +34,7 @@ class AppController extends BaseController public function showSetup() { - if (Utils::isNinja() || (Utils::isDatabaseSetup() && Account::count() > 0)) { + if (Utils::isNinjaProd() || (Utils::isDatabaseSetup() && Account::count() > 0)) { return Redirect::to('/'); } @@ -43,7 +43,7 @@ class AppController extends BaseController public function doSetup() { - if (Utils::isNinja() || (Utils::isDatabaseSetup() && Account::count() > 0)) { + if (Utils::isNinjaProd() || (Utils::isDatabaseSetup() && Account::count() > 0)) { return Redirect::to('/'); } @@ -88,7 +88,7 @@ class AppController extends BaseController "MAIL_HOST={$mail['host']}\n". "MAIL_USERNAME={$mail['username']}\n". "MAIL_FROM_NAME={$mail['from']['name']}\n". - "MAIL_PASSWORD={$mail['password']}\n"; + "MAIL_PASSWORD={$mail['password']}"; // Write Config Settings $fp = fopen(base_path()."/.env", 'w'); @@ -101,6 +101,7 @@ class AppController extends BaseController if (Industry::count() == 0) { Artisan::call('db:seed', array('--force' => true)); } + Cache::flush(); Artisan::call('optimize', array('--force' => true)); $firstName = trim(Input::get('first_name')); @@ -159,7 +160,7 @@ class AppController extends BaseController public function install() { - if (!Utils::isNinja() && !Utils::isDatabaseSetup()) { + if (!Utils::isNinjaProd() && !Utils::isDatabaseSetup()) { try { Artisan::call('migrate', array('--force' => true)); if (Industry::count() == 0) { @@ -176,7 +177,7 @@ class AppController extends BaseController public function update() { - if (!Utils::isNinja()) { + if (!Utils::isNinjaProd()) { try { Artisan::call('migrate', array('--force' => true)); Artisan::call('db:seed', array('--force' => true, '--class' => 'PaymentLibrariesSeeder')); diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index e17135fe71..a5897ac687 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -62,7 +62,7 @@ class AuthController extends Controller { $userId = Auth::check() ? Auth::user()->id : null; $user = User::where('email', '=', $request->input('email'))->first(); - if ($user->failed_logins >= 3) { + if ($user && $user->failed_logins >= 3) { Session::flash('error', 'These credentials do not match our records.'); return redirect()->to('login'); } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 9072f6960d..065e3e073a 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -268,7 +268,7 @@ class ClientController extends BaseController $record = Contact::createNew(); } - $record->email = trim(strtolower($contact->email)); + $record->email = trim($contact->email); $record->first_name = trim($contact->first_name); $record->last_name = trim($contact->last_name); $record->phone = trim($contact->phone); @@ -306,7 +306,7 @@ class ClientController extends BaseController Session::flash('message', $message); if ($action == 'restore' && $count == 1) { - return Redirect::to('clients/'.$ids[0]); + return Redirect::to('clients/'.Utils::getFirst($ids)); } else { return Redirect::to('clients'); } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 9defb3e9af..189b277c2c 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -5,6 +5,7 @@ use DB; use View; use App\Models\Activity; use App\Models\Invoice; +use App\Models\Payment; class DashboardController extends BaseController { @@ -44,47 +45,89 @@ class DashboardController extends BaseController ->where('accounts.id', '=', Auth::user()->account_id) ->where('clients.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false) + ->where('invoices.is_quote', '=', false) + ->where('invoices.is_recurring', '=', false) ->groupBy('accounts.id') ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')) ->get(); - + $select = DB::raw('SUM(clients.balance) as value, clients.currency_id as currency_id'); + $balances = DB::table('accounts') + ->select($select) + ->leftJoin('clients', 'accounts.id', '=', 'clients.account_id') + ->where('accounts.id', '=', Auth::user()->account_id) + ->where('clients.is_deleted', '=', false) + ->groupBy('accounts.id') + ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')) + ->get(); $activities = Activity::where('activities.account_id', '=', Auth::user()->account_id) ->where('activity_type_id', '>', 0) - ->orderBy('created_at', 'desc')->take(14)->get(); + ->orderBy('created_at', 'desc') + ->take(50) + ->get(); - $pastDue = Invoice::scope()->whereHas('client', function($query) { - $query->where('deleted_at', '=', null); - }) - ->where('due_date', '<', date('Y-m-d')) - ->where('balance', '>', 0) - ->where('is_recurring', '=', false) - ->where('is_quote', '=', false) - ->where('is_deleted', '=', false) - ->orderBy('due_date', 'asc')->take(6)->get(); + $pastDue = DB::table('invoices') + ->leftJoin('clients', 'clients.id', '=', 'invoices.client_id') + ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('invoices.account_id', '=', Auth::user()->account_id) + ->where('clients.deleted_at', '=', null) + ->where('contacts.deleted_at', '=', null) + ->where('invoices.is_recurring', '=', 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')) + ->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']) + ->orderBy('invoices.due_date', 'asc') + ->take(50) + ->get(); + + $upcoming = DB::table('invoices') + ->leftJoin('clients', 'clients.id', '=', 'invoices.client_id') + ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('invoices.account_id', '=', Auth::user()->account_id) + ->where('clients.deleted_at', '=', null) + ->where('contacts.deleted_at', '=', null) + ->where('invoices.is_recurring', '=', 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']) + ->get(); + + $payments = DB::table('payments') + ->leftJoin('clients', 'clients.id', '=', 'payments.client_id') + ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') + ->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->where('payments.account_id', '=', Auth::user()->account_id) + ->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') + ->take(50) + ->get(); - $upcoming = Invoice::scope()->whereHas('client', function($query) { - $query->where('deleted_at', '=', null); - }) - ->where('due_date', '>=', date('Y-m-d')) - ->where('balance', '>', 0) - ->where('is_recurring', '=', false) - ->where('is_quote', '=', false) - ->where('is_deleted', '=', false) - ->orderBy('due_date', 'asc')->take(6)->get(); $data = [ - 'paidToDate' => $paidToDate, - 'averageInvoice' => $averageInvoice, - //'billedClients' => $metrics ? $metrics->billed_clients : 0, - 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0, - 'activeClients' => $metrics ? $metrics->active_clients : 0, - 'activities' => $activities, - 'pastDue' => $pastDue, - 'upcoming' => $upcoming, - 'title' => trans('texts.dashboard'), - ]; + 'account' => Auth::user()->account, + 'paidToDate' => $paidToDate, + 'balances' => $balances, + 'averageInvoice' => $averageInvoice, + 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0, + 'activeClients' => $metrics ? $metrics->active_clients : 0, + 'activities' => $activities, + 'pastDue' => $pastDue, + 'upcoming' => $upcoming, + 'payments' => $payments, + 'title' => trans('texts.dashboard'), + ]; return View::make('dashboard', $data); } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index a58245e0e2..1308a4378b 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -43,14 +43,15 @@ class HomeController extends BaseController public function invoiceNow() { - if (Auth::check() && Input::get('new_account')) { + if (Auth::check() && Input::get('new_company')) { Session::put(PREV_USER_ID, Auth::user()->id); Auth::user()->clearSession(); Auth::logout(); } if (Auth::check()) { - return Redirect::to('invoices/create')->with('sign_up', Input::get('sign_up')); + $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]); } @@ -72,9 +73,9 @@ class HomeController extends BaseController $user->news_feed_id = $newsFeedId; $user->save(); } - - Session::forget('news_feed_message'); } + + Session::forget('news_feed_message'); return 'success'; } diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 43911db7d8..4c13259aec 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -26,7 +26,11 @@ class InvoiceApiController extends Controller public function index() { - $invoices = Invoice::scope()->with('client', 'invitations.account')->where('invoices.is_quote', '=', false)->orderBy('created_at', 'desc')->get(); + $invoices = Invoice::scope() + ->with('client', 'invitations.account') + ->where('invoices.is_quote', '=', false) + ->orderBy('created_at', 'desc') + ->get(); // Add the first invitation link to the data foreach ($invoices as $key => $invoice) { @@ -50,12 +54,14 @@ class InvoiceApiController extends Controller $error = null; // check if the invoice number is set and unique - if (!isset($data['invoice_number'])) { + if (!isset($data['invoice_number']) && !isset($data['id'])) { $data['invoice_number'] = Auth::user()->account->getNextInvoiceNumber(); - } else { + } else if (isset($data['invoice_number'])) { $invoice = Invoice::scope()->where('invoice_number', '=', $data['invoice_number'])->first(); if ($invoice) { $error = trans('validation.unique', ['attribute' => 'texts.invoice_number']); + } else { + $data['id'] = $invoice->public_id; } } @@ -100,11 +106,13 @@ class InvoiceApiController extends Controller $data['client_id'] = $client->id; $invoice = $this->invoiceRepo->save(false, $data, false); - $invitation = Invitation::createNew(); - $invitation->invoice_id = $invoice->id; - $invitation->contact_id = $client->contacts[0]->id; - $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); - $invitation->save(); + if (!isset($data['id'])) { + $invitation = Invitation::createNew(); + $invitation->invoice_id = $invoice->id; + $invitation->contact_id = $client->contacts[0]->id; + $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); + $invitation->save(); + } if (isset($data['email_invoice']) && $data['email_invoice']) { $this->mailer->sendInvoice($invoice); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 0a9220c727..b7e734ca14 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -60,8 +60,7 @@ class InvoiceController extends BaseController 'columns' => Utils::trans(['checkbox', 'invoice_number', 'client', 'invoice_date', 'invoice_total', 'balance_due', 'due_date', 'status', 'action']), ]; - $recurringInvoices = Invoice::scope() - ->where('is_recurring', '=', true); + $recurringInvoices = Invoice::scope()->where('is_recurring', '=', true); if (Session::get('show_trash:invoice')) { $recurringInvoices->withTrashed(); @@ -86,11 +85,12 @@ class InvoiceController extends BaseController } $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $color = $invitation->account->primary_color ? $invitation->account->primary_color : '#0b4d78'; + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => Session::get('white_label'), + 'hideLogo' => $account->isWhiteLabel(), 'title' => trans('texts.invoices'), 'entityType' => ENTITY_INVOICE, 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']), @@ -205,7 +205,6 @@ class InvoiceController extends BaseController Session::set($invitationKey, true); Session::set('invitation_key', $invitationKey); - Session::set('white_label', $account->isWhiteLabel()); $account->loadLocalizationSettings(); @@ -213,6 +212,12 @@ class InvoiceController extends BaseController $invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->is_pro = $account->isPro(); + if ($invoice->invoice_design_id == CUSTOM_DESIGN) { + $invoice->invoice_design->javascript = $account->custom_design; + } else { + $invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake; + } + $contact = $invitation->contact; $contact->setVisible([ 'first_name', @@ -250,7 +255,7 @@ class InvoiceController extends BaseController 'invoiceLabels' => $account->getInvoiceLabels(), 'contact' => $contact, 'paymentTypes' => $paymentTypes, - 'paymentURL' => $paymentURL + 'paymentURL' => $paymentURL, ); return View::make('invoices.view', $data); @@ -277,7 +282,7 @@ class InvoiceController extends BaseController $method = 'POST'; $url = "{$entityType}s"; } else { - Utils::trackViewed($invoice->invoice_number.' - '.$invoice->client->getDisplayName(), $invoice->getEntityType()); + Utils::trackViewed($invoice->getDisplayName().' - '.$invoice->client->getDisplayName(), $invoice->getEntityType()); $method = 'PUT'; $url = "{$entityType}s/{$publicId}"; } @@ -321,19 +326,21 @@ class InvoiceController extends BaseController $actions[] = ['url' => 'javascript:onArchiveClick()', 'label' => trans("texts.archive_{$entityType}")]; $actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans("texts.delete_{$entityType}")]; + $lastSent = ($invoice->is_recurring && $invoice->last_sent_date) ? $invoice->recurring_invoices->last() : null; $data = array( 'entityType' => $entityType, 'showBreadcrumbs' => $clone, - 'account' => $invoice->account, 'invoice' => $invoice, 'data' => false, 'method' => $method, 'invitationContactIds' => $contactIds, 'url' => $url, 'title' => trans("texts.edit_{$entityType}"), - 'client' => $invoice->client, - 'actions' => $actions); + 'client' => $invoice->client, + 'isRecurring' => $invoice->is_recurring, + 'actions' => $actions, + 'lastSent' => $lastSent); $data = array_merge($data, self::getViewModel()); // Set the invitation link on the client's contacts @@ -356,11 +363,10 @@ class InvoiceController extends BaseController return View::make('invoices.edit', $data); } - public function create($clientPublicId = 0) + public function create($clientPublicId = 0, $isRecurring = false) { $client = null; - $invoiceNumber = Auth::user()->account->getNextInvoiceNumber(); - $account = Account::with('country')->findOrFail(Auth::user()->account_id); + $invoiceNumber = $isRecurring ? microtime(true) : Auth::user()->account->getNextInvoiceNumber(); if ($clientPublicId) { $client = Client::scope($clientPublicId)->firstOrFail(); @@ -368,20 +374,24 @@ class InvoiceController extends BaseController $data = array( 'entityType' => ENTITY_INVOICE, - 'account' => $account, 'invoice' => null, 'data' => Input::old('data'), 'invoiceNumber' => $invoiceNumber, 'method' => 'POST', 'url' => 'invoices', 'title' => trans('texts.new_invoice'), - 'client' => $client, - 'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null); + 'isRecurring' => $isRecurring, + 'client' => $client); $data = array_merge($data, self::getViewModel()); - + return View::make('invoices.edit', $data); } + public function createRecurring($clientPublicId = 0) + { + return self::create($clientPublicId, true); + } + private static function getViewModel() { $recurringHelp = ''; @@ -396,7 +406,7 @@ class InvoiceController extends BaseController } return [ - 'account' => Auth::user()->account, + 'account' => Auth::user()->account->load('country'), 'products' => Product::scope()->orderBy('id')->get(array('product_key', 'notes', 'cost', 'qty')), 'countries' => Cache::get('countries'), 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), @@ -405,7 +415,7 @@ class InvoiceController extends BaseController 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), - 'invoiceDesigns' => InvoiceDesign::availableDesigns(), + 'invoiceDesigns' => InvoiceDesign::getDesigns(), 'frequencies' => array( 1 => 'Weekly', 2 => 'Two weeks', @@ -417,7 +427,7 @@ class InvoiceController extends BaseController ), 'recurringHelp' => $recurringHelp, 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), - + 'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null, ]; } @@ -511,7 +521,16 @@ class InvoiceController extends BaseController return $this->convertQuote($publicId); } elseif ($action == 'email') { if (Auth::user()->confirmed && !Auth::user()->isDemo()) { - $response = $this->mailer->sendInvoice($invoice); + if ($invoice->is_recurring) { + if ($invoice->shouldSendToday()) { + $invoice = $this->invoiceRepo->createRecurringInvoice($invoice); + $response = $this->mailer->sendInvoice($invoice); + } else { + $response = trans('texts.recurring_too_soon'); + } + } else { + $response = $this->mailer->sendInvoice($invoice); + } if ($response === true) { $message = trans("texts.emailed_{$entityType}"); Session::flash('message', $message); @@ -577,7 +596,7 @@ class InvoiceController extends BaseController } if ($action == 'restore' && $count == 1) { - return Redirect::to("{$entityType}s/".$ids[0]); + return Redirect::to("{$entityType}s/".Utils::getFirst($ids)); } else { return Redirect::to("{$entityType}s"); } @@ -646,7 +665,7 @@ class InvoiceController extends BaseController 'invoice' => $invoice, 'versionsJson' => json_encode($versionsJson), 'versionsSelect' => $versionsSelect, - 'invoiceDesigns' => InvoiceDesign::availableDesigns(), + 'invoiceDesigns' => InvoiceDesign::getDesigns(), ]; return View::make('invoices.history', $data); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index a68841d580..712f19dce6 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -61,11 +61,12 @@ class PaymentController extends BaseController } $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $color = $invitation->account->primary_color ? $invitation->account->primary_color : '#0b4d78'; + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => Session::get('white_label'), + 'hideLogo' => $account->isWhiteLabel(), 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) @@ -233,33 +234,41 @@ class PaymentController extends BaseController private function convertInputForOmnipay($input) { - $country = Country::find($input['country_id']); - - return [ + $data = [ 'firstName' => $input['first_name'], 'lastName' => $input['last_name'], 'number' => $input['card_number'], 'expiryMonth' => $input['expiration_month'], 'expiryYear' => $input['expiration_year'], 'cvv' => $input['cvv'], - 'billingAddress1' => $input['address1'], - 'billingAddress2' => $input['address2'], - 'billingCity' => $input['city'], - 'billingState' => $input['state'], - 'billingPostcode' => $input['postal_code'], - 'billingCountry' => $country->iso_3166_2, - 'shippingAddress1' => $input['address1'], - 'shippingAddress2' => $input['address2'], - 'shippingCity' => $input['city'], - 'shippingState' => $input['state'], - 'shippingPostcode' => $input['postal_code'], - 'shippingCountry' => $country->iso_3166_2 ]; + + if (isset($input['country_id'])) { + $country = Country::find($input['country_id']); + + $data = array_merge($data, [ + 'billingAddress1' => $input['address1'], + 'billingAddress2' => $input['address2'], + 'billingCity' => $input['city'], + 'billingState' => $input['state'], + 'billingPostcode' => $input['postal_code'], + 'billingCountry' => $country->iso_3166_2, + 'shippingAddress1' => $input['address1'], + 'shippingAddress2' => $input['address2'], + 'shippingCity' => $input['city'], + 'shippingState' => $input['state'], + 'shippingPostcode' => $input['postal_code'], + 'shippingCountry' => $country->iso_3166_2 + ]); + } + + return $data; } private function getPaymentDetails($invitation, $input = null) { $invoice = $invitation->invoice; + $account = $invoice->account; $key = $invoice->account_id.'-'.$invoice->invoice_number; $currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD'); @@ -328,8 +337,10 @@ class PaymentController extends BaseController 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, 'countries' => Cache::get('countries'), 'currencyId' => $client->getCurrencyId(), + 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), 'account' => $client->account, 'hideLogo' => $account->isWhiteLabel(), + 'showAddress' => $accountGateway->show_address, ]; return View::make('payments.payment', $data); @@ -378,6 +389,7 @@ class PaymentController extends BaseController 'currencyId' => 1, 'paymentTitle' => $affiliate->payment_title, 'paymentSubtitle' => $affiliate->payment_subtitle, + 'showAddress' => true, ]; return View::make('payments.payment', $data); @@ -498,19 +510,30 @@ class PaymentController extends BaseController public function do_payment($invitationKey, $onSite = true, $useToken = false) { - $rules = array( + $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail(); + $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; + $accountGateway = $account->getGatewayByType(Session::get('payment_type')); + + $rules = [ 'first_name' => 'required', 'last_name' => 'required', 'card_number' => 'required', 'expiration_month' => 'required', 'expiration_year' => 'required', 'cvv' => 'required', - 'address1' => 'required', - 'city' => 'required', - 'state' => 'required', - 'postal_code' => 'required', - 'country_id' => 'required', - ); + ]; + + if ($accountGateway->show_address) { + $rules = array_merge($rules, [ + 'address1' => 'required', + 'city' => 'required', + 'state' => 'required', + 'postal_code' => 'required', + 'country_id' => 'required', + ]); + } if ($onSite) { $validator = Validator::make(Input::all(), $rules); @@ -521,25 +544,19 @@ class PaymentController extends BaseController ->withErrors($validator) ->withInput(); } - } - $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail(); - $invoice = $invitation->invoice; - $client = $invoice->client; - $account = $client->account; - $accountGateway = $account->getGatewayByType(Session::get('payment_type')); - - /* - if ($onSite) { - $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->save(); + + if ($accountGateway->update_address) { + $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'); + $client->save(); + } } - */ - + try { $gateway = self::createGateway($accountGateway); $details = self::getPaymentDetails($invitation, ($useToken || !$onSite) ? false : Input::all()); diff --git a/app/Http/Controllers/QuoteApiController.php b/app/Http/Controllers/QuoteApiController.php index 70257644c5..83e5e87811 100644 --- a/app/Http/Controllers/QuoteApiController.php +++ b/app/Http/Controllers/QuoteApiController.php @@ -16,7 +16,11 @@ class QuoteApiController extends Controller public function index() { - $invoices = Invoice::scope()->with('client', 'user')->where('invoices.is_quote', '=', true)->orderBy('created_at', 'desc')->get(); + $invoices = Invoice::scope() + ->with('client', 'user') + ->where('invoices.is_quote', '=', true) + ->orderBy('created_at', 'desc') + ->get(); $invoices = Utils::remapPublicIds($invoices); $response = json_encode($invoices, JSON_PRETTY_PRINT); diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index ebfc8a6629..d84ce2fece 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -75,11 +75,12 @@ class QuoteController extends BaseController } $invitation = Invitation::with('account')->where('invitation_key', '=', $invitationKey)->first(); - $color = $invitation->account->primary_color ? $invitation->account->primary_color : '#0b4d78'; + $account = $invitation->account; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => Session::get('white_label'), + 'hideLogo' => $account->isWhiteLabel(), 'title' => trans('texts.quotes'), 'entityType' => ENTITY_QUOTE, 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), @@ -156,8 +157,9 @@ class QuoteController extends BaseController 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), - 'invoiceDesigns' => InvoiceDesign::availableDesigns(), - 'invoiceLabels' => Auth::user()->account->getInvoiceLabels() + 'invoiceDesigns' => InvoiceDesign::getDesigns(), + 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), + 'isRecurring' => false, ]; } @@ -183,7 +185,11 @@ class QuoteController extends BaseController Session::flash('message', $message); } - return Redirect::to('quotes'); + if ($action == 'restore' && $count == 1) { + return Redirect::to("quotes/".Utils::getFirst($ids)); + } else { + return Redirect::to("quotes"); + } } public function approve($invitationKey) diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index bf65492744..25114f8d36 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -1,6 +1,7 @@ account->isPro()) { - $account = Account::where('id', '=', Auth::user()->account->id)->with(['clients.invoices.invoice_items', 'clients.contacts'])->first(); + $account = Account::where('id', '=', Auth::user()->account->id) + ->with(['clients.invoices.invoice_items', 'clients.contacts']) + ->first(); $account = $account->hideFieldsForViz(); $clients = $account->clients->toJson(); } elseif (file_exists($fileName)) { @@ -149,53 +152,91 @@ class ReportController extends BaseController $reportTotals['balance'][$currencyId] += $record->balance; } - if ($action == 'export') { + if ($action == 'export') + { self::export($exportData, $reportTotals); } } - if ($enableChart) { - foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) { - $records = DB::table($entityType.'s') - ->select(DB::raw('sum(amount) as total, concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date)) 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 ($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; + } - if ($entityType == ENTITY_INVOICE) { + // 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); + $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); + $period = new DatePeriod($startDate, $interval, $endDate); $endDate->modify('-1 '.$padding); $totals = []; - foreach ($period as $d) { + foreach ($period as $d) + { $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); - $date = $d->format('Y'.$dateFormat); + // 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) { + + if ($entityType == ENTITY_INVOICE) + { $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); - $label = $d->format($labelFormat); + $label = $d->format($labelFormat); $labels[] = $label; } } - + $max = max($totals); - if ($max > 0) { + if ($max > 0) + { $datasets[] = [ 'totals' => $totals, 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index a1b12dad29..c9e65148a0 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -10,34 +10,23 @@ use Validator; use Redirect; use Session; use DropdownButton; +use DateTime; +use DateTimeZone; use App\Models\Client; use App\Models\Task; - -/* -use Auth; -use Cache; - -use App\Models\Activity; -use App\Models\Contact; -use App\Models\Invoice; -use App\Models\Size; -use App\Models\PaymentTerm; -use App\Models\Industry; -use App\Models\Currency; -use App\Models\Country; -*/ - use App\Ninja\Repositories\TaskRepository; +use App\Ninja\Repositories\InvoiceRepository; class TaskController extends BaseController { protected $taskRepo; - public function __construct(TaskRepository $taskRepo) + public function __construct(TaskRepository $taskRepo, InvoiceRepository $invoiceRepo) { parent::__construct(); $this->taskRepo = $taskRepo; + $this->invoiceRepo = $invoiceRepo; } /** @@ -47,10 +36,7 @@ class TaskController extends BaseController */ public function index() { - if (!Auth::user()->account->timezone) { - $link = link_to('/company/details', trans('texts.click_here'), ['target' => '_blank']); - Session::flash('warning', trans('texts.timezone_unset', ['link' => $link])); - } + self::checkTimezone(); return View::make('list', array( 'entityType' => ENTITY_TASK, @@ -71,8 +57,8 @@ class TaskController extends BaseController ->addColumn('client_name', function ($model) { return $model->client_public_id ? link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)) : ''; }); } - return $table->addColumn('start_time', function($model) { return Utils::fromSqlDateTime($model->start_time); }) - ->addColumn('duration', function($model) { return gmdate('H:i:s', $model->is_running ? time() - strtotime($model->start_time) : $model->duration); }) + 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) { @@ -142,12 +128,15 @@ class TaskController extends BaseController */ public function create($clientPublicId = 0) { + self::checkTimezone(); + $data = [ 'task' => null, 'clientPublicId' => Input::old('client') ? Input::old('client') : $clientPublicId, 'method' => 'POST', 'url' => 'tasks', 'title' => trans('texts.new_task'), + 'minuteOffset' => Utils::getTiemstampOffset(), ]; $data = array_merge($data, self::getViewModel()); @@ -163,14 +152,24 @@ class TaskController extends BaseController */ public function edit($publicId) { + self::checkTimezone(); + $task = Task::scope($publicId)->with('client', 'invoice')->firstOrFail(); $actions = []; if ($task->invoice) { $actions[] = ['url' => URL::to("inovices/{$task->invoice->public_id}/edit"), 'label' => trans("texts.view_invoice")]; } else { - $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.invoice_task")]; + $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.create_invoice")]; + + // 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 (!$task->trashed()) { $actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_task')]; @@ -178,15 +177,16 @@ class TaskController extends BaseController } else { $actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_task')]; } - + $data = [ 'task' => $task, 'clientPublicId' => $task->client ? $task->client->public_id : 0, 'method' => 'PUT', 'url' => 'tasks/'.$publicId, 'title' => trans('texts.edit_task'), - 'duration' => $task->resume_time ? ($task->duration + strtotime('now') - strtotime($task->resume_time)) : (strtotime('now') - strtotime($task->start_time)), - 'actions' => $actions + 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), + 'actions' => $actions, + 'minuteOffset' => Utils::getTiemstampOffset(), ]; $data = array_merge($data, self::getViewModel()); @@ -216,7 +216,7 @@ class TaskController extends BaseController { $action = Input::get('action'); - if (in_array($action, ['archive', 'delete', 'invoice', 'restore'])) { + if (in_array($action, ['archive', 'delete', 'invoice', 'restore', 'add_to_invoice'])) { return self::bulk(); } @@ -235,12 +235,11 @@ class TaskController extends BaseController $this->taskRepo->save($ids, ['action' => $action]); Session::flash('message', trans('texts.stopped_task')); return Redirect::to('tasks'); - } else if ($action == 'invoice') { - + } else if ($action == 'invoice' || $action == 'add_to_invoice') { $tasks = Task::scope($ids)->with('client')->get(); $clientPublicId = false; $data = []; - + foreach ($tasks as $task) { if ($task->client) { if (!$clientPublicId) { @@ -258,16 +257,21 @@ class TaskController extends BaseController Session::flash('error', trans('texts.task_error_invoiced')); return Redirect::to('tasks'); } - + $data[] = [ 'publicId' => $task->public_id, 'description' => $task->description, - 'startTime' => Utils::fromSqlDateTime($task->start_time), - 'duration' => round($task->duration / (60 * 60), 2) + 'startTime' => $task->getStartTime(), + 'duration' => $task->getHours(), ]; } - return Redirect::to("invoices/create/{$clientPublicId}")->with('tasks', $data); + if ($action == 'invoice') { + return Redirect::to("invoices/create/{$clientPublicId}")->with('tasks', $data); + } else { + $invoiceId = Input::get('invoice_id'); + return Redirect::to("invoices/{$invoiceId}/edit")->with('tasks', $data); + } } else { $count = $this->taskRepo->bulk($ids, $action); @@ -281,4 +285,12 @@ class TaskController extends BaseController } } } + + private function checkTimezone() + { + if (!Auth::user()->account->timezone) { + $link = link_to('/company/details?focus=timezone_id', trans('texts.click_here'), ['target' => '_blank']); + Session::flash('warning', trans('texts.timezone_unset', ['link' => $link])); + } + } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index dedd752190..85b4cede7d 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -95,7 +95,7 @@ class UserController extends BaseController $user->force_pdfjs = true; $user->save(); - Session::flash('message', trans('texts.security.updated_settings')); + Session::flash('message', trans('texts.updated_settings')); return Redirect::to('/dashboard'); } @@ -132,9 +132,12 @@ class UserController extends BaseController */ public function create() { - if (!Auth::user()->confirmed) { + if (!Auth::user()->registered) { Session::flash('error', trans('texts.register_to_add_user')); - + return Redirect::to('company/advanced_settings/user_management'); + } + if (!Auth::user()->confirmed) { + Session::flash('error', trans('texts.confirmation_required')); return Redirect::to('company/advanced_settings/user_management'); } @@ -374,6 +377,11 @@ class UserController extends BaseController Session::put(SESSION_USER_ACCOUNTS, $users); Session::flash('message', trans('texts.unlinked_account')); - return Redirect::to($referer); + return Redirect::to('/dashboard'); + } + + public function manageCompanies() + { + return View::make('users.account_management'); } } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 8c52993116..68ddfc7df0 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -49,6 +49,7 @@ class StartupCheck 'paymentTerms' => 'App\Models\PaymentTerm', 'paymentTypes' => 'App\Models\PaymentType', 'countries' => 'App\Models\Country', + 'invoiceDesigns' => 'App\Models\InvoiceDesign', ]; foreach ($cachedTables as $name => $class) { if (Input::has('clear_cache')) { @@ -74,7 +75,7 @@ class StartupCheck $count = Session::get(SESSION_COUNTER, 0); Session::put(SESSION_COUNTER, ++$count); - if (!Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && !Session::has('news_feed_id')) { + if (isset($_SERVER['REQUEST_URI']) && !Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && !Session::has('news_feed_id')) { $data = false; if (Utils::isNinja()) { $data = Utils::getNewsFeedResponse(); @@ -127,37 +128,39 @@ class StartupCheck } // Check if the user is claiming a license (ie, additional invoices, white label, etc.) - $claimingLicense = Utils::startsWith($_SERVER['REQUEST_URI'], '/claim_license'); - if (!$claimingLicense && Input::has('license_key') && Input::has('product_id')) { - $licenseKey = Input::get('license_key'); - $productId = Input::get('product_id'); + if (isset($_SERVER['REQUEST_URI'])) { + $claimingLicense = Utils::startsWith($_SERVER['REQUEST_URI'], '/claim_license'); + if (!$claimingLicense && Input::has('license_key') && Input::has('product_id')) { + $licenseKey = Input::get('license_key'); + $productId = Input::get('product_id'); - $data = trim(file_get_contents((Utils::isNinjaDev() ? 'http://www.ninja.dev' : NINJA_APP_URL)."/claim_license?license_key={$licenseKey}&product_id={$productId}")); + $data = trim(file_get_contents((Utils::isNinjaDev() ? SITE_URL : NINJA_APP_URL)."/claim_license?license_key={$licenseKey}&product_id={$productId}")); + + if ($productId == PRODUCT_INVOICE_DESIGNS) { + if ($data = json_decode($data)) { + foreach ($data as $item) { + $design = new InvoiceDesign(); + $design->id = $item->id; + $design->name = $item->name; + $design->javascript = $item->javascript; + $design->save(); + } - if ($productId == PRODUCT_INVOICE_DESIGNS) { - if ($data = json_decode($data)) { - foreach ($data as $item) { - $design = new InvoiceDesign(); - $design->id = $item->id; - $design->name = $item->name; - $design->javascript = $item->javascript; - $design->save(); + Session::flash('message', trans('texts.bought_designs')); } + } elseif ($productId == PRODUCT_WHITE_LABEL) { + if ($data == 'valid') { + $account = Auth::user()->account; + $account->pro_plan_paid = NINJA_DATE; + $account->save(); - Session::flash('message', trans('texts.bought_designs')); - } - } elseif ($productId == PRODUCT_WHITE_LABEL) { - if ($data == 'valid') { - $account = Auth::user()->account; - $account->pro_plan_paid = NINJA_DATE; - $account->save(); - - Session::flash('message', trans('texts.bought_white_label')); + Session::flash('message', trans('texts.bought_white_label')); + } } } } - - if (preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT'])) { + + if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT'])) { Session::flash('error', trans('texts.old_browser')); } diff --git a/app/Http/routes.php b/app/Http/routes.php index 0825afb383..63a678f357 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -26,6 +26,13 @@ Route::post('setup', 'AppController@doSetup'); Route::get('install', 'AppController@install'); Route::get('update', 'AppController@update'); +/* +// Codeception code coverage +Route::get('/c3.php', function () { + include '../c3.php'; +}); +*/ + // Public pages Route::get('/', 'HomeController@showIndex'); Route::get('terms', 'HomeController@showTerms'); @@ -95,6 +102,7 @@ Route::group(['middleware' => 'auth'], function() { Route::post('users/change_password', 'UserController@changePassword'); Route::get('/switch_account/{user_id}', 'UserController@switchAccount'); Route::get('/unlink_account/{user_account_id}/{user_id}', 'UserController@unlinkAccount'); + Route::get('/manage_companies', 'UserController@manageCompanies'); Route::get('api/tokens', array('as'=>'api.tokens', 'uses'=>'TokenController@getDatatable')); Route::resource('tokens', 'TokenController'); @@ -130,7 +138,6 @@ Route::group(['middleware' => 'auth'], function() { Route::get('tasks/create/{client_id?}', 'TaskController@create'); Route::post('tasks/bulk', 'TaskController@bulk'); - Route::get('recurring_invoices', 'InvoiceController@recurringIndex'); Route::get('api/recurring_invoices/{client_id?}', array('as'=>'api.recurring_invoices', 'uses'=>'InvoiceController@getRecurringDatatable')); Route::get('invoices/invoice_history/{invoice_id}', 'InvoiceController@invoiceHistory'); @@ -139,6 +146,7 @@ Route::group(['middleware' => 'auth'], function() { Route::resource('invoices', 'InvoiceController'); Route::get('api/invoices/{client_id?}', array('as'=>'api.invoices', 'uses'=>'InvoiceController@getDatatable')); Route::get('invoices/create/{client_id?}', 'InvoiceController@create'); + Route::get('recurring_invoices/create/{client_id?}', 'InvoiceController@createRecurring'); Route::get('invoices/{public_id}/clone', 'InvoiceController@cloneInvoice'); Route::post('invoices/bulk', 'InvoiceController@bulk'); @@ -211,225 +219,229 @@ Route::get('/forgot_password', function() { }); -define('CONTACT_EMAIL', Config::get('mail.from.address')); -define('CONTACT_NAME', Config::get('mail.from.name')); -define('SITE_URL', Config::get('app.url')); +if (!defined('CONTACT_EMAIL')) { + define('CONTACT_EMAIL', Config::get('mail.from.address')); + define('CONTACT_NAME', Config::get('mail.from.name')); + define('SITE_URL', Config::get('app.url')); -define('ENV_DEVELOPMENT', 'local'); -define('ENV_STAGING', 'staging'); -define('ENV_PRODUCTION', 'fortrabbit'); + define('ENV_DEVELOPMENT', 'local'); + define('ENV_STAGING', 'staging'); + define('ENV_PRODUCTION', 'fortrabbit'); -define('RECENTLY_VIEWED', 'RECENTLY_VIEWED'); -define('ENTITY_CLIENT', 'client'); -define('ENTITY_INVOICE', 'invoice'); -define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); -define('ENTITY_PAYMENT', 'payment'); -define('ENTITY_CREDIT', 'credit'); -define('ENTITY_QUOTE', 'quote'); -define('ENTITY_TASK', 'task'); + define('RECENTLY_VIEWED', 'RECENTLY_VIEWED'); + define('ENTITY_CLIENT', 'client'); + define('ENTITY_INVOICE', 'invoice'); + define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); + define('ENTITY_PAYMENT', 'payment'); + define('ENTITY_CREDIT', 'credit'); + define('ENTITY_QUOTE', 'quote'); + define('ENTITY_TASK', 'task'); -define('PERSON_CONTACT', 'contact'); -define('PERSON_USER', 'user'); + define('PERSON_CONTACT', 'contact'); + define('PERSON_USER', 'user'); -define('ACCOUNT_DETAILS', 'details'); -define('ACCOUNT_NOTIFICATIONS', 'notifications'); -define('ACCOUNT_IMPORT_EXPORT', 'import_export'); -define('ACCOUNT_PAYMENTS', 'payments'); -define('ACCOUNT_MAP', 'import_map'); -define('ACCOUNT_EXPORT', 'export'); -define('ACCOUNT_PRODUCTS', 'products'); -define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); -define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); -define('ACCOUNT_INVOICE_DESIGN', 'invoice_design'); -define('ACCOUNT_CHART_BUILDER', 'chart_builder'); -define('ACCOUNT_USER_MANAGEMENT', 'user_management'); -define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations'); -define('ACCOUNT_EMAIL_TEMPLATES', 'email_templates'); -define('ACCOUNT_TOKEN_MANAGEMENT', 'token_management'); - -define('ACTIVITY_TYPE_CREATE_CLIENT', 1); -define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); -define('ACTIVITY_TYPE_DELETE_CLIENT', 3); - -define('ACTIVITY_TYPE_CREATE_INVOICE', 4); -define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); -define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); -define('ACTIVITY_TYPE_VIEW_INVOICE', 7); -define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); -define('ACTIVITY_TYPE_DELETE_INVOICE', 9); - -define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); -define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); -define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); -define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); - -define('ACTIVITY_TYPE_CREATE_CREDIT', 14); -define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); -define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); -define('ACTIVITY_TYPE_DELETE_CREDIT', 17); - -define('ACTIVITY_TYPE_CREATE_QUOTE', 18); -define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); -define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); -define('ACTIVITY_TYPE_VIEW_QUOTE', 21); -define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); -define('ACTIVITY_TYPE_DELETE_QUOTE', 23); - -define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); -define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); -define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); -define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); -define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); -define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); - -define('DEFAULT_INVOICE_NUMBER', '0001'); -define('RECENTLY_VIEWED_LIMIT', 8); -define('LOGGED_ERROR_LIMIT', 100); -define('RANDOM_KEY_LENGTH', 32); -define('MAX_NUM_CLIENTS', 500); -define('MAX_NUM_CLIENTS_PRO', 20000); -define('MAX_NUM_USERS', 20); -define('MAX_SUBDOMAIN_LENGTH', 30); -define('DEFAULT_FONT_SIZE', 9); - -define('INVOICE_STATUS_DRAFT', 1); -define('INVOICE_STATUS_SENT', 2); -define('INVOICE_STATUS_VIEWED', 3); -define('INVOICE_STATUS_PARTIAL', 4); -define('INVOICE_STATUS_PAID', 5); - -define('PAYMENT_TYPE_CREDIT', 1); - -define('FREQUENCY_WEEKLY', 1); -define('FREQUENCY_TWO_WEEKS', 2); -define('FREQUENCY_FOUR_WEEKS', 3); -define('FREQUENCY_MONTHLY', 4); -define('FREQUENCY_THREE_MONTHS', 5); -define('FREQUENCY_SIX_MONTHS', 6); -define('FREQUENCY_ANNUALLY', 7); - -define('SESSION_TIMEZONE', 'timezone'); -define('SESSION_CURRENCY', 'currency'); -define('SESSION_DATE_FORMAT', 'dateFormat'); -define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); -define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); -define('SESSION_COUNTER', 'sessionCounter'); -define('SESSION_LOCALE', 'sessionLocale'); -define('SESSION_USER_ACCOUNTS', 'userAccounts'); - -define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); -define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); - -define('DEFAULT_TIMEZONE', 'US/Eastern'); -define('DEFAULT_CURRENCY', 1); // US Dollar -define('DEFAULT_DATE_FORMAT', 'M j, Y'); -define('DEFAULT_DATE_PICKER_FORMAT', 'M d, yyyy'); -define('DEFAULT_DATETIME_FORMAT', 'F j, Y, g:i a'); -define('DEFAULT_QUERY_CACHE', 120); // minutes -define('DEFAULT_LOCALE', 'en'); - -define('RESULT_SUCCESS', 'success'); -define('RESULT_FAILURE', 'failure'); + define('ACCOUNT_DETAILS', 'details'); + define('ACCOUNT_NOTIFICATIONS', 'notifications'); + define('ACCOUNT_IMPORT_EXPORT', 'import_export'); + define('ACCOUNT_PAYMENTS', 'payments'); + define('ACCOUNT_MAP', 'import_map'); + define('ACCOUNT_EXPORT', 'export'); + define('ACCOUNT_PRODUCTS', 'products'); + define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); + define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); + define('ACCOUNT_INVOICE_DESIGN', 'invoice_design'); + define('ACCOUNT_CHART_BUILDER', 'chart_builder'); + define('ACCOUNT_USER_MANAGEMENT', 'user_management'); + define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations'); + define('ACCOUNT_EMAIL_TEMPLATES', 'email_templates'); + define('ACCOUNT_TOKEN_MANAGEMENT', 'token_management'); + define('ACCOUNT_CUSTOMIZE_DESIGN', 'customize_design'); -define('PAYMENT_LIBRARY_OMNIPAY', 1); -define('PAYMENT_LIBRARY_PHP_PAYMENTS', 2); + define('ACTIVITY_TYPE_CREATE_CLIENT', 1); + define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); + define('ACTIVITY_TYPE_DELETE_CLIENT', 3); -define('GATEWAY_AUTHORIZE_NET', 1); -define('GATEWAY_AUTHORIZE_NET_SIM', 2); -define('GATEWAY_PAYPAL_EXPRESS', 17); -define('GATEWAY_PAYPAL_PRO', 18); -define('GATEWAY_STRIPE', 23); -define('GATEWAY_TWO_CHECKOUT', 27); -define('GATEWAY_BEANSTREAM', 29); -define('GATEWAY_PSIGATE', 30); -define('GATEWAY_MOOLAH', 31); -define('GATEWAY_BITPAY', 42); -define('GATEWAY_DWOLLA', 43); + define('ACTIVITY_TYPE_CREATE_INVOICE', 4); + define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); + define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); + define('ACTIVITY_TYPE_VIEW_INVOICE', 7); + define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); + define('ACTIVITY_TYPE_DELETE_INVOICE', 9); -define('EVENT_CREATE_CLIENT', 1); -define('EVENT_CREATE_INVOICE', 2); -define('EVENT_CREATE_QUOTE', 3); -define('EVENT_CREATE_PAYMENT', 4); + define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); + define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); + define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); + define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); -define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); -define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); -define('PREV_USER_ID', 'PREV_USER_ID'); -define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); -define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); -define('NINJA_GATEWAY_CONFIG', ''); -define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); -define('NINJA_APP_URL', 'https://app.invoiceninja.com'); -define('NINJA_VERSION', '2.2.2'); -define('NINJA_DATE', '2000-01-01'); -define('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'); -define('RELEASES_URL', 'https://github.com/hillelcoren/invoice-ninja/releases/'); -define('ZAPIER_URL', 'https://zapier.com/developer/invite/11276/85cf0ee4beae8e802c6c579eb4e351f1/'); -define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/'); + define('ACTIVITY_TYPE_CREATE_CREDIT', 14); + define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); + define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); + define('ACTIVITY_TYPE_DELETE_CREDIT', 17); -define('COUNT_FREE_DESIGNS', 4); -define('PRODUCT_ONE_CLICK_INSTALL', 1); -define('PRODUCT_INVOICE_DESIGNS', 2); -define('PRODUCT_WHITE_LABEL', 3); -define('PRODUCT_SELF_HOST', 4); -define('WHITE_LABEL_AFFILIATE_KEY', '92D2J5'); -define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74'); -define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); + define('ACTIVITY_TYPE_CREATE_QUOTE', 18); + define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); + define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); + define('ACTIVITY_TYPE_VIEW_QUOTE', 21); + define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); + define('ACTIVITY_TYPE_DELETE_QUOTE', 23); -define('PRO_PLAN_PRICE', 50); -define('WHITE_LABEL_PRICE', 20); -define('INVOICE_DESIGNS_PRICE', 10); + define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); + define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); + define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); + define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); + define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); + define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); -define('USER_TYPE_SELF_HOST', 'SELF_HOST'); -define('USER_TYPE_CLOUD_HOST', 'CLOUD_HOST'); -define('NEW_VERSION_AVAILABLE', 'NEW_VERSION_AVAILABLE'); + define('DEFAULT_INVOICE_NUMBER', '0001'); + define('RECENTLY_VIEWED_LIMIT', 8); + define('LOGGED_ERROR_LIMIT', 100); + define('RANDOM_KEY_LENGTH', 32); + define('MAX_NUM_CLIENTS', 500); + define('MAX_NUM_CLIENTS_PRO', 20000); + define('MAX_NUM_USERS', 20); + define('MAX_SUBDOMAIN_LENGTH', 30); + define('DEFAULT_FONT_SIZE', 9); -define('TOKEN_BILLING_DISABLED', 1); -define('TOKEN_BILLING_OPT_IN', 2); -define('TOKEN_BILLING_OPT_OUT', 3); -define('TOKEN_BILLING_ALWAYS', 4); + define('INVOICE_STATUS_DRAFT', 1); + define('INVOICE_STATUS_SENT', 2); + define('INVOICE_STATUS_VIEWED', 3); + define('INVOICE_STATUS_PARTIAL', 4); + define('INVOICE_STATUS_PAID', 5); -define('PAYMENT_TYPE_PAYPAL', 'PAYMENT_TYPE_PAYPAL'); -define('PAYMENT_TYPE_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD'); -define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN'); -define('PAYMENT_TYPE_DWOLLA', 'PAYMENT_TYPE_DWOLLA'); -define('PAYMENT_TYPE_TOKEN', 'PAYMENT_TYPE_TOKEN'); -define('PAYMENT_TYPE_ANY', 'PAYMENT_TYPE_ANY'); + define('PAYMENT_TYPE_CREDIT', 1); + define('CUSTOM_DESIGN', 11); -/* -define('GATEWAY_AMAZON', 30); -define('GATEWAY_BLUEPAY', 31); -define('GATEWAY_BRAINTREE', 32); -define('GATEWAY_GOOGLE', 33); -define('GATEWAY_QUICKBOOKS', 35); -*/ + define('FREQUENCY_WEEKLY', 1); + define('FREQUENCY_TWO_WEEKS', 2); + define('FREQUENCY_FOUR_WEEKS', 3); + define('FREQUENCY_MONTHLY', 4); + define('FREQUENCY_THREE_MONTHS', 5); + define('FREQUENCY_SIX_MONTHS', 6); + define('FREQUENCY_ANNUALLY', 7); -$creditCards = [ - 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], - 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], - 4 => ['card' => 'images/credit_cards/Test-AmericanExpress-Icon.png', 'text' => 'American Express'], - 8 => ['card' => 'images/credit_cards/Test-Diners-Icon.png', 'text' => 'Diners'], - 16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'] - ]; + define('SESSION_TIMEZONE', 'timezone'); + define('SESSION_CURRENCY', 'currency'); + define('SESSION_DATE_FORMAT', 'dateFormat'); + define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); + define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); + define('SESSION_COUNTER', 'sessionCounter'); + define('SESSION_LOCALE', 'sessionLocale'); + define('SESSION_USER_ACCOUNTS', 'userAccounts'); -define('CREDIT_CARDS', serialize($creditCards)); + define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); + define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); -function uctrans($text) -{ - return ucwords(trans($text)); -} + define('DEFAULT_TIMEZONE', 'US/Eastern'); + define('DEFAULT_CURRENCY', 1); // US Dollar + define('DEFAULT_LANGUAGE', 1); // English + define('DEFAULT_DATE_FORMAT', 'M j, Y'); + define('DEFAULT_DATE_PICKER_FORMAT', 'M d, yyyy'); + define('DEFAULT_DATETIME_FORMAT', 'F j, Y, g:i a'); + define('DEFAULT_QUERY_CACHE', 120); // minutes + define('DEFAULT_LOCALE', 'en'); -// optional trans: only return the string if it's translated -function otrans($text) -{ - $locale = Session::get(SESSION_LOCALE); + define('RESULT_SUCCESS', 'success'); + define('RESULT_FAILURE', 'failure'); - if ($locale == 'en') { - return trans($text); - } else { - $string = trans($text); - $english = trans($text, [], 'en'); - return $string != $english ? $string : ''; + + define('PAYMENT_LIBRARY_OMNIPAY', 1); + define('PAYMENT_LIBRARY_PHP_PAYMENTS', 2); + + define('GATEWAY_AUTHORIZE_NET', 1); + define('GATEWAY_AUTHORIZE_NET_SIM', 2); + define('GATEWAY_PAYPAL_EXPRESS', 17); + define('GATEWAY_PAYPAL_PRO', 18); + define('GATEWAY_STRIPE', 23); + define('GATEWAY_TWO_CHECKOUT', 27); + define('GATEWAY_BEANSTREAM', 29); + define('GATEWAY_PSIGATE', 30); + define('GATEWAY_MOOLAH', 31); + define('GATEWAY_BITPAY', 42); + define('GATEWAY_DWOLLA', 43); + + define('EVENT_CREATE_CLIENT', 1); + define('EVENT_CREATE_INVOICE', 2); + define('EVENT_CREATE_QUOTE', 3); + define('EVENT_CREATE_PAYMENT', 4); + + define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); + define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); + define('PREV_USER_ID', 'PREV_USER_ID'); + define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); + define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); + define('NINJA_GATEWAY_CONFIG', ''); + define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); + define('NINJA_APP_URL', 'https://app.invoiceninja.com'); + define('NINJA_VERSION', '2.3.4'); + define('NINJA_DATE', '2000-01-01'); + + define('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'); + define('RELEASES_URL', 'https://github.com/hillelcoren/invoice-ninja/releases/'); + define('ZAPIER_URL', 'https://zapier.com/developer/invite/11276/85cf0ee4beae8e802c6c579eb4e351f1/'); + define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/'); + define('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html'); + + define('COUNT_FREE_DESIGNS', 4); + define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design + define('PRODUCT_ONE_CLICK_INSTALL', 1); + define('PRODUCT_INVOICE_DESIGNS', 2); + define('PRODUCT_WHITE_LABEL', 3); + define('PRODUCT_SELF_HOST', 4); + define('WHITE_LABEL_AFFILIATE_KEY', '92D2J5'); + define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74'); + define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); + + define('PRO_PLAN_PRICE', 50); + define('WHITE_LABEL_PRICE', 20); + define('INVOICE_DESIGNS_PRICE', 10); + + define('USER_TYPE_SELF_HOST', 'SELF_HOST'); + define('USER_TYPE_CLOUD_HOST', 'CLOUD_HOST'); + define('NEW_VERSION_AVAILABLE', 'NEW_VERSION_AVAILABLE'); + + define('TEST_USERNAME', 'user@example.com'); + define('TEST_PASSWORD', 'password'); + + define('TOKEN_BILLING_DISABLED', 1); + define('TOKEN_BILLING_OPT_IN', 2); + define('TOKEN_BILLING_OPT_OUT', 3); + define('TOKEN_BILLING_ALWAYS', 4); + + define('PAYMENT_TYPE_PAYPAL', 'PAYMENT_TYPE_PAYPAL'); + define('PAYMENT_TYPE_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD'); + define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN'); + define('PAYMENT_TYPE_DWOLLA', 'PAYMENT_TYPE_DWOLLA'); + define('PAYMENT_TYPE_TOKEN', 'PAYMENT_TYPE_TOKEN'); + define('PAYMENT_TYPE_ANY', 'PAYMENT_TYPE_ANY'); + + $creditCards = [ + 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], + 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], + 4 => ['card' => 'images/credit_cards/Test-AmericanExpress-Icon.png', 'text' => 'American Express'], + 8 => ['card' => 'images/credit_cards/Test-Diners-Icon.png', 'text' => 'Diners'], + 16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'] + ]; + + define('CREDIT_CARDS', serialize($creditCards)); + + function uctrans($text) + { + return ucwords(trans($text)); + } + + // optional trans: only return the string if it's translated + function otrans($text) + { + $locale = Session::get(SESSION_LOCALE); + + if ($locale == 'en') { + return trans($text); + } else { + $string = trans($text); + $english = trans($text, [], 'en'); + return $string != $english ? $string : ''; + } } } @@ -465,4 +477,4 @@ if (Auth::check() && Auth::user()->id === 1) { Auth::loginUsingId(1); } -*/ +*/ \ No newline at end of file diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 08d5292007..a5eb3b9a4a 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -3,6 +3,7 @@ use Auth; use Cache; use DB; +use App; use Schema; use Session; use Request; @@ -61,7 +62,7 @@ class Utils public static function allowNewAccounts() { - return Utils::isNinja() || (isset($_ENV['ALLOW_NEW_ACCOUNTS']) && $_ENV['ALLOW_NEW_ACCOUNTS'] == 'true'); + return Utils::isNinja() || Auth::check(); } public static function isPro() @@ -69,6 +70,11 @@ class Utils return Auth::check() && Auth::user()->isPro(); } + public static function isEnglish() + { + return App::getLocale() == 'en'; + } + public static function getUserType() { if (Utils::isNinja()) { @@ -251,6 +257,10 @@ class Utils $currency = Currency::find(1); } + if (!$value) { + $value = 0; + } + Cache::add('currency', $currency, DEFAULT_QUERY_CACHE); return $currency->symbol.number_format($value, $currency->precision, $currency->decimal_separator, $currency->thousand_separator); @@ -315,16 +325,27 @@ class Utils return $date->format($format); } + public static function getTiemstampOffset() + { + $timezone = new DateTimeZone(Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE)); + $datetime = new DateTime('now', $timezone); + $offset = $timezone->getOffset($datetime); + $minutes = $offset / 60; + + return $minutes; + } + public static function toSqlDate($date, $formatResult = true) { if (!$date) { return; } - $timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); + //$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); - $dateTime = DateTime::createFromFormat($format, $date, new DateTimeZone($timezone)); + //$dateTime = DateTime::createFromFormat($format, $date, new DateTimeZone($timezone)); + $dateTime = DateTime::createFromFormat($format, $date); return $formatResult ? $dateTime->format('Y-m-d') : $dateTime; } @@ -335,11 +356,11 @@ class Utils return ''; } - $timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); + //$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); $dateTime = DateTime::createFromFormat('Y-m-d', $date); - $dateTime->setTimeZone(new DateTimeZone($timezone)); + //$dateTime->setTimeZone(new DateTimeZone($timezone)); return $formatResult ? $dateTime->format($format) : $dateTime; } @@ -352,7 +373,7 @@ class Utils $timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATETIME_FORMAT, DEFAULT_DATETIME_FORMAT); - + $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $date); $dateTime->setTimeZone(new DateTimeZone($timezone)); @@ -386,10 +407,12 @@ class Utils } $object = new stdClass(); + $object->accountId = Auth::user()->account_id; $object->url = $url; $object->name = ucwords($type).': '.$name; $data = []; + $counts = []; for ($i = 0; $iaccountId])) { + $counts[$item->accountId]++; + } else { + $counts[$item->accountId] = 1; + } } array_unshift($data, $object); - - if (count($data) > RECENTLY_VIEWED_LIMIT) { + + if (isset($counts[Auth::user()->account_id]) && $counts[Auth::user()->account_id] > RECENTLY_VIEWED_LIMIT) { array_pop($data); } @@ -677,4 +710,37 @@ class Utils fwrite($output, "\n"); } + + public static function stringToObjectResolution($baseObject, $rawPath) + { + $val = ''; + + if (!is_object($baseObject)) { + return $val; + } + + $path = preg_split('/->/', $rawPath); + $node = $baseObject; + + while (($prop = array_shift($path)) !== null) { + if (property_exists($node, $prop)) { + $val = $node->$prop; + $node = $node->$prop; + } else if (is_object($node) && isset($node->$prop)) { + $node = $node->{$prop}; + } else if ( method_exists($node, $prop)) { + $val = call_user_func(array($node, $prop)); + } + } + + return $val; + } + + public static function getFirst($values) { + if (is_array($values)) { + return count($values) ? $values[0] : false; + } else { + return $values; + } + } } diff --git a/app/Listeners/HandleUserSettingsChanged.php b/app/Listeners/HandleUserSettingsChanged.php index f15966db97..993e30141d 100644 --- a/app/Listeners/HandleUserSettingsChanged.php +++ b/app/Listeners/HandleUserSettingsChanged.php @@ -27,11 +27,13 @@ class HandleUserSettingsChanged { */ public function handle(UserSettingsChanged $event) { - $account = Auth::user()->account; - $account->loadLocalizationSettings(); + if (Auth::check()) { + $account = Auth::user()->account; + $account->loadLocalizationSettings(); - $users = $this->accountRepo->loadAccounts(Auth::user()->id); - Session::put(SESSION_USER_ACCOUNTS, $users); + $users = $this->accountRepo->loadAccounts(Auth::user()->id); + Session::put(SESSION_USER_ACCOUNTS, $users); + } } } diff --git a/app/Models/Account.php b/app/Models/Account.php index bc96c70415..fbd10efcd9 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -4,7 +4,9 @@ use Eloquent; use Utils; use Session; use DateTime; - +use Event; +use App; +use App\Events\UserSettingsChanged; use Illuminate\Database\Eloquent\SoftDeletes; class Account extends Eloquent @@ -12,6 +14,12 @@ class Account extends Eloquent use SoftDeletes; protected $dates = ['deleted_at']; + /* + protected $casts = [ + 'hide_quantity' => 'boolean', + ]; + */ + public function users() { return $this->hasMany('App\Models\User'); @@ -88,6 +96,11 @@ class Account extends Eloquent } } + public function isEnglish() + { + return !$this->language_id || $this->language_id == DEFAULT_LANGUAGE; + } + public function getDisplayName() { if ($this->name) { @@ -142,7 +155,9 @@ class Account extends Eloquent public function getLogoPath() { - return 'logo/'.$this->account_key.'.jpg'; + $fileName = 'logo/' . $this->account_key; + + return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg'; } public function getLogoWidth() @@ -171,34 +186,36 @@ class Account extends Eloquent { $counter = $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; $prefix .= $isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix; - + $counterOffset = 0; + // confirm the invoice number isn't already taken do { $number = $prefix.str_pad($counter, 4, "0", STR_PAD_LEFT); $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); $counter++; + $counterOffset++; } while ($check); + // update the invoice counter to be caught up + if ($counterOffset > 1) { + if ($isQuote && !$this->share_counter) { + $this->quote_number_counter += $counterOffset - 1; + } else { + $this->invoice_number_counter += $counterOffset - 1; + } + + $this->save(); + } + return $number; } - public function incrementCounter($invoiceNumber, $isQuote = false, $isRecurring) + public function incrementCounter($isQuote = false) { - // check if the user modified the invoice number - if (!$isRecurring && $invoiceNumber != $this->getNextInvoiceNumber($isQuote)) { - $number = intval(preg_replace('/[^0-9]/', '', $invoiceNumber)); - if ($isQuote && !$this->share_counter) { - $this->quote_number_counter = $number + 1; - } else { - $this->invoice_number_counter = $number + 1; - } - // otherwise, just increment the counter + if ($isQuote && !$this->share_counter) { + $this->quote_number_counter += 1; } else { - if ($isQuote && !$this->share_counter) { - $this->quote_number_counter += 1; - } else { - $this->invoice_number_counter += 1; - } + $this->invoice_number_counter += 1; } $this->save(); @@ -221,6 +238,8 @@ class Account extends Eloquent Session::put(SESSION_DATETIME_FORMAT, $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT); Session::put(SESSION_CURRENCY, $this->currency_id ? $this->currency_id : DEFAULT_CURRENCY); Session::put(SESSION_LOCALE, $this->language_id ? $this->language->locale : DEFAULT_LOCALE); + + App::setLocale(session(SESSION_LOCALE)); } public function getInvoiceLabels() @@ -258,13 +277,18 @@ class Account extends Eloquent 'rate', 'hours', 'balance', + 'from', + 'to', + 'invoice_to', + 'details', + 'invoice_no', ]; foreach ($fields as $field) { if (isset($custom[$field]) && $custom[$field]) { $data[$field] = $custom[$field]; } else { - $data[$field] = uctrans("texts.$field"); + $data[$field] = $this->isEnglish() ? uctrans("texts.$field") : trans("texts.$field"); } } @@ -303,10 +327,10 @@ class Account extends Eloquent public function isWhiteLabel() { if (Utils::isNinjaProd()) { - return false; + return self::isPro() && $this->pro_plan_paid != NINJA_DATE; + } else { + return $this->pro_plan_paid == NINJA_DATE; } - - return $this->pro_plan_paid == NINJA_DATE; } public function getSubscription($eventId) @@ -335,6 +359,8 @@ class Account extends Eloquent 'invoice_status_id', 'invoice_items', 'created_at', + 'is_recurring', + 'is_quote', ]); foreach ($invoice->invoice_items as $invoiceItem) { @@ -407,9 +433,6 @@ class Account extends Eloquent } } -Account::updating(function ($account) { - // Lithuanian requires UTF8 support - if (!Utils::isPro()) { - $account->utf8_invoices = ($account->language_id == 13) ? 1 : 0; - } +Account::updated(function ($account) { + Event::fire(new UserSettingsChanged()); }); diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 5997412ebf..0dd5ec54bf 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -214,6 +214,8 @@ class Activity extends Eloquent 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; } } } diff --git a/app/Models/Client.php b/app/Models/Client.php index f2357742e0..554f745569 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -76,15 +76,13 @@ class Client extends EntityModel { return $this->name; } - + public function getDisplayName() { if ($this->name) { return $this->name; } - - $this->load('contacts'); - + $contact = $this->contacts()->first(); return $contact->getDisplayName(); @@ -152,11 +150,15 @@ class Client extends EntityModel public function getCurrencyId() { + if ($this->currency_id) { + return $this->currency_id; + } + if (!$this->account) { $this->load('account'); } - return $this->currency_id ?: ($this->account->currency_id ?: DEFAULT_CURRENCY); + return $this->account->currency_id ?: DEFAULT_CURRENCY; } } diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index bf44b6f6d8..550de1d3ce 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -44,7 +44,7 @@ class EntityModel extends Eloquent public function getActivityKey() { - return '[' . $this->getEntityType().':'.$this->public_id.':'.$this->getName() . ']'; + return '[' . $this->getEntityType().':'.$this->public_id.':'.$this->getDisplayName() . ']'; } /* @@ -83,6 +83,11 @@ class EntityModel extends Eloquent return $this->public_id; } + public function getDisplayName() + { + return $this->getName(); + } + // Remap ids to public_ids and show name public function toPublicArray() { diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 6207bfb140..aeebfaed6a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -48,6 +48,11 @@ class Invoice extends EntityModel return $this->belongsTo('App\Models\Invoice'); } + public function recurring_invoices() + { + return $this->hasMany('App\Models\Invoice', 'recurring_invoice_id'); + } + public function invitations() { return $this->hasMany('App\Models\Invitation')->orderBy('invitations.contact_id'); @@ -55,7 +60,7 @@ class Invoice extends EntityModel public function getName() { - return $this->invoice_number; + return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number; } public function getFileName() @@ -69,9 +74,14 @@ class Invoice extends EntityModel return storage_path() . '/pdfcache/cache-' . $this->id . '.pdf'; } + public static function calcLink($invoice) + { + return link_to('invoices/' . $invoice->public_id, $invoice->invoice_number); + } + public function getLink() { - return link_to('invoices/'.$this->public_id, $this->invoice_number); + return self::calcLink($this); } public function getEntityType() @@ -252,8 +262,13 @@ class Invoice extends EntityModel } } +Invoice::creating(function ($invoice) { + if (!$invoice->is_recurring) { + $invoice->account->incrementCounter($invoice->is_quote); + } +}); + Invoice::created(function ($invoice) { - $invoice->account->incrementCounter($invoice->invoice_number, $invoice->is_quote, $invoice->recurring_invoice_id); Activity::createInvoice($invoice); }); @@ -267,4 +282,4 @@ Invoice::deleting(function ($invoice) { Invoice::restoring(function ($invoice) { Activity::restoreInvoice($invoice); -}); +}); \ No newline at end of file diff --git a/app/Models/InvoiceDesign.php b/app/Models/InvoiceDesign.php index a7010493bb..2b53ac6383 100644 --- a/app/Models/InvoiceDesign.php +++ b/app/Models/InvoiceDesign.php @@ -2,22 +2,35 @@ use Eloquent; use Auth; +use Cache; +use App\Models\InvoiceDesign; class InvoiceDesign extends Eloquent { public $timestamps = false; - public function scopeAvailableDesigns($query) + public static function getDesigns() { - $designs = $query->where('id', '<=', \Auth::user()->maxInvoiceDesignId())->orderBy('id')->get(); + $account = Auth::user()->account; + $designs = Cache::get('invoiceDesigns'); - foreach ($designs as $design) { - $fileName = public_path(strtolower("js/templates/{$design->name}.js")); - if (Auth::user()->account->utf8_invoices && file_exists($fileName)) { - $design->javascript = file_get_contents($fileName); + foreach ($designs as $design) { + if ($design->id > Auth::user()->maxInvoiceDesignId()) { + $designs->pull($design->id); + } + + $design->javascript = $design->pdfmake; + $design->pdfmake = null; + + if ($design->id == CUSTOM_DESIGN) { + if ($account->custom_design) { + $design->javascript = $account->custom_design; + } else { + $designs->pop(); + } } } - + return $designs; } -} +} \ No newline at end of file diff --git a/app/Models/Task.php b/app/Models/Task.php index 68cfaffe2e..4ccbf9688a 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -1,7 +1,7 @@ belongsTo('App\Models\Client')->withTrashed(); } + + public static function calcStartTime($task) + { + $parts = json_decode($task->time_log) ?: []; + + if (count($parts)) { + return Utils::timestampToDateTimeString($parts[0][0]); + } else { + return ''; + } + } + + public function getStartTime() + { + return self::calcStartTime($this); + } + + public static function calcDuration($task) + { + $duration = 0; + $parts = json_decode($task->time_log) ?: []; + + foreach ($parts as $part) { + if (count($part) == 1 || !$part[1]) { + $duration += time() - $part[0]; + } else { + $duration += $part[1] - $part[0]; + } + } + + return $duration; + } + + public function getDuration() + { + return self::calcDuration($this); + } + + public function getCurrentDuration() + { + $parts = json_decode($this->time_log) ?: []; + $part = $parts[count($parts)-1]; + + if (count($part) == 1 || !$part[1]) { + return time() - $part[0]; + } else { + return 0; + } + } + + public function hasPreviousDuration() + { + $parts = json_decode($this->time_log) ?: []; + return count($parts) && (count($parts[0]) && $parts[0][1]); + } + + public function getHours() + { + return round($this->getDuration() / (60 * 60), 2); + } } Task::created(function ($task) { diff --git a/app/Models/User.php b/app/Models/User.php index 98545d3100..93b263a55c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,7 +2,9 @@ use Session; use Auth; +use Event; use App\Libraries\Utils; +use App\Events\UserSettingsChanged; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Auth\Passwords\CanResetPassword; @@ -100,7 +102,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function maxInvoiceDesignId() { - return $this->isPro() ? 10 : COUNT_FREE_DESIGNS; + return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); } public function getDisplayName() @@ -213,3 +215,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon User::updating(function ($user) { User::updateUser($user); }); + +User::updated(function ($user) { + Event::fire(new UserSettingsChanged()); +}); + diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 03dfdd326f..f68633e834 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -23,6 +23,8 @@ class ContactMailer extends Mailer $emailTemplate = $invoice->account->getEmailTemplate($entityType); $invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()); + $this->initClosure($invoice); + foreach ($invoice->invitations as $invitation) { if (!$invitation->user || !$invitation->user->email || $invitation->user->trashed()) { return false; @@ -40,7 +42,8 @@ class ContactMailer extends Mailer '$client' => $invoice->client->getDisplayName(), '$account' => $accountName, '$contact' => $invitation->contact->getDisplayName(), - '$amount' => $invoiceAmount + '$amount' => $invoiceAmount, + '$advancedRawInvoice->' => '$' ]; // Add variables for available payment types @@ -49,6 +52,7 @@ class ContactMailer extends Mailer } $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; @@ -69,6 +73,8 @@ class ContactMailer extends Mailer } Event::fire(new InvoiceSent($invoice)); + + return $response; } public function sendPaymentConfirmation(Payment $payment) @@ -123,4 +129,22 @@ class ContactMailer extends Mailer $this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } + + private function initClosure($object) + { + $this->advancedTemplateHandler = function($match) use ($object) { + for ($i = 1; $i < count($match); $i++) { + $blobConversion = $match[$i]; + + 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); + } + + } + }; + } } diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index 47c7756ab5..9995a27c4d 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -17,10 +17,11 @@ class Mailer try { 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'])) { + 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( @@ -30,14 +31,22 @@ class Mailer } } - $message->to($toEmail)->from($fromEmail, $fromName)->replyTo($replyEmail, $fromName)->subject($subject); + $message->to($toEmail) + ->from($fromEmail, $fromName) + ->replyTo($replyEmail, $fromName) + ->subject($subject); + }); return true; - } catch (Exception $e) { - $response = $e->getResponse()->getBody()->getContents(); - $response = json_decode($response); - return nl2br($response->Message); + } 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(); + } } } } diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php index 710e436c11..3f2b4290a7 100644 --- a/app/Ninja/Mailers/UserMailer.php +++ b/app/Ninja/Mailers/UserMailer.php @@ -47,7 +47,7 @@ class UserMailer extends Mailer 'clientName' => $invoice->client->getDisplayName(), 'accountName' => $invoice->account->getDisplayName(), 'userName' => $user->getDisplayName(), - 'invoiceAmount' => Utils::formatMoney($invoice->amount, $invoice->client->getCurrencyId()), + 'invoiceAmount' => Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()), 'invoiceNumber' => $invoice->invoice_number, 'invoiceLink' => SITE_URL."/{$entityType}s/{$invoice->public_id}", ]; diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 98ef973f67..593d113072 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -6,7 +6,7 @@ use Session; use Utils; use DB; use stdClass; - +use Schema; use App\Models\AccountGateway; use App\Models\Invitation; use App\Models\Invoice; @@ -250,6 +250,10 @@ class AccountRepository public function findUserAccounts($userId1, $userId2 = false) { + if (!Schema::hasTable('user_accounts')) { + return false; + } + $query = UserAccount::where('user_id1', '=', $userId1) ->orWhere('user_id2', '=', $userId1) ->orWhere('user_id3', '=', $userId1) @@ -268,7 +272,6 @@ class AccountRepository } public function prepareUsersData($record) { - if (!$record) { return false; } @@ -294,7 +297,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->account_key = file_exists($user->account->getLogoPath()) ? $user->account->account_key : null; + $item->logo_path = file_exists($user->account->getLogoPath()) ? $user->account->getLogoPath() : null; $data[] = $item; } @@ -312,6 +315,9 @@ class AccountRepository } public function syncUserAccounts($users, $proPlanPaid = false) { + if (!$users) { + return; + } if (!$proPlanPaid) { foreach ($users as $user) { @@ -374,7 +380,6 @@ class AccountRepository } public function unlinkUser($userAccountId, $userId) { - $userAccount = UserAccount::whereId($userAccountId)->first(); if ($userAccount->hasUserId($userId)) { $userAccount->removeUserId($userId); diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index 5ff208f362..ab0baacda5 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -120,7 +120,7 @@ class ClientRepository if (isset($data['contact'])) { $info = $data['contact']; if (isset($info['email'])) { - $contact->email = trim(strtolower($info['email'])); + $contact->email = trim($info['email']); } if (isset($info['first_name'])) { $contact->first_name = trim($info['first_name']); @@ -145,7 +145,7 @@ class ClientRepository } if (isset($record['email'])) { - $contact->email = trim(strtolower($record['email'])); + $contact->email = trim($record['email']); } if (isset($record['first_name'])) { $contact->first_name = trim($record['first_name']); diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index f4caa63ee2..df73c6f6a1 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -1,11 +1,12 @@ save(); } - $invoice->client_id = $data['client_id']; + if (isset($data['invoice_number'])) { + $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->invoice_number = trim($data['invoice_number']); $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; } @@ -311,6 +315,7 @@ class InvoiceRepository } $total = 0; + $itemTax = 0; foreach ($data['invoice_items'] as $item) { $item = (array) $item; @@ -320,15 +325,29 @@ class InvoiceRepository $invoiceItemCost = round(Utils::parseFloat($item['cost']), 2); $invoiceItemQty = round(Utils::parseFloat($item['qty']), 2); - $invoiceItemTaxRate = 0; - - if (isset($item['tax_rate']) && Utils::parseFloat($item['tax_rate']) > 0) { - $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate']); - } $lineTotal = $invoiceItemCost * $invoiceItemQty; + $total += round($lineTotal, 2); + } - $total += round($lineTotal + ($lineTotal * $invoiceItemTaxRate / 100), 2); + foreach ($data['invoice_items'] as $item) { + $item = (array) $item; + if (isset($item['tax_rate']) && Utils::parseFloat($item['tax_rate']) > 0) { + $invoiceItemCost = round(Utils::parseFloat($item['cost']), 2); + $invoiceItemQty = round(Utils::parseFloat($item['qty']), 2); + $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate']); + $lineTotal = $invoiceItemCost * $invoiceItemQty; + + if ($invoice->discount > 0) { + if ($invoice->is_amount_discount) { + $lineTotal -= round(($lineTotal/$total) * $invoice->discount, 2); + } else { + $lineTotal -= round($lineTotal * ($invoice->discount/100), 2); + } + } + + $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); + } } if ($invoice->discount > 0) { @@ -354,6 +373,7 @@ class InvoiceRepository $total += $total * $invoice->tax_rate / 100; $total = round($total, 2); + $total += $itemTax; // custom fields not charged taxes if ($invoice->custom_value1 && !$invoice->custom_taxes1) { @@ -387,20 +407,19 @@ class InvoiceRepository $task->invoice_id = $invoice->id; $task->client_id = $invoice->client_id; $task->save(); - } else if ($item['product_key']) { + } else if ($item['product_key'] && !$invoice->has_tasks) { $product = Product::findProductByKey(trim($item['product_key'])); - if (!$product) { - $product = Product::createNew(); - $product->product_key = trim($item['product_key']); - } - if (\Auth::user()->account->update_products) { + if (!$product) { + $product = Product::createNew(); + $product->product_key = trim($item['product_key']); + } + $product->notes = $item['notes']; $product->cost = $item['cost']; + $product->save(); } - - $product->save(); } $invoiceItem = InvoiceItem::createNew(); @@ -536,4 +555,89 @@ class InvoiceRepository return count($invoices); } + + public function findOpenInvoices($clientId) + { + return Invoice::scope() + ->whereClientId($clientId) + ->whereIsQuote(false) + ->whereIsRecurring(false) + ->whereDeletedAt(null) + ->whereHasTasks(true) + ->where('invoice_status_id', '<', 5) + ->select(['public_id', 'invoice_number']) + ->get(); + } + + public function createRecurringInvoice($recurInvoice) + { + $recurInvoice->load('account.timezone', 'invoice_items', 'client', 'user'); + + if ($recurInvoice->client->deleted_at) { + return false; + } + + if (!$recurInvoice->user->confirmed) { + return false; + } + + if (!$recurInvoice->shouldSendToday()) { + return false; + } + + $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->amount = $recurInvoice->amount; + $invoice->balance = $recurInvoice->amount; + $invoice->invoice_date = date_create()->format('Y-m-d'); + $invoice->discount = $recurInvoice->discount; + $invoice->po_number = $recurInvoice->po_number; + $invoice->public_notes = Utils::processVariables($recurInvoice->public_notes); + $invoice->terms = Utils::processVariables($recurInvoice->terms); + $invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer); + $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->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->save(); + + foreach ($recurInvoice->invoice_items as $recurItem) { + $item = InvoiceItem::createNew($recurItem); + $item->product_id = $recurItem->product_id; + $item->qty = $recurItem->qty; + $item->cost = $recurItem->cost; + $item->notes = Utils::processVariables($recurItem->notes); + $item->product_key = Utils::processVariables($recurItem->product_key); + $item->tax_name = $recurItem->tax_name; + $item->tax_rate = $recurItem->tax_rate; + $invoice->invoice_items()->save($item); + } + + foreach ($recurInvoice->invitations as $recurInvitation) { + $invitation = Invitation::createNew($recurInvitation); + $invitation->contact_id = $recurInvitation->contact_id; + $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); + $invoice->invitations()->save($invitation); + } + + $recurInvoice->last_sent_date = Carbon::now()->toDateTimeString(); + $recurInvoice->save(); + + return $invoice; + } } diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php index feb764850a..47761900e1 100644 --- a/app/Ninja/Repositories/TaskRepository.php +++ b/app/Ninja/Repositories/TaskRepository.php @@ -23,7 +23,7 @@ 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.start_time', 'tasks.description', 'tasks.duration', 'tasks.is_deleted', 'tasks.deleted_at', 'invoices.invoice_number', 'invoices.public_id as invoice_public_id', 'tasks.is_running'); + ->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); @@ -46,7 +46,7 @@ class TaskRepository } public function save($publicId, $data) - { + { if ($publicId) { $task = Task::scope($publicId)->firstOrFail(); } else { @@ -60,36 +60,26 @@ class TaskRepository $task->description = trim($data['description']); } - $timeLog = $task->time_log ? json_decode($task->time_log, true) : []; + if (isset($data['time_log'])) { + $timeLog = json_decode($data['time_log']); + } elseif ($task->time_log) { + $timeLog = json_decode($task->time_log); + } else { + $timeLog = []; + } if ($data['action'] == 'start') { - $task->start_time = Carbon::now()->toDateTimeString(); $task->is_running = true; $timeLog[] = [strtotime('now'), false]; } else if ($data['action'] == 'resume') { - $task->break_duration = strtotime('now') - strtotime($task->start_time) + $task->duration; - $task->resume_time = Carbon::now()->toDateTimeString(); $task->is_running = true; $timeLog[] = [strtotime('now'), false]; } else if ($data['action'] == 'stop' && $task->is_running) { - if ($task->resume_time) { - $task->duration = $task->duration + strtotime('now') - strtotime($task->resume_time); - $task->resume_time = null; - } else { - $task->duration = strtotime('now') - strtotime($task->start_time); - } - $timeLog[count($timeLog)-1][1] = strtotime('now'); + $timeLog[count($timeLog)-1][1] = time(); $task->is_running = false; - } else if ($data['action'] == 'save' && !$task->is_running) { - $task->start_time = $data['start_time']; - $task->duration = $data['duration']; - $task->break_duration = $data['break_duration']; } - $task->duration = max($task->duration, 0); - $task->break_duration = max($task->break_duration, 0); $task->time_log = json_encode($timeLog); - $task->save(); return $task; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9efba90b91..56a8e8bca1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -37,13 +37,19 @@ class AppServiceProvider extends ServiceProvider { $str = '