1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-18 00:53:10 +01:00

Added auto-billing and per-client language support

This commit is contained in:
Hillel Coren 2015-09-10 20:50:09 +03:00
parent 3fdad48179
commit 4741fad4be
55 changed files with 926 additions and 397 deletions

View File

@ -41,8 +41,8 @@ class SendRecurringInvoices extends Command
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice) { if ($invoice && !$invoice->isPaid()) {
$recurInvoice->account->loadLocalizationSettings(); $recurInvoice->account->loadLocalizationSettings($invoice->client);
$this->mailer->sendInvoice($invoice); $this->mailer->sendInvoice($invoice);
} }
} }

View File

@ -40,6 +40,7 @@ use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Mailers\UserMailer; use App\Ninja\Mailers\UserMailer;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
use App\Events\UserLoggedIn; use App\Events\UserLoggedIn;
use App\Events\UserSettingsChanged;
class AccountController extends BaseController class AccountController extends BaseController
{ {
@ -341,8 +342,29 @@ class AccountController extends BaseController
private function saveInvoiceSettings() private function saveInvoiceSettings()
{ {
if (Auth::user()->account->isPro()) { if (Auth::user()->account->isPro()) {
$account = Auth::user()->account;
$rules = [];
$user = Auth::user();
$iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH));
$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";
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
return Redirect::to('company/details')
->withErrors($validator)
->withInput();
} else {
$account = Auth::user()->account;
$account->subdomain = $subdomain;
$account->iframe_url = $iframeURL;
$account->custom_label1 = trim(Input::get('custom_label1')); $account->custom_label1 = trim(Input::get('custom_label1'));
$account->custom_value1 = trim(Input::get('custom_value1')); $account->custom_value1 = trim(Input::get('custom_value1'));
$account->custom_label2 = trim(Input::get('custom_label2')); $account->custom_label2 = trim(Input::get('custom_label2'));
@ -375,6 +397,7 @@ class AccountController extends BaseController
Session::flash('message', trans('texts.updated_settings')); Session::flash('message', trans('texts.updated_settings'));
} }
} }
}
return Redirect::to('company/advanced_settings/invoice_settings'); return Redirect::to('company/advanced_settings/invoice_settings');
} }
@ -643,14 +666,6 @@ class AccountController extends BaseController
$rules['email'] = 'email|required|unique:users,email,'.$user->id.',id'; $rules['email'] = 'email|required|unique:users,email,'.$user->id.',id';
} }
$subdomain = preg_replace('/[^a-zA-Z0-9_\-]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH));
if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) {
$subdomain = null;
}
if ($subdomain) {
$rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id";
}
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) { if ($validator->fails()) {
@ -660,7 +675,6 @@ class AccountController extends BaseController
} else { } else {
$account = Auth::user()->account; $account = Auth::user()->account;
$account->name = trim(Input::get('name')); $account->name = trim(Input::get('name'));
$account->subdomain = $subdomain;
$account->id_number = trim(Input::get('id_number')); $account->id_number = trim(Input::get('id_number'));
$account->vat_number = trim(Input::get('vat_number')); $account->vat_number = trim(Input::get('vat_number'));
$account->work_email = trim(Input::get('work_email')); $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->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->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->language_id = Input::get('language_id') ? Input::get('language_id') : 1; // English
$account->military_time = Input::get('military_time') ? true : false;
$account->save(); $account->save();
if (Auth::user()->id === $user->id) { 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'); return Redirect::to('company/details');
} }
} }

View File

