diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 6acd87aa0c6e..1720e7dda75d 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -41,8 +41,8 @@ class SendRecurringInvoices extends Command $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); - if ($invoice) { - $recurInvoice->account->loadLocalizationSettings(); + if ($invoice && !$invoice->isPaid()) { + $recurInvoice->account->loadLocalizationSettings($invoice->client); $this->mailer->sendInvoice($invoice); } } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 7577601404d8..5b1d941e14d4 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -40,6 +40,7 @@ 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 { @@ -341,38 +342,60 @@ class AccountController extends BaseController private function saveInvoiceSettings() { if (Auth::user()->account->isPro()) { - $account = Auth::user()->account; - - $account->custom_label1 = trim(Input::get('custom_label1')); - $account->custom_value1 = trim(Input::get('custom_value1')); - $account->custom_label2 = trim(Input::get('custom_label2')); - $account->custom_value2 = trim(Input::get('custom_value2')); - $account->custom_client_label1 = trim(Input::get('custom_client_label1')); - $account->custom_client_label2 = trim(Input::get('custom_client_label2')); - $account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1')); - $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); - $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; - $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; - - $account->invoice_number_prefix = Input::get('invoice_number_prefix'); - $account->invoice_number_counter = Input::get('invoice_number_counter'); - $account->quote_number_prefix = Input::get('quote_number_prefix'); - $account->share_counter = Input::get('share_counter') ? true : false; - - $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; - $account->auto_wrap = Input::get('auto_wrap') ? true : false; - - if (!$account->share_counter) { - $account->quote_number_counter = Input::get('quote_number_counter'); + + $rules = []; + $user = Auth::user(); + $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); + $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('substr(string, start)')), 0, MAX_SUBDOMAIN_LENGTH)); + if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { + $subdomain = null; + } + if ($subdomain) { + $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id"; } - if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) { - Session::flash('error', trans('texts.invalid_counter')); + $validator = Validator::make(Input::all(), $rules); - return Redirect::to('company/advanced_settings/invoice_settings')->withInput(); + if ($validator->fails()) { + return Redirect::to('company/details') + ->withErrors($validator) + ->withInput(); } else { - $account->save(); - Session::flash('message', trans('texts.updated_settings')); + + $account = Auth::user()->account; + $account->subdomain = $subdomain; + $account->iframe_url = $iframeURL; + $account->custom_label1 = trim(Input::get('custom_label1')); + $account->custom_value1 = trim(Input::get('custom_value1')); + $account->custom_label2 = trim(Input::get('custom_label2')); + $account->custom_value2 = trim(Input::get('custom_value2')); + $account->custom_client_label1 = trim(Input::get('custom_client_label1')); + $account->custom_client_label2 = trim(Input::get('custom_client_label2')); + $account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1')); + $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); + $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; + $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; + + $account->invoice_number_prefix = Input::get('invoice_number_prefix'); + $account->invoice_number_counter = Input::get('invoice_number_counter'); + $account->quote_number_prefix = Input::get('quote_number_prefix'); + $account->share_counter = Input::get('share_counter') ? true : false; + + $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; + $account->auto_wrap = Input::get('auto_wrap') ? true : false; + + if (!$account->share_counter) { + $account->quote_number_counter = Input::get('quote_number_counter'); + } + + if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) { + Session::flash('error', trans('texts.invalid_counter')); + + return Redirect::to('company/advanced_settings/invoice_settings')->withInput(); + } else { + $account->save(); + Session::flash('message', trans('texts.updated_settings')); + } } } @@ -643,14 +666,6 @@ class AccountController extends BaseController $rules['email'] = 'email|required|unique:users,email,'.$user->id.',id'; } - $subdomain = preg_replace('/[^a-zA-Z0-9_\-]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); - if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { - $subdomain = null; - } - if ($subdomain) { - $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id"; - } - $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { @@ -660,7 +675,6 @@ class AccountController extends BaseController } else { $account = Auth::user()->account; $account->name = trim(Input::get('name')); - $account->subdomain = $subdomain; $account->id_number = trim(Input::get('id_number')); $account->vat_number = trim(Input::get('vat_number')); $account->work_email = trim(Input::get('work_email')); @@ -678,6 +692,7 @@ class AccountController extends BaseController $account->datetime_format_id = Input::get('datetime_format_id') ? Input::get('datetime_format_id') : null; $account->currency_id = Input::get('currency_id') ? Input::get('currency_id') : 1; // US Dollar $account->language_id = Input::get('language_id') ? Input::get('language_id') : 1; // English + $account->military_time = Input::get('military_time') ? true : false; $account->save(); if (Auth::user()->id === $user->id) { @@ -718,8 +733,9 @@ class AccountController extends BaseController } } - Session::flash('message', trans('texts.updated_settings')); + Event::fire(new UserSettingsChanged()); + Session::flash('message', trans('texts.updated_settings')); return Redirect::to('company/details'); } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 065e3e073a33..306a59fcda8b 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -194,10 +194,12 @@ class ClientController extends BaseController private static function getViewModel() { return [ + 'account' => Auth::user()->account, 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), 'countries' => Cache::get('countries'), 'customLabel1' => Auth::user()->account->custom_client_label1, 'customLabel2' => Auth::user()->account->custom_client_label2, @@ -252,6 +254,7 @@ class ClientController extends BaseController $client->size_id = Input::get('size_id') ?: null; $client->industry_id = Input::get('industry_id') ?: null; $client->currency_id = Input::get('currency_id') ?: null; + $client->language_id = Input::get('language_id') ?: null; $client->payment_terms = Input::get('payment_terms') ?: 0; $client->website = trim(Input::get('website')); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 565c637b0952..f702a85dd1f5 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -40,6 +40,11 @@ class HomeController extends BaseController { return View::make('public.terms', ['hideHeader' => true]); } + + public function viewLogo() + { + return View::make('public.logo'); + } public function invoiceNow() { diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index c12e909edc88..e876eb146f1f 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -108,7 +108,7 @@ class InvoiceApiController extends Controller if ($error) { $response = json_encode($error, JSON_PRETTY_PRINT); } else { - $data = self::prepareData($data); + $data = self::prepareData($data, $client); $data['client_id'] = $client->id; $invoice = $this->invoiceRepo->save(false, $data, false); @@ -136,10 +136,10 @@ class InvoiceApiController extends Controller return Response::make($response, $error ? 400 : 200, $headers); } - private function prepareData($data) + private function prepareData($data, $client) { $account = Auth::user()->account; - $account->loadLocalizationSettings(); + $account->loadLocalizationSettings($client); // set defaults for optional fields $fields = [ diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 6334d9f8cd93..07aaf81aa53b 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -206,7 +206,7 @@ class InvoiceController extends BaseController Session::set($invitationKey, true); Session::set('invitation_key', $invitationKey); - $account->loadLocalizationSettings(); + $account->loadLocalizationSettings($client); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); @@ -296,6 +296,7 @@ class InvoiceController extends BaseController $invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->end_date = Utils::fromSqlDate($invoice->end_date); + $invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date); $invoice->is_pro = Auth::user()->isPro(); $actions = [ @@ -417,6 +418,7 @@ class InvoiceController extends BaseController 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), 'taxRates' => TaxRate::scope()->orderBy('name')->get(), 'currencies' => Cache::get('currencies'), + 'languages' => Cache::get('languages'), 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), @@ -531,7 +533,12 @@ class InvoiceController extends BaseController if ($invoice->is_recurring) { if ($invoice->shouldSendToday()) { $invoice = $this->invoiceRepo->createRecurringInvoice($invoice); - $response = $this->mailer->sendInvoice($invoice); + // in case auto-bill is enabled + if ($invoice->isPaid()) { + $response = true; + } else { + $response = $this->mailer->sendInvoice($invoice); + } } else { $response = trans('texts.recurring_too_soon'); } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 0fec34184c45..7e1fb5418126 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -12,29 +12,22 @@ use Omnipay; use CreditCard; use URL; use Cache; -use Event; -use DateTime; -use App\Models\Account; use App\Models\Invoice; use App\Models\Invitation; use App\Models\Client; use App\Models\PaymentType; -use App\Models\Country; use App\Models\License; use App\Models\Payment; use App\Models\Affiliate; -use App\Models\AccountGatewayToken; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\AccountRepository; use App\Ninja\Mailers\ContactMailer; -use App\Events\InvoicePaid; +use App\Services\PaymentService; class PaymentController extends BaseController { - protected $creditRepo; - - public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer) + public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer, PaymentService $paymentService) { parent::__construct(); @@ -42,6 +35,7 @@ class PaymentController extends BaseController $this->invoiceRepo = $invoiceRepo; $this->accountRepo = $accountRepo; $this->contactMailer = $contactMailer; + $this->paymentService = $paymentService; } public function index() @@ -191,36 +185,9 @@ class PaymentController extends BaseController return View::make('payments.edit', $data); } - private function createGateway($accountGateway) - { - $gateway = Omnipay::create($accountGateway->gateway->provider); - $config = json_decode($accountGateway->config); - - foreach ($config as $key => $val) { - if (!$val) { - continue; - } - - $function = "set".ucfirst($key); - $gateway->$function($val); - } - - if ($accountGateway->gateway->id == GATEWAY_DWOLLA) { - if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { - $gateway->setKey($_ENV['DWOLLA_SANDBOX_KEY']); - $gateway->setSecret($_ENV['DWOLLA_SANSBOX_SECRET']); - } elseif (isset($_ENV['DWOLLA_KEY']) && isset($_ENV['DWOLLA_SECRET'])) { - $gateway->setKey($_ENV['DWOLLA_KEY']); - $gateway->setSecret($_ENV['DWOLLA_SECRET']); - } - } - - return $gateway; - } - private function getLicensePaymentDetails($input, $affiliate) { - $data = self::convertInputForOmnipay($input); + $data = $this->paymentService->convertInputForOmnipay($input); $card = new CreditCard($data); return [ @@ -232,67 +199,6 @@ class PaymentController extends BaseController ]; } - private function convertInputForOmnipay($input) - { - $data = [ - 'firstName' => $input['first_name'], - 'lastName' => $input['last_name'], - 'number' => $input['card_number'], - 'expiryMonth' => $input['expiration_month'], - 'expiryYear' => $input['expiration_year'], - 'cvv' => $input['cvv'], - ]; - - if (isset($input['country_id'])) { - $country = Country::find($input['country_id']); - - $data = array_merge($data, [ - 'billingAddress1' => $input['address1'], - 'billingAddress2' => $input['address2'], - 'billingCity' => $input['city'], - 'billingState' => $input['state'], - 'billingPostcode' => $input['postal_code'], - 'billingCountry' => $country->iso_3166_2, - 'shippingAddress1' => $input['address1'], - 'shippingAddress2' => $input['address2'], - 'shippingCity' => $input['city'], - 'shippingState' => $input['state'], - 'shippingPostcode' => $input['postal_code'], - 'shippingCountry' => $country->iso_3166_2 - ]); - } - - return $data; - } - - private function getPaymentDetails($invitation, $input = null) - { - $invoice = $invitation->invoice; - $account = $invoice->account; - $key = $invoice->account_id.'-'.$invoice->invoice_number; - $currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD'); - - if ($input) { - $data = self::convertInputForOmnipay($input); - Session::put($key, $data); - } elseif (Session::get($key)) { - $data = Session::get($key); - } else { - $data = []; - } - - $card = new CreditCard($data); - - return [ - 'amount' => $invoice->getRequestedAmount(), - 'card' => $card, - 'currency' => $currencyCode, - 'returnUrl' => URL::to('complete'), - 'cancelUrl' => $invitation->getLink(), - 'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}", - ]; - } - public function show_payment($invitationKey, $paymentType = false) { $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail(); @@ -434,21 +340,13 @@ class PaymentController extends BaseController if ($testMode) { $ref = 'TEST_MODE'; } else { - $gateway = self::createGateway($accountGateway); + $gateway = $this->paymentService->createGateway($accountGateway); $details = self::getLicensePaymentDetails(Input::all(), $affiliate); $response = $gateway->purchase($details)->send(); $ref = $response->getTransactionReference(); - if (!$ref) { - Session::flash('error', $response->getMessage()); - - return Redirect::to('license')->withInput(); - } - - if (!$response->isSuccessful()) { - Session::flash('error', $response->getMessage()); - Utils::logError('Payment Error [license]: ' . $response->getMessage()); - + if (!$response->isSuccessful() || !$ref) { + $this->error('License', $response->getMessage(), $accountGateway); return Redirect::to('license')->withInput(); } } @@ -482,10 +380,7 @@ class PaymentController extends BaseController return View::make('public.license', $data); } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage); - Utils::logError('Payment Error [license-uncaught]: ' . Utils::getErrorString($e)); - + $this->error('License-Uncaught', false, $accountGateway, $e); return Redirect::to('license')->withInput(); } } @@ -549,7 +444,6 @@ class PaymentController extends BaseController ->withInput(); } - if ($accountGateway->update_address) { $client->address1 = trim(Input::get('address1')); $client->address2 = trim(Input::get('address2')); @@ -560,49 +454,31 @@ class PaymentController extends BaseController $client->save(); } } - + try { - $gateway = self::createGateway($accountGateway); - $details = self::getPaymentDetails($invitation, ($useToken || !$onSite) ? false : Input::all()); - + $gateway = $this->paymentService->createGateway($accountGateway); + $details = $this->paymentService->getPaymentDetails($invitation, ($useToken || !$onSite) ? false : Input::all()); + + // check if we're creating/using a billing token if ($accountGateway->gateway_id == GATEWAY_STRIPE) { if ($useToken) { $details['cardReference'] = $client->getGatewayToken(); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $tokenResponse = $gateway->createCard($details)->send(); - $cardReference = $tokenResponse->getCardReference(); - - if ($cardReference) { - $details['cardReference'] = $cardReference; - - $token = AccountGatewayToken::where('client_id', '=', $client->id) - ->where('account_gateway_id', '=', $accountGateway->id)->first(); - - if (!$token) { - $token = new AccountGatewayToken(); - $token->account_id = $account->id; - $token->contact_id = $invitation->contact_id; - $token->account_gateway_id = $accountGateway->id; - $token->client_id = $client->id; - } - - $token->token = $cardReference; - $token->save(); + $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id); + if ($token) { + $details['cardReference'] = $token; } else { - Session::flash('error', $tokenResponse->getMessage()); - Utils::logError('Payment Error [no-token-ref]: ' . $tokenResponse->getMessage()); + $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); return Redirect::to('payment/'.$invitationKey)->withInput(); } } } - + $response = $gateway->purchase($details)->send(); $ref = $response->getTransactionReference(); if (!$ref) { - - Session::flash('error', $response->getMessage()); - Utils::logError('Payment Error [no-ref]: ' . $response->getMessage()); + $this->error('No-Ref', $response->getMessage(), $accountGateway); if ($onSite) { return Redirect::to('payment/'.$invitationKey)->withInput(); @@ -612,7 +488,7 @@ class PaymentController extends BaseController } if ($response->isSuccessful()) { - $payment = self::createPayment($invitation, $ref); + $payment = $this->paymentService->createPayment($invitation, $ref); Session::flash('message', trans('texts.applied_payment')); if ($account->account_key == NINJA_ACCOUNT_KEY) { @@ -629,16 +505,11 @@ class PaymentController extends BaseController Session::save(); $response->redirect(); } else { - Session::flash('error', $response->getMessage()); - Utils::logError('Payment Error [fatal]: ' . $response->getMessage()); - + $this->error('Fatal', $response->getMessage(), $accountGateway); return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.
', $response->getMessage()); } } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage."
".$e->getMessage()); - Utils::logError('Payment Error [uncaught]:' . Utils::getErrorString($e)); - + $this->error('Uncaught', false, $accountGateway, $e); if ($onSite) { return Redirect::to('payment/'.$invitationKey)->withInput(); } else { @@ -647,47 +518,6 @@ class PaymentController extends BaseController } } - private function createPayment($invitation, $ref, $payerId = null) - { - $invoice = $invitation->invoice; - $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); - - if ($invoice->account->account_key == NINJA_ACCOUNT_KEY - && $invoice->amount == PRO_PLAN_PRICE) { - $account = Account::with('users')->find($invoice->client->public_id); - if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); - $account->pro_plan_paid = $date->modify('+1 year')->format('Y-m-d'); - } else { - $account->pro_plan_paid = date_create()->format('Y-m-d'); - } - $account->save(); - - $user = $account->users()->first(); - $this->accountRepo->syncAccounts($user->id, $account->pro_plan_paid); - } - - $payment = Payment::createNew($invitation); - $payment->invitation_id = $invitation->id; - $payment->account_gateway_id = $accountGateway->id; - $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->getRequestedAmount(); - $payment->client_id = $invoice->client_id; - $payment->contact_id = $invitation->contact_id; - $payment->transaction_reference = $ref; - $payment->payment_date = date_create()->format('Y-m-d'); - - if ($payerId) { - $payment->payer_id = $payerId; - } - - $payment->save(); - - Event::fire(new InvoicePaid($payment)); - - return $payment; - } - public function offsite_payment() { $payerId = Request::query('PayerID'); @@ -705,45 +535,37 @@ class PaymentController extends BaseController $invoice = $invitation->invoice; $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); - $gateway = self::createGateway($accountGateway); + $gateway = $this->paymentService->createGateway($accountGateway); // Check for Dwolla payment error if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) { - $errorMessage = trans('texts.payment_error')."\n\n".Input::get('error_description'); - Session::flash('error', $errorMessage); - Utils::logError('Payment Error [dwolla]: ' . $errorMessage); + $this->error('Dwolla', Input::get('error_description'), $accountGateway); return Redirect::to('view/'.$invitation->invitation_key); } try { if (method_exists($gateway, 'completePurchase')) { - $details = self::getPaymentDetails($invitation); + $details = $this->paymentService->getPaymentDetails($invitation); $response = $gateway->completePurchase($details)->send(); $ref = $response->getTransactionReference(); if ($response->isSuccessful()) { - $payment = self::createPayment($invitation, $ref, $payerId); + $payment = $this->paymentService->createPayment($invitation, $ref, $payerId); Session::flash('message', trans('texts.applied_payment')); return Redirect::to('view/'.$invitation->invitation_key); } else { - $errorMessage = trans('texts.payment_error')."\n\n".$response->getMessage(); - Session::flash('error', $errorMessage); - Utils::logError('Payment Error [offsite]: ' . $errorMessage); - + $this->error('offsite', $response->getMessage(), $accountGateway); return Redirect::to('view/'.$invitation->invitation_key); } } else { - $payment = self::createPayment($invitation, $token, $payerId); + $payment = $this->paymentService->createPayment($invitation, $token, $payerId); Session::flash('message', trans('texts.applied_payment')); return Redirect::to('view/'.$invitation->invitation_key); } } catch (\Exception $e) { - $errorMessage = trans('texts.payment_error'); - Session::flash('error', $errorMessage); - Utils::logError('Payment Error [offsite-uncaught]: ' . $errorMessage."\n\n".$e->getMessage()); - + $this->error('Offsite-uncaught', false, $accountGateway, $e); return Redirect::to('view/'.$invitation->invitation_key); } } @@ -799,4 +621,24 @@ class PaymentController extends BaseController return Redirect::to('payments'); } + + private function error($type, $error, $accountGateway, $exception = false) + { + if (!$error) { + if ($exception) { + $error = $exception->getMessage(); + } else { + $error = trans('texts.payment_error'); + } + } + + $message = ''; + if ($accountGateway && $accountGateway->gateway) { + $message = $accountGateway->gateway->name . ': '; + } + $message .= $error; + + Session::flash('error', $message); + Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message)); + } } diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index ba1db21ad9cf..d6a9e31ce05c 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -156,6 +156,7 @@ class QuoteController extends BaseController 'currencies' => Cache::get('currencies'), 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), + 'languages' => Cache::get('languages'), 'industries' => Cache::get('industries'), 'invoiceDesigns' => InvoiceDesign::getDesigns(), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 3312b914f764..bb259260f790 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -133,7 +133,7 @@ class TaskController extends BaseController 'url' => 'tasks', 'title' => trans('texts.new_task'), 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, - 'datetimeFormat' => Auth::user()->account->datetime_format ? Auth::user()->account->datetime_format->format_moment : DEFAULT_DATETIME_MOMENT_FORMAT + 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), ]; $data = array_merge($data, self::getViewModel()); @@ -182,7 +182,7 @@ class TaskController extends BaseController 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), 'actions' => $actions, 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, - 'datetimeFormat' => Auth::user()->account->datetime_format ? Auth::user()->account->datetime_format->format_moment : DEFAULT_DATETIME_MOMENT_FORMAT + 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), ]; $data = array_merge($data, self::getViewModel()); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index e2fc72af1b6c..d1575db850f9 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -384,4 +384,29 @@ class UserController extends BaseController { return View::make('users.account_management'); } + + public function claimReferralCode($email) + { + $user = User::whereEmail($email) + ->whereReferralCode(null) + ->whereConfirmed(true) + ->first(); + + if ($user) { + do { + $code = strtoupper(str_random(8)); + $match = User::whereReferralCode($code) + ->withTrashed() + ->first(); + } while ($match); + + $user->referral_code = $code; + $user->save(); + + return $code; + } + + return Redirect::to('/'); + } + } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index cac2b9d0f41f..1b1a3feffcb9 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -118,10 +118,15 @@ class StartupCheck } } } elseif (Auth::check()) { - $locale = Session::get(SESSION_LOCALE, DEFAULT_LOCALE); + $locale = Auth::user()->account->language ? Auth::user()->account->language->locale : DEFAULT_LOCALE; App::setLocale($locale); } + // Track the referral code + if (Input::has('rc')) { + Session::set(SESSION_REFERRAL_CODE, Input::get('rc')); + } + // Make sure the account/user localization settings are in the session if (Auth::check() && !Session::has(SESSION_TIMEZONE)) { Event::fire(new UserSettingsChanged()); @@ -164,9 +169,8 @@ class StartupCheck Session::flash('error', trans('texts.old_browser')); } - // for security prevent displaying within an iframe $response = $next($request); - $response->headers->set('X-Frame-Options', 'DENY'); + //$response->headers->set('X-Frame-Options', 'DENY'); return $response; } diff --git a/app/Http/routes.php b/app/Http/routes.php index 128e1f34cd2d..0739e12784c4 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -39,10 +39,12 @@ Route::get('terms', 'HomeController@showTerms'); Route::get('log_error', 'HomeController@logError'); Route::get('invoice_now', 'HomeController@invoiceNow'); Route::get('keep_alive', 'HomeController@keepAlive'); +Route::get('referral_code/{email}', 'UserController@claimReferralCode'); Route::post('get_started', 'AccountController@getStarted'); // Client visible pages Route::get('view/{invitation_key}', 'InvoiceController@view'); +Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); Route::post('payment/{invitation_key}', 'PaymentController@do_payment'); @@ -306,6 +308,7 @@ if (!defined('CONTACT_EMAIL')) { define('MAX_NUM_CLIENTS_PRO', 20000); define('MAX_NUM_USERS', 20); define('MAX_SUBDOMAIN_LENGTH', 30); + define('MAX_IFRAME_URL_LENGTH', 250); define('DEFAULT_FONT_SIZE', 9); define('INVOICE_STATUS_DRAFT', 1); @@ -333,6 +336,7 @@ if (!defined('CONTACT_EMAIL')) { define('SESSION_COUNTER', 'sessionCounter'); define('SESSION_LOCALE', 'sessionLocale'); define('SESSION_USER_ACCOUNTS', 'userAccounts'); + define('SESSION_REFERRAL_CODE', 'referralCode'); define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); @@ -376,7 +380,7 @@ if (!defined('CONTACT_EMAIL')) { define('PREV_USER_ID', 'PREV_USER_ID'); define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); - define('NINJA_GATEWAY_CONFIG', ''); + define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG'); define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); define('NINJA_APP_URL', 'https://app.invoiceninja.com'); define('NINJA_VERSION', '2.3.4'); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 3b47d85e2ef1..073990e85d94 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -341,10 +341,7 @@ class Utils return; } - //$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); - - //$dateTime = DateTime::createFromFormat($format, $date, new DateTimeZone($timezone)); $dateTime = DateTime::createFromFormat($format, $date); return $formatResult ? $dateTime->format('Y-m-d') : $dateTime; @@ -356,11 +353,8 @@ class Utils return ''; } - //$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); - $dateTime = DateTime::createFromFormat('Y-m-d', $date); - //$dateTime->setTimeZone(new DateTimeZone($timezone)); return $formatResult ? $dateTime->format($format) : $dateTime; } @@ -752,4 +746,34 @@ class Utils } return $str; } + + public static function getSubdomainPlaceholder() { + $parts = parse_url(SITE_URL); + $subdomain = ''; + if (isset($parts['host'])) { + $host = explode('.', $parts['host']); + if (count($host) > 2) { + $subdomain = $host[0]; + } + } + return $subdomain; + } + + public static function getDomainPlaceholder() { + $parts = parse_url(SITE_URL); + $domain = ''; + if (isset($parts['host'])) { + $host = explode('.', $parts['host']); + if (count($host) > 2) { + array_shift($host); + $domain .= implode('.', $host); + } else { + $domain .= $parts['host']; + } + } + if (isset($parts['path'])) { + $domain .= $parts['path']; + } + return $domain; + } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 24884436f8b7..ac44e1950d21 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -113,6 +113,17 @@ class Account extends Eloquent return $user->getDisplayName(); } + public function getMomentDateTimeFormat() + { + $format = $this->datetime_format ? $this->datetime_format->format_moment : DEFAULT_DATETIME_MOMENT_FORMAT; + + if ($this->military_time) { + $format = str_replace('h:mm:ss a', 'H:mm:ss', $format); + } + + return $format; + } + public function getTimezone() { if ($this->timezone) { @@ -228,18 +239,27 @@ class Account extends Eloquent return $language->locale; } - public function loadLocalizationSettings() + public function loadLocalizationSettings($client = false) { $this->load('timezone', 'date_format', 'datetime_format', 'language'); Session::put(SESSION_TIMEZONE, $this->timezone ? $this->timezone->name : DEFAULT_TIMEZONE); Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT); Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT); - Session::put(SESSION_DATETIME_FORMAT, $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT); - Session::put(SESSION_CURRENCY, $this->currency_id ? $this->currency_id : DEFAULT_CURRENCY); - Session::put(SESSION_LOCALE, $this->language_id ? $this->language->locale : DEFAULT_LOCALE); - App::setLocale(session(SESSION_LOCALE)); + $currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY; + $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); + + Session::put(SESSION_CURRENCY, $currencyId); + Session::put(SESSION_LOCALE, $locale); + + App::setLocale($locale); + + $format = $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT; + if ($this->military_time) { + $format = str_replace('g:i a', 'H:i', $format); + } + Session::put(SESSION_DATETIME_FORMAT, $format); } public function getInvoiceLabels() diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 0dd5ec54bf37..40a65434e811 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -183,6 +183,10 @@ class Activity extends Eloquent $activity->balance = $invoice->client->balance; $activity->adjustment = $adjustment; $activity->save(); + + // Release any tasks associated with the deleted invoice + Task::where('invoice_id', '=', $invoice->id) + ->update(['invoice_id' => null]); } else { $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); diff --git a/app/Models/Client.php b/app/Models/Client.php index 554f74556910..ccb2064ae7bf 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -54,6 +54,11 @@ class Client extends EntityModel return $this->belongsTo('App\Models\Currency'); } + public function language() + { + return $this->belongsTo('App\Models\Language'); + } + public function size() { return $this->belongsTo('App\Models\Size'); diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 7941c81f4788..f31d102eebbb 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -32,10 +32,13 @@ class Invitation extends EntityModel if (!$this->account) { $this->load('account'); } - + $url = SITE_URL; + $iframe_url = $this->account->iframe_url; - if ($this->account->subdomain) { + if ($iframe_url) { + return "{$iframe_url}?{$this->invitation_key}"; + } else if ($this->account->subdomain) { $parsedUrl = parse_url($url); $host = explode('.', $parsedUrl['host']); $subdomain = $host[0]; diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index c4f4e82c7c41..3da2548a75ee 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -11,6 +11,7 @@ class Invoice extends EntityModel protected $casts = [ 'is_recurring' => 'boolean', 'has_tasks' => 'boolean', + 'auto_bill' => 'boolean', ]; public function account() diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 08cfb65b7490..5d955b1a9f16 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -14,14 +14,19 @@ class ContactMailer extends Mailer { public function sendInvoice(Invoice $invoice) { - $invoice->load('invitations', 'client', 'account'); + $invoice->load('invitations', 'client.language', 'account'); $entityType = $invoice->getEntityType(); + $client = $invoice->client; + $account = $invoice->account; + + $account->loadLocalizationSettings($client); + $view = 'invoice'; $subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]); $accountName = $invoice->account->getDisplayName(); $emailTemplate = $invoice->account->getEmailTemplate($entityType); - $invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()); + $invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $client->getCurrencyId()); $this->initClosure($invoice); @@ -39,7 +44,7 @@ class ContactMailer extends Mailer $variables = [ '$footer' => $invoice->account->getEmailFooter(), '$link' => $invitation->getLink(), - '$client' => $invoice->client->getDisplayName(), + '$client' => $client->getDisplayName(), '$account' => $accountName, '$contact' => $invitation->contact->getDisplayName(), '$amount' => $invoiceAmount, @@ -66,11 +71,13 @@ class ContactMailer extends Mailer Activity::emailInvoice($invitation); } - + if (!$invoice->isSent()) { $invoice->invoice_status_id = INVOICE_STATUS_SENT; $invoice->save(); } + + $account->loadLocalizationSettings(); Event::fire(new InvoiceSent($invoice)); @@ -79,17 +86,22 @@ class ContactMailer extends Mailer public function sendPaymentConfirmation(Payment $payment) { + $account = $payment->account; + $client = $payment->client; + + $account->loadLocalizationSettings($client); + $invoice = $payment->invoice; $view = 'payment_confirmation'; $subject = trans('texts.payment_subject', ['invoice' => $invoice->invoice_number]); - $accountName = $payment->account->getDisplayName(); - $emailTemplate = $invoice->account->getEmailTemplate(ENTITY_PAYMENT); + $accountName = $account->getDisplayName(); + $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT); $variables = [ - '$footer' => $payment->account->getEmailFooter(), - '$client' => $payment->client->getDisplayName(), + '$footer' => $account->getEmailFooter(), + '$client' => $client->getDisplayName(), '$account' => $accountName, - '$amount' => Utils::formatMoney($payment->amount, $payment->client->getCurrencyId()) + '$amount' => Utils::formatMoney($payment->amount, $client->getCurrencyId()) ]; if ($payment->invitation) { @@ -98,7 +110,7 @@ class ContactMailer extends Mailer $variables['$link'] = $payment->invitation->getLink(); } else { $user = $payment->user; - $contact = $payment->client->contacts[0]; + $contact = $client->contacts[0]; $variables['$link'] = $payment->invoice->invitations[0]->getLink(); } @@ -109,6 +121,8 @@ class ContactMailer extends Mailer if ($user->email && $contact->email) { $this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data); } + + $account->loadLocalizationSettings(); } public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId) diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 593d113072c7..cbd5d16f9f4f 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -26,8 +26,14 @@ class AccountRepository $account->ip = Request::getClientIp(); $account->account_key = str_random(RANDOM_KEY_LENGTH); - if (Session::has(SESSION_LOCALE)) { - $locale = Session::get(SESSION_LOCALE); + // Track referal code + if ($referralCode = Session::get(SESSION_REFERRAL_CODE)) { + if ($user = User::whereReferralCode($referralCode)->first()) { + $account->referral_user_id = $user->id; + } + } + + if ($locale = Session::get(SESSION_LOCALE)) { if ($language = Language::whereLocale($locale)->first()) { $account->language_id = $language->id; } @@ -188,7 +194,7 @@ class AccountRepository $accountGateway->user_id = $user->id; $accountGateway->gateway_id = NINJA_GATEWAY_ID; $accountGateway->public_id = 1; - $accountGateway->config = NINJA_GATEWAY_CONFIG; + $accountGateway->config = env(NINJA_GATEWAY_CONFIG); $account->account_gateways()->save($accountGateway); } @@ -206,7 +212,7 @@ class AccountRepository $client->public_id = $account->id; $client->user_id = $ninjaAccount->users()->first()->id; $client->currency_id = 1; - foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone'] as $field) { + foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id'] as $field) { $client->$field = $account->$field; } $ninjaAccount->clients()->save($client); diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index ab0baacda59f..091c66f9c5f6 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -105,6 +105,9 @@ class ClientRepository if (isset($data['currency_id'])) { $client->currency_id = $data['currency_id'] ? $data['currency_id'] : null; } + if (isset($data['language_id'])) { + $client->language_id = $data['language_id'] ? $data['language_id'] : null; + } if (isset($data['payment_terms'])) { $client->payment_terms = $data['payment_terms']; } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 6e43993f34d2..28a92351a082 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -7,9 +7,15 @@ use App\Models\InvoiceItem; use App\Models\Invitation; use App\Models\Product; use App\Models\Task; +use App\Services\PaymentService; class InvoiceRepository { + public function __construct(PaymentService $paymentService) + { + $this->paymentService = $paymentService; + } + public function getInvoices($accountId, $clientPublicId = false, $entityType = ENTITY_INVOICE, $filter = false) { $query = \DB::table('invoices') @@ -287,6 +293,12 @@ class InvoiceRepository $invoice->start_date = Utils::toSqlDate($data['start_date']); $invoice->end_date = Utils::toSqlDate($data['end_date']); $invoice->due_date = null; + $invoice->auto_bill = isset($data['auto_bill']) && $data['auto_bill'] ? true : false; + + if (isset($data['show_last_sent_date']) && $data['show_last_sent_date'] + && isset($data['last_sent_date']) && $data['last_sent_date']) { + $invoice->last_sent_date = Utils::toSqlDate($data['last_sent_date']); + } } else { $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']); $invoice->frequency_id = 0; @@ -635,9 +647,15 @@ class InvoiceRepository $invoice->invitations()->save($invitation); } - $recurInvoice->last_sent_date = Carbon::now()->toDateTimeString(); + $recurInvoice->last_sent_date = date('Y-m-d'); $recurInvoice->save(); + if ($recurInvoice->auto_bill) { + if ($this->paymentService->autoBillInvoice($invoice)) { + $invoice->invoice_status_id = INVOICE_STATUS_PAID; + } + } + return $invoice; } } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php new file mode 100644 index 000000000000..dc29efa0f2fb --- /dev/null +++ b/app/Services/PaymentService.php @@ -0,0 +1,204 @@ +accountRepo = $accountRepo; + } + + public function createGateway($accountGateway) + { + $gateway = Omnipay::create($accountGateway->gateway->provider); + $config = json_decode($accountGateway->config); + + foreach ($config as $key => $val) { + if (!$val) { + continue; + } + + $function = "set".ucfirst($key); + $gateway->$function($val); + } + + if ($accountGateway->gateway->id == GATEWAY_DWOLLA) { + if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { + $gateway->setKey($_ENV['DWOLLA_SANDBOX_KEY']); + $gateway->setSecret($_ENV['DWOLLA_SANSBOX_SECRET']); + } elseif (isset($_ENV['DWOLLA_KEY']) && isset($_ENV['DWOLLA_SECRET'])) { + $gateway->setKey($_ENV['DWOLLA_KEY']); + $gateway->setSecret($_ENV['DWOLLA_SECRET']); + } + } + + return $gateway; + } + + public function getPaymentDetails($invitation, $input = null) + { + $invoice = $invitation->invoice; + $account = $invoice->account; + $key = $invoice->account_id.'-'.$invoice->invoice_number; + $currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD'); + + if ($input) { + $data = self::convertInputForOmnipay($input); + Session::put($key, $data); + } elseif (Session::get($key)) { + $data = Session::get($key); + } else { + $data = []; + } + + $card = new CreditCard($data); + + return [ + 'amount' => $invoice->getRequestedAmount(), + 'card' => $card, + 'currency' => $currencyCode, + 'returnUrl' => URL::to('complete'), + 'cancelUrl' => $invitation->getLink(), + 'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}", + ]; + } + + private function convertInputForOmnipay($input) + { + $data = [ + 'firstName' => $input['first_name'], + 'lastName' => $input['last_name'], + 'number' => $input['card_number'], + 'expiryMonth' => $input['expiration_month'], + 'expiryYear' => $input['expiration_year'], + 'cvv' => $input['cvv'], + ]; + + if (isset($input['country_id'])) { + $country = Country::find($input['country_id']); + + $data = array_merge($data, [ + 'billingAddress1' => $input['address1'], + 'billingAddress2' => $input['address2'], + 'billingCity' => $input['city'], + 'billingState' => $input['state'], + 'billingPostcode' => $input['postal_code'], + 'billingCountry' => $country->iso_3166_2, + 'shippingAddress1' => $input['address1'], + 'shippingAddress2' => $input['address2'], + 'shippingCity' => $input['city'], + 'shippingState' => $input['state'], + 'shippingPostcode' => $input['postal_code'], + 'shippingCountry' => $country->iso_3166_2 + ]); + } + + return $data; + } + + public function createToken($gateway, $details, $accountGateway, $client, $contactId) + { + $tokenResponse = $gateway->createCard($details)->send(); + $cardReference = $tokenResponse->getCardReference(); + + if ($cardReference) { + $token = AccountGatewayToken::where('client_id', '=', $client->id) + ->where('account_gateway_id', '=', $accountGateway->id)->first(); + + if (!$token) { + $token = new AccountGatewayToken(); + $token->account_id = $client->account->id; + $token->contact_id = $contactId; + $token->account_gateway_id = $accountGateway->id; + $token->client_id = $client->id; + } + + $token->token = $cardReference; + $token->save(); + } else { + $this->lastError = $tokenResponse->getMessage(); + } + + return $cardReference; + } + + public function createPayment($invitation, $ref, $payerId = null) + { + $invoice = $invitation->invoice; + $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); + + // sync pro accounts + if ($invoice->account->account_key == NINJA_ACCOUNT_KEY + && $invoice->amount == PRO_PLAN_PRICE) { + $account = Account::with('users')->find($invoice->client->public_id); + if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { + $date = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); + $account->pro_plan_paid = $date->modify('+1 year')->format('Y-m-d'); + } else { + $account->pro_plan_paid = date_create()->format('Y-m-d'); + } + $account->save(); + + $user = $account->users()->first(); + $this->accountRepo->syncAccounts($user->id, $account->pro_plan_paid); + } + + $payment = Payment::createNew($invitation); + $payment->invitation_id = $invitation->id; + $payment->account_gateway_id = $accountGateway->id; + $payment->invoice_id = $invoice->id; + $payment->amount = $invoice->getRequestedAmount(); + $payment->client_id = $invoice->client_id; + $payment->contact_id = $invitation->contact_id; + $payment->transaction_reference = $ref; + $payment->payment_date = date_create()->format('Y-m-d'); + + if ($payerId) { + $payment->payer_id = $payerId; + } + + $payment->save(); + + Event::fire(new InvoicePaid($payment)); + + return $payment; + } + + public function autoBillInvoice($invoice) + { + $client = $invoice->client; + $account = $invoice->account; + $invitation = $invoice->invitations->first(); + $accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE); + + if (!$invitation || !$accountGateway) { + return false; + } + + // setup the gateway/payment info + $gateway = $this->createGateway($accountGateway); + $details = $this->getPaymentDetails($invitation); + $details['cardReference'] = $client->getGatewayToken(); + + // submit purchase/get response + $response = $gateway->purchase($details)->send(); + $ref = $response->getTransactionReference(); + + // create payment record + return $this->createPayment($invitation, $ref); + } +} diff --git a/config/app.php b/config/app.php index 24efe42b0a39..8eaa0582bf68 100644 --- a/config/app.php +++ b/config/app.php @@ -187,14 +187,11 @@ return [ 'Eloquent' => 'Illuminate\Database\Eloquent\Model', 'Event' => 'Illuminate\Support\Facades\Event', 'File' => 'Illuminate\Support\Facades\File', - //'Form' => 'Illuminate\Support\Facades\Form', 'Hash' => 'Illuminate\Support\Facades\Hash', - //'HTML' => 'Illuminate\Support\Facades\HTML', 'Input' => 'Illuminate\Support\Facades\Input', 'Lang' => 'Illuminate\Support\Facades\Lang', 'Log' => 'Illuminate\Support\Facades\Log', 'Mail' => 'Illuminate\Support\Facades\Mail', - //'Paginator' => 'Illuminate\Support\Facades\Paginator', 'Password' => 'Illuminate\Support\Facades\Password', 'Queue' => 'Illuminate\Support\Facades\Queue', 'Redirect' => 'Illuminate\Support\Facades\Redirect', @@ -210,41 +207,8 @@ return [ 'Validator' => 'Illuminate\Support\Facades\Validator', 'View' => 'Illuminate\Support\Facades\View', - /*'App' => 'Illuminate\Support\Facades\App', - 'Artisan' => 'Illuminate\Support\Facades\Artisan', - 'Auth' => 'Illuminate\Support\Facades\Auth', - 'Blade' => 'Illuminate\Support\Facades\Blade', - 'Bus' => 'Illuminate\Support\Facades\Bus', - 'Cache' => 'Illuminate\Support\Facades\Cache', - 'Config' => 'Illuminate\Support\Facades\Config', - 'Cookie' => 'Illuminate\Support\Facades\Cookie', - 'Crypt' => 'Illuminate\Support\Facades\Crypt', - 'DB' => 'Illuminate\Support\Facades\DB', - 'Eloquent' => 'Illuminate\Database\Eloquent\Model', - 'Event' => 'Illuminate\Support\Facades\Event', - 'File' => 'Illuminate\Support\Facades\File', - 'Hash' => 'Illuminate\Support\Facades\Hash', - 'Input' => 'Illuminate\Support\Facades\Input', - 'Inspiring' => 'Illuminate\Foundation\Inspiring', - 'Lang' => 'Illuminate\Support\Facades\Lang', - 'Log' => 'Illuminate\Support\Facades\Log', - 'Mail' => 'Illuminate\Support\Facades\Mail', - 'Password' => 'Illuminate\Support\Facades\Password', - 'Queue' => 'Illuminate\Support\Facades\Queue', - 'Redirect' => 'Illuminate\Support\Facades\Redirect', - 'Redis' => 'Illuminate\Support\Facades\Redis', - 'Request' => 'Illuminate\Support\Facades\Request', - 'Response' => 'Illuminate\Support\Facades\Response', - 'Route' => 'Illuminate\Support\Facades\Route', - 'Schema' => 'Illuminate\Support\Facades\Schema', - 'Session' => 'Illuminate\Support\Facades\Session', - 'Storage' => 'Illuminate\Support\Facades\Storage', - 'URL' => 'Illuminate\Support\Facades\URL', - 'Validator' => 'Illuminate\Support\Facades\Validator', - 'View' => 'Illuminate\Support\Facades\View',*/ // Added Class Aliases - 'Utils' => 'App\Libraries\Utils', 'Form' => 'Collective\Html\FormFacade', 'HTML' => 'Collective\Html\HtmlFacade', @@ -257,10 +221,8 @@ return [ 'ButtonToolbar' => 'Bootstrapper\Facades\ButtonToolbar', 'Carousel' => 'Bootstrapper\Facades\Carousel', 'DropdownButton' => 'Bootstrapper\Facades\DropdownButton', - //'Form' => 'Bootstrapper\Facades\Form', //need to clarify this guy 'Helpers' => 'Bootstrapper\Facades\Helpers', 'Icon' => 'Bootstrapper\Facades\Icon', - //'Image' => 'Bootstrapper\Facades\Image', 'Label' => 'Bootstrapper\Facades\Label', 'MediaObject' => 'Bootstrapper\Facades\MediaObject', 'Navbar' => 'Bootstrapper\Facades\Navbar', diff --git a/database/migrations/2015_09_07_135935_add_account_domain.php b/database/migrations/2015_09_07_135935_add_account_domain.php new file mode 100644 index 000000000000..3d91d1de8e3b --- /dev/null +++ b/database/migrations/2015_09_07_135935_add_account_domain.php @@ -0,0 +1,64 @@ +string('iframe_url')->nullable(); + $table->boolean('military_time')->default(false); + $table->unsignedInteger('referral_user_id')->nullable(); + }); + + Schema::table('clients', function ($table) { + $table->unsignedInteger('language_id')->nullable(); + $table->foreign('language_id')->references('id')->on('languages'); + }); + + Schema::table('invoices', function ($table) { + $table->boolean('auto_bill')->default(false); + }); + + Schema::table('users', function ($table) { + $table->string('referral_code')->nullable(); + }); + + DB::statement('ALTER TABLE invoices MODIFY COLUMN last_sent_date DATE'); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function ($table) { + $table->dropColumn('iframe_url'); + $table->dropColumn('military_time'); + $table->dropColumn('referral_user_id'); + }); + + Schema::table('clients', function ($table) { + $table->dropForeign('clients_language_id_foreign'); + $table->dropColumn('language_id'); + }); + + Schema::table('invoices', function ($table) { + $table->dropColumn('auto_bill'); + }); + + Schema::table('users', function ($table) { + $table->dropColumn('referral_code'); + }); + + DB::statement('ALTER TABLE invoices MODIFY COLUMN last_sent_date TIMESTAMP'); + } +} diff --git a/database/seeds/PaymentLibrariesSeeder.php b/database/seeds/PaymentLibrariesSeeder.php index 4dd8a7cd2e94..a13844fd7d69 100644 --- a/database/seeds/PaymentLibrariesSeeder.php +++ b/database/seeds/PaymentLibrariesSeeder.php @@ -160,7 +160,7 @@ class PaymentLibrariesSeeder extends Seeder 'label' => 'March 10, 2013 6:15 pm' ], [ - 'format' => 'D M jS, Y g:ia', + 'format' => 'D M jS, Y g:i a', 'format_moment' => 'ddd MMM Do, YYYY h:mm:ss a', 'label' => 'Mon March 10th, 2013 6:15 pm' ], @@ -175,8 +175,8 @@ class PaymentLibrariesSeeder extends Seeder 'label' => '20-03-2013 6:15 pm' ], [ - 'format' => 'm/d/Y H:i', - 'format_moment' => 'MM/DD/YYYY HH:mm:ss', + 'format' => 'm/d/Y g:i', + 'format_moment' => 'MM/DD/YYYY h:mm:ss', 'label' => '03/20/2013 6:15 pm' ] ]; diff --git a/public/images/round_logo.png b/public/images/round_logo.png new file mode 100644 index 000000000000..26f4f4dbdd88 Binary files /dev/null and b/public/images/round_logo.png differ diff --git a/public/js/built.js b/public/js/built.js index 9ba59e134b8c..63ce4dcc3b82 100644 --- a/public/js/built.js +++ b/public/js/built.js @@ -30402,17 +30402,25 @@ if (window.ko) { }; } +function getContactDisplayName(contact) +{ + var str = ''; + if (contact.first_name || contact.last_name) { + str += contact.first_name + ' ' + contact.last_name; + } + if (str && contact.email) { + str += ' - '; + } + return str + contact.email; +} + function getClientDisplayName(client) { var contact = client.contacts ? client.contacts[0] : false; if (client.name) { return client.name; } else if (contact) { - if (contact.first_name || contact.last_name) { - return contact.first_name + ' ' + contact.last_name; - } else { - return contact.email; - } + return getContactDisplayName(contact); } return ''; } @@ -31806,8 +31814,7 @@ NINJA.invoiceLines = function(invoice) { var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); if (showItemTaxes && tax) { - tax = lineTotal * tax / 100; - lineTotal += tax; + lineTotal += lineTotal * tax / 100; } lineTotal = formatMoney(lineTotal, currencyId); @@ -31820,7 +31827,7 @@ NINJA.invoiceLines = function(invoice) { row.push({style:["quantity", rowStyle], text:qty || ' '}); } if (showItemTaxes) { - row.push({style:["tax", rowStyle], text:tax ? tax.toFixed(2) : ' '}); + row.push({style:["tax", rowStyle], text:tax ? tax.toString() + '%' : ' '}); } row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '}); @@ -31987,6 +31994,8 @@ NINJA.clientDetails = function(invoice) { data = [ {text:clientName || ' ', style: ['clientName']}, + {text:client.id_number}, + {text:client.vat_number}, {text:client.address1}, {text:client.address2}, {text:cityStatePostal}, diff --git a/public/js/pdf.pdfmake.js b/public/js/pdf.pdfmake.js index 4f65389c9233..1962a1adc83a 100644 --- a/public/js/pdf.pdfmake.js +++ b/public/js/pdf.pdfmake.js @@ -266,8 +266,7 @@ NINJA.invoiceLines = function(invoice) { var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); if (showItemTaxes && tax) { - tax = lineTotal * tax / 100; - lineTotal += tax; + lineTotal += lineTotal * tax / 100; } lineTotal = formatMoney(lineTotal, currencyId); @@ -280,7 +279,7 @@ NINJA.invoiceLines = function(invoice) { row.push({style:["quantity", rowStyle], text:qty || ' '}); } if (showItemTaxes) { - row.push({style:["tax", rowStyle], text:tax ? tax.toFixed(2) : ' '}); + row.push({style:["tax", rowStyle], text:(tax ? tax.toString() + '%') : ' '}); } row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '}); @@ -447,6 +446,8 @@ NINJA.clientDetails = function(invoice) { data = [ {text:clientName || ' ', style: ['clientName']}, + {text:client.id_number}, + {text:client.vat_number}, {text:client.address1}, {text:client.address2}, {text:cityStatePostal}, diff --git a/public/js/script.js b/public/js/script.js index 739944ac2990..16e993da91bd 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -524,17 +524,25 @@ if (window.ko) { }; } +function getContactDisplayName(contact) +{ + var str = ''; + if (contact.first_name || contact.last_name) { + str += contact.first_name + ' ' + contact.last_name; + } + if (str && contact.email) { + str += ' - '; + } + return str + contact.email; +} + function getClientDisplayName(client) { var contact = client.contacts ? client.contacts[0] : false; if (client.name) { return client.name; } else if (contact) { - if (contact.first_name || contact.last_name) { - return contact.first_name + ' ' + contact.last_name; - } else { - return contact.email; - } + return getContactDisplayName(contact); } return ''; } diff --git a/readme.md b/readme.md index 55971ec67f9e..05c29f7ed6e0 100644 --- a/readme.md +++ b/readme.md @@ -19,8 +19,8 @@ If you'd like to translate the site please use [caouecs/Laravel4-long](https://g * Built using Laravel 5 * Live PDF generation using [pdfmake](http://pdfmake.org/) -* Integrates with 30+ payment providers -* Recurring invoices +* Integrates with 30+ payment providers with [OmniPay](https://github.com/thephpleague/omnipay) +* Recurring invoices with auto-billing * Tasks with time-tracking * Multi-user/multi-company support * Tax rates and payment terms diff --git a/resources/lang/da/texts.php b/resources/lang/da/texts.php index 05aef97fef59..f3862eb07edd 100644 --- a/resources/lang/da/texts.php +++ b/resources/lang/da/texts.php @@ -768,5 +768,14 @@ 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + + ); \ No newline at end of file diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php index 0aa939ee4776..a4e6993c5ed9 100644 --- a/resources/lang/de/texts.php +++ b/resources/lang/de/texts.php @@ -767,6 +767,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + ); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0e7e605bddb2..d71ec28b56c8 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -35,7 +35,7 @@ return array( 'invoice_number_short' => 'Invoice #', 'po_number' => 'PO Number', 'po_number_short' => 'PO #', - 'frequency_id' => 'How often', + 'frequency_id' => 'How Often', 'discount' => 'Discount', 'taxes' => 'Taxes', 'tax' => 'Tax', @@ -207,7 +207,7 @@ return array( 'client_will_create' => 'client will be created', 'clients_will_create' => 'clients will be created', 'email_settings' => 'Email Settings', - 'pdf_email_attachment' => 'Attach to Emails', + 'pdf_email_attachment' => 'Attach PDFs', // application messages 'created_client' => 'Successfully created client', @@ -767,5 +767,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + + ); diff --git a/resources/lang/es/texts.php b/resources/lang/es/texts.php index 4ead48f37a8e..60eebf1387ef 100644 --- a/resources/lang/es/texts.php +++ b/resources/lang/es/texts.php @@ -745,6 +745,15 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + + ); \ No newline at end of file diff --git a/resources/lang/es_ES/texts.php b/resources/lang/es_ES/texts.php index e46005f97a44..5a917db546a2 100644 --- a/resources/lang/es_ES/texts.php +++ b/resources/lang/es_ES/texts.php @@ -767,6 +767,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + ); \ No newline at end of file diff --git a/resources/lang/fr/texts.php b/resources/lang/fr/texts.php index 5a6005dfae0d..20634a928d47 100644 --- a/resources/lang/fr/texts.php +++ b/resources/lang/fr/texts.php @@ -758,6 +758,15 @@ return array( 'status_partial' => 'Partial', 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + diff --git a/resources/lang/fr_CA/texts.php b/resources/lang/fr_CA/texts.php index 224f6f21137a..c835b32508e5 100644 --- a/resources/lang/fr_CA/texts.php +++ b/resources/lang/fr_CA/texts.php @@ -760,6 +760,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + diff --git a/resources/lang/it/texts.php b/resources/lang/it/texts.php index b999643fff33..a6a12aa71294 100644 --- a/resources/lang/it/texts.php +++ b/resources/lang/it/texts.php @@ -762,6 +762,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + ); diff --git a/resources/lang/lt/texts.php b/resources/lang/lt/texts.php index a0957572f616..84eb47985807 100644 --- a/resources/lang/lt/texts.php +++ b/resources/lang/lt/texts.php @@ -769,6 +769,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + diff --git a/resources/lang/nb_NO/texts.php b/resources/lang/nb_NO/texts.php index 12fb585562c1..774ddf20096c 100644 --- a/resources/lang/nb_NO/texts.php +++ b/resources/lang/nb_NO/texts.php @@ -767,6 +767,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + ); \ No newline at end of file diff --git a/resources/lang/nl/texts.php b/resources/lang/nl/texts.php index 7c452a446a85..44a649c40e07 100644 --- a/resources/lang/nl/texts.php +++ b/resources/lang/nl/texts.php @@ -762,6 +762,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + ); diff --git a/resources/lang/pt_BR/texts.php b/resources/lang/pt_BR/texts.php index 083919ab7482..3f2d4ba6aa65 100644 --- a/resources/lang/pt_BR/texts.php +++ b/resources/lang/pt_BR/texts.php @@ -762,5 +762,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + + ); diff --git a/resources/lang/sv/texts.php b/resources/lang/sv/texts.php index 96cdb1e885a0..b406e17fb38c 100644 --- a/resources/lang/sv/texts.php +++ b/resources/lang/sv/texts.php @@ -765,6 +765,14 @@ return array( 'status_paid' => 'Paid', 'show_line_item_tax' => 'Display line item taxes inline', + 'iframe_url' => 'Website', + 'iframe_url_help1' => 'Copy the following code to a page on your site.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + + 'auto_bill' => 'Auto Bill', + 'military_time' => '24 Hour Time', + 'last_sent' => 'Last Sent', + diff --git a/resources/views/accounts/details.blade.php b/resources/views/accounts/details.blade.php index 0460c12ee316..a4b1af9d0df2 100644 --- a/resources/views/accounts/details.blade.php +++ b/resources/views/accounts/details.blade.php @@ -17,6 +17,7 @@ )) !!} {{ Former::populate($account) }} + {{ Former::populateField('military_time', intval($account->military_time)) }} @if ($showUser) {{ Former::populateField('first_name', $primaryUser->first_name) }} {{ Former::populateField('last_name', $primaryUser->last_name) }} @@ -37,12 +38,6 @@
{!! $client->getWebsite() !!}
@endif + @if ($client->language) +{{ $client->language->name }}
+ @endif +{{ $client->payment_terms ? trans('texts.payment_terms') . ": Net " . $client->payment_terms : '' }}