@ -194,10 +194,12 @@ class ClientController extends BaseController
private static function getViewModel() private static function getViewModel()
{ {
return [ return [
'account' => Auth::user()->account,
'sizes' => Cache::get('sizes'), 'sizes' => Cache::get('sizes'),
'paymentTerms' => Cache::get('paymentTerms'), 'paymentTerms' => Cache::get('paymentTerms'),
'industries' => Cache::get('industries'), 'industries' => Cache::get('industries'),
'currencies' => Cache::get('currencies'), 'currencies' => Cache::get('currencies'),
'languages' => Cache::get('languages'),
'countries' => Cache::get('countries'), 'countries' => Cache::get('countries'),
'customLabel1' => Auth::user()->account->custom_client_label1, 'customLabel1' => Auth::user()->account->custom_client_label1,
'customLabel2' => Auth::user()->account->custom_client_label2, 'customLabel2' => Auth::user()->account->custom_client_label2,
@ -252,6 +254,7 @@ class ClientController extends BaseController
$client->size_id = Input::get('size_id') ?: null; $client->size_id = Input::get('size_id') ?: null;
$client->industry_id = Input::get('industry_id') ?: null; $client->industry_id = Input::get('industry_id') ?: null;
$client->currency_id = Input::get('currency_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->payment_terms = Input::get('payment_terms') ?: 0;
$client->website = trim(Input::get('website')); $client->website = trim(Input::get('website'));

View File

@ -41,6 +41,11 @@ class HomeController extends BaseController
return View::make('public.terms', ['hideHeader' => true]); return View::make('public.terms', ['hideHeader' => true]);
} }
public function viewLogo()
{
return View::make('public.logo');
}
public function invoiceNow() public function invoiceNow()
{ {
if (Auth::check() && Input::get('new_company')) { if (Auth::check() && Input::get('new_company')) {

View File

@ -108,7 +108,7 @@ class InvoiceApiController extends Controller
if ($error) { if ($error) {
$response = json_encode($error, JSON_PRETTY_PRINT); $response = json_encode($error, JSON_PRETTY_PRINT);
} else { } else {
$data = self::prepareData($data); $data = self::prepareData($data, $client);
$data['client_id'] = $client->id; $data['client_id'] = $client->id;
$invoice = $this->invoiceRepo->save(false, $data, false); $invoice = $this->invoiceRepo->save(false, $data, false);
@ -136,10 +136,10 @@ class InvoiceApiController extends Controller
return Response::make($response, $error ? 400 : 200, $headers); return Response::make($response, $error ? 400 : 200, $headers);
} }
private function prepareData($data) private function prepareData($data, $client)
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$account->loadLocalizationSettings(); $account->loadLocalizationSettings($client);
// set defaults for optional fields // set defaults for optional fields
$fields = [ $fields = [

View File

@ -206,7 +206,7 @@ class InvoiceController extends BaseController
Session::set($invitationKey, true); Session::set($invitationKey, true);
Session::set('invitation_key', $invitationKey); Session::set('invitation_key', $invitationKey);
$account->loadLocalizationSettings(); $account->loadLocalizationSettings($client);
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
$invoice->due_date = Utils::fromSqlDate($invoice->due_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->due_date = Utils::fromSqlDate($invoice->due_date);
$invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->start_date = Utils::fromSqlDate($invoice->start_date);
$invoice->end_date = Utils::fromSqlDate($invoice->end_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(); $invoice->is_pro = Auth::user()->isPro();
$actions = [ $actions = [
@ -417,6 +418,7 @@ class InvoiceController extends BaseController
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->orderBy('name')->get(), 'taxRates' => TaxRate::scope()->orderBy('name')->get(),
'currencies' => Cache::get('currencies'), 'currencies' => Cache::get('currencies'),
'languages' => Cache::get('languages'),
'sizes' => Cache::get('sizes'), 'sizes' => Cache::get('sizes'),
'paymentTerms' => Cache::get('paymentTerms'), 'paymentTerms' => Cache::get('paymentTerms'),
'industries' => Cache::get('industries'), 'industries' => Cache::get('industries'),
@ -531,7 +533,12 @@ class InvoiceController extends BaseController
if ($invoice->is_recurring) { if ($invoice->is_recurring) {
if ($invoice->shouldSendToday()) { if ($invoice->shouldSendToday()) {
$invoice = $this->invoiceRepo->createRecurringInvoice($invoice); $invoice = $this->invoiceRepo->createRecurringInvoice($invoice);
// in case auto-bill is enabled
if ($invoice->isPaid()) {
$response = true;
} else {
$response = $this->mailer->sendInvoice($invoice); $response = $this->mailer->sendInvoice($invoice);
}
} else { } else {
$response = trans('texts.recurring_too_soon'); $response = trans('texts.recurring_too_soon');
} }

View File

@ -12,29 +12,22 @@ use Omnipay;
use CreditCard; use CreditCard;
use URL; use URL;
use Cache; use Cache;
use Event;
use DateTime;
use App\Models\Account;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\Client; use App\Models\Client;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\Country;
use App\Models\License; use App\Models\License;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Affiliate; use App\Models\Affiliate;
use App\Models\AccountGatewayToken;
use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
use App\Events\InvoicePaid; use App\Services\PaymentService;
class PaymentController extends BaseController class PaymentController extends BaseController
{ {
protected $creditRepo; public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer, PaymentService $paymentService)
public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer)
{ {
parent::__construct(); parent::__construct();
@ -42,6 +35,7 @@ class PaymentController extends BaseController
$this->invoiceRepo = $invoiceRepo; $this->invoiceRepo = $invoiceRepo;
$this->accountRepo = $accountRepo; $this->accountRepo = $accountRepo;
$this->contactMailer = $contactMailer; $this->contactMailer = $contactMailer;
$this->paymentService = $paymentService;
} }
public function index() public function index()
@ -191,36 +185,9 @@ class PaymentController extends BaseController
return View::make('payments.edit', $data); 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) private function getLicensePaymentDetails($input, $affiliate)
{ {
$data = self::convertInputForOmnipay($input); $data = $this->paymentService->convertInputForOmnipay($input);
$card = new CreditCard($data); $card = new CreditCard($data);
return [ 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) 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(); $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) { if ($testMode) {
$ref = 'TEST_MODE'; $ref = 'TEST_MODE';
} else { } else {
$gateway = self::createGateway($accountGateway); $gateway = $this->paymentService->createGateway($accountGateway);
$details = self::getLicensePaymentDetails(Input::all(), $affiliate); $details = self::getLicensePaymentDetails(Input::all(), $affiliate);
$response = $gateway->purchase($details)->send(); $response = $gateway->purchase($details)->send();
$ref = $response->getTransactionReference(); $ref = $response->getTransactionReference();
if (!$ref) { if (!$response->isSuccessful() || !$ref) {
Session::flash('error', $response->getMessage()); $this->error('License', $response->getMessage(), $accountGateway);
return Redirect::to('license')->withInput();
}
if (!$response->isSuccessful()) {
Session::flash('error', $response->getMessage());
Utils::logError('Payment Error [license]: ' . $response->getMessage());
return Redirect::to('license')->withInput(); return Redirect::to('license')->withInput();
} }
} }
@ -482,10 +380,7 @@ class PaymentController extends BaseController
return View::make('public.license', $data); return View::make('public.license', $data);
} catch (\Exception $e) { } catch (\Exception $e) {
$errorMessage = trans('texts.payment_error'); $this->error('License-Uncaught', false, $accountGateway, $e);
Session::flash('error', $errorMessage);
Utils::logError('Payment Error [license-uncaught]: ' . Utils::getErrorString($e));
return Redirect::to('license')->withInput(); return Redirect::to('license')->withInput();
} }
} }
@ -549,7 +444,6 @@ class PaymentController extends BaseController
->withInput(); ->withInput();
} }
if ($accountGateway->update_address) { if ($accountGateway->update_address) {
$client->address1 = trim(Input::get('address1')); $client->address1 = trim(Input::get('address1'));
$client->address2 = trim(Input::get('address2')); $client->address2 = trim(Input::get('address2'));
@ -562,35 +456,19 @@ class PaymentController extends BaseController
} }
try { try {
$gateway = self::createGateway($accountGateway); $gateway = $this->paymentService->createGateway($accountGateway);
$details = self::getPaymentDetails($invitation, ($useToken || !$onSite) ? false : Input::all()); $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 ($accountGateway->gateway_id == GATEWAY_STRIPE) {
if ($useToken) { if ($useToken) {
$details['cardReference'] = $client->getGatewayToken(); $details['cardReference'] = $client->getGatewayToken();
} elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) {
$tokenResponse = $gateway->createCard($details)->send(); $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id);
$cardReference = $tokenResponse->getCardReference(); if ($token) {
$details['cardReference'] = $token;
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();
} else { } else {
Session::flash('error', $tokenResponse->getMessage()); $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
Utils::logError('Payment Error [no-token-ref]: ' . $tokenResponse->getMessage());
return Redirect::to('payment/'.$invitationKey)->withInput(); return Redirect::to('payment/'.$invitationKey)->withInput();
} }
} }
@ -600,9 +478,7 @@ class PaymentController extends BaseController
$ref = $response->getTransactionReference(); $ref = $response->getTransactionReference();
if (!$ref) { if (!$ref) {
$this->error('No-Ref', $response->getMessage(), $accountGateway);
Session::flash('error', $response->getMessage());
Utils::logError('Payment Error [no-ref]: ' . $response->getMessage());
if ($onSite) { if ($onSite) {
return Redirect::to('payment/'.$invitationKey)->withInput(); return Redirect::to('payment/'.$invitationKey)->withInput();
@ -612,7 +488,7 @@ class PaymentController extends BaseController
} }
if ($response->isSuccessful()) { if ($response->isSuccessful()) {
$payment = self::createPayment($invitation, $ref); $payment = $this->paymentService->createPayment($invitation, $ref);
Session::flash('message', trans('texts.applied_payment')); Session::flash('message', trans('texts.applied_payment'));
if ($account->account_key == NINJA_ACCOUNT_KEY) { if ($account->account_key == NINJA_ACCOUNT_KEY) {
@ -629,16 +505,11 @@ class PaymentController extends BaseController
Session::save(); Session::save();
$response->redirect(); $response->redirect();
} else { } else {
Session::flash('error', $response->getMessage()); $this->error('Fatal', $response->getMessage(), $accountGateway);
Utils::logError('Payment Error [fatal]: ' . $response->getMessage());
return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.<p>', $response->getMessage()); return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.<p>', $response->getMessage());
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$errorMessage = trans('texts.payment_error'); $this->error('Uncaught', false, $accountGateway, $e);
Session::flash('error', $errorMessage."<p>".$e->getMessage());
Utils::logError('Payment Error [uncaught]:' . Utils::getErrorString($e));
if ($onSite) { if ($onSite) {
return Redirect::to('payment/'.$invitationKey)->withInput(); return Redirect::to('payment/'.$invitationKey)->withInput();
} else { } 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() public function offsite_payment()
{ {
$payerId = Request::query('PayerID'); $payerId = Request::query('PayerID');
@ -705,45 +535,37 @@ class PaymentController extends BaseController
$invoice = $invitation->invoice; $invoice = $invitation->invoice;
$accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type'));
$gateway = self::createGateway($accountGateway); $gateway = $this->paymentService->createGateway($accountGateway);
// Check for Dwolla payment error // Check for Dwolla payment error
if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) { if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) {
$errorMessage = trans('texts.payment_error')."\n\n".Input::get('error_description'); $this->error('Dwolla', Input::get('error_description'), $accountGateway);
Session::flash('error', $errorMessage);
Utils::logError('Payment Error [dwolla]: ' . $errorMessage);
return Redirect::to('view/'.$invitation->invitation_key); return Redirect::to('view/'.$invitation->invitation_key);
} }
try { try {
if (method_exists($gateway, 'completePurchase')) { if (method_exists($gateway, 'completePurchase')) {
$details = self::getPaymentDetails($invitation); $details = $this->paymentService->getPaymentDetails($invitation);
$response = $gateway->completePurchase($details)->send(); $response = $gateway->completePurchase($details)->send();
$ref = $response->getTransactionReference(); $ref = $response->getTransactionReference();
if ($response->isSuccessful()) { if ($response->isSuccessful()) {
$payment = self::createPayment($invitation, $ref, $payerId); $payment = $this->paymentService->createPayment($invitation, $ref, $payerId);
Session::flash('message', trans('texts.applied_payment')); Session::flash('message', trans('texts.applied_payment'));
return Redirect::to('view/'.$invitation->invitation_key); return Redirect::to('view/'.$invitation->invitation_key);
} else { } else {
$errorMessage = trans('texts.payment_error')."\n\n".$response->getMessage(); $this->error('offsite', $response->getMessage(), $accountGateway);
Session::flash('error', $errorMessage);
Utils::logError('Payment Error [offsite]: ' . $errorMessage);
return Redirect::to('view/'.$invitation->invitation_key); return Redirect::to('view/'.$invitation->invitation_key);
} }
} else { } else {
$payment = self::createPayment($invitation, $token, $payerId); $payment = $this->paymentService->createPayment($invitation, $token, $payerId);
Session::flash('message', trans('texts.applied_payment')); Session::flash('message', trans('texts.applied_payment'));
return Redirect::to('view/'.$invitation->invitation_key); return Redirect::to('view/'.$invitation->invitation_key);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$errorMessage = trans('texts.payment_error'); $this->error('Offsite-uncaught', false, $accountGateway, $e);
Session::flash('error', $errorMessage);
Utils::logError('Payment Error [offsite-uncaught]: ' . $errorMessage."\n\n".$e->getMessage());
return Redirect::to('view/'.$invitation->invitation_key); return Redirect::to('view/'.$invitation->invitation_key);
} }
} }
@ -799,4 +621,24 @@ class PaymentController extends BaseController
return Redirect::to('payments'); 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));
}
} }

View File

@ -156,6 +156,7 @@ class QuoteController extends BaseController
'currencies' => Cache::get('currencies'), 'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'), 'sizes' => Cache::get('sizes'),
'paymentTerms' => Cache::get('paymentTerms'), 'paymentTerms' => Cache::get('paymentTerms'),
'languages' => Cache::get('languages'),
'industries' => Cache::get('industries'), 'industries' => Cache::get('industries'),
'invoiceDesigns' => InvoiceDesign::getDesigns(), 'invoiceDesigns' => InvoiceDesign::getDesigns(),
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),

View File

@ -133,7 +133,7 @@ class TaskController extends BaseController
'url' => 'tasks', 'url' => 'tasks',
'title' => trans('texts.new_task'), 'title' => trans('texts.new_task'),
'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, '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()); $data = array_merge($data, self::getViewModel());
@ -182,7 +182,7 @@ class TaskController extends BaseController
'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(),
'actions' => $actions, 'actions' => $actions,
'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, '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()); $data = array_merge($data, self::getViewModel());

View File

@ -384,4 +384,29 @@ class UserController extends BaseController
{ {
return View::make('users.account_management'); 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('/');
}
} }

View File

@ -118,10 +118,15 @@ class StartupCheck
} }
} }
} elseif (Auth::check()) { } 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); 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 // Make sure the account/user localization settings are in the session
if (Auth::check() && !Session::has(SESSION_TIMEZONE)) { if (Auth::check() && !Session::has(SESSION_TIMEZONE)) {
Event::fire(new UserSettingsChanged()); Event::fire(new UserSettingsChanged());
@ -164,9 +169,8 @@ class StartupCheck
Session::flash('error', trans('texts.old_browser')); Session::flash('error', trans('texts.old_browser'));
} }
// for security prevent displaying within an iframe
$response = $next($request); $response = $next($request);
$response->headers->set('X-Frame-Options', 'DENY'); //$response->headers->set('X-Frame-Options', 'DENY');
return $response; return $response;
} }

View File

@ -39,10 +39,12 @@ Route::get('terms', 'HomeController@showTerms');
Route::get('log_error', 'HomeController@logError'); Route::get('log_error', 'HomeController@logError');
Route::get('invoice_now', 'HomeController@invoiceNow'); Route::get('invoice_now', 'HomeController@invoiceNow');
Route::get('keep_alive', 'HomeController@keepAlive'); Route::get('keep_alive', 'HomeController@keepAlive');
Route::get('referral_code/{email}', 'UserController@claimReferralCode');
Route::post('get_started', 'AccountController@getStarted'); Route::post('get_started', 'AccountController@getStarted');
// Client visible pages // Client visible pages
Route::get('view/{invitation_key}', 'InvoiceController@view'); Route::get('view/{invitation_key}', 'InvoiceController@view');
Route::get('view', 'HomeController@viewLogo');
Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('approve/{invitation_key}', 'QuoteController@approve');
Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment');
Route::post('payment/{invitation_key}', 'PaymentController@do_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_CLIENTS_PRO', 20000);
define('MAX_NUM_USERS', 20); define('MAX_NUM_USERS', 20);
define('MAX_SUBDOMAIN_LENGTH', 30); define('MAX_SUBDOMAIN_LENGTH', 30);
define('MAX_IFRAME_URL_LENGTH', 250);
define('DEFAULT_FONT_SIZE', 9); define('DEFAULT_FONT_SIZE', 9);
define('INVOICE_STATUS_DRAFT', 1); define('INVOICE_STATUS_DRAFT', 1);
@ -333,6 +336,7 @@ if (!defined('CONTACT_EMAIL')) {
define('SESSION_COUNTER', 'sessionCounter'); define('SESSION_COUNTER', 'sessionCounter');
define('SESSION_LOCALE', 'sessionLocale'); define('SESSION_LOCALE', 'sessionLocale');
define('SESSION_USER_ACCOUNTS', 'userAccounts'); define('SESSION_USER_ACCOUNTS', 'userAccounts');
define('SESSION_REFERRAL_CODE', 'referralCode');
define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE');
define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); 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('PREV_USER_ID', 'PREV_USER_ID');
define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h');
define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); 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_WEB_URL', 'https://www.invoiceninja.com');
define('NINJA_APP_URL', 'https://app.invoiceninja.com'); define('NINJA_APP_URL', 'https://app.invoiceninja.com');
define('NINJA_VERSION', '2.3.4'); define('NINJA_VERSION', '2.3.4');

View File

@ -341,10 +341,7 @@ class Utils
return; return;
} }
//$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE);
$format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT);
//$dateTime = DateTime::createFromFormat($format, $date, new DateTimeZone($timezone));
$dateTime = DateTime::createFromFormat($format, $date); $dateTime = DateTime::createFromFormat($format, $date);
return $formatResult ? $dateTime->format('Y-m-d') : $dateTime; return $formatResult ? $dateTime->format('Y-m-d') : $dateTime;
@ -356,11 +353,8 @@ class Utils
return ''; return '';
} }
//$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE);
$format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT);
$dateTime = DateTime::createFromFormat('Y-m-d', $date); $dateTime = DateTime::createFromFormat('Y-m-d', $date);
//$dateTime->setTimeZone(new DateTimeZone($timezone));
return $formatResult ? $dateTime->format($format) : $dateTime; return $formatResult ? $dateTime->format($format) : $dateTime;
} }
@ -752,4 +746,34 @@ class Utils
} }
return $str; return $str;
} }
public static function getSubdomainPlaceholder() {
$parts = parse_url(SITE_URL);
$subdomain = '';
if (isset($parts['host'])) {
$host = explode('.', $parts['host']);
if (count($host) > 2) {
$subdomain = $host[0];
}
}
return $subdomain;
}
public static function getDomainPlaceholder() {
$parts = parse_url(SITE_URL);
$domain = '';
if (isset($parts['host'])) {
$host = explode('.', $parts['host']);
if (count($host) > 2) {
array_shift($host);
$domain .= implode('.', $host);
} else {
$domain .= $parts['host'];
}
}
if (isset($parts['path'])) {
$domain .= $parts['path'];
}
return $domain;
}
} }

View File

@ -113,6 +113,17 @@ class Account extends Eloquent
return $user->getDisplayName(); 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() public function getTimezone()
{ {
if ($this->timezone) { if ($this->timezone) {
@ -228,18 +239,27 @@ class Account extends Eloquent
return $language->locale; return $language->locale;
} }
public function loadLocalizationSettings() public function loadLocalizationSettings($client = false)
{ {
$this->load('timezone', 'date_format', 'datetime_format', 'language'); $this->load('timezone', 'date_format', 'datetime_format', 'language');
Session::put(SESSION_TIMEZONE, $this->timezone ? $this->timezone->name : DEFAULT_TIMEZONE); 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_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_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() public function getInvoiceLabels()

View File

@ -183,6 +183,10 @@ class Activity extends Eloquent
$activity->balance = $invoice->client->balance; $activity->balance = $invoice->client->balance;
$activity->adjustment = $adjustment; $activity->adjustment = $adjustment;
$activity->save(); $activity->save();
// Release any tasks associated with the deleted invoice
Task::where('invoice_id', '=', $invoice->id)
->update(['invoice_id' => null]);
} else { } else {
$diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount'));

View File

@ -54,6 +54,11 @@ class Client extends EntityModel
return $this->belongsTo('App\Models\Currency'); return $this->belongsTo('App\Models\Currency');
} }
public function language()
{
return $this->belongsTo('App\Models\Language');
}
public function size() public function size()
{ {
return $this->belongsTo('App\Models\Size'); return $this->belongsTo('App\Models\Size');

View File

@ -34,8 +34,11 @@ class Invitation extends EntityModel
} }
$url = SITE_URL; $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); $parsedUrl = parse_url($url);
$host = explode('.', $parsedUrl['host']); $host = explode('.', $parsedUrl['host']);
$subdomain = $host[0]; $subdomain = $host[0];

View File

@ -11,6 +11,7 @@ class Invoice extends EntityModel
protected $casts = [ protected $casts = [
'is_recurring' => 'boolean', 'is_recurring' => 'boolean',
'has_tasks' => 'boolean', 'has_tasks' => 'boolean',
'auto_bill' => 'boolean',
]; ];
public function account() public function account()

View File

@ -14,14 +14,19 @@ class ContactMailer extends Mailer
{ {
public function sendInvoice(Invoice $invoice) public function sendInvoice(Invoice $invoice)
{ {
$invoice->load('invitations', 'client', 'account'); $invoice->load('invitations', 'client.language', 'account');
$entityType = $invoice->getEntityType(); $entityType = $invoice->getEntityType();
$client = $invoice->client;
$account = $invoice->account;
$account->loadLocalizationSettings($client);
$view = 'invoice'; $view = 'invoice';
$subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]); $subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]);
$accountName = $invoice->account->getDisplayName(); $accountName = $invoice->account->getDisplayName();
$emailTemplate = $invoice->account->getEmailTemplate($entityType); $emailTemplate = $invoice->account->getEmailTemplate($entityType);
$invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->getCurrencyId()); $invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $client->getCurrencyId());
$this->initClosure($invoice); $this->initClosure($invoice);
@ -39,7 +44,7 @@ class ContactMailer extends Mailer
$variables = [ $variables = [
'$footer' => $invoice->account->getEmailFooter(), '$footer' => $invoice->account->getEmailFooter(),
'$link' => $invitation->getLink(), '$link' => $invitation->getLink(),
'$client' => $invoice->client->getDisplayName(), '$client' => $client->getDisplayName(),
'$account' => $accountName, '$account' => $accountName,
'$contact' => $invitation->contact->getDisplayName(), '$contact' => $invitation->contact->getDisplayName(),
'$amount' => $invoiceAmount, '$amount' => $invoiceAmount,
@ -72,6 +77,8 @@ class ContactMailer extends Mailer
$invoice->save(); $invoice->save();
} }
$account->loadLocalizationSettings();
Event::fire(new InvoiceSent($invoice)); Event::fire(new InvoiceSent($invoice));
return $response; return $response;
@ -79,17 +86,22 @@ class ContactMailer extends Mailer
public function sendPaymentConfirmation(Payment $payment) public function sendPaymentConfirmation(Payment $payment)
{ {
$account = $payment->account;
$client = $payment->client;
$account->loadLocalizationSettings($client);
$invoice = $payment->invoice; $invoice = $payment->invoice;
$view = 'payment_confirmation'; $view = 'payment_confirmation';
$subject = trans('texts.payment_subject', ['invoice' => $invoice->invoice_number]); $subject = trans('texts.payment_subject', ['invoice' => $invoice->invoice_number]);
$accountName = $payment->account->getDisplayName(); $accountName = $account->getDisplayName();
$emailTemplate = $invoice->account->getEmailTemplate(ENTITY_PAYMENT); $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT);
$variables = [ $variables = [
'$footer' => $payment->account->getEmailFooter(), '$footer' => $account->getEmailFooter(),
'$client' => $payment->client->getDisplayName(), '$client' => $client->getDisplayName(),
'$account' => $accountName, '$account' => $accountName,
'$amount' => Utils::formatMoney($payment->amount, $payment->client->getCurrencyId()) '$amount' => Utils::formatMoney($payment->amount, $client->getCurrencyId())
]; ];
if ($payment->invitation) { if ($payment->invitation) {
@ -98,7 +110,7 @@ class ContactMailer extends Mailer
$variables['$link'] = $payment->invitation->getLink(); $variables['$link'] = $payment->invitation->getLink();
} else { } else {
$user = $payment->user; $user = $payment->user;
$contact = $payment->client->contacts[0]; $contact = $client->contacts[0];
$variables['$link'] = $payment->invoice->invitations[0]->getLink(); $variables['$link'] = $payment->invoice->invitations[0]->getLink();
} }
@ -109,6 +121,8 @@ class ContactMailer extends Mailer
if ($user->email && $contact->email) { if ($user->email && $contact->email) {
$this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data); $this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data);
} }
$account->loadLocalizationSettings();
} }
public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId) public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId)

View File

@ -26,8 +26,14 @@ class AccountRepository
$account->ip = Request::getClientIp(); $account->ip = Request::getClientIp();
$account->account_key = str_random(RANDOM_KEY_LENGTH); $account->account_key = str_random(RANDOM_KEY_LENGTH);
if (Session::has(SESSION_LOCALE)) { // Track referal code
$locale = Session::get(SESSION_LOCALE); 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()) { if ($language = Language::whereLocale($locale)->first()) {
$account->language_id = $language->id; $account->language_id = $language->id;
} }
@ -188,7 +194,7 @@ class AccountRepository
$accountGateway->user_id = $user->id; $accountGateway->user_id = $user->id;
$accountGateway->gateway_id = NINJA_GATEWAY_ID; $accountGateway->gateway_id = NINJA_GATEWAY_ID;
$accountGateway->public_id = 1; $accountGateway->public_id = 1;
$accountGateway->config = NINJA_GATEWAY_CONFIG; $accountGateway->config = env(NINJA_GATEWAY_CONFIG);
$account->account_gateways()->save($accountGateway); $account->account_gateways()->save($accountGateway);
} }
@ -206,7 +212,7 @@ class AccountRepository
$client->public_id = $account->id; $client->public_id = $account->id;
$client->user_id = $ninjaAccount->users()->first()->id; $client->user_id = $ninjaAccount->users()->first()->id;
$client->currency_id = 1; $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; $client->$field = $account->$field;
} }
$ninjaAccount->clients()->save($client); $ninjaAccount->clients()->save($client);

View File

@ -105,6 +105,9 @@ class ClientRepository
if (isset($data['currency_id'])) { if (isset($data['currency_id'])) {
$client->currency_id = $data['currency_id'] ? $data['currency_id'] : null; $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'])) { if (isset($data['payment_terms'])) {
$client->payment_terms = $data['payment_terms']; $client->payment_terms = $data['payment_terms'];
} }

View File

@ -7,9 +7,15 @@ use App\Models\InvoiceItem;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\Product; use App\Models\Product;
use App\Models\Task; use App\Models\Task;
use App\Services\PaymentService;
class InvoiceRepository class InvoiceRepository
{ {
public function __construct(PaymentService $paymentService)
{
$this->paymentService = $paymentService;
}
public function getInvoices($accountId, $clientPublicId = false, $entityType = ENTITY_INVOICE, $filter = false) public function getInvoices($accountId, $clientPublicId = false, $entityType = ENTITY_INVOICE, $filter = false)
{ {
$query = \DB::table('invoices') $query = \DB::table('invoices')
@ -287,6 +293,12 @@ class InvoiceRepository
$invoice->start_date = Utils::toSqlDate($data['start_date']); $invoice->start_date = Utils::toSqlDate($data['start_date']);
$invoice->end_date = Utils::toSqlDate($data['end_date']); $invoice->end_date = Utils::toSqlDate($data['end_date']);
$invoice->due_date = null; $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 { } else {
$invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']); $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']);
$invoice->frequency_id = 0; $invoice->frequency_id = 0;
@ -635,9 +647,15 @@ class InvoiceRepository
$invoice->invitations()->save($invitation); $invoice->invitations()->save($invitation);
} }
$recurInvoice->last_sent_date = Carbon::now()->toDateTimeString(); $recurInvoice->last_sent_date = date('Y-m-d');
$recurInvoice->save(); $recurInvoice->save();
if ($recurInvoice->auto_bill) {
if ($this->paymentService->autoBillInvoice($invoice)) {
$invoice->invoice_status_id = INVOICE_STATUS_PAID;
}
}
return $invoice; return $invoice;
} }
} }

View File

@ -0,0 +1,204 @@
<?php namespace App\Services;
use URL;
use DateTime;
use Event;
use Omnipay;
use Session;
use CreditCard;
use App\Models\Payment;
use App\Models\Account;
use App\Models\Country;
use App\Models\AccountGatewayToken;
use App\Ninja\Repositories\AccountRepository;
use App\Events\InvoicePaid;
class PaymentService {
public $lastError;
public function __construct(AccountRepository $accountRepo)
{
$this->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);
}
}

View File

@ -187,14 +187,11 @@ return [
'Eloquent' => 'Illuminate\Database\Eloquent\Model', 'Eloquent' => 'Illuminate\Database\Eloquent\Model',
'Event' => 'Illuminate\Support\Facades\Event', 'Event' => 'Illuminate\Support\Facades\Event',
'File' => 'Illuminate\Support\Facades\File', 'File' => 'Illuminate\Support\Facades\File',
//'Form' => 'Illuminate\Support\Facades\Form',
'Hash' => 'Illuminate\Support\Facades\Hash', 'Hash' => 'Illuminate\Support\Facades\Hash',
//'HTML' => 'Illuminate\Support\Facades\HTML',
'Input' => 'Illuminate\Support\Facades\Input', 'Input' => 'Illuminate\Support\Facades\Input',
'Lang' => 'Illuminate\Support\Facades\Lang', 'Lang' => 'Illuminate\Support\Facades\Lang',
'Log' => 'Illuminate\Support\Facades\Log', 'Log' => 'Illuminate\Support\Facades\Log',
'Mail' => 'Illuminate\Support\Facades\Mail', 'Mail' => 'Illuminate\Support\Facades\Mail',
//'Paginator' => 'Illuminate\Support\Facades\Paginator',
'Password' => 'Illuminate\Support\Facades\Password', 'Password' => 'Illuminate\Support\Facades\Password',
'Queue' => 'Illuminate\Support\Facades\Queue', 'Queue' => 'Illuminate\Support\Facades\Queue',
'Redirect' => 'Illuminate\Support\Facades\Redirect', 'Redirect' => 'Illuminate\Support\Facades\Redirect',
@ -210,41 +207,8 @@ return [
'Validator' => 'Illuminate\Support\Facades\Validator', 'Validator' => 'Illuminate\Support\Facades\Validator',
'View' => 'Illuminate\Support\Facades\View', '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 // Added Class Aliases
'Utils' => 'App\Libraries\Utils', 'Utils' => 'App\Libraries\Utils',
'Form' => 'Collective\Html\FormFacade', 'Form' => 'Collective\Html\FormFacade',
'HTML' => 'Collective\Html\HtmlFacade', 'HTML' => 'Collective\Html\HtmlFacade',
@ -257,10 +221,8 @@ return [
'ButtonToolbar' => 'Bootstrapper\Facades\ButtonToolbar', 'ButtonToolbar' => 'Bootstrapper\Facades\ButtonToolbar',
'Carousel' => 'Bootstrapper\Facades\Carousel', 'Carousel' => 'Bootstrapper\Facades\Carousel',
'DropdownButton' => 'Bootstrapper\Facades\DropdownButton', 'DropdownButton' => 'Bootstrapper\Facades\DropdownButton',
//'Form' => 'Bootstrapper\Facades\Form', //need to clarify this guy
'Helpers' => 'Bootstrapper\Facades\Helpers', 'Helpers' => 'Bootstrapper\Facades\Helpers',
'Icon' => 'Bootstrapper\Facades\Icon', 'Icon' => 'Bootstrapper\Facades\Icon',
//'Image' => 'Bootstrapper\Facades\Image',
'Label' => 'Bootstrapper\Facades\Label', 'Label' => 'Bootstrapper\Facades\Label',
'MediaObject' => 'Bootstrapper\Facades\MediaObject', 'MediaObject' => 'Bootstrapper\Facades\MediaObject',
'Navbar' => 'Bootstrapper\Facades\Navbar', 'Navbar' => 'Bootstrapper\Facades\Navbar',

View File

@ -0,0 +1,64 @@
<?php
use Illuminate\Database\Migrations\Migration;
class AddAccountDomain extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function ($table) {
$table->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');
}
}

View File

@ -175,8 +175,8 @@ class PaymentLibrariesSeeder extends Seeder
'label' => '20-03-2013 6:15 pm' 'label' => '20-03-2013 6:15 pm'
], ],
[ [
'format' => 'm/d/Y H:i', 'format' => 'm/d/Y g:i',
'format_moment' => 'MM/DD/YYYY HH:mm:ss', 'format_moment' => 'MM/DD/YYYY h:mm:ss',
'label' => '03/20/2013 6:15 pm' 'label' => '03/20/2013 6:15 pm'
] ]
]; ];

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -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) function getClientDisplayName(client)
{ {
var contact = client.contacts ? client.contacts[0] : false; var contact = client.contacts ? client.contacts[0] : false;
if (client.name) { if (client.name) {
return client.name; return client.name;
} else if (contact) { } else if (contact) {
if (contact.first_name || contact.last_name) { return getContactDisplayName(contact);
return contact.first_name + ' ' + contact.last_name;
} else {
return contact.email;
}
} }
return ''; return '';
} }
@ -31806,8 +31814,7 @@ NINJA.invoiceLines = function(invoice) {
var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty));
if (showItemTaxes && tax) { if (showItemTaxes && tax) {
tax = lineTotal * tax / 100; lineTotal += lineTotal * tax / 100;
lineTotal += tax;
} }
lineTotal = formatMoney(lineTotal, currencyId); lineTotal = formatMoney(lineTotal, currencyId);
@ -31820,7 +31827,7 @@ NINJA.invoiceLines = function(invoice) {
row.push({style:["quantity", rowStyle], text:qty || ' '}); row.push({style:["quantity", rowStyle], text:qty || ' '});
} }
if (showItemTaxes) { 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 || ' '}); row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '});
@ -31987,6 +31994,8 @@ NINJA.clientDetails = function(invoice) {
data = [ data = [
{text:clientName || ' ', style: ['clientName']}, {text:clientName || ' ', style: ['clientName']},
{text:client.id_number},
{text:client.vat_number},
{text:client.address1}, {text:client.address1},
{text:client.address2}, {text:client.address2},
{text:cityStatePostal}, {text:cityStatePostal},

View File

@ -266,8 +266,7 @@ NINJA.invoiceLines = function(invoice) {
var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty));
if (showItemTaxes && tax) { if (showItemTaxes && tax) {
tax = lineTotal * tax / 100; lineTotal += lineTotal * tax / 100;
lineTotal += tax;
} }
lineTotal = formatMoney(lineTotal, currencyId); lineTotal = formatMoney(lineTotal, currencyId);
@ -280,7 +279,7 @@ NINJA.invoiceLines = function(invoice) {
row.push({style:["quantity", rowStyle], text:qty || ' '}); row.push({style:["quantity", rowStyle], text:qty || ' '});
} }
if (showItemTaxes) { 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 || ' '}); row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '});
@ -447,6 +446,8 @@ NINJA.clientDetails = function(invoice) {
data = [ data = [
{text:clientName || ' ', style: ['clientName']}, {text:clientName || ' ', style: ['clientName']},
{text:client.id_number},
{text:client.vat_number},
{text:client.address1}, {text:client.address1},
{text:client.address2}, {text:client.address2},
{text:cityStatePostal}, {text:cityStatePostal},

View File

@ -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) function getClientDisplayName(client)
{ {
var contact = client.contacts ? client.contacts[0] : false; var contact = client.contacts ? client.contacts[0] : false;
if (client.name) { if (client.name) {
return client.name; return client.name;
} else if (contact) { } else if (contact) {
if (contact.first_name || contact.last_name) { return getContactDisplayName(contact);
return contact.first_name + ' ' + contact.last_name;
} else {
return contact.email;
}
} }
return ''; return '';
} }

View File

@ -19,8 +19,8 @@ If you'd like to translate the site please use [caouecs/Laravel4-long](https://g
* Built using Laravel 5 * Built using Laravel 5
* Live PDF generation using [pdfmake](http://pdfmake.org/) * Live PDF generation using [pdfmake](http://pdfmake.org/)
* Integrates with 30+ payment providers * Integrates with 30+ payment providers with [OmniPay](https://github.com/thephpleague/omnipay)
* Recurring invoices * Recurring invoices with auto-billing
* Tasks with time-tracking * Tasks with time-tracking
* Multi-user/multi-company support * Multi-user/multi-company support
* Tax rates and payment terms * Tax rates and payment terms

View File

@ -768,5 +768,14 @@
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -767,6 +767,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -35,7 +35,7 @@ return array(
'invoice_number_short' => 'Invoice #', 'invoice_number_short' => 'Invoice #',
'po_number' => 'PO Number', 'po_number' => 'PO Number',
'po_number_short' => 'PO #', 'po_number_short' => 'PO #',
'frequency_id' => 'How often', 'frequency_id' => 'How Often',
'discount' => 'Discount', 'discount' => 'Discount',
'taxes' => 'Taxes', 'taxes' => 'Taxes',
'tax' => 'Tax', 'tax' => 'Tax',
@ -207,7 +207,7 @@ return array(
'client_will_create' => 'client will be created', 'client_will_create' => 'client will be created',
'clients_will_create' => 'clients will be created', 'clients_will_create' => 'clients will be created',
'email_settings' => 'Email Settings', 'email_settings' => 'Email Settings',
'pdf_email_attachment' => 'Attach to Emails', 'pdf_email_attachment' => 'Attach PDFs',
// application messages // application messages
'created_client' => 'Successfully created client', 'created_client' => 'Successfully created client',
@ -767,5 +767,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -745,6 +745,15 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -767,6 +767,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -759,6 +759,15 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -760,6 +760,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',

View File

@ -762,6 +762,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -769,6 +769,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',

View File

@ -767,6 +767,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -762,6 +762,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -762,5 +762,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',
); );

View File

@ -765,6 +765,14 @@ return array(
'status_paid' => 'Paid', 'status_paid' => 'Paid',
'show_line_item_tax' => 'Display <b>line item taxes</b> inline', 'show_line_item_tax' => 'Display <b>line item taxes</b> 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',

View File

@ -17,6 +17,7 @@
)) !!} )) !!}
{{ Former::populate($account) }} {{ Former::populate($account) }}
{{ Former::populateField('military_time', intval($account->military_time)) }}
@if ($showUser) @if ($showUser)
{{ Former::populateField('first_name', $primaryUser->first_name) }} {{ Former::populateField('first_name', $primaryUser->first_name) }}
{{ Former::populateField('last_name', $primaryUser->last_name) }} {{ Former::populateField('last_name', $primaryUser->last_name) }}
@ -37,12 +38,6 @@
<div class="panel-body"> <div class="panel-body">
{!! Former::text('name') !!} {!! Former::text('name') !!}
@if (Auth::user()->isPro() && Utils::isNinja())
{{ Former::setOption('capitalize_translations', false) }}
{!! Former::text('subdomain')->placeholder('texts.www')->onchange('onSubdomainChange()') !!}
@endif
{!! Former::text('id_number') !!} {!! Former::text('id_number') !!}
{!! Former::text('vat_number') !!} {!! Former::text('vat_number') !!}
{!! Former::text('work_email') !!} {!! Former::text('work_email') !!}
@ -111,16 +106,18 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
{!! Former::select('language_id')->addOption('','')
->fromQuery($languages, 'name', 'id') !!}
{!! Former::select('currency_id')->addOption('','') {!! Former::select('currency_id')->addOption('','')
->fromQuery($currencies, 'name', 'id') !!} ->fromQuery($currencies, 'name', 'id') !!}
{!! Former::select('language_id')->addOption('','')
->fromQuery($languages, 'name', 'id') !!}
{!! Former::select('timezone_id')->addOption('','') {!! Former::select('timezone_id')->addOption('','')
->fromQuery($timezones, 'location', 'id') !!} ->fromQuery($timezones, 'location', 'id') !!}
{!! Former::select('date_format_id')->addOption('','') {!! Former::select('date_format_id')->addOption('','')
->fromQuery($dateFormats, 'label', 'id') !!} ->fromQuery($dateFormats, 'label', 'id') !!}
{!! Former::select('datetime_format_id')->addOption('','') {!! Former::select('datetime_format_id')->addOption('','')
->fromQuery($datetimeFormats, 'label', 'id') !!} ->fromQuery($datetimeFormats, 'label', 'id') !!}
{!! Former::checkbox('military_time')->text(trans('texts.enable')) !!}
</div> </div>
</div> </div>
</div> </div>
@ -286,15 +283,6 @@
} }
}); });
} }
function onSubdomainChange() {
var input = $('#subdomain');
var val = input.val();
if (!val) return;
val = val.replace(/[^a-zA-Z0-9_\-]/g, '').toLowerCase().substring(0, {{ MAX_SUBDOMAIN_LENGTH }});
input.val(val);
}
</script> </script>
@stop @stop

View File

@ -67,6 +67,19 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.email_settings') !!}</h3>
</div>
<div class="panel-body">
{{ Former::setOption('capitalize_translations', false) }}
{!! Former::text('subdomain')->placeholder(trans('texts.www'))->onchange('onSubdomainChange()') !!}
{!! Former::text('iframe_url')->placeholder('http://invoices.example.com/')
->onchange('onDomainChange()')->appendIcon('question-sign')->addGroupClass('iframe_url') !!}
{!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.invoice_number') !!}</h3> <h3 class="panel-title">{!! trans('texts.invoice_number') !!}</h3>
@ -90,14 +103,6 @@
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.pdf_settings') !!}</h3>
</div>
<div class="panel-body">
{!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
</div>
</div>
</div> </div>
</div> </div>
@ -114,6 +119,35 @@
</script> </script>
@endif @endif
<div class="modal fade" id="iframeHelpModal" tabindex="-1" role="dialog" aria-labelledby="iframeHelpModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="iframeHelpModalLabel">{{ trans('texts.iframe_url') }}</h4>
</div>
<div class="modal-body">
<p>{{ trans('texts.iframe_url_help1') }}</p>
<pre>&lt;iframe id="iFrame" width="800" height="1000"&gt;&lt;/iframe&gt;
&lt;script language="javascript"&gt;
var iframe = document.getElementById('iFrame');
iframe.src = '{{ SITE_URL }}/view/'
+ window.location.search.substring(1);
&lt;/script&gt;</pre>
<p>{{ trans('texts.iframe_url_help2') }}</p>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div>
</div>
</div>
</div>
{!! Former::close() !!} {!! Former::close() !!}
@ -125,6 +159,22 @@
$('#quote_number_counter').val(disabled ? '' : '{!! $account->quote_number_counter !!}'); $('#quote_number_counter').val(disabled ? '' : '{!! $account->quote_number_counter !!}');
} }
function onSubdomainChange() {
var input = $('#subdomain');
var val = input.val();
if (!val) return;
val = val.replace(/[^a-zA-Z0-9_\-]/g, '').toLowerCase().substring(0, {{ MAX_SUBDOMAIN_LENGTH }});
input.val(val);
}
function onDomainChange() {
}
$('.iframe_url .input-group-addon').click(function() {
$('#iframeHelpModal').modal('show');
});
$(function() { $(function() {
setQuoteNumberEnabled(); setQuoteNumberEnabled();
}); });

View File

@ -102,7 +102,11 @@
<div class="panel-body"> <div class="panel-body">
{!! Former::select('currency_id')->addOption('','') {!! Former::select('currency_id')->addOption('','')
->placeholder($account->currency ? $account->currency->name : '')
->fromQuery($currencies, 'name', 'id') !!} ->fromQuery($currencies, 'name', 'id') !!}
{!! Former::select('language_id')->addOption('','')
->placeholder($account->language ? $account->language->name : '')
->fromQuery($languages, 'name', 'id') !!}
{!! Former::select('payment_terms')->addOption('','') {!! Former::select('payment_terms')->addOption('','')
->fromQuery($paymentTerms, 'name', 'num_days') ->fromQuery($paymentTerms, 'name', 'num_days')
->help(trans('texts.payment_terms_help')) !!} ->help(trans('texts.payment_terms_help')) !!}

View File

@ -99,6 +99,10 @@
<p>{!! $client->getWebsite() !!}</p> <p>{!! $client->getWebsite() !!}</p>
@endif @endif
@if ($client->language)
<p><i class="fa fa-language" style="width: 20px"></i>{{ $client->language->name }}</p>
@endif
<p>{{ $client->payment_terms ? trans('texts.payment_terms') . ": Net " . $client->payment_terms : '' }}</p> <p>{{ $client->payment_terms ? trans('texts.payment_terms') . ": Net " . $client->payment_terms : '' }}</p>
</div> </div>

View File

@ -50,7 +50,7 @@
<div style="display:none"> <div style="display:none">
@endif @endif
{!! Former::select('client')->addOption('', '')->data_bind("dropdown: client")->addGroupClass('client_select closer-row') !!} {!! Former::select('client')->addOption('', '')->data_bind("dropdown: client")->addClass('client-input')->addGroupClass('client_select closer-row') !!}
<div class="form-group" style="margin-bottom: 8px"> <div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4"> <div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4">
@ -101,10 +101,20 @@
{!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!} {!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!}
</div> </div>
@elseif ($invoice && isset($lastSent) && $lastSent) @elseif ($invoice && isset($lastSent) && $lastSent)
<div class="pull-right" style="padding-top: 6px"> <div class="form-group" data-bind="visible: !show_last_sent_date()" >
{!! trans('texts.last_invoice_sent', [ <label for="client" class="control-label col-lg-4 col-sm-4">{{ trans('texts.last_sent') }}</label>
'date' => link_to('/invoices/'.$lastSent->public_id, Utils::dateToString($invoice->last_sent_date), ['id' => 'lastInvoiceSent']) <div class="col-lg-8 col-sm-8">
]) !!} <div style="padding-top:10px">
<a href="#" data-bind="click: $root.clickLastSentDate">{{ Utils::dateToString($invoice->last_sent_date) }}</a> -
{!! link_to('/invoices/'.$lastSent->public_id, trans('texts.view_invoice'), ['id' => 'lastInvoiceSent', 'target' => '_blank']) !!}
</div>
</div>
</div>
<div data-bind="visible: show_last_sent_date()" style="display:none">
{!! Former::text('last_sent_date')->data_bind("datePicker: last_sent_date, valueUpdate: 'afterkeydown', visible: show_last_sent_date()")
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))
->label(trans('texts.last_sent'))
->appendIcon('calendar')->addGroupClass('last_sent_date') !!}
</div> </div>
@endif @endif
@endif @endif
@ -113,7 +123,15 @@
<div class="col-md-4" id="col_2"> <div class="col-md-4" id="col_2">
<span data-bind="visible: !is_recurring()"> <span data-bind="visible: !is_recurring()">
{!! Former::text('invoice_number')->label(trans("texts.{$entityType}_number_short"))->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!} {!! Former::text('invoice_number')
->label(trans("texts.{$entityType}_number_short"))
->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!}
</span>
<span data-bind="visible: is_recurring()" style="display: none">
{!! Former::checkbox('auto_bill')
->label(trans('texts.auto_bill'))
->text(trans('texts.enable'))
->data_bind("checked: auto_bill, valueUpdate: 'afterkeydown'") !!}
</span> </span>
{!! Former::text('po_number')->label(trans('texts.po_number_short'))->data_bind("value: po_number, valueUpdate: 'afterkeydown'") !!} {!! Former::text('po_number')->label(trans('texts.po_number_short'))->data_bind("value: po_number, valueUpdate: 'afterkeydown'") !!}
{!! Former::text('discount')->data_bind("value: discount, valueUpdate: 'afterkeydown'") {!! Former::text('discount')->data_bind("value: discount, valueUpdate: 'afterkeydown'")
@ -124,7 +142,7 @@
<div class="form-group" style="margin-bottom: 8px"> <div class="form-group" style="margin-bottom: 8px">
<label for="taxes" class="control-label col-lg-4 col-sm-4">{{ trans('texts.taxes') }}</label> <label for="taxes" class="control-label col-lg-4 col-sm-4">{{ trans('texts.taxes') }}</label>
<div class="col-lg-8 col-sm-8" style="padding-top: 7px"> <div class="col-lg-8 col-sm-8" style="padding-top: 10px">
<a href="#" data-bind="click: $root.showTaxesForm"><i class="glyphicon glyphicon-list-alt"></i> {{ trans('texts.manage_rates') }}</a> <a href="#" data-bind="click: $root.showTaxesForm"><i class="glyphicon glyphicon-list-alt"></i> {{ trans('texts.manage_rates') }}</a>
</div> </div>
</div> </div>
@ -439,10 +457,16 @@
&nbsp; &nbsp;
</span> </span>
{!! Former::select('currency_id')->addOption('','')->data_bind('value: currency_id') {!! Former::select('currency_id')->addOption('','')
->placeholder($account->currency ? $account->currency->name : '')
->data_bind('value: currency_id')
->fromQuery($currencies, 'name', 'id') !!} ->fromQuery($currencies, 'name', 'id') !!}
<span data-bind="visible: $root.showMore"> <span data-bind="visible: $root.showMore">
{!! Former::select('language_id')->addOption('','')
->placeholder($account->language ? $account->language->name : '')
->data_bind('value: language_id')
->fromQuery($languages, 'name', 'id') !!}
{!! Former::select('payment_terms')->addOption('','')->data_bind('value: payment_terms') {!! Former::select('payment_terms')->addOption('','')->data_bind('value: payment_terms')
->fromQuery($paymentTerms, 'name', 'num_days') ->fromQuery($paymentTerms, 'name', 'num_days')
->help(trans('texts.payment_terms_help')) !!} ->help(trans('texts.payment_terms_help')) !!}
@ -569,7 +593,7 @@
$('[rel=tooltip]').tooltip({'trigger':'manual'}); $('[rel=tooltip]').tooltip({'trigger':'manual'});
$('#invoice_date, #due_date, #start_date, #end_date').datepicker(); $('#invoice_date, #due_date, #start_date, #end_date, #last_sent_date').datepicker();
@if ($client && !$invoice) @if ($client && !$invoice)
$('input[name=client]').val({{ $client->public_id }}); $('input[name=client]').val({{ $client->public_id }});
@ -586,6 +610,8 @@
var clientId = parseInt($('input[name=client]').val(), 10); var clientId = parseInt($('input[name=client]').val(), 10);
if (clientId > 0) { if (clientId > 0) {
model.loadClient(clientMap[clientId]); model.loadClient(clientMap[clientId]);
// we enable searching by contact but the selection must be the client
$('.client-input').val(getClientDisplayName(clientMap[clientId]));
} else { } else {
model.loadClient($.parseJSON(ko.toJSON(new ClientModel()))); model.loadClient($.parseJSON(ko.toJSON(new ClientModel())));
model.invoice().client().country = false; model.invoice().client().country = false;
@ -610,7 +636,7 @@
showLearnMore(); showLearnMore();
}); });
var fields = ['invoice_date', 'due_date', 'start_date', 'end_date']; var fields = ['invoice_date', 'due_date', 'start_date', 'end_date', 'last_sent_date'];
for (var i=0; i<fields.length; i++) { for (var i=0; i<fields.length; i++) {
var field = fields[i]; var field = fields[i];
(function (_field) { (function (_field) {
@ -796,7 +822,7 @@
return; return;
} }
if (confirm('{!! trans("texts.confirm_recurring_email_$entityType") !!}' + '\n\n' + getSendToEmails() + '\n' + '{!! trans("texts.confirm_recurring_timing") !!}')) { if (confirm("{!! trans("texts.confirm_recurring_email_$entityType") !!}" + '\n\n' + getSendToEmails() + '\n' + "{!! trans("texts.confirm_recurring_timing") !!}")) {
submitAction(''); submitAction('');
} }
} else { } else {
@ -936,10 +962,15 @@
function ViewModel(data) { function ViewModel(data) {
var self = this; var self = this;
self.showMore = ko.observable(false); self.showMore = ko.observable(false);
//self.invoice = data ? false : new InvoiceModel(); //self.invoice = data ? false : new InvoiceModel();
self.invoice = ko.observable(data ? false : new InvoiceModel()); self.invoice = ko.observable(data ? false : new InvoiceModel());
self.tax_rates = ko.observableArray(); self.tax_rates = ko.observableArray();
self.clickLastSentDate = function() {
self.invoice().show_last_sent_date(true);
}
self.loadClient = function(client) { self.loadClient = function(client) {
ko.mapping.fromJS(client, model.invoice().client().mapping, model.invoice().client); ko.mapping.fromJS(client, model.invoice().client().mapping, model.invoice().client);
@if (!$invoice) @if (!$invoice)
@ -1175,9 +1206,11 @@
self.due_date = ko.observable(''); self.due_date = ko.observable('');
self.start_date = ko.observable('{{ Utils::today() }}'); self.start_date = ko.observable('{{ Utils::today() }}');
self.end_date = ko.observable(''); self.end_date = ko.observable('');
self.last_sent_date = ko.observable('');
self.tax_name = ko.observable(); self.tax_name = ko.observable();
self.tax_rate = ko.observable(); self.tax_rate = ko.observable();
self.is_recurring = ko.observable({{ $isRecurring ? 'true' : 'false' }}); self.is_recurring = ko.observable({{ $isRecurring ? 'true' : 'false' }});
self.auto_bill = ko.observable();
self.invoice_status_id = ko.observable(0); self.invoice_status_id = ko.observable(0);
self.invoice_items = ko.observableArray(); self.invoice_items = ko.observableArray();
self.amount = ko.observable(0); self.amount = ko.observable(0);
@ -1185,6 +1218,7 @@
self.invoice_design_id = ko.observable({{ $account->invoice_design_id }}); self.invoice_design_id = ko.observable({{ $account->invoice_design_id }});
self.partial = ko.observable(0); self.partial = ko.observable(0);
self.has_tasks = ko.observable(false); self.has_tasks = ko.observable(false);
self.show_last_sent_date = ko.observable(false);
self.custom_value1 = ko.observable(0); self.custom_value1 = ko.observable(0);
self.custom_value2 = ko.observable(0); self.custom_value2 = ko.observable(0);
@ -1489,6 +1523,7 @@
self.size_id = ko.observable(''); self.size_id = ko.observable('');
self.industry_id = ko.observable(''); self.industry_id = ko.observable('');
self.currency_id = ko.observable(''); self.currency_id = ko.observable('');
self.language_id = ko.observable('');
self.website = ko.observable(''); self.website = ko.observable('');
self.payment_terms = ko.observable(0); self.payment_terms = ko.observable(0);
self.contacts = ko.observableArray(); self.contacts = ko.observableArray();
@ -1814,14 +1849,19 @@
for (var i=0; i<clients.length; i++) { for (var i=0; i<clients.length; i++) {
var client = clients[i]; var client = clients[i];
var clientName = getClientDisplayName(client);
for (var j=0; j<client.contacts.length; j++) { for (var j=0; j<client.contacts.length; j++) {
var contact = client.contacts[j]; var contact = client.contacts[j];
var contactName = getContactDisplayName(contact);
if (contact.is_primary) { if (contact.is_primary) {
contact.send_invoice = true; contact.send_invoice = true;
} }
if (clientName != contactName) {
$clientSelect.append(new Option(contactName, client.public_id));
}
} }
clientMap[client.public_id] = client; clientMap[client.public_id] = client;
$clientSelect.append(new Option(getClientDisplayName(client), client.public_id)); $clientSelect.append(new Option(clientName, client.public_id));
} }
@if ($data) @if ($data)

View File

@ -133,12 +133,13 @@
}) })
window.onDatatableReady = function() { window.onDatatableReady = function() {
$(':checkbox').click(function() { $(':checkbox').unbind('click').click(function() {
setBulkActionsEnabled(); setBulkActionsEnabled();
}); });
$('tbody tr').click(function(event) { $('tbody tr').unbind('click').click(function(event) {
if (event.target.type !== 'checkbox' && event.target.type !== 'button' && event.target.tagName.toLowerCase() !== 'a') { if (event.target.type !== 'checkbox' && event.target.type !== 'button' && event.target.tagName.toLowerCase() !== 'a') {
console.log('click');
$checkbox = $(this).closest('tr').find(':checkbox:not(:disabled)'); $checkbox = $(this).closest('tr').find(':checkbox:not(:disabled)');
var checked = $checkbox.prop('checked'); var checked = $checkbox.prop('checked');
$checkbox.prop('checked', !checked); $checkbox.prop('checked', !checked);

View File

@ -0,0 +1,11 @@
@extends('master')
@section('body')
<center style="padding-top:160px">
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/round_logo.png') }}"/>
</a>
</center>
@stop

View File

@ -30,7 +30,7 @@
"table": { "table": {
"body": "$invoiceDetails" "body": "$invoiceDetails"
}, },
"margin": [0, 4, 12, 4], "margin": [0, 0, 12, 4],
"layout": "noBorders" "layout": "noBorders"
}, },
{ {
@ -48,8 +48,8 @@
"hLineColor": "#D8D8D8", "hLineColor": "#D8D8D8",
"paddingLeft": "$amount:8", "paddingLeft": "$amount:8",
"paddingRight": "$amount:8", "paddingRight": "$amount:8",
"paddingTop": "$amount:4", "paddingTop": "$amount:6",
"paddingBottom": "$amount:4" "paddingBottom": "$amount:2"
} }
}, },
{ {

View File

@ -0,0 +1,60 @@
<?php
use Codeception\Util\Fixtures;
use \AcceptanceTester;
use Faker\Factory;
class GoProCest
{
private $faker;
public function _before(AcceptanceTester $I)
{
$this->faker = Factory::create();
}
public function signUpAndGoPro(AcceptanceTester $I)
{
$userEmail = $this->faker->safeEmail;
$userPassword = $this->faker->password;
$I->wantTo('test purchasing a pro plan');
$I->amOnPage('/invoice_now');
$I->click('Sign Up');
$I->wait(1);
$I->fillField(['name' =>'new_first_name'], $this->faker->firstName);
$I->fillField(['name' =>'new_last_name'], $this->faker->lastName);
$I->fillField(['name' =>'new_email'], $userEmail);
$I->fillField(['name' =>'new_password'], $userPassword);
$I->checkOption('#terms_checkbox');
$I->click('Save');
$I->wait(1);
$I->amOnPage('/dashboard');
$I->click('Go Pro');
$I->wait(1);
$I->click('Upgrade Now!');
$I->wait(1);
$I->fillField(['name' => 'address1'], $this->faker->streetAddress);
$I->fillField(['name' => 'address2'], $this->faker->streetAddress);
$I->fillField(['name' => 'city'], $this->faker->city);
$I->fillField(['name' => 'state'], $this->faker->state);
$I->fillField(['name' => 'postal_code'], $this->faker->postcode);
$I->selectDropdown($I, 'United States', '.country-select .dropdown-toggle');
$I->fillField(['name' => 'card_number'], '4242424242424242');
$I->fillField(['name' => 'cvv'], '1234');
$I->selectOption('#expiration_month', 12);
$I->selectOption('#expiration_year', date('Y'));
$I->click('.btn-success');
$I->wait(1);
$I->see('Successfully applied payment');
$I->amOnPage('/dashboard');
$I->dontSee('Go Pro');
}
}

View File

@ -79,5 +79,14 @@ class OnlinePaymentCest
$I->click('.btn-success'); $I->click('.btn-success');
$I->see('Successfully applied payment'); $I->see('Successfully applied payment');
}); });
// create recurring invoice and auto-bill
$I->amOnPage('/recurring_invoices/create');
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
$I->checkOption('#auto_bill');
$I->executeJS('preparePdfData(\'email\')');
$I->see("$0.00");
} }
} }