diff --git a/.env.example b/.env.example index c0d6668fb52a..41ce2039f7e5 100644 --- a/.env.example +++ b/.env.example @@ -43,4 +43,28 @@ API_SECRET=password #GOOGLE_CLIENT_SECRET= #GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google -#GOOGLE_MAPS_API_KEY= \ No newline at end of file +#GOOGLE_MAPS_API_KEY= + +#S3_KEY= +#S3_SECRET= +#S3_REGION= +#S3_BUCKET= + +#RACKSPACE_USERNAME= +#RACKSPACE_KEY= +#RACKSPACE_CONTAINER= +#RACKSPACE_REGION= + +#RACKSPACE_TEMP_URL_SECRET= + +# If this is set to anything, the URL secret will be set the next +# time a file is downloaded through the client portal. +# Only set this temporarily, as it slows things down. +#RACKSPACE_TEMP_URL_SECRET_SET= + +#DOCUMENT_FILESYSTEM= + +#MAX_DOCUMENT_SIZE # KB +#MAX_EMAIL_DOCUMENTS_SIZE # Total KB +#MAX_ZIP_DOCUMENTS_SIZE # Total KB (uncompressed) +#DOCUMENT_PREVIEW_SIZE # Pixels \ No newline at end of file diff --git a/.htaccess b/.htaccess index 27a6945d38f8..8b1f582ca455 100644 --- a/.htaccess +++ b/.htaccess @@ -6,3 +6,141 @@ # https://coderwall.com/p/erbaig/laravel-s-htaccess-to-remove-public-from-url # RewriteRule ^(.*)$ public/$1 [L] + +# https://github.com/h5bp/server-configs-apache/blob/master/dist/.htaccess + + +# ###################################################################### +# # INTERNET EXPLORER # +# ###################################################################### + +# ---------------------------------------------------------------------- +# | Iframes cookies | +# ---------------------------------------------------------------------- + +# Allow cookies to be set from iframes in Internet Explorer. +# +# https://msdn.microsoft.com/en-us/library/ms537343.aspx +# http://www.w3.org/TR/2000/CR-P3P-20001215/ + + + Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" + + + +# ###################################################################### +# # MEDIA TYPES AND CHARACTER ENCODINGS # +# ###################################################################### + +# ---------------------------------------------------------------------- +# | Character encodings | +# ---------------------------------------------------------------------- + +# Serve all resources labeled as `text/html` or `text/plain` +# with the media type `charset` parameter set to `UTF-8`. +# +# https://httpd.apache.org/docs/current/mod/core.html#adddefaultcharset + +AddDefaultCharset utf-8 + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Serve the following file types with the media type `charset` +# parameter set to `UTF-8`. +# +# https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset + + + AddCharset utf-8 .atom \ + .bbaw \ + .css \ + .geojson \ + .js \ + .json \ + .jsonld \ + .manifest \ + .rdf \ + .rss \ + .topojson \ + .vtt \ + .webapp \ + .webmanifest \ + .xloc \ + .xml + + + +# ###################################################################### +# # WEB PERFORMANCE # +# ###################################################################### + +# ---------------------------------------------------------------------- +# | Compression | +# ---------------------------------------------------------------------- + + + + # Force compression for mangled headers. + # https://developer.yahoo.com/blogs/ydn/pushing-beyond-gzipping-25601.html + + + + SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding + RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding + + + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + # Map certain file types to the specified encoding type in order to + # make Apache serve them with the appropriate `Content-Encoding` HTTP + # response header (this will NOT make Apache compress them!). + + # If the following file types wouldn't be served without the appropriate + # `Content-Enable` HTTP response header, client applications (e.g.: + # browsers) wouldn't know that they first need to uncompress the response, + # and thus, wouldn't be able to understand the content. + + # http://httpd.apache.org/docs/current/mod/mod_mime.html#addencoding + + + AddEncoding gzip svgz + + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + # Compress all output labeled with one of the following media types. + + # IMPORTANT: For Apache versions below 2.3.7 you don't need to enable + # `mod_filter` and can remove the `` & `` + # lines as `AddOutputFilterByType` is still in the core directives. + + + AddOutputFilterByType DEFLATE "application/atom+xml" \ + "application/javascript" \ + "application/json" \ + "application/ld+json" \ + "application/manifest+json" \ + "application/rdf+xml" \ + "application/rss+xml" \ + "application/schema+json" \ + "application/vnd.geo+json" \ + "application/vnd.ms-fontobject" \ + "application/x-font-ttf" \ + "application/x-web-app-manifest+json" \ + "application/xhtml+xml" \ + "application/xml" \ + "font/opentype" \ + "image/svg+xml" \ + "image/x-icon" \ + "text/cache-manifest" \ + "text/css" \ + "text/html" \ + "text/javascript" \ + "text/plain" \ + "text/vtt" \ + "text/x-component" \ + "text/xml" + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index f76fe32447b1..1f66f7aa29f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,8 @@ before_script: - php artisan key:generate --no-interaction - sed -i 's/APP_ENV=production/APP_ENV=development/g' .env - sed -i 's/APP_DEBUG=false/APP_DEBUG=true/g' .env - - sed -i 's/REQUIRE_HTTPS=false/NINJA_DEV=true/g' .env + - sed -i '$a NINJA_DEV=true' .env + - sed -i '$a TRAVIS=true' .env # create the database and user - mysql -u root -e "create database IF NOT EXISTS ninja;" - mysql -u root -e "GRANT ALL PRIVILEGES ON ninja.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;" @@ -87,7 +88,10 @@ after_script: - mysql -u root -e 'select * from clients;' ninja - mysql -u root -e 'select * from invoices;' ninja - mysql -u root -e 'select * from invoice_items;' ninja - - cat storage/logs/laravel.log + - mysql -u root -e 'select * from payments;' ninja + - mysql -u root -e 'select * from credits;' ninja + - cat storage/logs/laravel-error.log + - cat storage/logs/laravel-info.log notifications: email: diff --git a/Gruntfile.js b/Gruntfile.js index e0a13ccd6a0d..ce22ba6de51a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -95,6 +95,7 @@ module.exports = function(grunt) { 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.no.min.js', 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.es.min.js', 'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.sv.min.js', + 'public/vendor/dropzone/dist/min/dropzone.min.js', 'public/vendor/typeahead.js/dist/typeahead.jquery.min.js', 'public/vendor/accounting/accounting.min.js', 'public/vendor/spectrum/spectrum.js', @@ -137,6 +138,7 @@ module.exports = function(grunt) { 'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css', 'public/vendor/font-awesome/css/font-awesome.min.css', 'public/vendor/bootstrap-datepicker/dist/css/bootstrap-datepicker3.css', + 'public/vendor/dropzone/dist/min/dropzone.min.css', 'public/vendor/spectrum/spectrum.css', 'public/css/bootstrap-combobox.css', 'public/css/typeahead.js-bootstrap.css', @@ -169,7 +171,7 @@ module.exports = function(grunt) { 'public/js/pdf_viewer.js', 'public/js/compatibility.js', 'public/js/pdfmake.min.js', - 'public/js/vfs_fonts.js', + 'public/js/vfs.js', ], dest: 'public/pdf.built.js', nonull: true diff --git a/app/Commands/Command.php b/app/Commands/Command.php index 5bc48501167e..d6a8d61150ae 100644 --- a/app/Commands/Command.php +++ b/app/Commands/Command.php @@ -1,4 +1,4 @@ -faker = Factory::create(); + + $this->clientRepo = $clientRepo; + $this->invoiceRepo = $invoiceRepo; + $this->paymentRepo = $paymentRepo; + $this->vendorRepo = $vendorRepo; + $this->expenseRepo = $expenseRepo; + } + + public function fire() + { + if (Utils::isNinjaProd()) { + return false; + } + + $this->info(date('Y-m-d').' Running CreateTestData...'); + + Auth::loginUsingId(1); + $this->count = $this->argument('count'); + + $this->createClients(); + $this->createVendors(); + + $this->info('Done'); + } + + private function createClients() + { + for ($i=0; $i<$this->count; $i++) { + $data = [ + 'name' => $this->faker->name, + 'address1' => $this->faker->streetAddress, + 'address2' => $this->faker->secondaryAddress, + 'city' => $this->faker->city, + 'state' => $this->faker->state, + 'postal_code' => $this->faker->postcode, + 'contacts' => [[ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $this->faker->safeEmail, + 'phone' => $this->faker->phoneNumber, + ]] + ]; + + $client = $this->clientRepo->save($data); + $this->info('Client: ' . $client->name); + + $this->createInvoices($client); + } + } + + private function createInvoices($client) + { + for ($i=0; $i<$this->count; $i++) { + $data = [ + 'client_id' => $client->id, + 'invoice_items' => [[ + 'product_key' => $this->faker->word, + 'qty' => $this->faker->randomDigit + 1, + 'cost' => $this->faker->randomFloat(2, 1, 10), + 'notes' => $this->faker->text($this->faker->numberBetween(50, 300)) + ]] + ]; + + $invoice = $this->invoiceRepo->save($data); + $this->info('Invoice: ' . $invoice->invoice_number); + + $this->createPayment($client, $invoice); + } + } + + private function createPayment($client, $invoice) + { + $data = [ + 'invoice_id' => $invoice->id, + 'client_id' => $client->id, + 'amount' => $this->faker->randomFloat(2, 0, $invoice->amount) + ]; + + $payment = $this->paymentRepo->save($data); + + $this->info('Payment: ' . $payment->amount); + } + + private function createVendors() + { + for ($i=0; $i<$this->count; $i++) { + $data = [ + 'name' => $this->faker->name, + 'address1' => $this->faker->streetAddress, + 'address2' => $this->faker->secondaryAddress, + 'city' => $this->faker->city, + 'state' => $this->faker->state, + 'postal_code' => $this->faker->postcode, + 'vendor_contacts' => [[ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $this->faker->safeEmail, + 'phone' => $this->faker->phoneNumber, + ]] + ]; + + $vendor = $this->vendorRepo->save($data); + $this->info('Vendor: ' . $vendor->name); + + $this->createExpense($vendor); + } + } + + private function createExpense($vendor) + { + for ($i=0; $i<$this->count; $i++) { + $data = [ + 'vendor_id' => $vendor->id, + 'amount' => $this->faker->randomFloat(2, 1, 10), + 'expense_date' => null, + 'public_notes' => null, + ]; + + $expense = $this->expenseRepo->save($data); + $this->info('Expense: ' . $expense->amount); + } + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + //array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null), + ); + } +} diff --git a/app/Console/Commands/GenerateResources.php b/app/Console/Commands/GenerateResources.php index a1dc404f79bb..e7139c85be63 100644 --- a/app/Console/Commands/GenerateResources.php +++ b/app/Console/Commands/GenerateResources.php @@ -1,4 +1,4 @@ -info(date('Y-m-d').' Running PruneData...'); + + // delete accounts who never registered, didn't create any invoices, + // hansn't logged in within the past 6 months and isn't linked to another account + $sql = 'select a.id + from (select id, last_login from accounts) a + left join users u on u.account_id = a.id and u.public_id = 0 + left join invoices i on i.account_id = a.id + left join user_accounts ua1 on ua1.user_id1 = u.id + left join user_accounts ua2 on ua2.user_id2 = u.id + left join user_accounts ua3 on ua3.user_id3 = u.id + left join user_accounts ua4 on ua4.user_id4 = u.id + left join user_accounts ua5 on ua5.user_id5 = u.id + where u.registered = 0 + and a.last_login < DATE_SUB(now(), INTERVAL 6 MONTH) + and (ua1.id is null and ua2.id is null and ua3.id is null and ua4.id is null and ua5.id is null) + group by a.id + having count(i.id) = 0'; + + $results = DB::select($sql); + + foreach ($results as $result) { + $this->info("Deleting {$result->id}"); + DB::table('accounts') + ->where('id', '=', $result->id) + ->delete(); + } + + $this->info('Done'); + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + //array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null), + ); + } +} diff --git a/app/Console/Commands/RemoveOrphanedDocuments.php b/app/Console/Commands/RemoveOrphanedDocuments.php new file mode 100644 index 000000000000..3c7fe1bb537f --- /dev/null +++ b/app/Console/Commands/RemoveOrphanedDocuments.php @@ -0,0 +1,41 @@ +info(date('Y-m-d').' Running RemoveOrphanedDocuments...'); + + $documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', array(new DateTime('-1 hour'))) + ->get(); + + $this->info(count($documents).' orphaned document(s) found'); + + foreach ($documents as $document) { + $document->delete(); + } + + $this->info('Done'); + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + //array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null), + ); + } +} diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php index 3243ac016eaf..58c38a7d8163 100644 --- a/app/Console/Commands/SendReminders.php +++ b/app/Console/Commands/SendReminders.php @@ -36,7 +36,7 @@ class SendReminders extends Command $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { - if (!$account->isPro()) { + if (!$account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { continue; } diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php index 0faf1304a964..87a840aed2c0 100644 --- a/app/Console/Commands/SendRenewalInvoices.php +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -5,7 +5,7 @@ use DateTime; use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; -use App\Models\Account; +use App\Models\Company; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\AccountRepository; @@ -30,32 +30,44 @@ class SendRenewalInvoices extends Command $today = new DateTime(); $sentTo = []; - // get all accounts with pro plans expiring in 10 days - $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355') + // get all accounts with plans expiring in 10 days + $companies = Company::whereRaw('datediff(plan_expires, curdate()) = 10') ->orderBy('id') ->get(); - $this->info(count($accounts).' accounts found'); + $this->info(count($companies).' companies found'); - foreach ($accounts as $account) { - // don't send multiple invoices to multi-company users - if ($userAccountId = $this->accountRepo->getUserAccountId($account)) { - if (isset($sentTo[$userAccountId])) { - continue; - } else { - $sentTo[$userAccountId] = true; - } + foreach ($companies as $company) { + if (!count($company->accounts)) { + continue; } - + + $account = $company->accounts->sortBy('id')->first(); + $plan = $company->plan; + $term = $company->plan_term; + + if ($company->pending_plan) { + $plan = $company->pending_plan; + $term = $company->pending_term; + } + + if ($plan == PLAN_FREE || !$plan || !$term ){ + continue; + } + $client = $this->accountRepo->getNinjaClient($account); - $invitation = $this->accountRepo->createNinjaInvoice($client, $account); + $invitation = $this->accountRepo->createNinjaInvoice($client, $account, $plan, $term); // set the due date to 10 days from now $invoice = $invitation->invoice; $invoice->due_date = date('Y-m-d', strtotime('+ 10 days')); $invoice->save(); - $this->mailer->sendInvoice($invoice); - $this->info("Sent invoice to {$client->getDisplayName()}"); + if ($term == PLAN_TERM_YEARLY) { + $this->mailer->sendInvoice($invoice); + $this->info("Sent {$term}ly {$plan} invoice to {$client->getDisplayName()}"); + } else { + $this->info("Created {$term}ly {$plan} invoice for {$client->getDisplayName()}"); + } } $this->info('Done'); diff --git a/app/Console/Commands/TestOFX.php b/app/Console/Commands/TestOFX.php index 243e30744831..f7eb2b748b03 100644 --- a/app/Console/Commands/TestOFX.php +++ b/app/Console/Commands/TestOFX.php @@ -1,4 +1,4 @@ - get_class($e), 'hideHeader' => true, diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php index 02697ba039de..65175cdd5ba1 100644 --- a/app/Http/Controllers/AccountApiController.php +++ b/app/Http/Controllers/AccountApiController.php @@ -34,6 +34,13 @@ class AccountApiController extends BaseAPIController $this->accountRepo = $accountRepo; } + public function ping() + { + $headers = Utils::getApiHeaders(); + + return Response::make(RESULT_SUCCESS, 200, $headers); + } + public function register(RegisterRequest $request) { diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 912ade56f962..b3bd13fad409 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -9,6 +9,7 @@ use Session; use Utils; use Validator; use View; +use URL; use stdClass; use Cache; use Response; @@ -18,6 +19,7 @@ use App\Models\License; use App\Models\Invoice; use App\Models\User; use App\Models\Account; +use App\Models\Document; use App\Models\Gateway; use App\Models\InvoiceDesign; use App\Models\TaxRate; @@ -29,6 +31,7 @@ use App\Ninja\Mailers\ContactMailer; use App\Events\UserSignedUp; use App\Events\UserSettingsChanged; use App\Services\AuthService; +use App\Services\PaymentService; use App\Http\Requests\UpdateAccountRequest; @@ -38,8 +41,9 @@ class AccountController extends BaseController protected $userMailer; protected $contactMailer; protected $referralRepository; + protected $paymentService; - public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository) + public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository, PaymentService $paymentService) { //parent::__construct(); @@ -47,6 +51,7 @@ class AccountController extends BaseController $this->userMailer = $userMailer; $this->contactMailer = $contactMailer; $this->referralRepository = $referralRepository; + $this->paymentService = $paymentService; } public function demo() @@ -109,10 +114,135 @@ class AccountController extends BaseController public function enableProPlan() { - $invitation = $this->accountRepo->enableProPlan(); + if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { + return false; + } + + $invitation = $this->accountRepo->enablePlan(); return $invitation->invitation_key; } + + public function changePlan() { + $user = Auth::user(); + $account = $user->account; + + $plan = Input::get('plan'); + $term = Input::get('plan_term'); + + $planDetails = $account->getPlanDetails(false, false); + + $credit = 0; + if ($planDetails) { + if ($planDetails['plan'] == PLAN_PRO && $plan == PLAN_ENTERPRISE) { + // Upgrade from pro to enterprise + if($planDetails['term'] == PLAN_TERM_YEARLY && $term == PLAN_TERM_MONTHLY) { + // Upgrade to yearly for now; switch to monthly in a year + $pending_monthly = true; + $term = PLAN_TERM_YEARLY; + } + + $new_plan = array( + 'plan' => PLAN_ENTERPRISE, + 'term' => $term, + ); + } elseif ($planDetails['plan'] == $plan) { + // Term switch + if ($planDetails['term'] == PLAN_TERM_YEARLY && $term == PLAN_TERM_MONTHLY) { + $pending_change = array( + 'plan' => $plan, + 'term' => $term + ); + } elseif ($planDetails['term'] == PLAN_TERM_MONTHLY && $term == PLAN_TERM_YEARLY) { + $new_plan = array( + 'plan' => $plan, + 'term' => $term, + ); + } else { + // Cancel the pending change + $account->company->pending_plan = null; + $account->company->pending_term = null; + $account->company->save(); + Session::flash('message', trans('texts.updated_plan')); + } + } elseif (!empty($planDetails['started'])) { + // Downgrade + $refund_deadline = clone $planDetails['started']; + $refund_deadline->modify('+30 days'); + + if ($plan == PLAN_FREE && $refund_deadline >= date_create()) { + // Refund + $account->company->plan = null; + $account->company->plan_term = null; + $account->company->plan_started = null; + $account->company->plan_expires = null; + $account->company->plan_paid = null; + $account->company->pending_plan = null; + $account->company->pending_term = null; + + if ($account->company->payment) { + $payment = $account->company->payment; + + $gateway = $this->paymentService->createGateway($payment->account_gateway); + $refund = $gateway->refund(array( + 'transactionReference' => $payment->transaction_reference, + 'amount' => $payment->amount + )); + $refund->send(); + $payment->delete(); + Session::flash('message', trans('texts.plan_refunded')); + \Log::info("Refunded Plan Payment: {$account->name} - {$user->email}"); + } else { + Session::flash('message', trans('texts.updated_plan')); + } + + $account->company->save(); + + } else { + $pending_change = array( + 'plan' => $plan, + 'term' => $plan == PLAN_FREE ? null : $term, + ); + } + } + + if (!empty($new_plan)) { + $time_used = $planDetails['paid']->diff(date_create()); + $days_used = $time_used->days; + + if ($time_used->invert) { + // They paid in advance + $days_used *= -1; + } + + $days_total = $planDetails['paid']->diff($planDetails['expires'])->days; + + $percent_used = $days_used / $days_total; + $old_plan_price = Account::$plan_prices[$planDetails['plan']][$planDetails['term']]; + $credit = $old_plan_price * (1 - $percent_used); + } + } else { + $new_plan = array( + 'plan' => $plan, + 'term' => $term, + ); + } + + if (!empty($pending_change) && empty($new_plan)) { + $account->company->pending_plan = $pending_change['plan']; + $account->company->pending_term = $pending_change['term']; + $account->company->save(); + + Session::flash('message', trans('texts.updated_plan')); + } + + if (!empty($new_plan)) { + $invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly)); + return Redirect::to('view/'.$invitation->invitation_key); + } + + return Redirect::to('/settings/'.ACCOUNT_MANAGEMENT, 301); + } public function setTrashVisible($entityType, $visible) { @@ -123,8 +253,7 @@ class AccountController extends BaseController public function getSearchData() { - $account = Auth::user()->account; - $data = $this->accountRepo->getSearchData($account); + $data = $this->accountRepo->getSearchData(Auth::user()); return Response::json($data); } @@ -147,6 +276,8 @@ class AccountController extends BaseController return self::showInvoiceSettings(); } elseif ($section == ACCOUNT_IMPORT_EXPORT) { return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); + } elseif ($section == ACCOUNT_MANAGEMENT) { + return self::showAccountManagement(); } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { return self::showInvoiceDesign($section); } elseif ($section == ACCOUNT_CLIENT_PORTAL) { @@ -230,11 +361,23 @@ class AccountController extends BaseController return View::make('accounts.details', $data); } + private function showAccountManagement() + { + $account = Auth::user()->account; + $data = [ + 'account' => $account, + 'planDetails' => $account->getPlanDetails(true), + 'title' => trans('texts.account_management'), + ]; + + return View::make('accounts.management', $data); + } + public function showUserDetails() { $oauthLoginUrls = []; foreach (AuthService::$providers as $provider) { - $oauthLoginUrls[] = ['label' => $provider, 'url' => '/auth/'.strtolower($provider)]; + $oauthLoginUrls[] = ['label' => $provider, 'url' => URL::to('/auth/'.strtolower($provider))]; } $data = [ @@ -347,6 +490,7 @@ class AccountController extends BaseController $client = new stdClass(); $contact = new stdClass(); $invoiceItem = new stdClass(); + $document = new stdClass(); $client->name = 'Sample Client'; $client->address1 = trans('texts.address1'); @@ -372,8 +516,12 @@ class AccountController extends BaseController $invoiceItem->notes = 'Notes'; $invoiceItem->product_key = 'Item'; + $document->base64 = 'data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAyAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNBCUAAAAAABAAAAAAAAAAAAAAAAAAAAAA/+4AIUFkb2JlAGTAAAAAAQMAEAMDBgkAAAW8AAALrQAAEWf/2wCEAAgGBgYGBggGBggMCAcIDA4KCAgKDhANDQ4NDRARDA4NDQ4MEQ8SExQTEg8YGBoaGBgjIiIiIycnJycnJycnJycBCQgICQoJCwkJCw4LDQsOEQ4ODg4REw0NDg0NExgRDw8PDxEYFhcUFBQXFhoaGBgaGiEhICEhJycnJycnJycnJ//CABEIAGQAlgMBIgACEQEDEQH/xADtAAABBQEBAAAAAAAAAAAAAAAAAQIDBAUGBwEBAAMBAQEAAAAAAAAAAAAAAAIDBAUBBhAAAQQCAQMDBQEAAAAAAAAAAgABAwQRBRIQIBMwIQYxIiMUFUARAAIBAgMFAwgHBwUBAAAAAAECAwARIRIEMUFRYROhIkIgcYGRsdFSIzDBMpKyFAVA4WJyM0MkUPGiU3OTEgABAgQBCQYEBwAAAAAAAAABEQIAITESAyBBUWFxkaGxIhAwgdEyE8HxYnLw4UJSgiMUEwEAAgIBAwQCAwEBAAAAAAABABEhMVFBYXEQgZGhILEwwdHw8f/aAAwDAQACEQMRAAAA9ScqiDlGjgRUUcqSCOVfTEeETZI/TABQBHCxAiDmcvz1O3rM7i7HG29J1nGW6c/ZO4i1ry9ZZwJOzk2Gc11N8YVe6FsZKEQqwR8v0vnEpz4isza7FaovCjNThxulztSxiz6597PwkfQ99R6vxT0S7N2yuXJpQceKrkIq3L9kK/OuR9F8rpjCsmdZXLUN+H0Obp9Hp8azkdPd1q58T21bV6XK6dcjW2UPGl0amXp5VdnIV3c5n6t508/srbbd+3Hbl2Ib8GXV2E59tXOvLwNmfv5sueVzWhPqsNggNdcKwOifnXlS4iDvkho4bP8ASEeyPrpZktFYLMbCPudZsNzzcsTdVc5CemqECqHoAEQBABXAOABAGtD0AH//2gAIAQIAAQUB9TkSnkPEFiKNhvcnhfysQuPbJwZijLkNUGZicWCZ3X1DsIRdZZlnKmPMnOImhsWBQSifR/o7sy+5fb0OIuU8EblCBxtFGQv14ssdjQxMXqf/2gAIAQMAAQUB9Qa5LwxipBck8bMjIY0BsXYJ4Q2QT2BdFK7uMGW/QJmKIo5OrimGZ0MDm4xjEw+PMhDibBi7Y6DjkIkT/iZn8uEzoSLBYdE7dcrzGmkFn68nx6n/2gAIAQEAAQUB9HCwsLHq5XJkxC/+ByZmsbSpCi2JG3GOM68rcOZOuU7IJuRJ+uFjsd8K1tCE55wIYpBYqrzHIAQlKdmty5KG6POC2RSTXwjUGxm8ywsLHX6KMJLrXNdLXCarQd4jeY5ZrHmLYwk0Vo5k85FJZlPjTOxYDySNa2H4wpTNYrLHZKQxhHJsHGzYsRFHe17KbYHI5tVZeGlxI67yOZmTx2wYbDpmsSu9iKCL49M/DtswNZrjb2GvjtW9XsY/EKliOSQXAXnaubRQ2JWoNJWvXbu1G0FmS0MOur+L+VPKNGs0FzvvaSjZUma8xwX5isVyhUFOWwUGg2LtV+OiSOnLAMNeig1tJ1Jr5RNor9Zq91pHz12N0dfTCtvbkcl7f6xr/wAjjvUKW3LgWv2VlRaXVg8NWnHG1aBNBaFmmtiQVDIJIJIyCyYEF1ibDSms9NlUa/THY7vXtb2tSzshj+JbBF8TeI/2vklNVvkVOeV61ck9SB1+qQLx3UVa9C47HDhHDJKEQw2eS5LKz0wzqbX1LCsfF6Mqajv6S/s7eurtmbeRg/EeS5LKyjCORnpCzxxNGsrksrKysrKysrKysrKysrKysrPXK917r3Xuvde/rf/aAAgBAgIGPwHvOlq6z0t3wbnNAFWg1+mS84LiQC6drJgfCJYTrf3UHlxhWA1T8GJ5KEF1aRb7YaD6cNovcmcn5xPDnXq6o9QaIQ9Z1S/OC3OyfgckXL/FxaeESBHjAkvARd7RxGNVtLgNJatYH+XG9p6+k9LdgFF2Q9uJhh7gJoUcQaEKoO8QUUJUGRG3slFSDrhQVifHsuY8jV6m7s3hDi9rsIn9Y6mH7tEe5h4oQuDNN2YIDDnPdc5yUCBBSU8jRsiuReGNu0pPvf/aAAgBAwIGPwHvFdLnEq6awBXWUhC8LojqcIlkETU6NEI5xJGq3eYJYiCpJQecJ7hI0Ycod/SVdS4pxcnKFb0pWrifhxgPUFuJ0+I05CgpEgHbacYAMytEoBXq+cG1zcMlM1x5+UTMzUhGkmEtKZ86iGNCMa1yyElHLtF1FnsijXN+kDdmi1zS3OLgUWJIn0JyHYhA5GJG7VQwhGZdkIM2Qh6vunzi4MC7Sm7IRe9//9oACAEBAQY/Af2u18eH7Bjsq2bO3wpjQUrldsRED3wvxGlkGpbvYAtgQeOHDzVYTdf+I7f+N/ZXcYX4Gx/CQeysYwfM1vxCspRkPP3j6MxQAYYGR9noG+i+q1Dtw8CUrRfNP2sO6gA8TE7qkeRMkUpvfHPMeWw5aMussuXBIr7uYW/qoJFpgzHYcAMOdXkyIN1+9b0sbVkXW7d+FhblsrLJKGTaGAC+uu4Q5pV1GQxObBk8J3X+g6rgvcmwZssY5ALiaZxNg7fZC4JzBONXn62olH/YTl7KJy5kG24GUEbBYbbbhXXDBpVwyKLqF3hicMaPX06cdpAvzzHGm6EkcEY4WUdgzH0CssbjUMONx3ud8ppRPpelN4Zdg9GXbSZFjY+IsQT90mo5XcRMD0mVAtrfFaszsGK3ubANy+ztxqOXiMfP5TPJgqgsTyFGXTuNPBISVVw5w43AIpfzMqzq++KS34lwodXSl5PCSc/Ze1dOJQFawyLhbje9hQSR3aTeLgKvIZb+2nZ5cbd1AM3o3UhddgtfxYbMBWWOMkbl/wBsTV54nEe0KFbtNArkj4bj7GolXTL8Ze1z671G6SNK4/qxnvxm+BymwtUulP8AbN18x8qSC9uopW/npYtVozLHGMomgN8Bh9miA/SnA7okGUE8G3dtG36fKrn+7G90B4gi+FWnMmYWsxxJvwzWvsoxh2yri4Pd5bi9Hpl5bDFU7q+ktc9lHoBQvEkAe+o1lkUByEkZTsW/xCpAJzB02ISFLgADZev8zRpqD8QBVv8A6Jann0yNplkFssq9RVIO0MmK7N4oMZBKhPe6FmHZa3qqPKdkdpBwPD6Bpf6L4szqbDmTfCsn6fqGmO54wV9m2upqcyse6WlNvRdhXSzJlOLMDm9GFZNMjytwQfXWX8uYv59nrx9lP+aPUbYFUlFHp2mguqTqxKLJK+LKP/VMfWKvKrsu5y5ZfWmFdTRytAx8UbYdtxQMpDFjhqYflSA7s4XBquttRz2NaunIpR+DeRJqiuYrgq8WOAoaiXVPEzYqkZCKOVt9X1DJPFsvKMp+8hqTStE0Er2xBDobG5FxY40kGi02nifZfMSSfNtr/OlcRHwxKO0A3q8smduDfL/FXTiQCPbbKHHrF6+WbH+B3TsufZRyTSfyu1/usR7ayPKM3wulj2VnAVGOJTZjxBGNZiuVvi+w331wPprLIbkbn7resd013hbz4fupbDYb38iTTE2z7DzGIoJrNN+ZjXDOO61h5rg0mp1Wmkk0yplEDG2Vt5wwNWH+NIdxJj9t1pZ/0/V5WQhk6gvzGI91fP0sesUeKI5W9X7qXTauJ9JM2AWYd0nhermNb+a3srxfeP118qdhyYBhWEkf81jf1Vnim658QfA+giulqUyNwbC/1GiLfLOOU7jypek3d8Q3Vw8r5sKt6PdV4i0Z5Yjtq2k1YmQbI5cfxe+ra39OLD44fd3qXSQaJ0uwJnlFsluFBSb2Fr+TldQw518pynLaO2rli7cT9Q/0r//aAAgBAgMBPxD8BHIj4/gUu+n/AKDL7Eqh2LDnpJp36uxcBVJSQBqzju2/1Mo/rVB3tkuO1ZHHZYne4pQ3+A1jS9SIA5pdrL6FN29E1HHIwAiNNrOl06RtUaBbO7u6gApbHBXuAv3EB7MGADleztFGRKsm7wY7RPX6jyyGlEcPVK65Tfd263KMLBdl5vh/uDZC0O5wdmKVo4YKKAOVMbNnutFAI9eEuQ4e6ahKuKj2+B/en0tbqrHmAfYICaGFNJdQyMh/5uV4l03drL4SfIR6aL1b1BlPXXmNhFlAM7NwL0U7zACUS0VtC3J6+u9zqhb2fqLSlI+JcuIO5SQ4R9ofyf/aAAgBAwMBPxD+RAWF0BeXwHuzQV9CbX26fUGyI3Q+OsxIrVsvtv6l5UovefjcHV637+PwAhSpEW03npcCcYFf6CUJoVSLxaKfBDaWsSw47vyTCEodeVls2/8AUQ7CBsMHauvOIZ9gwKrOdefH4MthVWOO9y9BzaCnDeJ8kzpIwbaLNkqtAQS0QFwTYlN+IQGULuC0pXHSWlpFWocCQV3A4dhwVblrrFrfXSZH08asO7MfiaKWfA2PeN7MUMgK5fu4Urrgge+T6jfLDqw7/wBkMAgG2DxzG9uzsd1xQBRbbbn1ENij2hXaE6AkMCOSsjnKOW/Qai9iTi/5f//aAAgBAQMBPxAIEqVKlSpUCEHoUiRjGX6BAlSpUqIIaIhUI6G34hXMIeiRjE9OkqB63HygG1aCOt3TKzCFkCino59iplOlzY8tvCMIxuwf0/mBqJ40DUb89L4/sgg43QRGuFT0ESVfo0gRlyha0dVlpKlKrm6raQySjYol1lVfgj8C3g6iJbHNxPeAW9yDaQdgrpMZAK1eq2o7Q7EFEVS8X6HaIQYrdr7U0YQobDxRja4mPhsgnSp/cLbjYA4K51OOKoU0zRiegjSEq4oFegvxGpy4QRr5JcRHqajXulVBqlghaxQnLR092G41E0g3djqcHWMXuExr0VmhZdW7FsLT+gynKYpXXjGV7wreJppoapXL7oQD0sBYvCAX4tIpESrHmFyooWQqCbMCN1vpBgtacBgtAYVZcF7afsYf9lQisQlRdvDkWyqGZBthXx7RPvKkUrlb5Q/CrdFT5neoWdIZSWgR/VBQwZ0nUGPeBAJdZvWE38qghbIlumjVcdMzdAL5o/BAVDYFa5xT2qVhDQIAA5pB+5aemryoxhX0jk3pALPvUXhzAK5y/XUnskCEqEqMLSHNUwwLAQBRotLMeIdlDn5FpRZUUm5R2ZJ7EpNZRMobAO5K5hOAUuBYHYG+8SddNHz0+EKEOCcKzlT1BZYb4uB90OpYUAVM2rcL3vCknNK+bjWGKs6bZa9oVhmRdpg/YWAAlUVJkcjdXD11Lgke0VcU2MbHfygaFKWEnTL5GJZzMyGuGMPMbSQlbPagPOZaKOHjusEyaLtXgeW3iK4+oDc4bNYnwcKiQaks/Caxh5wK7kdeZvb3LEJhAMqbKrhAqim522Qv5gPgqp9FxlL7mnZpXi3MxIMgDkG/ug65qHbsEF8zXvjwBFAU4jmwArRmKjV6XLdNd1TvoiF1X5vX/fMHBChWDvd+4paeJz4FDgzLjs70CdhHznQBjzv7Sxo8bd2NfcZmYNWs8RxQGYGe1+olGV9n7Z+0UPFyYwlYvmDNJctGQPGwnyQAWPv0haPhQ4abtsUxZfaFBalqvypK8pGizJpYO+aShBw+h2xgHf3CNeSAXzRnTRxS/szKo3P+IMAszsGE7iUiOwZy99tXZg3BCqz2L+qH0gU09RzxfaMDrstvwgKoDsPRrCLj7jcKSy6oH5pLZC0I+L/UPAvRNDQUa9oMU7aNedH3NWIKBWuO+m4lsAS60VfopKsCajNR6AT7l8D418EaQCisod0YIUK9U/PBh6loQegqKly/QfkBmNzMzM/i+jOk/9k='; + $invoice->client = $client; $invoice->invoice_items = [$invoiceItem]; + //$invoice->documents = $account->hasFeature(FEATURE_DOCUMENTS) ? [$document] : []; + $invoice->documents = []; $data['account'] = $account; $data['invoice'] = $invoice; @@ -382,6 +530,58 @@ class AccountController extends BaseController $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); $data['invoiceFonts'] = Cache::get('fonts'); $data['section'] = $section; + + $pageSizes = [ + 'A0', + 'A1', + 'A2', + 'A3', + 'A4', + 'A5', + 'A6', + 'A7', + 'A8', + 'A9', + 'A10', + 'B0', + 'B1', + 'B2', + 'B3', + 'B4', + 'B5', + 'B6', + 'B7', + 'B8', + 'B9', + 'B10', + 'C0', + 'C1', + 'C2', + 'C3', + 'C4', + 'C5', + 'C6', + 'C7', + 'C8', + 'C9', + 'C10', + 'RA0', + 'RA1', + 'RA2', + 'RA3', + 'RA4', + 'SRA0', + 'SRA1', + 'SRA2', + 'SRA3', + 'SRA4', + 'Executive', + 'Folio', + 'Legal', + 'Letter', + 'Tabloid', + ]; + $data['pageSizes'] = array_combine($pageSizes, $pageSizes); $design = false; foreach ($data['invoiceDesigns'] as $item) { @@ -494,7 +694,7 @@ class AccountController extends BaseController private function saveCustomizeDesign() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $account = Auth::user()->account; $account->custom_design = Input::get('custom_design'); $account->invoice_design_id = CUSTOM_DESIGN; @@ -509,7 +709,7 @@ class AccountController extends BaseController private function saveClientPortal() { // Only allowed for pro Invoice Ninja users or white labeled self-hosted users - if ((Utils::isNinja() && Auth::user()->account->isPro()) || Auth::user()->account->isWhiteLabel()) { + if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { $input_css = Input::get('client_view_css'); if (Utils::isNinja()) { // Allow referencing the body element @@ -546,6 +746,7 @@ class AccountController extends BaseController $account->client_view_css = $sanitized_css; $account->enable_client_portal = !!Input::get('enable_client_portal'); + $account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard'); $account->enable_portal_password = !!Input::get('enable_portal_password'); $account->send_portal_password = !!Input::get('send_portal_password'); @@ -559,7 +760,7 @@ class AccountController extends BaseController private function saveEmailTemplates() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { $account = Auth::user()->account; foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { @@ -621,7 +822,7 @@ class AccountController extends BaseController private function saveEmailSettings() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOM_EMAILS)) { $rules = []; $user = Auth::user(); $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); @@ -647,6 +848,7 @@ class AccountController extends BaseController $account->subdomain = $subdomain; $account->iframe_url = $iframeURL; $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; + $account->document_email_attachment = Input::get('document_email_attachment') ? true : false; $account->email_design_id = Input::get('email_design_id'); if (Utils::isNinja()) { @@ -663,7 +865,7 @@ class AccountController extends BaseController private function saveInvoiceSettings() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_INVOICE_SETTINGS)) { $rules = [ 'invoice_number_pattern' => 'has_counter', 'quote_number_pattern' => 'has_counter', @@ -692,6 +894,7 @@ class AccountController extends BaseController $account->custom_invoice_item_label1 = trim(Input::get('custom_invoice_item_label1')); $account->custom_invoice_item_label2 = trim(Input::get('custom_invoice_item_label2')); + $account->invoice_number_padding = Input::get('invoice_number_padding'); $account->invoice_number_counter = Input::get('invoice_number_counter'); $account->quote_number_prefix = Input::get('quote_number_prefix'); $account->share_counter = Input::get('share_counter') ? true : false; @@ -743,20 +946,32 @@ class AccountController extends BaseController private function saveInvoiceDesign() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $account = Auth::user()->account; $account->hide_quantity = Input::get('hide_quantity') ? true : false; $account->hide_paid_to_date = Input::get('hide_paid_to_date') ? true : false; $account->all_pages_header = Input::get('all_pages_header') ? true : false; $account->all_pages_footer = Input::get('all_pages_footer') ? true : false; + $account->invoice_embed_documents = Input::get('invoice_embed_documents') ? true : false; $account->header_font_id = Input::get('header_font_id'); $account->body_font_id = Input::get('body_font_id'); $account->primary_color = Input::get('primary_color'); $account->secondary_color = Input::get('secondary_color'); $account->invoice_design_id = Input::get('invoice_design_id'); + $account->font_size = intval(Input::get('font_size')); + $account->page_size = Input::get('page_size'); + $account->live_preview = Input::get('live_preview') ? true : false; - if (Input::has('font_size')) { - $account->font_size = intval(Input::get('font_size')); + // Automatically disable live preview when using a large font + $fonts = Cache::get('fonts')->filter(function($font) use ($account) { + if ($font->google_font) { + return false; + } + return $font->id == $account->header_font_id || $font->id == $account->body_font_id; + }); + if ($account->live_preview && count($fonts)) { + $account->live_preview = false; + Session::flash('warning', trans('texts.live_preview_disabled')); } $labels = []; @@ -793,39 +1008,77 @@ class AccountController extends BaseController $this->accountRepo->save($request->input(), $account); /* Logo image file */ - if ($file = Input::file('logo')) { + if ($uploaded = Input::file('logo')) { $path = Input::file('logo')->getRealPath(); - File::delete('logo/'.$account->account_key.'.jpg'); - File::delete('logo/'.$account->account_key.'.png'); + + $disk = $account->getLogoDisk(); + if ($account->hasLogo()) { + $disk->delete($account->logo); + } + + $extension = strtolower($uploaded->getClientOriginalExtension()); + if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ + $documentType = Document::$extraExtensions[$extension]; + } + else{ + $documentType = $extension; + } - $mimeType = $file->getMimeType(); - - if ($mimeType == 'image/jpeg') { - $path = 'logo/'.$account->account_key.'.jpg'; - $file->move('logo/', $account->account_key.'.jpg'); - } elseif ($mimeType == 'image/png') { - $path = 'logo/'.$account->account_key.'.png'; - $file->move('logo/', $account->account_key.'.png'); + if(!in_array($documentType, array('jpeg', 'png', 'gif'))){ + Session::flash('warning', 'Unsupported file type'); } else { - if (extension_loaded('fileinfo')) { - $image = Image::make($path); - $image->resize(200, 120, function ($constraint) { - $constraint->aspectRatio(); - }); - $path = 'logo/'.$account->account_key.'.jpg'; - Image::canvas($image->width(), $image->height(), '#FFFFFF') - ->insert($image)->save($path); + $documentTypeData = Document::$types[$documentType]; + + $filePath = $uploaded->path(); + $size = filesize($filePath); + + if($size/1000 > MAX_DOCUMENT_SIZE){ + Session::flash('warning', 'File too large'); } else { - Session::flash('warning', 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.'); + if ($documentType != 'gif') { + $account->logo = $account->account_key.'.'.$documentType; + + $imageSize = getimagesize($filePath); + $account->logo_width = $imageSize[0]; + $account->logo_height = $imageSize[1]; + $account->logo_size = $size; + + // make sure image isn't interlaced + if (extension_loaded('fileinfo')) { + $image = Image::make($path); + $image->interlace(false); + $imageStr = (string) $image->encode($documentType); + $disk->put($account->logo, $imageStr); + + $account->logo_size = strlen($imageStr); + } else { + $stream = fopen($filePath, 'r'); + $disk->getDriver()->putStream($account->logo, $stream, ['mimetype'=>$documentTypeData['mime']]); + fclose($stream); + } + } else { + if (extension_loaded('fileinfo')) { + $image = Image::make($path); + $image->resize(200, 120, function ($constraint) { + $constraint->aspectRatio(); + }); + + $account->logo = $account->account_key.'.png'; + $image = Image::canvas($image->width(), $image->height(), '#FFFFFF')->insert($image); + $imageStr = (string) $image->encode('png'); + $disk->put($account->logo, $imageStr); + + $account->logo_size = strlen($imageStr); + $account->logo_width = $image->width(); + $account->logo_height = $image->height(); + } else { + Session::flash('warning', 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.'); + } + } } } - - // make sure image isn't interlaced - if (extension_loaded('fileinfo')) { - $img = Image::make($path); - $img->interlace(false); - $img->save(); - } + + $account->save(); } event(new UserSettingsChanged()); @@ -891,10 +1144,18 @@ class AccountController extends BaseController public function removeLogo() { - File::delete('logo/'.Auth::user()->account->account_key.'.jpg'); - File::delete('logo/'.Auth::user()->account->account_key.'.png'); + $account = Auth::user()->account; + if ($account->hasLogo()) { + $account->getLogoDisk()->delete($account->logo); + + $account->logo = null; + $account->logo_size = null; + $account->logo_width = null; + $account->logo_height = null; + $account->save(); - Session::flash('message', trans('texts.removed_logo')); + Session::flash('message', trans('texts.removed_logo')); + } return Redirect::to('settings/'.ACCOUNT_COMPANY_DETAILS); } @@ -934,7 +1195,7 @@ class AccountController extends BaseController $user->registered = true; $user->save(); - $user->account->startTrial(); + $user->account->startTrial(PLAN_PRO); if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); @@ -990,6 +1251,9 @@ class AccountController extends BaseController \Log::info("Canceled Account: {$account->name} - {$user->email}"); $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); Auth::logout(); @@ -1006,12 +1270,12 @@ class AccountController extends BaseController return Redirect::to('/settings/'.ACCOUNT_USER_DETAILS)->with('message', trans('texts.confirmation_resent')); } - public function startTrial() + public function startTrial($plan) { $user = Auth::user(); - if ($user->isEligibleForTrial()) { - $user->account->startTrial(); + if ($user->isEligibleForTrial($plan)) { + $user->account->startTrial($plan); } return Redirect::back()->with('message', trans('texts.trial_success')); @@ -1036,4 +1300,37 @@ class AccountController extends BaseController return Redirect::to("/settings/$section/", 301); } + + public function previewEmail(\App\Services\TemplateService $templateService) + { + $template = Input::get('template'); + $invoice = Invoice::scope() + ->invoices() + ->withTrashed() + ->first(); + + if ( ! $invoice) { + return trans('texts.create_invoice_for_sample'); + } + + $account = Auth::user()->account; + + // replace the variables with sample data + $data = [ + 'account' => $account, + 'invoice' => $invoice, + 'invitation' => $invoice->invitations->first(), + 'client' => $invoice->client, + 'amount' => $invoice->amount + ]; + + // create the email view + $view = 'emails.' . $account->getTemplateView(ENTITY_INVOICE) . '_html'; + $data = array_merge($data, [ + 'body' => $templateService->processVariables($template, $data), + 'entityType' => ENTITY_INVOICE, + ]); + + return Response::view($view, $data); + } } diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index 6a78290e0634..a602ae7ecb85 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -83,20 +83,23 @@ class AppController extends BaseController return Redirect::to('/'); } - $_ENV['APP_ENV']='production'; - $_ENV['APP_DEBUG']=$app['debug']; - $_ENV['APP_URL']=$app['url']; - $_ENV['APP_KEY']=$app['key']; - $_ENV['DB_TYPE']=$dbType; - $_ENV['DB_HOST']=$database['type']['host']; - $_ENV['DB_DATABASE']=$database['type']['database']; - $_ENV['DB_USERNAME']=$database['type']['username']; - $_ENV['DB_PASSWORD']=$database['type']['password']; - $_ENV['MAIL_DRIVER']=$mail['driver']; - $_ENV['MAIL_PORT']=$mail['port']; - $_ENV['MAIL_ENCRYPTION']=$mail['encryption']; - $_ENV['MAIL_HOST']=$mail['host']; - $_ENV['MAIL_USERNAME']=$mail['username'];; + $_ENV['APP_ENV'] = 'production'; + $_ENV['APP_DEBUG'] = $app['debug']; + $_ENV['APP_URL'] = $app['url']; + $_ENV['APP_KEY'] = $app['key']; + $_ENV['DB_TYPE'] = $dbType; + $_ENV['DB_HOST'] = $database['type']['host']; + $_ENV['DB_DATABASE'] = $database['type']['database']; + $_ENV['DB_USERNAME'] = $database['type']['username']; + $_ENV['DB_PASSWORD'] = $database['type']['password']; + $_ENV['MAIL_DRIVER'] = $mail['driver']; + $_ENV['MAIL_PORT'] = $mail['port']; + $_ENV['MAIL_ENCRYPTION'] = $mail['encryption']; + $_ENV['MAIL_HOST'] = $mail['host']; + $_ENV['MAIL_USERNAME'] = $mail['username']; + $_ENV['MAIL_FROM_NAME'] = $mail['from']['name']; + $_ENV['MAIL_PASSWORD'] = $mail['password']; + $_ENV['PHANTOMJS_CLOUD_KEY'] = 'a-demo-key-with-low-quota-per-ip-address'; $config = ''; foreach ($_ENV as $key => $val) { @@ -175,9 +178,12 @@ class AppController extends BaseController $config = ''; foreach ($_ENV as $key => $val) { - if (preg_match('/\s/',$val)) { - $val = "'{$val}'"; - } + if (is_array($val)) { + continue; + } + if (preg_match('/\s/', $val)) { + $val = "'{$val}'"; + } $config .= "{$key}={$val}\n"; } @@ -260,18 +266,7 @@ class AppController extends BaseController Cache::flush(); Session::flush(); Artisan::call('migrate', array('--force' => true)); - foreach ([ - 'PaymentLibraries', - 'Fonts', - 'Banks', - 'InvoiceStatus', - 'Currencies', - 'DateFormats', - 'InvoiceDesigns', - 'PaymentTerms', - ] as $seeder) { - Artisan::call('db:seed', array('--force' => true, '--class' => "{$seeder}Seeder")); - } + Artisan::call('db:seed', array('--force' => true, '--class' => "UpdateSeeder")); Event::fire(new UserSettingsChanged()); Session::flash('message', trans('texts.processed_updates')); } catch (Exception $e) { @@ -300,7 +295,7 @@ class AppController extends BaseController public function stats() { - if (Input::get('password') != env('RESELLER_PASSWORD')) { + if ( ! hash_equals(Input::get('password'), env('RESELLER_PASSWORD'))) { sleep(3); return ''; } @@ -323,4 +318,4 @@ class AppController extends BaseController return json_encode($data); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index bd8813912d25..e599890c6a90 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -133,6 +133,9 @@ class AuthController extends Controller { if (Auth::check() && !Auth::user()->registered) { $account = Auth::user()->account; $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); } diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php index 3a3a1e0bece9..0718e95b91e4 100644 --- a/app/Http/Controllers/BaseAPIController.php +++ b/app/Http/Controllers/BaseAPIController.php @@ -2,6 +2,9 @@ use Session; use Utils; +use Auth; +use Log; +use Input; use Response; use Request; use League\Fractal; @@ -9,8 +12,10 @@ use League\Fractal\Manager; use League\Fractal\Resource\Item; use League\Fractal\Resource\Collection; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Models\EntityModel; use App\Ninja\Serializers\ArraySerializer; use League\Fractal\Serializer\JsonApiSerializer; +use Illuminate\Pagination\LengthAwarePaginator; /** * @SWG\Swagger( @@ -62,6 +67,74 @@ class BaseAPIController extends Controller } else { $this->manager->setSerializer(new ArraySerializer()); } + + if (Utils::isNinjaDev()) { + \DB::enableQueryLog(); + } + } + + protected function handleAction($request) + { + $entity = $request->entity(); + $action = $request->action; + + $repo = Utils::toCamelCase($this->entityType) . 'Repo'; + + $this->$repo->$action($entity); + + return $this->itemResponse($entity); + } + + protected function listResponse($query) + { + $transformerClass = EntityModel::getTransformerName($this->entityType); + $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); + + $includes = $transformer->getDefaultIncludes(); + $includes = $this->getRequestIncludes($includes); + + $query->with($includes); + + if ($updatedAt = Input::get('updated_at')) { + $updatedAt = date('Y-m-d H:i:s', $updatedAt); + $query->where(function($query) use ($includes, $updatedAt) { + $query->where('updated_at', '>=', $updatedAt); + foreach ($includes as $include) { + $query->orWhereHas($include, function($query) use ($updatedAt) { + $query->where('updated_at', '>=', $updatedAt); + }); + } + }); + } + + if ($clientPublicId = Input::get('client_id')) { + $filter = function($query) use ($clientPublicId) { + $query->where('public_id', '=', $clientPublicId); + }; + $query->whereHas('client', $filter); + } + + if ( ! Utils::hasPermission('view_all')){ + if ($this->entityType == ENTITY_USER) { + $query->where('id', '=', Auth::user()->id); + } else { + $query->where('user_id', '=', Auth::user()->id); + } + } + + $data = $this->createCollection($query, $transformer, $this->entityType); + + return $this->response($data); + } + + protected function itemResponse($item) + { + $transformerClass = EntityModel::getTransformerName($this->entityType); + $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); + + $data = $this->createItem($item, $transformer, $this->entityType); + + return $this->response($data); } protected function createItem($data, $transformer, $entityType) @@ -74,23 +147,31 @@ class BaseAPIController extends Controller return $this->manager->createData($resource)->toArray(); } - protected function createCollection($data, $transformer, $entityType, $paginator = false) + protected function createCollection($query, $transformer, $entityType) { if ($this->serializer && $this->serializer != API_SERIALIZER_JSON) { $entityType = null; } - $resource = new Collection($data, $transformer, $entityType); - - if ($paginator) { - $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + if (is_a($query, "Illuminate\Database\Eloquent\Builder")) { + $limit = min(MAX_API_PAGE_SIZE, Input::get('per_page', DEFAULT_API_PAGE_SIZE)); + $resource = new Collection($query->get(), $transformer, $entityType); + $resource->setPaginator(new IlluminatePaginatorAdapter($query->paginate($limit))); + } else { + $resource = new Collection($query, $transformer, $entityType); } - + return $this->manager->createData($resource)->toArray(); } protected function response($response) { + if (Utils::isNinjaDev()) { + $count = count(\DB::getQueryLog()); + Log::info(Request::method() . ' - ' . Request::url() . ": $count queries"); + Log::info(json_encode(\DB::getQueryLog())); + } + $index = Request::get('index') ?: 'data'; if ($index == 'none') { @@ -123,26 +204,21 @@ class BaseAPIController extends Controller } - - protected function getIncluded() + protected function getRequestIncludes($data) { - $data = ['user']; - $included = Request::get('include'); $included = explode(',', $included); foreach ($included as $include) { if ($include == 'invoices') { $data[] = 'invoices.invoice_items'; - $data[] = 'invoices.user'; + } elseif ($include == 'client') { + $data[] = 'client.contacts'; } elseif ($include == 'clients') { $data[] = 'clients.contacts'; - $data[] = 'clients.user'; } elseif ($include == 'vendors') { - $data[] = 'vendors.vendorcontacts'; - $data[] = 'vendors.user'; - } - elseif ($include) { + $data[] = 'vendors.vendor_contacts'; + } elseif ($include) { $data[] = $include; } } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 5124097636a9..2ce7a633f179 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -2,13 +2,16 @@ use App\Http\Middleware\PermissionsRequired; use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Input; use Auth; +use Utils; class BaseController extends Controller { - use DispatchesJobs; + use DispatchesJobs, AuthorizesRequests; - protected $model = 'App\Models\EntityModel'; + protected $entityType; /** * Setup the layout used by the controller. @@ -21,40 +24,4 @@ class BaseController extends Controller $this->layout = View::make($this->layout); } } - - protected function checkViewPermission($object, &$response = null){ - if(!$object->canView()){ - $response = response('Unauthorized.', 401); - return false; - } - return true; - } - - protected function checkEditPermission($object, &$response = null){ - if(!$object->canEdit()){ - $response = response('Unauthorized.', 401); - return false; - } - return true; - } - - protected function checkCreatePermission(&$response = null){ - if(!call_user_func(array($this->model, 'canCreate'))){ - $response = response('Unauthorized.', 401); - return false; - } - return true; - } - - protected function checkUpdatePermission($input, &$response = null){ - $creating = empty($input['public_id']) || $input['public_id'] == '-1'; - - if($creating){ - return $this->checkCreatePermission($response); - } - else{ - $object = call_user_func(array($this->model, 'scope'), $input['public_id'])->firstOrFail(); - return $this->checkEditPermission($object, $response); - } - } } diff --git a/app/Http/Controllers/ClientApiController.php b/app/Http/Controllers/ClientApiController.php index fd3b33df6aa5..dd82e9116131 100644 --- a/app/Http/Controllers/ClientApiController.php +++ b/app/Http/Controllers/ClientApiController.php @@ -10,27 +10,19 @@ use App\Ninja\Repositories\ClientRepository; use App\Http\Requests\CreateClientRequest; use App\Http\Controllers\BaseAPIController; use App\Ninja\Transformers\ClientTransformer; -use App\Services\ClientService; use App\Http\Requests\UpdateClientRequest; class ClientApiController extends BaseAPIController { protected $clientRepo; - protected $clientService; - public function __construct(ClientRepository $clientRepo, ClientService $clientService) + protected $entityType = ENTITY_CLIENT; + + public function __construct(ClientRepository $clientRepo) { parent::__construct(); $this->clientRepo = $clientRepo; - $this->clientService = $clientService; - } - - public function ping() - { - $headers = Utils::getApiHeaders(); - - return Response::make('', 200, $headers); } /** @@ -52,27 +44,17 @@ class ClientApiController extends BaseAPIController public function index() { $clients = Client::scope() - ->with($this->getIncluded()) - ->orderBy('created_at', 'desc')->withTrashed(); + ->orderBy('created_at', 'desc') + ->withTrashed(); // Filter by email - if (Input::has('email')) { - - $email = Input::get('email'); + if ($email = Input::get('email')) { $clients = $clients->whereHas('contacts', function ($query) use ($email) { $query->where('email', $email); }); - } - - $clients = $clients->paginate(); - - $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); - $paginator = Client::scope()->withTrashed()->paginate(); - - $data = $this->createCollection($clients, $transformer, ENTITY_CLIENT, $paginator); - - return $this->response($data); + + return $this->listResponse($clients); } /** @@ -100,14 +82,7 @@ class ClientApiController extends BaseAPIController { $client = $this->clientRepo->save($request->input()); - $client = Client::scope($client->public_id) - ->with('country', 'contacts', 'industry', 'size', 'currency') - ->first(); - - $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($client, $transformer, ENTITY_CLIENT); - - return $this->response($data); + return $this->itemResponse($client); } /** @@ -134,51 +109,15 @@ class ClientApiController extends BaseAPIController public function update(UpdateClientRequest $request, $publicId) { - if ($request->action == ACTION_ARCHIVE) { - - - $client = Client::scope($publicId)->withTrashed()->first(); - - if(!$client) - return $this->errorResponse(['message'=>'Record not found'], 400); - - $this->clientRepo->archive($client); - - $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($client, $transformer, ENTITY_CLIENT); - - return $this->response($data); + if ($request->action) { + return $this->handleAction($request); } - else if ($request->action == ACTION_RESTORE){ - - $client = Client::scope($publicId)->withTrashed()->first(); - - if(!$client) - return $this->errorResponse(['message'=>'Client not found.'], 400); - - $this->clientRepo->restore($client); - - $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($client, $transformer, ENTITY_CLIENT); - - return $this->response($data); - } - + $data = $request->input(); $data['public_id'] = $publicId; - $this->clientRepo->save($data); + $client = $this->clientRepo->save($data, $request->entity()); - $client = Client::scope($publicId) - ->with('country', 'contacts', 'industry', 'size', 'currency') - ->first(); - - if(!$client) - return $this->errorResponse(['message'=>'Client not found.'],400); - - $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($client, $transformer, ENTITY_CLIENT); - - return $this->response($data); + return $this->itemResponse($client); } @@ -204,23 +143,13 @@ class ClientApiController extends BaseAPIController * ) */ - public function destroy($publicId) + public function destroy(UpdateClientRequest $request) { - - $client = Client::scope($publicId)->withTrashed()->first(); + $client = $request->entity(); + $this->clientRepo->delete($client); - $client = Client::scope($publicId) - ->with('country', 'contacts', 'industry', 'size', 'currency') - ->withTrashed() - ->first(); - - $transformer = new ClientTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($client, $transformer, ENTITY_CLIENT); - - return $this->response($data); - + return $this->itemResponse($client); } - - -} + +} \ No newline at end of file diff --git a/app/Http/Controllers/ClientAuth/AuthController.php b/app/Http/Controllers/ClientAuth/AuthController.php index c88c8a4b85ea..ed5f199ac255 100644 --- a/app/Http/Controllers/ClientAuth/AuthController.php +++ b/app/Http/Controllers/ClientAuth/AuthController.php @@ -33,7 +33,7 @@ class AuthController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } diff --git a/app/Http/Controllers/ClientAuth/PasswordController.php b/app/Http/Controllers/ClientAuth/PasswordController.php index beefb016123e..35576b47ae39 100644 --- a/app/Http/Controllers/ClientAuth/PasswordController.php +++ b/app/Http/Controllers/ClientAuth/PasswordController.php @@ -50,7 +50,7 @@ class PasswordController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } @@ -117,7 +117,7 @@ class PasswordController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 7e1c01de9e6e..90aedfb897c2 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -28,6 +28,7 @@ use App\Models\Task; use App\Ninja\Repositories\ClientRepository; use App\Services\ClientService; +use App\Http\Requests\ClientRequest; use App\Http\Requests\CreateClientRequest; use App\Http\Requests\UpdateClientRequest; @@ -35,7 +36,7 @@ class ClientController extends BaseController { protected $clientService; protected $clientRepo; - protected $model = 'App\Models\Client'; + protected $entityType = ENTITY_CLIENT; public function __construct(ClientRepository $clientRepo, ClientService $clientService) { @@ -81,13 +82,7 @@ class ClientController extends BaseController */ public function store(CreateClientRequest $request) { - $data = $request->input(); - - if(!$this->checkUpdatePermission($data, $response)){ - return $response; - } - - $client = $this->clientService->save($data); + $client = $this->clientService->save($request->input()); Session::flash('message', trans('texts.created_client')); @@ -100,38 +95,35 @@ class ClientController extends BaseController * @param int $id * @return Response */ - public function show($publicId) + public function show(ClientRequest $request) { - $client = Client::withTrashed()->scope($publicId)->with('contacts', 'size', 'industry')->firstOrFail(); - - if(!$this->checkViewPermission($client, $response)){ - return $response; - } + $client = $request->entity(); + $user = Auth::user(); Utils::trackViewed($client->getDisplayName(), ENTITY_CLIENT); $actionLinks = []; - if(Task::canCreate()){ - $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => '/tasks/create/'.$client->public_id]; + if($user->can('create', ENTITY_TASK)){ + $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; } - if (Utils::isPro() && Invoice::canCreate()) { - $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => '/quotes/create/'.$client->public_id]; + if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_INVOICE)) { + $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)]; } if(!empty($actionLinks)){ $actionLinks[] = \DropdownButton::DIVIDER; } - if(Payment::canCreate()){ - $actionLinks[] = ['label' => trans('texts.enter_payment'), 'url' => '/payments/create/'.$client->public_id]; + if($user->can('create', ENTITY_PAYMENT)){ + $actionLinks[] = ['label' => trans('texts.enter_payment'), 'url' => URL::to('/payments/create/'.$client->public_id)]; } - if(Credit::canCreate()){ - $actionLinks[] = ['label' => trans('texts.enter_credit'), 'url' => '/credits/create/'.$client->public_id]; + if($user->can('create', ENTITY_CREDIT)){ + $actionLinks[] = ['label' => trans('texts.enter_credit'), 'url' => URL::to('/credits/create/'.$client->public_id)]; } - if(Expense::canCreate()){ - $actionLinks[] = ['label' => trans('texts.enter_expense'), 'url' => '/expenses/create/0/'.$client->public_id]; + if($user->can('create', ENTITY_EXPENSE)){ + $actionLinks[] = ['label' => trans('texts.enter_expense'), 'url' => URL::to('/expenses/create/0/'.$client->public_id)]; } $data = array( @@ -154,12 +146,8 @@ class ClientController extends BaseController * * @return Response */ - public function create() + public function create(ClientRequest $request) { - if(!$this->checkCreatePermission($response)){ - return $response; - } - if (Client::scope()->withTrashed()->count() > Auth::user()->getMaxNumClients()) { return View::make('error', ['hideHeader' => true, 'error' => "Sorry, you've exceeded the limit of ".Auth::user()->getMaxNumClients()." clients"]); } @@ -182,18 +170,14 @@ class ClientController extends BaseController * @param int $id * @return Response */ - public function edit($publicId) + public function edit(ClientRequest $request) { - $client = Client::scope($publicId)->with('contacts')->firstOrFail(); - - if(!$this->checkEditPermission($client, $response)){ - return $response; - } - + $client = $request->entity(); + $data = [ 'client' => $client, 'method' => 'PUT', - 'url' => 'clients/'.$publicId, + 'url' => 'clients/'.$client->public_id, 'title' => trans('texts.edit_client'), ]; @@ -201,7 +185,7 @@ class ClientController extends BaseController if (Auth::user()->account->isNinjaAccount()) { if ($account = Account::whereId($client->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; + $data['planDetails'] = $account->getPlanDetails(false, false); } } @@ -232,13 +216,7 @@ class ClientController extends BaseController */ public function update(UpdateClientRequest $request) { - $data = $request->input(); - - if(!$this->checkUpdatePermission($data, $response)){ - return $response; - } - - $client = $this->clientService->save($data); + $client = $this->clientService->save($request->input(), $request->entity()); Session::flash('message', trans('texts.updated_client')); diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php index 26085c3d6b32..c4250903fd32 100644 --- a/app/Http/Controllers/CreditController.php +++ b/app/Http/Controllers/CreditController.php @@ -12,12 +12,13 @@ use App\Models\Client; use App\Services\CreditService; use App\Ninja\Repositories\CreditRepository; use App\Http\Requests\CreateCreditRequest; +use App\Http\Requests\CreditRequest; class CreditController extends BaseController { protected $creditRepo; protected $creditService; - protected $model = 'App\Models\Credit'; + protected $entityType = ENTITY_CREDIT; public function __construct(CreditRepository $creditRepo, CreditService $creditService) { @@ -55,32 +56,26 @@ class CreditController extends BaseController return $this->creditService->getDatatable($clientPublicId, Input::get('sSearch')); } - public function create($clientPublicId = 0) + public function create(CreditRequest $request) { - if(!$this->checkCreatePermission($response)){ - return $response; - } - $data = array( - 'clientPublicId' => Input::old('client') ? Input::old('client') : $clientPublicId, - //'invoicePublicId' => Input::old('invoice') ? Input::old('invoice') : $invoicePublicId, + 'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0), 'credit' => null, 'method' => 'POST', 'url' => 'credits', 'title' => trans('texts.new_credit'), - //'invoices' => Invoice::scope()->with('client', 'invoice_status')->orderBy('invoice_number')->get(), - 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ); + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + ); return View::make('credits.edit', $data); } + /* public function edit($publicId) { $credit = Credit::scope($publicId)->firstOrFail(); - if(!$this->checkEditPermission($credit, $response)){ - return $response; - } + $this->authorize('edit', $credit); $credit->credit_date = Utils::fromSqlDate($credit->credit_date); @@ -94,7 +89,8 @@ class CreditController extends BaseController return View::make('credit.edit', $data); } - + */ + public function store(CreateCreditRequest $request) { $credit = $this->creditRepo->save($request->input()); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index b66f38615d9b..5daa8827883a 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -11,7 +11,7 @@ class DashboardController extends BaseController { public function index() { - $view_all = !Auth::user()->hasPermission('view_all'); + $view_all = Auth::user()->hasPermission('view_all'); $user_id = Auth::user()->id; // total_income, billed_clients, invoice_sent and active_clients @@ -105,6 +105,7 @@ class DashboardController extends BaseController ->where('contacts.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) //->where('invoices.is_quote', '=', false) + ->where('invoices.quote_invoice_id', '=', null) ->where('invoices.balance', '>', 0) ->where('invoices.is_deleted', '=', false) ->where('invoices.deleted_at', '=', null) @@ -129,6 +130,7 @@ class DashboardController extends BaseController ->where('invoices.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) //->where('invoices.is_quote', '=', false) + ->where('invoices.quote_invoice_id', '=', null) ->where('invoices.balance', '>', 0) ->where('invoices.is_deleted', '=', false) ->where('contacts.is_primary', '=', true) diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php new file mode 100644 index 000000000000..d597ba004474 --- /dev/null +++ b/app/Http/Controllers/DocumentController.php @@ -0,0 +1,123 @@ +documentRepo = $documentRepo; + } + + public function get(DocumentRequest $request) + { + return static::getDownloadResponse($request->entity()); + } + + public static function getDownloadResponse($document){ + $direct_url = $document->getDirectUrl(); + if($direct_url){ + return redirect($direct_url); + } + + $stream = $document->getStream(); + + if($stream){ + $headers = [ + 'Content-Type' => Document::$types[$document->type]['mime'], + 'Content-Length' => $document->size, + ]; + + $response = Response::stream(function() use ($stream) { + fpassthru($stream); + }, 200, $headers); + } + else{ + $response = Response::make($document->getRaw(), 200); + $response->header('content-type', Document::$types[$document->type]['mime']); + } + + return $response; + } + + public function getPreview(DocumentRequest $request) + { + $document = $request->entity(); + + if(empty($document->preview)){ + return Response::view('error', array('error'=>'Preview does not exist!'), 404); + } + + $direct_url = $document->getDirectPreviewUrl(); + if($direct_url){ + return redirect($direct_url); + } + + $previewType = pathinfo($document->preview, PATHINFO_EXTENSION); + $response = Response::make($document->getRawPreview(), 200); + $response->header('content-type', Document::$types[$previewType]['mime']); + + return $response; + } + + public function getVFSJS(DocumentRequest $request, $publicId, $name) + { + $document = $request->entity(); + + if(substr($name, -3)=='.js'){ + $name = substr($name, 0, -3); + } + + if(!$document->isPDFEmbeddable()){ + return Response::view('error', array('error'=>'Image does not exist!'), 404); + } + + $content = $document->preview?$document->getRawPreview():$document->getRaw(); + $content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")'; + $response = Response::make($content, 200); + $response->header('content-type', 'text/javascript'); + $response->header('cache-control', 'max-age=31536000'); + + return $response; + } + + public function postUpload(CreateDocumentRequest $request) + { + if (!Utils::hasFeature(FEATURE_DOCUMENTS)) { + return; + } + + $result = $this->documentRepo->upload(Input::all()['file'], $doc_array); + + if(is_string($result)){ + return Response::json([ + 'error' => $result, + 'code' => 400 + ], 400); + } else { + return Response::json([ + 'error' => false, + 'document' => $doc_array, + 'code' => 200 + ], 200); + } + } +} diff --git a/app/Http/Controllers/ExpenseApiController.php b/app/Http/Controllers/ExpenseApiController.php index 88ff5497cefd..725067aa1f2c 100644 --- a/app/Http/Controllers/ExpenseApiController.php +++ b/app/Http/Controllers/ExpenseApiController.php @@ -1,5 +1,5 @@ withTrashed() + ->with('client', 'invoice', 'vendor') ->orderBy('created_at','desc'); - $expenses = $expenses->paginate(); - - $transformer = new ExpenseTransformer(Auth::user()->account, Input::get('serializer')); - $paginator = Expense::scope()->withTrashed()->paginate(); - - $data = $this->createCollection($expenses, $transformer, ENTITY_EXPENSE, $paginator); - - return $this->response($data); - + return $this->listResponse($expenses); } public function update() diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index b1fd28e41941..d4184abf3fca 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -17,6 +17,8 @@ use App\Models\Expense; use App\Models\Client; use App\Services\ExpenseService; use App\Ninja\Repositories\ExpenseRepository; + +use App\Http\Requests\ExpenseRequest; use App\Http\Requests\CreateExpenseRequest; use App\Http\Requests\UpdateExpenseRequest; @@ -25,7 +27,7 @@ class ExpenseController extends BaseController // Expenses protected $expenseRepo; protected $expenseService; - protected $model = 'App\Models\Expense'; + protected $entityType = ENTITY_EXPENSE; public function __construct(ExpenseRepository $expenseRepo, ExpenseService $expenseService) { @@ -69,42 +71,35 @@ class ExpenseController extends BaseController return $this->expenseService->getDatatableVendor($vendorPublicId); } - public function create($vendorPublicId = null, $clientPublicId = null) + public function create(ExpenseRequest $request) { - if(!$this->checkCreatePermission($response)){ - return $response; - } - - if($vendorPublicId != 0) { - $vendor = Vendor::scope($vendorPublicId)->with('vendorcontacts')->firstOrFail(); + if ($request->vendor_id != 0) { + $vendor = Vendor::scope($request->vendor_id)->with('vendor_contacts')->firstOrFail(); } else { $vendor = null; } + $data = array( - 'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $vendorPublicId, + 'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $request->vendor_id, 'expense' => null, 'method' => 'POST', 'url' => 'expenses', 'title' => trans('texts.new_expense'), - 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(), + 'vendors' => Vendor::scope()->with('vendor_contacts')->orderBy('name')->get(), 'vendor' => $vendor, 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), - 'clientPublicId' => $clientPublicId, - ); + 'clientPublicId' => $request->client_id, + ); $data = array_merge($data, self::getViewModel()); return View::make('expenses.edit', $data); } - public function edit($publicId) + public function edit(ExpenseRequest $request) { - $expense = Expense::scope($publicId)->firstOrFail(); - - if(!$this->checkEditPermission($expense, $response)){ - return $response; - } - + $expense = $request->entity(); + $expense->expense_date = Utils::fromSqlDate($expense->expense_date); $actions = []; @@ -112,15 +107,6 @@ class ExpenseController extends BaseController $actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans("texts.view_invoice")]; } else { $actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans("texts.invoice_expense")]; - - /* - // check for any open invoices - $invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : []; - - foreach ($invoices as $invoice) { - $actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans("texts.add_to_invoice", ["invoice" => $invoice->invoice_number])]; - } - */ } $actions[] = \DropdownButton::DIVIDER; @@ -135,10 +121,10 @@ class ExpenseController extends BaseController 'vendor' => null, 'expense' => $expense, 'method' => 'PUT', - 'url' => 'expenses/'.$publicId, + 'url' => 'expenses/'.$expense->public_id, 'title' => 'Edit Expense', 'actions' => $actions, - 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(), + 'vendors' => Vendor::scope()->with('vendor_contacts')->orderBy('name')->get(), 'vendorPublicId' => $expense->vendor ? $expense->vendor->public_id : null, 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), 'clientPublicId' => $expense->client ? $expense->client->public_id : null, @@ -146,12 +132,6 @@ class ExpenseController extends BaseController $data = array_merge($data, self::getViewModel()); - if (Auth::user()->account->isNinjaAccount()) { - if ($account = Account::whereId($client->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; - } - } - return View::make('expenses.edit', $data); } @@ -163,7 +143,10 @@ class ExpenseController extends BaseController */ public function update(UpdateExpenseRequest $request) { - $expense = $this->expenseService->save($request->input()); + $data = $request->input(); + $data['documents'] = $request->file('documents'); + + $expense = $this->expenseService->save($data, $request->entity()); Session::flash('message', trans('texts.updated_expense')); @@ -177,7 +160,10 @@ class ExpenseController extends BaseController public function store(CreateExpenseRequest $request) { - $expense = $this->expenseService->save($request->input()); + $data = $request->input(); + $data['documents'] = $request->file('documents'); + + $expense = $this->expenseService->save($data); Session::flash('message', trans('texts.created_expense')); @@ -195,8 +181,7 @@ class ExpenseController extends BaseController $expenses = Expense::scope($ids)->with('client')->get(); $clientPublicId = null; $currencyId = null; - $data = []; - + // Validate that either all expenses do not have a client or if there is a client, it is the same client foreach ($expenses as $expense) { @@ -220,19 +205,11 @@ class ExpenseController extends BaseController Session::flash('error', trans('texts.expense_error_invoiced')); return Redirect::to('expenses'); } - - $account = Auth::user()->account; - $data[] = [ - 'publicId' => $expense->public_id, - 'description' => $expense->public_notes, - 'qty' => 1, - 'cost' => $expense->present()->converted_amount, - ]; } return Redirect::to("invoices/create/{$clientPublicId}") ->with('expenseCurrencyId', $currencyId) - ->with('expenses', $data); + ->with('expenses', $ids); break; default: diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index db74adb89cee..ebaaa1f6c306 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -164,12 +164,12 @@ class ExportController extends BaseController if ($request->input(ENTITY_VENDOR)) { $data['clients'] = Vendor::scope() - ->with('user', 'vendorcontacts', 'country') + ->with('user', 'vendor_contacts', 'country') ->withArchived() ->get(); $data['vendor_contacts'] = VendorContact::scope() - ->with('user', 'vendor.contacts') + ->with('user', 'vendor.vendor_contacts') ->withTrashed() ->get(); diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 38618475064b..fe006332ca48 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -1,4 +1,4 @@ -withTrashed(); - $invoices = Invoice::scope()->withTrashed() - ->with(array_merge(['invoice_items'], $this->getIncluded())); + $invoices = Invoice::scope() + ->withTrashed() + ->with('invoice_items', 'client') + ->orderBy('created_at', 'desc'); - if ($clientPublicId = Input::get('client_id')) { - $filter = function($query) use ($clientPublicId) { - $query->where('public_id', '=', $clientPublicId); - }; - $invoices->whereHas('client', $filter); - $paginator->whereHas('client', $filter); - } - - $invoices = $invoices->orderBy('created_at', 'desc')->paginate(); - - /* - // Add the first invitation link to the data - foreach ($invoices as $key => $invoice) { - foreach ($invoice->invitations as $subKey => $invitation) { - $invoices[$key]['link'] = $invitation->getLink(); - } - unset($invoice['invitations']); - } - */ - - $transformer = new InvoiceTransformer(Auth::user()->account, Input::get('serializer')); - $paginator = $paginator->paginate(); - - $data = $this->createCollection($invoices, $transformer, 'invoices', $paginator); - - return $this->response($data); + return $this->listResponse($invoices); } /** @@ -104,18 +83,9 @@ class InvoiceApiController extends BaseAPIController * ) */ - public function show($publicId) + public function show(InvoiceRequest $request) { - - $invoice = Invoice::scope($publicId)->withTrashed()->first(); - - if(!$invoice) - return $this->errorResponse(['message'=>'Invoice does not exist!'], 404); - - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); + return $this->itemResponse($request->entity()); } /** @@ -139,7 +109,7 @@ class InvoiceApiController extends BaseAPIController * ) * ) */ - public function store(CreateInvoiceRequest $request) + public function store(CreateInvoiceAPIRequest $request) { $data = Input::all(); $error = null; @@ -166,6 +136,7 @@ class InvoiceApiController extends BaseAPIController 'state', 'postal_code', 'private_notes', + 'currency_code', ] as $field) { if (isset($data[$field])) { $clientData[$field] = $data[$field]; @@ -209,11 +180,11 @@ class InvoiceApiController extends BaseAPIController } } - $invoice = Invoice::scope($invoice->public_id)->with('client', 'invoice_items', 'invitations')->first(); - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); + $invoice = Invoice::scope($invoice->public_id) + ->with('client', 'invoice_items', 'invitations') + ->first(); + + return $this->itemResponse($invoice); } private function prepareData($data, $client) @@ -258,10 +229,11 @@ class InvoiceApiController extends BaseAPIController // initialize the line items if (isset($data['product_key']) || isset($data['cost']) || isset($data['notes']) || isset($data['qty'])) { $data['invoice_items'] = [self::prepareItem($data)]; - // make sure the tax isn't applied twice (for the invoice and the line item) - unset($data['invoice_items'][0]['tax_name']); - unset($data['invoice_items'][0]['tax_rate']); + unset($data['invoice_items'][0]['tax_name1']); + unset($data['invoice_items'][0]['tax_rate1']); + unset($data['invoice_items'][0]['tax_name2']); + unset($data['invoice_items'][0]['tax_rate2']); } else { foreach ($data['invoice_items'] as $index => $item) { $data['invoice_items'][$index] = self::prepareItem($item); @@ -298,36 +270,21 @@ class InvoiceApiController extends BaseAPIController $item[$key] = $val; } } - + return $item; } - public function emailInvoice() + public function emailInvoice(InvoiceRequest $request) { - $data = Input::all(); - $error = null; + $invoice = $request->entity(); - $invoice = Invoice::scope($data['id'])->withTrashed()->first(); - - if(!$invoice) - return $this->errorResponse(['message'=>'Invoice does not exist.'], 400); - - - $this->mailer->sendInvoice($invoice, false, false); - - - if($error) { - return $this->errorResponse(['message'=>'There was an error sending the invoice'], 400); - } - else { - $response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT); - } + $this->mailer->sendInvoice($invoice); + $response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT); $headers = Utils::getApiHeaders(); - return Response::make($response, $error ? 400 : 200, $headers); + return Response::make($response, 200, $headers); } - /** * @SWG\Put( * path="/invoices", @@ -349,45 +306,25 @@ class InvoiceApiController extends BaseAPIController * ) * ) */ - public function update(UpdateInvoiceRequest $request, $publicId) + public function update(UpdateInvoiceAPIRequest $request, $publicId) { - if ($request->action == ACTION_ARCHIVE) { - $invoice = Invoice::scope($publicId)->firstOrFail(); - $this->invoiceRepo->archive($invoice); - - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); - } - else if ($request->action == ACTION_CONVERT) { - $quote = Invoice::scope($publicId)->firstOrFail(); + if ($request->action == ACTION_CONVERT) { + $quote = $request->entity(); $invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id); - - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); - } - else if ($request->action == ACTION_RESTORE) { - $invoice = Invoice::scope($publicId)->withTrashed()->firstOrFail(); - $this->invoiceRepo->restore($invoice); - - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); + return $this->itemResponse($invoice); + } elseif ($request->action) { + return $this->handleAction($request); } $data = $request->input(); $data['public_id'] = $publicId; - $this->invoiceService->save($data); + $this->invoiceService->save($data, $request->entity()); - $invoice = Invoice::scope($publicId)->with('client', 'invoice_items', 'invitations')->firstOrFail(); - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); + $invoice = Invoice::scope($publicId) + ->with('client', 'invoice_items', 'invitations') + ->firstOrFail(); + + return $this->itemResponse($invoice); } /** @@ -412,18 +349,13 @@ class InvoiceApiController extends BaseAPIController * ) */ - public function destroy($publicId) + public function destroy(UpdateInvoiceAPIRequest $request) { - $data['public_id'] = $publicId; - $invoice = Invoice::scope($publicId)->firstOrFail(); - + $invoice = $request->entity(); + $this->invoiceRepo->delete($invoice); - $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($invoice, $transformer, 'invoice'); - - return $this->response($data); - + return $this->itemResponse($invoice); } } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 5a14ea624449..9a0dadf494c4 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -17,26 +17,32 @@ use App\Models\Invoice; use App\Models\Client; use App\Models\Account; use App\Models\Product; +use App\Models\Expense; use App\Models\TaxRate; use App\Models\InvoiceDesign; use App\Models\Activity; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; +use App\Ninja\Repositories\DocumentRepository; use App\Services\InvoiceService; use App\Services\RecurringInvoiceService; -use App\Http\Requests\SaveInvoiceWithClientRequest; + +use App\Http\Requests\InvoiceRequest; +use App\Http\Requests\CreateInvoiceRequest; +use App\Http\Requests\UpdateInvoiceRequest; class InvoiceController extends BaseController { protected $mailer; protected $invoiceRepo; protected $clientRepo; + protected $documentRepo; protected $invoiceService; protected $recurringInvoiceService; - protected $model = 'App\Models\Invoice'; + protected $entityType = ENTITY_INVOICE; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, RecurringInvoiceService $recurringInvoiceService) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, DocumentRepository $documentRepo, RecurringInvoiceService $recurringInvoiceService) { // parent::__construct(); @@ -85,20 +91,13 @@ class InvoiceController extends BaseController return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_INVOICE, $search); } - public function edit($publicId, $clone = false) + public function edit(InvoiceRequest $request, $publicId, $clone = false) { $account = Auth::user()->account; - $invoice = Invoice::scope($publicId) - ->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'payments') - ->withTrashed() - ->firstOrFail(); - - if(!$this->checkEditPermission($invoice, $response)){ - return $response; - } + $invoice = $request->entity()->load('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'payments'); $entityType = $invoice->getEntityType(); - + $contactIds = DB::table('invitations') ->join('contacts', 'contacts.id', '=', 'invitations.contact_id') ->where('invitations.invoice_id', '=', $invoice->id) @@ -119,7 +118,7 @@ class InvoiceController extends BaseController } else { Utils::trackViewed($invoice->getDisplayName().' - '.$invoice->client->getDisplayName(), $invoice->getEntityType()); $method = 'PUT'; - $url = "{$entityType}s/{$publicId}"; + $url = "{$entityType}s/{$invoice->public_id}"; $clients->whereId($invoice->client_id); } @@ -129,7 +128,11 @@ class InvoiceController extends BaseController $invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->end_date = Utils::fromSqlDate($invoice->end_date); $invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date); - $invoice->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $actions = [ ['url' => 'javascript:onCloneClick()', 'label' => trans("texts.clone_{$entityType}")], @@ -191,7 +194,7 @@ class InvoiceController extends BaseController 'isRecurring' => $invoice->is_recurring, 'actions' => $actions, 'lastSent' => $lastSent); - $data = array_merge($data, self::getViewModel()); + $data = array_merge($data, self::getViewModel($invoice)); if ($clone) { $data['formIsChanged'] = true; @@ -211,6 +214,7 @@ class InvoiceController extends BaseController $contact->email_error = $invitation->email_error; $contact->invitation_link = $invitation->getLink(); $contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false; + $contact->invitation_openend = $invitation->opened_date && $invitation->opened_date != '0000-00-00 00:00:00' ? $invitation->opened_date : false; $contact->invitation_status = $contact->email_error ? false : $invitation->getStatus(); } } @@ -223,25 +227,27 @@ class InvoiceController extends BaseController return View::make('invoices.edit', $data); } - public function create($clientPublicId = 0, $isRecurring = false) + public function create(InvoiceRequest $request, $clientPublicId = 0, $isRecurring = false) { - if(!$this->checkCreatePermission($response)){ - return $response; - } + $account = Auth::user()->account; - $account = Auth::user()->account; $entityType = $isRecurring ? ENTITY_RECURRING_INVOICE : ENTITY_INVOICE; $clientId = null; - if ($clientPublicId) { - $clientId = Client::getPrivateId($clientPublicId); + if ($request->client_id) { + $clientId = Client::getPrivateId($request->client_id); } $invoice = $account->createInvoice($entityType, $clientId); $invoice->public_id = 0; + + if (Session::get('expenses')) { + $invoice->expenses = Expense::scope(Session::get('expenses'))->with('documents')->get(); + } + $clients = Client::scope()->with('contacts', 'country')->orderBy('name'); - if(!Auth::user()->hasPermission('view_all')){ + if (!Auth::user()->hasPermission('view_all')) { $clients = $clients->where('clients.user_id', '=', Auth::user()->id); } @@ -253,17 +259,17 @@ class InvoiceController extends BaseController 'url' => 'invoices', 'title' => trans('texts.new_invoice'), ]; - $data = array_merge($data, self::getViewModel()); + $data = array_merge($data, self::getViewModel($invoice)); return View::make('invoices.edit', $data); } - public function createRecurring($clientPublicId = 0) + public function createRecurring(InvoiceRequest $request, $clientPublicId = 0) { - return self::create($clientPublicId, true); + return self::create($request, $clientPublicId, true); } - private static function getViewModel() + private static function getViewModel($invoice) { $recurringHelp = ''; foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_help')) as $line) { @@ -324,11 +330,37 @@ class InvoiceController extends BaseController } } + // Tax rate $options + $account = Auth::user()->account; + $rates = TaxRate::scope()->orderBy('name')->get(); + $options = []; + $defaultTax = false; + + foreach ($rates as $rate) { + $options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%'; + + // load default invoice tax + if ($rate->id == $account->default_tax_rate_id) { + $defaultTax = $rate; + } + } + + // Check for any taxes which have been deleted + if ($invoice->exists) { + foreach ($invoice->getTaxes() as $key => $rate) { + if (isset($options[$key])) { + continue; + } + $options[$key] = $rate['name'] . ' ' . $rate['rate'] . '%'; + } + } + return [ 'data' => Input::old('data'), 'account' => Auth::user()->account->load('country'), 'products' => Product::scope()->with('default_tax_rate')->orderBy('product_key')->get(), - 'taxRates' => TaxRate::scope()->orderBy('name')->get(), + 'taxRateOptions' => $options, + 'defaultTax' => $defaultTax, 'currencies' => Cache::get('currencies'), 'languages' => Cache::get('languages'), 'sizes' => Cache::get('sizes'), @@ -350,7 +382,6 @@ class InvoiceController extends BaseController 'recurringDueDateHelp' => $recurringDueDateHelp, 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), 'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null, - 'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null, 'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null, ]; @@ -361,18 +392,15 @@ class InvoiceController extends BaseController * * @return Response */ - public function store(SaveInvoiceWithClientRequest $request) + public function store(CreateInvoiceRequest $request) { $data = $request->input(); - - if(!$this->checkUpdatePermission($data, $response)){ - return $response; - } - + $data['documents'] = $request->file('documents'); + $action = Input::get('action'); $entityType = Input::get('entityType'); - $invoice = $this->invoiceService->save($data, true); + $invoice = $this->invoiceService->save($data); $entityType = $invoice->getEntityType(); $message = trans("texts.created_{$entityType}"); @@ -401,26 +429,23 @@ class InvoiceController extends BaseController * @param int $id * @return Response */ - public function update(SaveInvoiceWithClientRequest $request) + public function update(UpdateInvoiceRequest $request) { $data = $request->input(); - - if(!$this->checkUpdatePermission($data, $response)){ - return $response; - } - + $data['documents'] = $request->file('documents'); + $action = Input::get('action'); $entityType = Input::get('entityType'); - $invoice = $this->invoiceService->save($data, true); + $invoice = $this->invoiceService->save($data, $request->entity()); $entityType = $invoice->getEntityType(); $message = trans("texts.updated_{$entityType}"); Session::flash('message', $message); if ($action == 'clone') { - return $this->cloneInvoice($invoice->public_id); + return $this->cloneInvoice($request, $invoice->public_id); } elseif ($action == 'convert') { - return $this->convertQuote($invoice->public_id); + return $this->convertQuote($request, $invoice->public_id); } elseif ($action == 'email') { return $this->emailInvoice($invoice, Input::get('pdfupload')); } @@ -489,7 +514,7 @@ class InvoiceController extends BaseController { Session::reflash(); - return Redirect::to("invoices/{$publicId}/edit"); + return Redirect::to("invoices/$publicId/edit"); } /** @@ -517,27 +542,31 @@ class InvoiceController extends BaseController } } - public function convertQuote($publicId) + public function convertQuote(InvoiceRequest $request) { - $invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail(); - $clone = $this->invoiceService->convertQuote($invoice); + $clone = $this->invoiceService->convertQuote($request->entity()); Session::flash('message', trans('texts.converted_to_invoice')); - return Redirect::to('invoices/'.$clone->public_id); + + return Redirect::to('invoices/' . $clone->public_id); } - public function cloneInvoice($publicId) + public function cloneInvoice(InvoiceRequest $request, $publicId) { - return self::edit($publicId, true); + return self::edit($request, $publicId, true); } - public function invoiceHistory($publicId) + public function invoiceHistory(InvoiceRequest $request) { - $invoice = Invoice::withTrashed()->scope($publicId)->firstOrFail(); - $invoice->load('user', 'invoice_items', 'account.country', 'client.contacts', 'client.country'); + $invoice = $request->entity(); + $invoice->load('user', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'account.country', 'client.contacts', 'client.country'); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $invoice->is_quote = intval($invoice->is_quote); $activityTypeId = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; @@ -555,7 +584,11 @@ class InvoiceController extends BaseController $backup = json_decode($activity->json_backup); $backup->invoice_date = Utils::fromSqlDate($backup->invoice_date); $backup->due_date = Utils::fromSqlDate($backup->due_date); - $backup->is_pro = Auth::user()->isPro(); + $backup->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $backup->is_quote = isset($backup->is_quote) && intval($backup->is_quote); $backup->account = $invoice->account->toArray(); diff --git a/app/Http/Controllers/PaymentApiController.php b/app/Http/Controllers/PaymentApiController.php index 7022f0c3e840..3355b6bff8f4 100644 --- a/app/Http/Controllers/PaymentApiController.php +++ b/app/Http/Controllers/PaymentApiController.php @@ -12,18 +12,21 @@ use App\Ninja\Repositories\PaymentRepository; use App\Http\Controllers\BaseAPIController; use App\Ninja\Transformers\PaymentTransformer; use App\Ninja\Transformers\InvoiceTransformer; +use App\Http\Requests\UpdatePaymentRequest; +use App\Http\Requests\CreatePaymentAPIRequest; class PaymentApiController extends BaseAPIController { protected $paymentRepo; + protected $entityType = ENTITY_PAYMENT; + public function __construct(PaymentRepository $paymentRepo, ContactMailer $contactMailer) { parent::__construct(); $this->paymentRepo = $paymentRepo; $this->contactMailer = $contactMailer; - } /** @@ -44,85 +47,49 @@ class PaymentApiController extends BaseAPIController */ public function index() { - $paginator = Payment::scope(); $payments = Payment::scope() - ->with('client.contacts', 'invitation', 'user', 'invoice')->withTrashed(); + ->withTrashed() + ->with(['invoice']) + ->orderBy('created_at', 'desc'); - if ($clientPublicId = Input::get('client_id')) { - $filter = function($query) use ($clientPublicId) { - $query->where('public_id', '=', $clientPublicId); - }; - $payments->whereHas('client', $filter); - $paginator->whereHas('client', $filter); - } - - $payments = $payments->orderBy('created_at', 'desc')->paginate(); - $paginator = $paginator->paginate(); - - $transformer = new PaymentTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createCollection($payments, $transformer, 'payments', $paginator); - - return $this->response($data); + return $this->listResponse($payments); } + /** + * @SWG\Put( + * path="/payments/{payment_id", + * summary="Update a payment", + * tags={"payment"}, + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Payment") + * ), + * @SWG\Response( + * response=200, + * description="Update payment", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ - /** - * @SWG\Put( - * path="/payments/{payment_id", - * summary="Update a payment", - * tags={"payment"}, - * @SWG\Parameter( - * in="body", - * name="body", - * @SWG\Schema(ref="#/definitions/Payment") - * ), - * @SWG\Response( - * response=200, - * description="Update payment", - * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment")) - * ), - * @SWG\Response( - * response="default", - * description="an ""unexpected"" error" - * ) - * ) - */ - - public function update(Request $request, $publicId) - { - $data = Input::all(); - $data['public_id'] = $publicId; - $error = false; - - if ($request->action == ACTION_ARCHIVE) { - $payment = Payment::scope($publicId)->withTrashed()->firstOrFail(); - $this->paymentRepo->archive($payment); - - $transformer = new PaymentTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($payment, $transformer, 'invoice'); - - return $this->response($data); - } - - $payment = $this->paymentRepo->save($data); - - if ($error) { - return $error; - } - - /* - $invoice = Invoice::scope($data['invoice_id'])->with('client', 'invoice_items', 'invitations')->with(['payments' => function($query) { - $query->withTrashed(); - }])->withTrashed()->first(); - */ - - $transformer = new PaymentTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($payment, $transformer, 'invoice'); - - return $this->response($data); - + public function update(UpdatePaymentRequest $request, $publicId) + { + if ($request->action) { + return $this->handleAction($request); } + $data = $request->input(); + $data['public_id'] = $publicId; + $payment = $this->paymentRepo->save($data, $request->entity()); + + return $this->itemResponse($payment); + } + /** * @SWG\Post( @@ -145,89 +112,46 @@ class PaymentApiController extends BaseAPIController * ) * ) */ - public function store() + public function store(CreatePaymentAPIRequest $request) { - $data = Input::all(); - $error = false; - - if (isset($data['invoice_id'])) { - $invoice = Invoice::scope($data['invoice_id'])->with('client')->first(); - - if ($invoice) { - $data['invoice_id'] = $invoice->id; - $data['client_id'] = $invoice->client->id; - } else { - $error = trans('validation.not_in', ['attribute' => 'invoice_id']); - } - } else { - $error = trans('validation.not_in', ['attribute' => 'invoice_id']); - } - - if (!isset($data['transaction_reference'])) { - $data['transaction_reference'] = ''; - } - - if ($error) { - return $error; - } - - $payment = $this->paymentRepo->save($data); + $payment = $this->paymentRepo->save($request->input()); if (Input::get('email_receipt')) { $this->contactMailer->sendPaymentConfirmation($payment); } - /* - $invoice = Invoice::scope($invoice->public_id)->with('client', 'invoice_items', 'invitations')->with(['payments' => function($query) { - $query->withTrashed(); - }])->first(); - */ - - $transformer = new PaymentTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($payment, $transformer, 'invoice'); - - return $this->response($data); - + return $this->itemResponse($payment); } - /** - * @SWG\Delete( - * path="/payments/{payment_id}", - * summary="Delete a payment", - * tags={"payment"}, - * @SWG\Parameter( - * in="body", - * name="body", - * @SWG\Schema(ref="#/definitions/Payment") - * ), - * @SWG\Response( - * response=200, - * description="Delete payment", - * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment")) - * ), - * @SWG\Response( - * response="default", - * description="an ""unexpected"" error" - * ) - * ) - */ + /** + * @SWG\Delete( + * path="/payments/{payment_id}", + * summary="Delete a payment", + * tags={"payment"}, + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Payment") + * ), + * @SWG\Response( + * response=200, + * description="Delete payment", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ - public function destroy($publicId) - { + public function destroy(UpdatePaymentRequest $request) + { + $payment = $request->entity(); + + $this->clientRepo->delete($payment); - $payment = Payment::scope($publicId)->withTrashed()->first(); - $invoiceId = $payment->invoice->public_id; + return $this->itemResponse($payment); + } - $this->paymentRepo->delete($payment); - - /* - $invoice = Invoice::scope($invoiceId)->with('client', 'invoice_items', 'invitations')->with(['payments' => function($query) { - $query->withTrashed(); - }])->first(); - */ - $transformer = new PaymentTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($payment, $transformer, 'invoice'); - - return $this->response($data); - } } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index ebede5711bd5..0b93d45374b0 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -25,12 +25,13 @@ use App\Ninja\Repositories\AccountRepository; use App\Ninja\Mailers\ContactMailer; use App\Services\PaymentService; +use App\Http\Requests\PaymentRequest; use App\Http\Requests\CreatePaymentRequest; use App\Http\Requests\UpdatePaymentRequest; class PaymentController extends BaseController { - protected $model = 'App\Models\Payment'; + protected $entityType = ENTITY_PAYMENT; public function __construct(PaymentRepository $paymentRepo, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, ContactMailer $contactMailer, PaymentService $paymentService) { @@ -67,12 +68,8 @@ class PaymentController extends BaseController return $this->paymentService->getDatatable($clientPublicId, Input::get('sSearch')); } - public function create($clientPublicId = 0, $invoicePublicId = 0) + public function create(PaymentRequest $request) { - if(!$this->checkCreatePermission($response)){ - return $response; - } - $invoices = Invoice::scope() ->where('is_recurring', '=', false) ->where('is_quote', '=', false) @@ -81,8 +78,8 @@ class PaymentController extends BaseController ->orderBy('invoice_number')->get(); $data = array( - 'clientPublicId' => Input::old('client') ? Input::old('client') : $clientPublicId, - 'invoicePublicId' => Input::old('invoice') ? Input::old('invoice') : $invoicePublicId, + 'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0), + 'invoicePublicId' => Input::old('invoice') ? Input::old('invoice') : ($request->invoice_id ?: 0), 'invoice' => null, 'invoices' => $invoices, 'payment' => null, @@ -96,14 +93,10 @@ class PaymentController extends BaseController return View::make('payments.edit', $data); } - public function edit($publicId) + public function edit(PaymentRequest $request) { - $payment = Payment::scope($publicId)->firstOrFail(); - - if(!$this->checkEditPermission($payment, $response)){ - return $response; - } - + $payment = $request->entity(); + $payment->payment_date = Utils::fromSqlDate($payment->payment_date); $data = array( @@ -113,7 +106,7 @@ class PaymentController extends BaseController ->with('client', 'invoice_status')->orderBy('invoice_number')->get(), 'payment' => $payment, 'method' => 'PUT', - 'url' => 'payments/'.$publicId, + 'url' => 'payments/'.$payment->public_id, 'title' => trans('texts.edit_payment'), 'paymentTypes' => Cache::get('paymentTypes'), 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ); @@ -191,7 +184,7 @@ class PaymentController extends BaseController 'currencyId' => $client->getCurrencyId(), 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), 'account' => $client->account, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideHeader' => $account->isNinjaAccount(), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), @@ -355,9 +348,18 @@ class PaymentController extends BaseController $license->save(); } - return $productId == PRODUCT_INVOICE_DESIGNS ? file_get_contents(storage_path() . '/invoice_designs.txt') : 'valid'; + if ($productId == PRODUCT_INVOICE_DESIGNS) { + return file_get_contents(storage_path() . '/invoice_designs.txt'); + } else { + // temporary fix to enable previous version to work + if (Input::get('get_date')) { + return $license->created_at->format('Y-m-d'); + } else { + return 'valid'; + } + } } else { - return 'invalid'; + return RESULT_FAILURE; } } @@ -460,6 +462,8 @@ class PaymentController extends BaseController $ref = $response->getData()['m_payment_id']; } elseif ($accountGateway->gateway_id == GATEWAY_GOCARDLESS) { $ref = $response->getData()['signature']; + } elseif ($accountGateway->gateway_id == GATEWAY_CYBERSOURCE) { + $ref = $response->getData()['transaction_uuid']; } else { $ref = $response->getTransactionReference(); } @@ -482,6 +486,7 @@ class PaymentController extends BaseController if ($account->account_key == NINJA_ACCOUNT_KEY) { Session::flash('trackEventCategory', '/account'); Session::flash('trackEventAction', '/buy_pro_plan'); + Session::flash('trackEventAmount', $payment->amount); } return Redirect::to('view/'.$payment->invitation->invitation_key); @@ -551,7 +556,16 @@ class PaymentController extends BaseController } try { - if (method_exists($gateway, 'completePurchase') + if ($accountGateway->isGateway(GATEWAY_CYBERSOURCE)) { + if (Input::get('decision') == 'ACCEPT') { + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $token, $payerId); + Session::flash('message', trans('texts.applied_payment')); + } else { + $message = Input::get('message') . ': ' . Input::get('invalid_fields'); + Session::flash('error', $message); + } + return Redirect::to($invitation->getLink()); + } elseif (method_exists($gateway, 'completePurchase') && !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT) && !$accountGateway->isGateway(GATEWAY_CHECKOUT_COM)) { $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway); @@ -572,11 +586,9 @@ class PaymentController extends BaseController } else { $payment = $this->paymentService->createPayment($invitation, $accountGateway, $token, $payerId); Session::flash('message', trans('texts.applied_payment')); - return Redirect::to($invitation->getLink()); } } catch (\Exception $e) { - $this->error('Offsite-uncaught', false, $accountGateway, $e); return Redirect::to($invitation->getLink()); } @@ -585,11 +597,7 @@ class PaymentController extends BaseController public function store(CreatePaymentRequest $request) { $input = $request->input(); - - if(!$this->checkUpdatePermission($input, $response)){ - return $response; - } - + $input['invoice_id'] = Invoice::getPrivateId($input['invoice']); $input['client_id'] = Client::getPrivateId($input['client']); $payment = $this->paymentRepo->save($input); @@ -606,13 +614,7 @@ class PaymentController extends BaseController public function update(UpdatePaymentRequest $request) { - $input = $request->input(); - - if(!$this->checkUpdatePermission($input, $response)){ - return $response; - } - - $payment = $this->paymentRepo->save($input); + $payment = $this->paymentRepo->save($request->input(), $request->entity()); Session::flash('message', trans('texts.updated_payment')); diff --git a/app/Http/Controllers/ProductApiController.php b/app/Http/Controllers/ProductApiController.php index 87bf89403b8a..6a8756eda4b6 100644 --- a/app/Http/Controllers/ProductApiController.php +++ b/app/Http/Controllers/ProductApiController.php @@ -1,103 +1,54 @@ productService = $productService; $this->productRepo = $productRepo; } public function index() { + $products = Product::scope() + ->withTrashed() + ->orderBy('created_at', 'desc'); - $products = Product::scope()->withTrashed(); - $products = $products->paginate(); - - $paginator = Product::scope()->withTrashed()->paginate(); - - $transformer = new ProductTransformer(\Auth::user()->account, $this->serializer); - $data = $this->createCollection($products, $transformer, 'products', $paginator); - - return $this->response($data); - + return $this->listResponse($products); } - public function getDatatable() + public function store(CreateProductRequest $request) { - return $this->productService->getDatatable(Auth::user()->account_id); + $product = $this->productRepo->save($request->input()); + + return $this->itemResponse($product); } - public function store() + public function update(UpdateProductRequest $request, $publicId) { - return $this->save(); - } - - public function update(\Illuminate\Http\Request $request, $publicId) - { - - if ($request->action == ACTION_ARCHIVE) { - $product = Product::scope($publicId)->withTrashed()->firstOrFail(); - $this->productRepo->archive($product); - - $transformer = new ProductTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($product, $transformer, 'products'); - - return $this->response($data); + if ($request->action) { + return $this->handleAction($request); } - else - return $this->save($publicId); + + $data = $request->input(); + $data['public_id'] = $publicId; + $product = $this->productRepo->save($data, $request->entity()); + + return $this->itemResponse($product); } public function destroy($publicId) { //stub } - - private function save($productPublicId = false) - { - if ($productPublicId) { - $product = Product::scope($productPublicId)->firstOrFail(); - } else { - $product = Product::createNew(); - } - - $product->product_key = trim(Input::get('product_key')); - $product->notes = trim(Input::get('notes')); - $product->cost = trim(Input::get('cost')); - //$product->default_tax_rate_id = Input::get('default_tax_rate_id'); - - $product->save(); - - $transformer = new ProductTransformer(\Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($product, $transformer, 'products'); - - return $this->response($data); - - } - - } diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 39b65b02e001..32d6d53c3887 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -7,27 +7,33 @@ use URL; use Input; use Utils; use Request; +use Response; use Session; use Datatable; use App\Models\Gateway; use App\Models\Invitation; +use App\Models\Document; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ActivityRepository; +use App\Ninja\Repositories\DocumentRepository; use App\Events\InvoiceInvitationWasViewed; use App\Events\QuoteInvitationWasViewed; use App\Services\PaymentService; +use Barracuda\ArchiveStream\ZipArchive; class PublicClientController extends BaseController { private $invoiceRepo; private $paymentRepo; + private $documentRepo; - public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, PaymentService $paymentService) + public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, DocumentRepository $documentRepo, PaymentService $paymentService) { $this->invoiceRepo = $invoiceRepo; $this->paymentRepo = $paymentRepo; $this->activityRepo = $activityRepo; + $this->documentRepo = $documentRepo; $this->paymentService = $paymentService; } @@ -66,7 +72,11 @@ class PublicClientController extends BaseController $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = $account->isPro(); + $invoice->features = [ + 'customize_invoice_design' => $account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => $account->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => $account->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $invoice->invoice_fonts = $account->getFontsData(); if ($invoice->invoice_design_id == CUSTOM_DESIGN) { @@ -116,9 +126,10 @@ class PublicClientController extends BaseController 'account' => $account, 'showApprove' => $showApprove, 'showBreadcrumbs' => false, - 'hideLogo' => $account->isWhiteLabel(), - 'hideHeader' => $account->isNinjaAccount(), - 'hideDashboard' => !$account->enable_client_portal, + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'hideHeader' => $account->isNinjaAccount() || !$account->enable_client_portal, + 'hideDashboard' => !$account->enable_client_portal_dashboard, + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'invoice' => $invoice->hidePrivateFields(), @@ -132,6 +143,15 @@ class PublicClientController extends BaseController 'checkoutComDebug' => $checkoutComDebug, 'phantomjs' => Input::has('phantomjs'), ); + + if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ + $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); + + if(count($zipDocs) > 1){ + $data['documentsZipURL'] = URL::to("client/documents/{$invitation->invitation_key}"); + $data['documentsZipSize'] = $size; + } + } return View::make('invoices.view', $data); } @@ -196,7 +216,7 @@ class PublicClientController extends BaseController $client = $invoice->client; $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - if (!$account->enable_client_portal) { + if (!$account->enable_client_portal || !$account->enable_client_portal_dashboard) { return $this->returnError(); } @@ -204,7 +224,8 @@ class PublicClientController extends BaseController 'color' => $color, 'account' => $account, 'client' => $client, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), ]; @@ -245,13 +266,20 @@ class PublicClientController extends BaseController if (!$invitation = $this->getInvitation()) { return $this->returnError(); } + $account = $invitation->account; + + if (!$account->enable_client_portal) { + return $this->returnError(); + } + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'hideDashboard' => !$account->enable_client_portal, + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'hideDashboard' => !$account->enable_client_portal_dashboard, + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.invoices'), @@ -278,12 +306,17 @@ class PublicClientController extends BaseController return $this->returnError(); } $account = $invitation->account; - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - + + if (!$account->enable_client_portal) { + return $this->returnError(); + } + + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'hideDashboard' => !$account->enable_client_portal, + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'hideDashboard' => !$account->enable_client_portal_dashboard, + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'entityType' => ENTITY_PAYMENT, @@ -315,13 +348,19 @@ class PublicClientController extends BaseController if (!$invitation = $this->getInvitation()) { return $this->returnError(); } + $account = $invitation->account; + + if (!$account->enable_client_portal) { + return $this->returnError(); + } + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), - 'hideDashboard' => !$account->enable_client_portal, + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'hideDashboard' => !$account->enable_client_portal_dashboard, + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.quotes'), @@ -342,6 +381,44 @@ class PublicClientController extends BaseController return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, Input::get('sSearch')); } + public function documentIndex() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $account = $invitation->account; + + if (!$account->enable_client_portal) { + return $this->returnError(); + } + + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ + 'color' => $color, + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'hideDashboard' => !$account->enable_client_portal_dashboard, + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.documents'), + 'entityType' => ENTITY_DOCUMENT, + 'columns' => Utils::trans(['invoice_number', 'name', 'document_date', 'document_size']), + ]; + + return response()->view('public_list', $data); + } + + + public function documentDatatable() + { + if (!$invitation = $this->getInvitation()) { + return false; + } + + return $this->documentRepo->getClientDatatable($invitation->contact_id, ENTITY_DOCUMENT, Input::get('sSearch')); + } + private function returnError($error = false) { return response()->view('error', [ @@ -372,5 +449,148 @@ class PublicClientController extends BaseController return $invitation; } + + public function getDocumentVFSJS($publicId, $name){ + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $clientId = $invitation->invoice->client_id; + $document = Document::scope($publicId, $invitation->account_id)->first(); + + + if(!$document->isPDFEmbeddable()){ + return Response::view('error', array('error'=>'Image does not exist!'), 404); + } + + $authorized = false; + if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ + $authorized = true; + } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ + $authorized = true; + } + + if(!$authorized){ + return Response::view('error', array('error'=>'Not authorized'), 403); + } + + if(substr($name, -3)=='.js'){ + $name = substr($name, 0, -3); + } + + $content = $document->preview?$document->getRawPreview():$document->getRaw(); + $content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")'; + $response = Response::make($content, 200); + $response->header('content-type', 'text/javascript'); + $response->header('cache-control', 'max-age=31536000'); + + return $response; + } + + protected function canCreateZip(){ + return function_exists('gmp_init'); + } + + protected function getInvoiceZipDocuments($invoice, &$size=0){ + $documents = $invoice->documents; + + foreach($invoice->expenses as $expense){ + $documents = $documents->merge($expense->documents); + } + + $documents = $documents->sortBy('size'); + + $size = 0; + $maxSize = MAX_ZIP_DOCUMENTS_SIZE * 1000; + $toZip = array(); + foreach($documents as $document){ + if($size + $document->size > $maxSize)break; + + if(!empty($toZip[$document->name])){ + // This name is taken + if($toZip[$document->name]->hash != $document->hash){ + // 2 different files with the same name + $nameInfo = pathinfo($document->name); + + for($i = 1;; $i++){ + $name = $nameInfo['filename'].' ('.$i.').'.$nameInfo['extension']; + + if(empty($toZip[$name])){ + $toZip[$name] = $document; + $size += $document->size; + break; + } else if ($toZip[$name]->hash == $document->hash){ + // We're not adding this after all + break; + } + } + + } + } + else{ + $toZip[$document->name] = $document; + $size += $document->size; + } + } + + return $toZip; + } + + public function getInvoiceDocumentsZip($invitationKey){ + if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { + return $this->returnError(); + } + + Session::put('invitation_key', $invitationKey); // track current invitation + + $invoice = $invitation->invoice; + + $toZip = $this->getInvoiceZipDocuments($invoice); + + if(!count($toZip)){ + return Response::view('error', array('error'=>'No documents small enough'), 404); + } + + $zip = new ZipArchive($invitation->account->name.' Invoice '.$invoice->invoice_number.'.zip'); + return Response::stream(function() use ($toZip, $zip) { + foreach($toZip as $name=>$document){ + $fileStream = $document->getStream(); + if($fileStream){ + $zip->init_file_stream_transfer($name, $document->size, array('time'=>$document->created_at->timestamp)); + while ($buffer = fread($fileStream, 256000))$zip->stream_file_part($buffer); + fclose($fileStream); + $zip->complete_file_stream(); + } + else{ + $zip->add_file($name, $document->getRaw()); + } + } + $zip->finish(); + }, 200); + } + + public function getDocument($invitationKey, $publicId){ + if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { + return $this->returnError(); + } + + Session::put('invitation_key', $invitationKey); // track current invitation + + $clientId = $invitation->invoice->client_id; + $document = Document::scope($publicId, $invitation->account_id)->firstOrFail(); + + $authorized = false; + if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ + $authorized = true; + } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ + $authorized = true; + } + + if(!$authorized){ + return Response::view('error', array('error'=>'Not authorized'), 403); + } + + return DocumentController::getDownloadResponse($document); + } } diff --git a/app/Http/Controllers/QuoteApiController.php b/app/Http/Controllers/QuoteApiController.php deleted file mode 100644 index 0868fbe97188..000000000000 --- a/app/Http/Controllers/QuoteApiController.php +++ /dev/null @@ -1,75 +0,0 @@ -invoiceRepo = $invoiceRepo; - } - - /** - * @SWG\Get( - * path="/quotes", - * tags={"quote"}, - * summary="List of quotes", - * @SWG\Response( - * response=200, - * description="A list with quotes", - * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice")) - * ), - * @SWG\Response( - * response="default", - * description="an ""unexpected"" error" - * ) - * ) - */ - public function index() - { - $paginator = Invoice::scope(); - $invoices = Invoice::scope() - ->with('client', 'invitations', 'user', 'invoice_items') - ->where('invoices.is_quote', '=', true); - - if ($clientPublicId = Input::get('client_id')) { - $filter = function($query) use ($clientPublicId) { - $query->where('public_id', '=', $clientPublicId); - }; - $invoices->whereHas('client', $filter); - $paginator->whereHas('client', $filter); - } - - $invoices = $invoices->orderBy('created_at', 'desc')->paginate(); - - $transformer = new QuoteTransformer(\Auth::user()->account, Input::get('serializer')); - $paginator = $paginator->paginate(); - - $data = $this->createCollection($invoices, $transformer, 'quotes', $paginator); - - return $this->response($data); - } - - /* - public function store() - { - $data = Input::all(); - $invoice = $this->invoiceRepo->save(false, $data, false); - - $response = json_encode($invoice, JSON_PRETTY_PRINT); - $headers = Utils::getApiHeaders(); - return Response::make($response, 200, $headers); - } - */ -} diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 0c2dc3a8a5ba..a8ea0beaa476 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -26,6 +26,7 @@ use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; use App\Events\QuoteInvitationWasApproved; use App\Services\InvoiceService; +use App\Http\Requests\InvoiceRequest; class QuoteController extends BaseController { @@ -33,7 +34,7 @@ class QuoteController extends BaseController protected $invoiceRepo; protected $clientRepo; protected $invoiceService; - protected $model = 'App\Models\Invoice'; + protected $entityType = ENTITY_INVOICE; public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService) { @@ -47,7 +48,7 @@ class QuoteController extends BaseController public function index() { - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_QUOTES)) { return Redirect::to('/invoices/create'); } @@ -78,13 +79,9 @@ class QuoteController extends BaseController return $this->invoiceService->getDatatable($accountId, $clientPublicId, ENTITY_QUOTE, $search); } - public function create($clientPublicId = 0) + public function create(InvoiceRequest $request, $clientPublicId = 0) { - if(!$this->checkCreatePermission($response)){ - return $response; - } - - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_QUOTES)) { return Redirect::to('/invoices/create'); } @@ -111,10 +108,27 @@ class QuoteController extends BaseController private static function getViewModel() { + // Tax rate $options + $account = Auth::user()->account; + $rates = TaxRate::scope()->orderBy('name')->get(); + $options = []; + $defaultTax = false; + + foreach ($rates as $rate) { + $options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%'; + + // load default invoice tax + if ($rate->id == $account->default_tax_rate_id) { + $defaultTax = $rate; + } + } + return [ 'entityType' => ENTITY_QUOTE, 'account' => Auth::user()->account, 'products' => Product::scope()->orderBy('id')->get(array('product_key', 'notes', 'cost', 'qty')), + 'taxRateOptions' => $options, + 'defaultTax' => $defaultTax, 'countries' => Cache::get('countries'), 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), 'taxRates' => TaxRate::scope()->orderBy('name')->get(), diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index cfb9caaddef6..a63b16d9fcc4 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -21,7 +21,7 @@ class ReportController extends BaseController $message = ''; $fileName = storage_path().'/dataviz_sample.txt'; - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { $account = Account::where('id', '=', Auth::user()->account->id) ->with(['clients.invoices.invoice_items', 'clients.contacts']) ->first(); @@ -99,13 +99,13 @@ class ReportController extends BaseController 'title' => trans('texts.charts_and_reports'), ]; - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { if ($enableReport) { $isExport = $action == 'export'; $params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport)); if ($isExport) { - self::export($params['displayData'], $params['columns'], $params['reportTotals']); + self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']); } } if ($enableChart) { @@ -514,11 +514,14 @@ class ReportController extends BaseController return $data; } - private function export($data, $columns, $totals) + private function export($reportType, $data, $columns, $totals) { $output = fopen('php://output', 'w') or Utils::fatalError(); + $reportType = trans("texts.{$reportType}s"); + $date = date('Y-m-d'); + header('Content-Type:application/csv'); - header('Content-Disposition:attachment;filename=ninja-report.csv'); + header("Content-Disposition:attachment;filename={$date}_Ninja_{$reportType}.csv"); Utils::exportData($output, $data, Utils::trans($columns)); diff --git a/app/Http/Controllers/TaskApiController.php b/app/Http/Controllers/TaskApiController.php index 926ed2f98ad3..7945008bcc40 100644 --- a/app/Http/Controllers/TaskApiController.php +++ b/app/Http/Controllers/TaskApiController.php @@ -13,6 +13,8 @@ class TaskApiController extends BaseAPIController { protected $taskRepo; + protected $entityType = ENTITY_TASK; + public function __construct(TaskRepository $taskRepo) { parent::__construct(); @@ -38,25 +40,11 @@ class TaskApiController extends BaseAPIController */ public function index() { - $paginator = Task::scope(); - $tasks = Task::scope() - ->with($this->getIncluded()); + $payments = Task::scope() + ->withTrashed() + ->orderBy('created_at', 'desc'); - if ($clientPublicId = Input::get('client_id')) { - $filter = function($query) use ($clientPublicId) { - $query->where('public_id', '=', $clientPublicId); - }; - $tasks->whereHas('client', $filter); - $paginator->whereHas('client', $filter); - } - - $tasks = $tasks->orderBy('created_at', 'desc')->paginate(); - $paginator = $paginator->paginate(); - $transformer = new TaskTransformer(\Auth::user()->account, Input::get('serializer')); - - $data = $this->createCollection($tasks, $transformer, 'tasks', $paginator); - - return $this->response($data); + return $this->listResponse($payments); } /** diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 59f49da2514f..229a4751116e 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -18,11 +18,15 @@ use App\Ninja\Repositories\TaskRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Services\TaskService; +use App\Http\Requests\TaskRequest; +use App\Http\Requests\CreateTaskRequest; +use App\Http\Requests\UpdateTaskRequest; + class TaskController extends BaseController { protected $taskRepo; protected $taskService; - protected $model = 'App\Models\Task'; + protected $entityType = ENTITY_TASK; public function __construct(TaskRepository $taskRepo, InvoiceRepository $invoiceRepo, TaskService $taskService) { @@ -66,7 +70,7 @@ class TaskController extends BaseController * * @return Response */ - public function store() + public function store(CreateTaskRequest $request) { return $this->save(); } @@ -83,16 +87,13 @@ class TaskController extends BaseController * * @return Response */ - public function create($clientPublicId = 0) + public function create(TaskRequest $request) { - if(!$this->checkCreatePermission($response)){ - return $response; - } $this->checkTimezone(); $data = [ 'task' => null, - 'clientPublicId' => Input::old('client') ? Input::old('client') : $clientPublicId, + 'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0), 'method' => 'POST', 'url' => 'tasks', 'title' => trans('texts.new_task'), @@ -111,15 +112,11 @@ class TaskController extends BaseController * @param int $id * @return Response */ - public function edit($publicId) + public function edit(TaskRequest $request) { $this->checkTimezone(); - $task = Task::scope($publicId)->with('client', 'invoice')->withTrashed()->firstOrFail(); - - if(!$this->checkEditPermission($task, $response)){ - return $response; - } + $task = $request->entity(); $actions = []; if ($task->invoice) { @@ -147,7 +144,7 @@ class TaskController extends BaseController 'task' => $task, 'clientPublicId' => $task->client ? $task->client->public_id : 0, 'method' => 'PUT', - 'url' => 'tasks/'.$publicId, + 'url' => 'tasks/'.$task->public_id, 'title' => trans('texts.edit_task'), 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), 'actions' => $actions, @@ -167,9 +164,11 @@ class TaskController extends BaseController * @param int $id * @return Response */ - public function update($publicId) + public function update(UpdateTaskRequest $request) { - return $this->save($publicId); + $task = $request->entity(); + + return $this->save($task->public_id); } private static function getViewModel() @@ -184,22 +183,10 @@ class TaskController extends BaseController { $action = Input::get('action'); - if(!$this->checkUpdatePermission(array('public_id'=>$publicId)/* Hacky, but works */, $response)){ - return $response; - } - if (in_array($action, ['archive', 'delete', 'restore'])) { return self::bulk(); } - if ($validator = $this->taskRepo->getErrors(Input::all())) { - $url = $publicId ? 'tasks/'.$publicId.'/edit' : 'tasks/create'; - Session::flash('error', trans('texts.task_errors')); - return Redirect::to($url) - ->withErrors($validator) - ->withInput(); - } - $task = $this->taskRepo->save($publicId, Input::all()); Session::flash('message', trans($publicId ? 'texts.updated_task' : 'texts.created_task')); diff --git a/app/Http/Controllers/TaxRateApiController.php b/app/Http/Controllers/TaxRateApiController.php index 7cacfcf8311b..85756205d9a1 100644 --- a/app/Http/Controllers/TaxRateApiController.php +++ b/app/Http/Controllers/TaxRateApiController.php @@ -1,68 +1,54 @@ taxRateService = $taxRateService; $this->taxRateRepo = $taxRateRepo; } public function index() { - $taxRates = TaxRate::scope()->withTrashed(); - $taxRates = $taxRates->paginate(); + $taxRates = TaxRate::scope() + ->withTrashed() + ->orderBy('created_at', 'desc'); - $paginator = TaxRate::scope()->withTrashed()->paginate(); - - $transformer = new TaxRateTransformer(Auth::user()->account, $this->serializer); - $data = $this->createCollection($taxRates, $transformer, 'tax_rates', $paginator); - - return $this->response($data); + return $this->listResponse($taxRates); } public function store(CreateTaxRateRequest $request) { - return $this->save($request); + $taxRate = $this->taxRateRepo->save($request->input()); + + return $this->itemResponse($taxRate); } - public function update(UpdateTaxRateRequest $request, $taxRatePublicId) + public function update(UpdateTaxRateRequest $request, $publicId) { - $taxRate = TaxRate::scope($taxRatePublicId)->firstOrFail(); - - if ($request->action == ACTION_ARCHIVE) { - $this->taxRateRepo->archive($taxRate); - - $transformer = new TaxRateTransformer(Auth::user()->account, $request->serializer); - $data = $this->createItem($taxRate, $transformer, 'tax_rates'); - - return $this->response($data); - } else { - return $this->save($request, $taxRate); + if ($request->action) { + return $this->handleAction($request); } + + $data = $request->input(); + $data['public_id'] = $publicId; + $taxRate = $this->taxRateRepo->save($data, $request->entity()); + + return $this->itemResponse($taxRate); } - private function save($request, $taxRate = false) + public function destroy($publicId) { - $taxRate = $this->taxRateRepo->save($request->input(), $taxRate); - - $transformer = new TaxRateTransformer(\Auth::user()->account, $request->serializer); - $data = $this->createItem($taxRate, $transformer, 'tax_rates'); - - return $this->response($data); + //stub } } diff --git a/app/Http/Controllers/TaxRateController.php b/app/Http/Controllers/TaxRateController.php index 223f7491092e..cba4058756de 100644 --- a/app/Http/Controllers/TaxRateController.php +++ b/app/Http/Controllers/TaxRateController.php @@ -75,9 +75,7 @@ class TaxRateController extends BaseController public function update(UpdateTaxRateRequest $request, $publicId) { - $taxRate = TaxRate::scope($publicId)->firstOrFail(); - - $this->taxRateRepo->save($request->input(), $taxRate); + $this->taxRateRepo->save($request->input(), $request->entity()); Session::flash('message', trans('texts.updated_tax_rate')); return Redirect::to('settings/' . ACCOUNT_TAX_RATES); diff --git a/app/Http/Controllers/TokenController.php b/app/Http/Controllers/TokenController.php index 8e255d6057be..d5ad3b41b739 100644 --- a/app/Http/Controllers/TokenController.php +++ b/app/Http/Controllers/TokenController.php @@ -32,7 +32,7 @@ class TokenController extends BaseController public function getDatatable() { - return $this->tokenService->getDatatable(Auth::user()->account_id); + return $this->tokenService->getDatatable(Auth::user()->id); } public function edit($publicId) @@ -93,7 +93,7 @@ class TokenController extends BaseController */ public function save($tokenPublicId = false) { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_API)) { $rules = [ 'name' => 'required', ]; diff --git a/app/Http/Controllers/UserApiController.php b/app/Http/Controllers/UserApiController.php index fcd48787b134..2869c3512f5a 100644 --- a/app/Http/Controllers/UserApiController.php +++ b/app/Http/Controllers/UserApiController.php @@ -14,6 +14,8 @@ class UserApiController extends BaseAPIController protected $userService; protected $userRepo; + protected $entityType = ENTITY_USER; + public function __construct(UserService $userService, UserRepository $userRepo) { parent::__construct(); @@ -24,16 +26,11 @@ class UserApiController extends BaseAPIController public function index() { - $user = Auth::user(); - $users = User::whereAccountId($user->account_id)->withTrashed(); - $users = $users->paginate(); - - $paginator = User::whereAccountId($user->account_id)->withTrashed()->paginate(); - - $transformer = new UserTransformer(Auth::user()->account, $this->serializer); - $data = $this->createCollection($users, $transformer, 'users', $paginator); - - return $this->response($data); + $users = User::whereAccountId(Auth::user()->account_id) + ->withTrashed() + ->orderBy('created_at', 'desc'); + + return $this->listResponse($users); } /* @@ -45,11 +42,6 @@ class UserApiController extends BaseAPIController public function update(UpdateUserRequest $request, $userPublicId) { - /* - // temporary fix for ids starting at 0 - $userPublicId -= 1; - $user = User::scope($userPublicId)->firstOrFail(); - */ $user = Auth::user(); if ($request->action == ACTION_ARCHIVE) { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 2e3f675aa53e..6b3ab9ca5b58 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -164,7 +164,7 @@ class UserController extends BaseController */ public function save($userPublicId = false) { - if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { + if (Auth::user()->hasFeature(FEATURE_USERS)) { $rules = [ 'first_name' => 'required', 'last_name' => 'required', @@ -190,8 +190,10 @@ class UserController extends BaseController $user->last_name = trim(Input::get('last_name')); $user->username = trim(Input::get('email')); $user->email = trim(Input::get('email')); - $user->is_admin = boolval(Input::get('is_admin')); - $user->permissions = Input::get('permissions'); + if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { + $user->is_admin = boolval(Input::get('is_admin')); + $user->permissions = Input::get('permissions'); + } } else { $lastUser = User::withTrashed()->where('account_id', '=', Auth::user()->account_id) ->orderBy('public_id', 'DESC')->first(); @@ -202,12 +204,14 @@ class UserController extends BaseController $user->last_name = trim(Input::get('last_name')); $user->username = trim(Input::get('email')); $user->email = trim(Input::get('email')); - $user->is_admin = boolval(Input::get('is_admin')); $user->registered = true; $user->password = str_random(RANDOM_KEY_LENGTH); $user->confirmation_code = str_random(RANDOM_KEY_LENGTH); $user->public_id = $lastUser->public_id + 1; - $user->permissions = Input::get('permissions'); + if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { + $user->is_admin = boolval(Input::get('is_admin')); + $user->permissions = Input::get('permissions'); + } } $user->save(); @@ -286,6 +290,9 @@ class UserController extends BaseController if (!Auth::user()->registered) { $account = Auth::user()->account; $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); } } diff --git a/app/Http/Controllers/VendorApiController.php b/app/Http/Controllers/VendorApiController.php index 4c32ee1eb3c5..e15207934cd8 100644 --- a/app/Http/Controllers/VendorApiController.php +++ b/app/Http/Controllers/VendorApiController.php @@ -14,6 +14,8 @@ class VendorApiController extends BaseAPIController { protected $vendorRepo; + protected $entityType = ENTITY_VENDOR; + public function __construct(VendorRepository $vendorRepo) { parent::__construct(); @@ -46,17 +48,11 @@ class VendorApiController extends BaseAPIController */ public function index() { - $vendors = Vendor::scope() - ->with($this->getIncluded()) + $vendors = Vendor::scope() ->withTrashed() - ->orderBy('created_at', 'desc') - ->paginate(); + ->orderBy('created_at', 'desc'); - $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer')); - $paginator = Vendor::scope()->paginate(); - $data = $this->createCollection($vendors, $transformer, ENTITY_VENDOR, $paginator); - - return $this->response($data); + return $this->listResponse($vendors); } /** @@ -85,11 +81,9 @@ class VendorApiController extends BaseAPIController $vendor = $this->vendorRepo->save($request->input()); $vendor = Vendor::scope($vendor->public_id) - ->with('country', 'vendorcontacts', 'industry', 'size', 'currency') + ->with('country', 'vendor_contacts', 'industry', 'size', 'currency') ->first(); - $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer')); - $data = $this->createItem($vendor, $transformer, ENTITY_VENDOR); - return $this->response($data); + return $this->itemResponse($vendor); } } diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index 989246f824ca..f1952e20dee3 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -23,14 +23,15 @@ use App\Models\Country; use App\Ninja\Repositories\VendorRepository; use App\Services\VendorService; +use App\Http\Requests\VendorRequest; use App\Http\Requests\CreateVendorRequest; use App\Http\Requests\UpdateVendorRequest; -// vendor + class VendorController extends BaseController { protected $vendorService; protected $vendorRepo; - protected $model = 'App\Models\Vendor'; + protected $entityType = ENTITY_VENDOR; public function __construct(VendorRepository $vendorRepo, VendorService $vendorService) { @@ -38,8 +39,6 @@ class VendorController extends BaseController $this->vendorRepo = $vendorRepo; $this->vendorService = $vendorService; - - } /** @@ -77,13 +76,7 @@ class VendorController extends BaseController */ public function store(CreateVendorRequest $request) { - $data = $request->input(); - - if(!$this->checkUpdatePermission($data, $response)){ - return $response; - } - - $vendor = $this->vendorService->save($data); + $vendor = $this->vendorService->save($request->input()); Session::flash('message', trans('texts.created_vendor')); @@ -96,18 +89,14 @@ class VendorController extends BaseController * @param int $id * @return Response */ - public function show($publicId) + public function show(VendorRequest $request) { - $vendor = Vendor::withTrashed()->scope($publicId)->with('vendorcontacts', 'size', 'industry')->firstOrFail(); - - if(!$this->checkViewPermission($vendor, $response)){ - return $response; - } - + $vendor = $request->entity(); + Utils::trackViewed($vendor->getDisplayName(), 'vendor'); $actionLinks = [ - ['label' => trans('texts.new_vendor'), 'url' => '/vendors/create/' . $vendor->public_id] + ['label' => trans('texts.new_vendor'), 'url' => URL::to('/vendors/create/' . $vendor->public_id)] ]; $data = array( @@ -129,12 +118,8 @@ class VendorController extends BaseController * * @return Response */ - public function create() + public function create(VendorRequest $request) { - if(!$this->checkCreatePermission($response)){ - return $response; - } - if (Vendor::scope()->count() > Auth::user()->getMaxNumVendors()) { return View::make('error', ['hideHeader' => true, 'error' => "Sorry, you've exceeded the limit of ".Auth::user()->getMaxNumVendors()." vendors"]); } @@ -157,26 +142,22 @@ class VendorController extends BaseController * @param int $id * @return Response */ - public function edit($publicId) + public function edit(VendorRequest $request) { - $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail(); - - if(!$this->checkEditPermission($vendor, $response)){ - return $response; - } + $vendor = $request->entity(); $data = [ 'vendor' => $vendor, 'method' => 'PUT', - 'url' => 'vendors/'.$publicId, + 'url' => 'vendors/'.$vendor->public_id, 'title' => trans('texts.edit_vendor'), ]; $data = array_merge($data, self::getViewModel()); if (Auth::user()->account->isNinjaAccount()) { - if ($account = Account::whereId($vendor->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; + if ($account = Account::whereId($client->public_id)->first()) { + $data['planDetails'] = $account->getPlanDetails(false, false); } } @@ -201,13 +182,7 @@ class VendorController extends BaseController */ public function update(UpdateVendorRequest $request) { - $data = $request->input(); - - if(!$this->checkUpdatePermission($data, $response)){ - return $response; - } - - $vendor = $this->vendorService->save($data); + $vendor = $this->vendorService->save($request->input(), $request->entity()); Session::flash('message', trans('texts.updated_vendor')); diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 63e370cf4192..b20b19841fcb 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -34,7 +34,8 @@ class ApiCheck { // check for a valid token $token = AccountToken::where('token', '=', Request::header('X-Ninja-Token'))->first(['id', 'user_id']); - if ($token) { + // check if user is archived + if ($token && $token->user) { Auth::loginUsingId($token->user_id); Session::set('token_id', $token->id); } else { @@ -47,7 +48,7 @@ class ApiCheck { return $next($request); } - if (!Utils::isPro() && !$loggingIn) { + if (!Utils::hasFeature(FEATURE_API) && !$loggingIn) { return Response::json('API requires pro plan', 403, $headers); } else { $key = Auth::check() ? Auth::user()->account->id : $request->getClientIp(); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index a6d1363e4cdd..81fde62439e2 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -42,7 +42,7 @@ class Authenticate { // Does this account require portal passwords? $account = Account::whereId($account_id)->first(); - if(!$account->enable_portal_password || !$account->isPro()){ + if($account && (!$account->enable_portal_password || !$account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD))){ $authenticated = true; } diff --git a/app/Http/Middleware/DuplicateSubmissionCheck.php b/app/Http/Middleware/DuplicateSubmissionCheck.php index 407ffab60071..6f3374a47ebf 100644 --- a/app/Http/Middleware/DuplicateSubmissionCheck.php +++ b/app/Http/Middleware/DuplicateSubmissionCheck.php @@ -1,4 +1,4 @@ -account; - $account->pro_plan_paid = date_create()->format('Y-m-d'); - $account->save(); + if ($data && $data != RESULT_FAILURE) { + $company = Auth::user()->account->company; + $company->plan_term = PLAN_TERM_YEARLY; + $company->plan_paid = $data; + $company->plan_expires = date_create($data)->modify('+1 year')->format('Y-m-d'); + $company->plan = PLAN_WHITE_LABEL; + $company->save(); Session::flash('message', trans('texts.bought_white_label')); } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 15eeb65c7a8a..7766a0991337 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -6,6 +6,7 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier; class VerifyCsrfToken extends BaseVerifier { private $openRoutes = [ + 'complete', 'signup/register', 'api/v1/*', 'api/v1/login', diff --git a/app/Http/Requests/ClientRequest.php b/app/Http/Requests/ClientRequest.php new file mode 100644 index 000000000000..ec28cdb77d1d --- /dev/null +++ b/app/Http/Requests/ClientRequest.php @@ -0,0 +1,18 @@ +relationLoaded('contacts')) { + $client->load('contacts'); + } + + return $client; + } +} \ No newline at end of file diff --git a/app/Http/Requests/CreateBankAccountRequest.php b/app/Http/Requests/CreateBankAccountRequest.php index 6c2fea62ec47..eac988349c8d 100644 --- a/app/Http/Requests/CreateBankAccountRequest.php +++ b/app/Http/Requests/CreateBankAccountRequest.php @@ -1,4 +1,4 @@ -user()->can('create', ENTITY_CLIENT); } /** @@ -26,21 +23,4 @@ class CreateClientRequest extends Request 'contacts' => 'valid_contacts', ]; } - - public function validator($factory) - { - // support submiting the form with a single contact record - $input = $this->input(); - if (isset($input['contact'])) { - $input['contacts'] = [$input['contact']]; - unset($input['contact']); - $this->replace($input); - } - - return $factory->make( - $this->input(), - $this->container->call([$this, 'rules']), - $this->messages() - ); - } } diff --git a/app/Http/Requests/CreateCreditRequest.php b/app/Http/Requests/CreateCreditRequest.php index f2dc44d31aa0..b6f4fe3b37e3 100644 --- a/app/Http/Requests/CreateCreditRequest.php +++ b/app/Http/Requests/CreateCreditRequest.php @@ -1,9 +1,6 @@ -user()->can('create', ENTITY_CREDIT); } /** diff --git a/app/Http/Requests/UpdatePaymentTermRequest.php b/app/Http/Requests/CreateDocumentRequest.php similarity index 60% rename from app/Http/Requests/UpdatePaymentTermRequest.php rename to app/Http/Requests/CreateDocumentRequest.php index b3d4f536bc6e..33330a90895e 100644 --- a/app/Http/Requests/UpdatePaymentTermRequest.php +++ b/app/Http/Requests/CreateDocumentRequest.php @@ -1,9 +1,6 @@ -user()->can('create', ENTITY_DOCUMENT); } /** @@ -23,8 +20,7 @@ class UpdateExpenseRequest extends Request public function rules() { return [ - 'amount' => 'required|positive', + ]; - } } diff --git a/app/Http/Requests/CreateExpenseRequest.php b/app/Http/Requests/CreateExpenseRequest.php index 85a93eb1974d..dd096eebdcbf 100644 --- a/app/Http/Requests/CreateExpenseRequest.php +++ b/app/Http/Requests/CreateExpenseRequest.php @@ -1,9 +1,6 @@ -user()->can('create', ENTITY_EXPENSE); } /** diff --git a/app/Http/Requests/CreateInvoiceAPIRequest.php b/app/Http/Requests/CreateInvoiceAPIRequest.php new file mode 100644 index 000000000000..141d8788abc3 --- /dev/null +++ b/app/Http/Requests/CreateInvoiceAPIRequest.php @@ -0,0 +1,32 @@ +user()->can('create', ENTITY_INVOICE); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = [ + 'email' => 'required_without:client_id', + 'client_id' => 'required_without:email', + 'invoice_items' => 'valid_invoice_items', + 'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id, + 'discount' => 'positive', + ]; + + return $rules; + } +} diff --git a/app/Http/Requests/CreateInvoiceRequest.php b/app/Http/Requests/CreateInvoiceRequest.php index 4a11ea56044a..a3f556d408e9 100644 --- a/app/Http/Requests/CreateInvoiceRequest.php +++ b/app/Http/Requests/CreateInvoiceRequest.php @@ -1,11 +1,6 @@ -user()->can('create', ENTITY_INVOICE); } /** @@ -25,13 +20,18 @@ class CreateInvoiceRequest extends Request public function rules() { $rules = [ - 'email' => 'required_without:client_id', - 'client_id' => 'required_without:email', + 'client.contacts' => 'valid_contacts', 'invoice_items' => 'valid_invoice_items', - 'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,'.Auth::user()->account_id, + 'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id, 'discount' => 'positive', ]; + /* There's a problem parsing the dates + if (Request::get('is_recurring') && Request::get('start_date') && Request::get('end_date')) { + $rules['end_date'] = 'after' . Request::get('start_date'); + } + */ + return $rules; } } diff --git a/app/Http/Requests/CreatePaymentAPIRequest.php b/app/Http/Requests/CreatePaymentAPIRequest.php new file mode 100644 index 000000000000..08a520c16655 --- /dev/null +++ b/app/Http/Requests/CreatePaymentAPIRequest.php @@ -0,0 +1,48 @@ +user()->can('create', ENTITY_PAYMENT); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + if ( ! $this->invoice_id || ! $this->amount) { + return [ + 'invoice_id' => 'required', + 'amount' => 'required', + ]; + } + + $invoice = Invoice::scope($this->invoice_id)->firstOrFail(); + + $this->merge([ + 'invoice_id' => $invoice->id, + 'client_id' => $invoice->client->id, + ]); + + $rules = array( + 'amount' => "required|less_than:{$invoice->balance}|positive", + ); + + if ($this->payment_type_id == PAYMENT_TYPE_CREDIT) { + $rules['payment_type_id'] = 'has_credit:' . $invoice->client->public_id . ',' . $this->amount; + } + + return $rules; + } +} diff --git a/app/Http/Requests/CreatePaymentRequest.php b/app/Http/Requests/CreatePaymentRequest.php index 52e9d313bf9f..d14d1ddba616 100644 --- a/app/Http/Requests/CreatePaymentRequest.php +++ b/app/Http/Requests/CreatePaymentRequest.php @@ -1,10 +1,8 @@ -user()->can('create', ENTITY_PAYMENT); } /** diff --git a/app/Http/Requests/CreatePaymentTermRequest.php b/app/Http/Requests/CreateProductRequest.php similarity index 57% rename from app/Http/Requests/CreatePaymentTermRequest.php rename to app/Http/Requests/CreateProductRequest.php index d8581793160e..0fad2af14a23 100644 --- a/app/Http/Requests/CreatePaymentTermRequest.php +++ b/app/Http/Requests/CreateProductRequest.php @@ -1,9 +1,6 @@ -user()->can('create', ENTITY_PRODUCT); } /** @@ -23,8 +20,7 @@ class CreatePaymentTermRequest extends Request public function rules() { return [ - 'num_days' => 'required', - 'name' => 'required', + 'product_key' => 'required', ]; } } diff --git a/app/Http/Requests/CreateTaskRequest.php b/app/Http/Requests/CreateTaskRequest.php new file mode 100644 index 000000000000..00d75c2f1249 --- /dev/null +++ b/app/Http/Requests/CreateTaskRequest.php @@ -0,0 +1,26 @@ +user()->can('create', ENTITY_TASK); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'time_log' => 'time_log', + ]; + } +} diff --git a/app/Http/Requests/CreateTaxRateRequest.php b/app/Http/Requests/CreateTaxRateRequest.php index bcf1ad1115b7..d8fef50093b7 100644 --- a/app/Http/Requests/CreateTaxRateRequest.php +++ b/app/Http/Requests/CreateTaxRateRequest.php @@ -1,9 +1,9 @@ -user()->can('create', ENTITY_TAX_RATE); } /** diff --git a/app/Http/Requests/CreateVendorRequest.php b/app/Http/Requests/CreateVendorRequest.php index d901f9e481c8..59bcf668fa68 100644 --- a/app/Http/Requests/CreateVendorRequest.php +++ b/app/Http/Requests/CreateVendorRequest.php @@ -1,9 +1,6 @@ -user()->can('create', ENTITY_VENDOR); } /** @@ -26,21 +23,4 @@ class CreateVendorRequest extends Request 'name' => 'required', ]; } - - /* - public function validator($factory) - { - // support submiting the form with a single contact record - $input = $this->input(); - if (isset($input['vendor_contact'])) { - $input['vendor_contacts'] = [$input['vendor_contact']]; - unset($input['vendor_contact']); - $this->replace($input); - } - - return $factory->make( - $this->input(), $this->container->call([$this, 'rules']), $this->messages() - ); - } - */ } diff --git a/app/Http/Requests/CreditRequest.php b/app/Http/Requests/CreditRequest.php new file mode 100644 index 000000000000..7968005555f4 --- /dev/null +++ b/app/Http/Requests/CreditRequest.php @@ -0,0 +1,7 @@ +entity) { + return $this->entity; + } + + // The entity id can appear as invoices, invoice_id, public_id or id + $publicId = false; + foreach (['_id', 's'] as $suffix) { + $field = $this->entityType . $suffix; + if ($this->$field) { + $publicId= $this->$field; + } + } + if ( ! $publicId) { + $publicId = Input::get('public_id') ?: Input::get('id'); + } + if ( ! $publicId) { + return null; + } + + $class = Utils::getEntityClass($this->entityType); + + if (method_exists($class, 'withTrashed')) { + $this->entity = $class::scope($publicId)->withTrashed()->firstOrFail(); + } else { + $this->entity = $class::scope($publicId)->firstOrFail(); + } + + return $this->entity; + } + + public function authorize() + { + if ($this->entity()) { + return $this->user()->can('view', $this->entity()); + } else { + return $this->user()->can('create', $this->entityType); + } + } + + public function rules() + { + return []; + } +} diff --git a/app/Http/Requests/ExpenseRequest.php b/app/Http/Requests/ExpenseRequest.php new file mode 100644 index 000000000000..ae2e83b6d12f --- /dev/null +++ b/app/Http/Requests/ExpenseRequest.php @@ -0,0 +1,18 @@ +relationLoaded('documents')) { + $expense->load('documents'); + } + + return $expense; + } +} \ No newline at end of file diff --git a/app/Http/Requests/InvoiceRequest.php b/app/Http/Requests/InvoiceRequest.php new file mode 100644 index 000000000000..5e2d93139003 --- /dev/null +++ b/app/Http/Requests/InvoiceRequest.php @@ -0,0 +1,19 @@ +relationLoaded('invoice_items')) { + $invoice->load('invoice_items'); + } + + return $invoice; + } + +} \ No newline at end of file diff --git a/app/Http/Requests/PaymentRequest.php b/app/Http/Requests/PaymentRequest.php new file mode 100644 index 000000000000..cb34349f5d20 --- /dev/null +++ b/app/Http/Requests/PaymentRequest.php @@ -0,0 +1,7 @@ + 'valid_contacts', - 'invoice_items' => 'valid_invoice_items', - 'invoice_number' => 'required|unique:invoices,invoice_number,'.$invoiceId.',id,account_id,'.Auth::user()->account_id, - 'discount' => 'positive', - ]; - - /* There's a problem parsing the dates - if (Request::get('is_recurring') && Request::get('start_date') && Request::get('end_date')) { - $rules['end_date'] = 'after' . Request::get('start_date'); - } - */ - - return $rules; - } -} diff --git a/app/Http/Requests/TaskRequest.php b/app/Http/Requests/TaskRequest.php new file mode 100644 index 000000000000..1e2783781f06 --- /dev/null +++ b/app/Http/Requests/TaskRequest.php @@ -0,0 +1,7 @@ +user()->can('edit', $this->entity()); } /** diff --git a/app/Http/Requests/UpdateExpenseRequest.php b/app/Http/Requests/UpdateExpenseRequest.php index a4d3855deaec..e64384b57a54 100644 --- a/app/Http/Requests/UpdateExpenseRequest.php +++ b/app/Http/Requests/UpdateExpenseRequest.php @@ -1,10 +1,6 @@ -user()->can('edit', $this->entity()); } /** diff --git a/app/Http/Requests/UpdateInvoiceAPIRequest.php b/app/Http/Requests/UpdateInvoiceAPIRequest.php new file mode 100644 index 000000000000..6fa3c4e92e45 --- /dev/null +++ b/app/Http/Requests/UpdateInvoiceAPIRequest.php @@ -0,0 +1,36 @@ +user()->can('edit', $this->entity()); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + if ($this->action == ACTION_ARCHIVE) { + return []; + } + + $invoiceId = $this->entity()->id; + + $rules = [ + 'invoice_items' => 'valid_invoice_items', + 'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, + 'discount' => 'positive', + ]; + + return $rules; + } +} diff --git a/app/Http/Requests/UpdateInvoiceRequest.php b/app/Http/Requests/UpdateInvoiceRequest.php index 4b32bf4ccead..96d112b157c2 100644 --- a/app/Http/Requests/UpdateInvoiceRequest.php +++ b/app/Http/Requests/UpdateInvoiceRequest.php @@ -1,11 +1,6 @@ -user()->can('edit', $this->entity()); } /** @@ -24,19 +19,21 @@ class UpdateInvoiceRequest extends Request */ public function rules() { - if ($this->action == ACTION_ARCHIVE) { - return []; - } - - $publicId = $this->route('invoices'); - $invoiceId = Invoice::getPrivateId($publicId); - + $invoiceId = $this->entity()->id; + $rules = [ + 'client.contacts' => 'valid_contacts', 'invoice_items' => 'valid_invoice_items', - 'invoice_number' => 'unique:invoices,invoice_number,'.$invoiceId.',id,account_id,'.Auth::user()->account_id, + 'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'discount' => 'positive', ]; + /* There's a problem parsing the dates + if (Request::get('is_recurring') && Request::get('start_date') && Request::get('end_date')) { + $rules['end_date'] = 'after' . Request::get('start_date'); + } + */ + return $rules; } } diff --git a/app/Http/Requests/UpdatePaymentRequest.php b/app/Http/Requests/UpdatePaymentRequest.php index 29ac70e85e74..70a328d26772 100644 --- a/app/Http/Requests/UpdatePaymentRequest.php +++ b/app/Http/Requests/UpdatePaymentRequest.php @@ -1,9 +1,6 @@ -user()->can('edit', $this->entity()); } /** diff --git a/app/Http/Requests/UpdateProductRequest.php b/app/Http/Requests/UpdateProductRequest.php new file mode 100644 index 000000000000..f45495a26922 --- /dev/null +++ b/app/Http/Requests/UpdateProductRequest.php @@ -0,0 +1,26 @@ +user()->can('edit', $this->entity()); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'product_key' => 'required', + ]; + } +} diff --git a/app/Http/Requests/UpdateTaskRequest.php b/app/Http/Requests/UpdateTaskRequest.php new file mode 100644 index 000000000000..83ce23ab31ab --- /dev/null +++ b/app/Http/Requests/UpdateTaskRequest.php @@ -0,0 +1,26 @@ +user()->can('edit', $this->entity()); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'time_log' => 'time_log', + ]; + } +} diff --git a/app/Http/Requests/UpdateTaxRateRequest.php b/app/Http/Requests/UpdateTaxRateRequest.php index 6e562ac0d0ab..a4bdc6301ca2 100644 --- a/app/Http/Requests/UpdateTaxRateRequest.php +++ b/app/Http/Requests/UpdateTaxRateRequest.php @@ -1,9 +1,9 @@ -user()->can('edit', $this->entity()); } /** diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php index 1bbcc3d7eaea..b3149d2b61ff 100644 --- a/app/Http/Requests/UpdateUserRequest.php +++ b/app/Http/Requests/UpdateUserRequest.php @@ -1,4 +1,4 @@ -user()->can('edit', $this->entity()); } /** diff --git a/app/Http/Requests/UpdateVendorRequest.php b/app/Http/Requests/UpdateVendorRequest.php index 6e17b79bf12b..9e7e2fe513fb 100644 --- a/app/Http/Requests/UpdateVendorRequest.php +++ b/app/Http/Requests/UpdateVendorRequest.php @@ -1,9 +1,6 @@ -user()->can('edit', $this->entity()); } /** diff --git a/app/Http/Requests/VendorRequest.php b/app/Http/Requests/VendorRequest.php new file mode 100644 index 000000000000..8f96e7c55025 --- /dev/null +++ b/app/Http/Requests/VendorRequest.php @@ -0,0 +1,19 @@ +relationLoaded('vendor_contacts')) { + $vendor->load('vendor_contacts'); + } + + return $vendor; + } + +} \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 33c8db1c5f5c..15f0b247a144 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -43,17 +43,23 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); Route::post('payment/{invitation_key}', 'PaymentController@do_payment'); - Route::get('complete', 'PaymentController@offsite_payment'); + Route::match(['GET', 'POST'], 'complete', 'PaymentController@offsite_payment'); Route::get('client/quotes', 'PublicClientController@quoteIndex'); Route::get('client/invoices', 'PublicClientController@invoiceIndex'); + Route::get('client/documents', 'PublicClientController@documentIndex'); Route::get('client/payments', 'PublicClientController@paymentIndex'); Route::get('client/dashboard', 'PublicClientController@dashboard'); + Route::get('client/documents/js/{documents}/{filename}', 'PublicClientController@getDocumentVFSJS'); + Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'PublicClientController@getDocument'); + Route::get('client/documents/{invitation_key}/{filename?}', 'PublicClientController@getInvoiceDocumentsZip'); + + Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); + Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); + Route::get('api/client.documents', array('as'=>'api.client.documents', 'uses'=>'PublicClientController@documentDatatable')); + Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable')); + Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable')); }); -Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); -Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); -Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable')); -Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable')); Route::get('license', 'PaymentController@show_license_payment'); Route::post('license', 'PaymentController@do_license_payment'); @@ -74,8 +80,8 @@ Route::post('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@po Route::get('/login', array('as' => 'login', 'uses' => 'Auth\AuthController@getLoginWrapper')); Route::post('/login', array('as' => 'login', 'uses' => 'Auth\AuthController@postLoginWrapper')); Route::get('/logout', array('as' => 'logout', 'uses' => 'Auth\AuthController@getLogoutWrapper')); -Route::get('/forgot', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@getEmail')); -Route::post('/forgot', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@postEmail')); +Route::get('/recover_password', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@getEmail')); +Route::post('/recover_password', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@postEmail')); Route::get('/password/reset/{token}', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@getReset')); Route::post('/password/reset', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@postReset')); Route::get('/user/confirm/{code}', 'UserController@confirm'); @@ -84,8 +90,8 @@ Route::get('/user/confirm/{code}', 'UserController@confirm'); Route::get('/client/login', array('as' => 'login', 'uses' => 'ClientAuth\AuthController@getLogin')); Route::post('/client/login', array('as' => 'login', 'uses' => 'ClientAuth\AuthController@postLogin')); Route::get('/client/logout', array('as' => 'logout', 'uses' => 'ClientAuth\AuthController@getLogout')); -Route::get('/client/forgot', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getEmail')); -Route::post('/client/forgot', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postEmail')); +Route::get('/client/recover_password', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getEmail')); +Route::post('/client/recover_password', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postEmail')); Route::get('/client/password/reset/{invitation_key}/{token}', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getReset')); Route::post('/client/password/reset', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postReset')); @@ -105,9 +111,11 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible'); Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); + Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); + Route::post('users/change_password', 'UserController@changePassword'); Route::resource('clients', 'ClientController'); Route::get('api/clients', array('as'=>'api.clients', 'uses'=>'ClientController@getDatatable')); @@ -129,15 +137,20 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('invoices/create/{client_id?}', 'InvoiceController@create'); Route::get('recurring_invoices/create/{client_id?}', 'InvoiceController@createRecurring'); Route::get('recurring_invoices', 'RecurringInvoiceController@index'); - Route::get('invoices/{public_id}/clone', 'InvoiceController@cloneInvoice'); + Route::get('invoices/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::post('invoices/bulk', 'InvoiceController@bulk'); Route::post('recurring_invoices/bulk', 'InvoiceController@bulk'); + Route::get('documents/{documents}/{filename?}', 'DocumentController@get'); + Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); + Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); + Route::post('document', 'DocumentController@postUpload'); + Route::get('quotes/create/{client_id?}', 'QuoteController@create'); - Route::get('quotes/{public_id}/clone', 'InvoiceController@cloneInvoice'); - Route::get('quotes/{public_id}/edit', 'InvoiceController@edit'); - Route::put('quotes/{public_id}', 'InvoiceController@update'); - Route::get('quotes/{public_id}', 'InvoiceController@edit'); + Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); + Route::get('quotes/{invoices}/edit', 'InvoiceController@edit'); + Route::put('quotes/{invoices}', 'InvoiceController@update'); + Route::get('quotes/{invoices}', 'InvoiceController@edit'); Route::post('quotes', 'InvoiceController@store'); Route::get('quotes', 'QuoteController@index'); Route::get('api/quotes/{client_id?}', array('as'=>'api.quotes', 'uses'=>'QuoteController@getDatatable')); @@ -178,9 +191,9 @@ Route::group([ Route::resource('users', 'UserController'); Route::post('users/bulk', 'UserController@bulk'); Route::get('send_confirmation/{user_id}', 'UserController@sendConfirmation'); - Route::get('start_trial', 'AccountController@startTrial'); + Route::get('start_trial/{plan}', 'AccountController@startTrial') + ->where(['plan'=>'pro']); Route::get('restore_user/{user_id}', 'UserController@restoreUser'); - Route::post('users/change_password', 'UserController@changePassword'); Route::get('/switch_account/{user_id}', 'UserController@switchAccount'); Route::get('/unlink_account/{user_account_id}/{user_id}', 'UserController@unlinkAccount'); Route::get('/manage_companies', 'UserController@manageCompanies'); @@ -197,21 +210,18 @@ Route::group([ Route::resource('tax_rates', 'TaxRateController'); Route::post('tax_rates/bulk', 'TaxRateController@bulk'); + Route::get('settings/email_preview', 'AccountController@previewEmail'); Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy'); Route::get('settings/data_visualizations', 'ReportController@d3'); Route::get('settings/charts_and_reports', 'ReportController@showReports'); Route::post('settings/charts_and_reports', 'ReportController@showReports'); + Route::post('settings/change_plan', 'AccountController@changePlan'); Route::post('settings/cancel_account', 'AccountController@cancelAccount'); Route::post('settings/company_details', 'AccountController@updateDetails'); Route::get('settings/{section?}', 'AccountController@showSection'); Route::post('settings/{section?}', 'AccountController@doSection'); - //Route::get('api/payment_terms', array('as'=>'api.payment_terms', 'uses'=>'PaymentTermController@getDatatable')); - //Route::resource('payment_terms', 'PaymentTermController'); - //Route::post('payment_terms/bulk', 'PaymentTermController@bulk'); - - Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); Route::post('user/setTheme', 'UserController@setTheme'); Route::post('remove_logo', 'AccountController@removeLogo'); Route::post('account/go_pro', 'AccountController@enableProPlan'); @@ -234,15 +244,15 @@ Route::group([ // Route groups for API Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() { - Route::get('ping', 'ClientApiController@ping'); + Route::get('ping', 'AccountApiController@ping'); Route::post('login', 'AccountApiController@login'); Route::post('register', 'AccountApiController@register'); Route::get('static', 'AccountApiController@getStaticData'); Route::get('accounts', 'AccountApiController@show'); Route::put('accounts', 'AccountApiController@update'); Route::resource('clients', 'ClientApiController'); - Route::get('quotes', 'QuoteApiController@index'); - Route::resource('quotes', 'QuoteApiController'); + //Route::get('quotes', 'QuoteApiController@index'); + //Route::resource('quotes', 'QuoteApiController'); Route::get('invoices', 'InvoiceApiController@index'); Route::resource('invoices', 'InvoiceApiController'); Route::get('payments', 'PaymentApiController@index'); @@ -268,7 +278,6 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() }); // Redirects for legacy links -/* Route::get('/rocksteady', function() { return Redirect::to(NINJA_WEB_URL, 301); }); @@ -296,7 +305,7 @@ Route::get('/compare-online-invoicing{sites?}', function() { Route::get('/forgot_password', function() { return Redirect::to(NINJA_APP_URL.'/forgot', 301); }); -*/ + if (!defined('CONTACT_EMAIL')) { define('CONTACT_EMAIL', Config::get('mail.from.address')); @@ -311,6 +320,7 @@ if (!defined('CONTACT_EMAIL')) { define('ENTITY_CLIENT', 'client'); define('ENTITY_CONTACT', 'contact'); define('ENTITY_INVOICE', 'invoice'); + define('ENTITY_DOCUMENT', 'document'); define('ENTITY_INVOICE_ITEMS', 'invoice_items'); define('ENTITY_INVITATION', 'invitation'); define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); @@ -344,6 +354,7 @@ if (!defined('CONTACT_EMAIL')) { define('ACCOUNT_LOCALIZATION', 'localization'); define('ACCOUNT_NOTIFICATIONS', 'notifications'); define('ACCOUNT_IMPORT_EXPORT', 'import_export'); + define('ACCOUNT_MANAGEMENT', 'account_management'); define('ACCOUNT_PAYMENTS', 'online_payments'); define('ACCOUNT_BANKS', 'bank_accounts'); define('ACCOUNT_IMPORT_EXPENSES', 'import_expenses'); @@ -426,6 +437,10 @@ if (!defined('CONTACT_EMAIL')) { define('MAX_IFRAME_URL_LENGTH', 250); define('MAX_LOGO_FILE_SIZE', 200); // KB define('MAX_FAILED_LOGINS', 10); + define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB + define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000));// Total KB + define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000));// Total KB (uncompressed) + define('DOCUMENT_PREVIEW_SIZE', env('DOCUMENT_PREVIEW_SIZE', 300));// pixels define('DEFAULT_FONT_SIZE', 9); define('DEFAULT_HEADER_FONT', 1);// Roboto define('DEFAULT_BODY_FONT', 1);// Roboto @@ -521,6 +536,7 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_BITPAY', 42); define('GATEWAY_DWOLLA', 43); define('GATEWAY_CHECKOUT_COM', 47); + define('GATEWAY_CYBERSOURCE', 49); define('EVENT_CREATE_CLIENT', 1); define('EVENT_CREATE_INVOICE', 2); @@ -534,25 +550,26 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG'); - define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); - define('NINJA_APP_URL', 'https://app.invoiceninja.com'); - define('NINJA_VERSION', '2.5.1.3'); + define('NINJA_WEB_URL', env('NINJA_WEB_URL', 'https://www.invoiceninja.com')); + define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); define('NINJA_DATE', '2000-01-01'); + define('NINJA_VERSION', '2.5.2' . env('NINJA_VERSION_SUFFIX')); - define('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'); - define('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'); - define('SOCIAL_LINK_GITHUB', 'https://github.com/invoiceninja/invoiceninja/'); + define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); + define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); + define('SOCIAL_LINK_GITHUB', env('SOCIAL_LINK_GITHUB', 'https://github.com/invoiceninja/invoiceninja/')); - define('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'); - define('RELEASES_URL', 'https://trello.com/b/63BbiVVe/invoice-ninja'); - define('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja'); - define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/'); - define('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html'); - define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/api/browser/v2/'); - define('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php'); - define('REFERRAL_PROGRAM_URL', 'https://www.invoiceninja.com/referral-program/'); - define('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup'); - define('OFX_HOME_URL', 'http://www.ofxhome.com/index.php/home/directory/all'); + define('NINJA_FROM_EMAIL', env('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com')); + define('RELEASES_URL', env('RELEASES_URL', 'https://trello.com/b/63BbiVVe/invoice-ninja')); + define('ZAPIER_URL', env('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja')); + define('OUTDATE_BROWSER_URL', env('OUTDATE_BROWSER_URL', 'http://browsehappy.com/')); + define('PDFMAKE_DOCS', env('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html')); + define('PHANTOMJS_CLOUD', env('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/api/browser/v2/')); + define('PHP_DATE_FORMATS', env('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php')); + define('REFERRAL_PROGRAM_URL', env('REFERRAL_PROGRAM_URL', 'https://www.invoiceninja.com/referral-program/')); + define('EMAIL_MARKUP_URL', env('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup')); + define('OFX_HOME_URL', env('OFX_HOME_URL', 'http://www.ofxhome.com/index.php/home/directory/all')); + define('GOOGLE_ANALYITCS_URL', env('GOOGLE_ANALYITCS_URL', 'https://www.google-analytics.com/collect')); define('BLANK_IMAGE', 'data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); @@ -566,9 +583,12 @@ if (!defined('CONTACT_EMAIL')) { define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74'); define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); - define('PRO_PLAN_PRICE', 50); - define('WHITE_LABEL_PRICE', 20); - define('INVOICE_DESIGNS_PRICE', 10); + define('PLAN_PRICE_PRO_MONTHLY', env('PLAN_PRICE_PRO_MONTHLY', 5)); + define('PLAN_PRICE_PRO_YEARLY', env('PLAN_PRICE_PRO_YEARLY', 50)); + define('PLAN_PRICE_ENTERPRISE_MONTHLY', env('PLAN_PRICE_ENTERPRISE_MONTHLY', 10)); + define('PLAN_PRICE_ENTERPRISE_YEARLY', env('PLAN_PRICE_ENTERPRISE_YEARLY', 100)); + define('WHITE_LABEL_PRICE', env('WHITE_LABEL_PRICE', 20)); + define('INVOICE_DESIGNS_PRICE', env('INVOICE_DESIGNS_PRICE', 10)); define('USER_TYPE_SELF_HOST', 'SELF_HOST'); define('USER_TYPE_CLOUD_HOST', 'CLOUD_HOST'); @@ -577,9 +597,11 @@ if (!defined('CONTACT_EMAIL')) { define('TEST_USERNAME', 'user@example.com'); define('TEST_PASSWORD', 'password'); define('API_SECRET', 'API_SECRET'); + define('DEFAULT_API_PAGE_SIZE', 15); + define('MAX_API_PAGE_SIZE', 100); - define('IOS_PRODUCTION_PUSH','ninjaIOS'); - define('IOS_DEV_PUSH','devNinjaIOS'); + define('IOS_PRODUCTION_PUSH', env('IOS_PRODUCTION_PUSH', 'ninjaIOS')); + define('IOS_DEV_PUSH', env('IOS_DEV_PUSH', 'devNinjaIOS')); define('TOKEN_BILLING_DISABLED', 1); define('TOKEN_BILLING_OPT_IN', 2); @@ -629,7 +651,46 @@ if (!defined('CONTACT_EMAIL')) { define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); + + // These must be lowercase + define('PLAN_FREE', 'free'); + define('PLAN_PRO', 'pro'); + define('PLAN_ENTERPRISE', 'enterprise'); + define('PLAN_WHITE_LABEL', 'white_label'); + define('PLAN_TERM_MONTHLY', 'month'); + define('PLAN_TERM_YEARLY', 'year'); + + // Pro + define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); + define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); + define('FEATURE_DIFFERENT_DESIGNS', 'different_designs'); + define('FEATURE_EMAIL_TEMPLATES_REMINDERS', 'email_templates_reminders'); + define('FEATURE_INVOICE_SETTINGS', 'invoice_settings'); + define('FEATURE_CUSTOM_EMAILS', 'custom_emails'); + define('FEATURE_PDF_ATTACHMENT', 'pdf_attachment'); + define('FEATURE_MORE_INVOICE_DESIGNS', 'more_invoice_designs'); + define('FEATURE_QUOTES', 'quotes'); + define('FEATURE_REPORTS', 'reports'); + define('FEATURE_API', 'api'); + define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password'); + define('FEATURE_CUSTOM_URL', 'custom_url'); + + define('FEATURE_MORE_CLIENTS', 'more_clients'); // No trial allowed + + // Whitelabel + define('FEATURE_CLIENT_PORTAL_CSS', 'client_portal_css'); + define('FEATURE_WHITE_LABEL', 'feature_white_label'); + // Enterprise + define('FEATURE_DOCUMENTS', 'documents'); + + // No Trial allowed + define('FEATURE_USERS', 'users');// Grandfathered for old Pro users + define('FEATURE_USER_PERMISSIONS', 'user_permissions'); + + // Pro users who started paying on or before this date will be able to manage users + define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -679,30 +740,6 @@ if (!defined('CONTACT_EMAIL')) { } } -/* -// Log all SQL queries to laravel.log -if (Utils::isNinjaDev()) { - Event::listen('illuminate.query', function($query, $bindings, $time, $name) { - $data = compact('bindings', 'time', 'name'); - - // Format binding data for sql insertion - foreach ($bindings as $i => $binding) { - if ($binding instanceof \DateTime) { - $bindings[$i] = $binding->format('\'Y-m-d H:i:s\''); - } elseif (is_string($binding)) { - $bindings[$i] = "'$binding'"; - } - } - - // Insert bindings into query - $query = str_replace(array('%', '?'), array('%%', '%s'), $query); - $query = vsprintf($query, $bindings); - - Log::info($query, $data); - }); -} -*/ - /* if (Utils::isNinjaDev()) { diff --git a/app/Libraries/OFX.php b/app/Libraries/OFX.php index 5563e917acf7..721c9f529f85 100644 --- a/app/Libraries/OFX.php +++ b/app/Libraries/OFX.php @@ -2,6 +2,8 @@ // https://github.com/denvertimothy/OFX +use Utils; +use Log; use SimpleXMLElement; class OFX @@ -21,13 +23,19 @@ class OFX $c = curl_init(); curl_setopt($c, CURLOPT_URL, $this->bank->url); curl_setopt($c, CURLOPT_POST, 1); - curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx')); + // User-Agent: http://www.ofxhome.com/ofxforum/viewtopic.php?pid=108091#p108091 + curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx', 'User-Agent: httpclient')); curl_setopt($c, CURLOPT_POSTFIELDS, $this->request); curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); + $this->response = curl_exec($c); - //print_r($this->response); - //\Log::info(print_r($this->response, true)); + + if (Utils::isNinjaDev()) { + Log::info(print_r($this->response, true)); + } + curl_close($c); + $tmp = explode('', $this->response); $this->responseHeader = $tmp[0]; $this->responseBody = ''.$tmp[1]; @@ -35,14 +43,15 @@ class OFX public function xml() { $xml = $this->responseBody; - self::closeTags($xml); + $xml = self::closeTags($xml); $x = new SimpleXMLElement($xml); return $x; } - public static function closeTags(&$x) + public static function closeTags($x) { - $x = preg_replace('/(<([^<\/]+)>)(?!.*?<\/\2>)([^<]+)/', '\1\3', $x); + $x = preg_replace('/\s+/', '', $x); + return preg_replace('/(<([^<\/]+)>)(?!.*?<\/\2>)([^<]+)/', '\1\3', $x); } } @@ -224,3 +233,4 @@ class Account } } } + diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 439247f337c0..c02c719169a4 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -51,6 +51,11 @@ class Utils return php_sapi_name() == 'cli'; } + public static function isTravis() + { + return env('TRAVIS') == 'true'; + } + public static function isNinja() { return self::isNinjaProd() || self::isNinjaDev(); @@ -118,6 +123,11 @@ class Utils return Auth::check() && Auth::user()->isPro(); } + public static function hasFeature($feature) + { + return Auth::check() && Auth::user()->hasFeature($feature); + } + public static function isAdmin() { return Auth::check() && Auth::user()->is_admin; @@ -130,7 +140,7 @@ class Utils public static function hasAllPermissions($permission) { - return Auth::check() && Auth::user()->hasPermissions($permission); + return Auth::check() && Auth::user()->hasPermission($permission); } public static function isTrial() @@ -331,6 +341,7 @@ class Utils $currency = self::getFromCache($currencyId, 'currencies'); $thousand = $currency->thousand_separator; $decimal = $currency->decimal_separator; + $precision = $currency->precision; $code = $currency->code; $swapSymbol = false; @@ -345,7 +356,7 @@ class Utils } } - $value = number_format($value, $currency->precision, $decimal, $thousand); + $value = number_format($value, $precision, $decimal, $thousand); $symbol = $currency->symbol; if ($showCode || !$symbol) { @@ -440,7 +451,12 @@ class Utils return false; } - $dateTime = new DateTime($date); + if ($date instanceof DateTime) { + $dateTime = $date; + } else { + $dateTime = new DateTime($date); + } + $timestamp = $dateTime->getTimestamp(); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); @@ -659,9 +675,14 @@ class Utils return $year + $offset; } + public static function getEntityClass($entityType) + { + return 'App\\Models\\' . static::getEntityName($entityType); + } + public static function getEntityName($entityType) { - return ucwords(str_replace('_', ' ', $entityType)); + return ucwords(Utils::toCamelCase($entityType)); } public static function getClientDisplayName($model) @@ -961,38 +982,6 @@ class Utils return $entity1; } - public static function withinPastYear($date) - { - if (!$date || $date == '0000-00-00') { - return false; - } - - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $date); - $interval = $today->diff($datePaid); - - return $interval->y == 0; - } - - public static function getInterval($date) - { - if (!$date || $date == '0000-00-00') { - return false; - } - - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $date); - - return $today->diff($datePaid); - } - - public static function withinPastTwoWeeks($date) - { - $interval = Utils::getInterval($date); - - return $interval && $interval->d <= 14; - } - public static function addHttp($url) { if (!preg_match("~^(?:f|ht)tps?://~i", $url)) { diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index 52c2e26f9027..7edd065f915d 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -1,4 +1,4 @@ -payment; + $invoice = $payment->invoice; + $account = $payment->account; + + if ($account->account_key != NINJA_ACCOUNT_KEY) { + return; + } + + $analyticsId = env('ANALYTICS_KEY'); + $client = $payment->client; + $amount = $payment->amount; + + $base = "v=1&tid={$analyticsId}&cid{$client->public_id}&cu=USD&ti={$invoice->invoice_number}"; + + $url = $base . "&t=transaction&ta=ninja&tr={$amount}"; + $this->sendAnalytics($url); + //Log::info($url); + + $url = $base . "&t=item&in=plan&ip={$amount}&iq=1"; + $this->sendAnalytics($url); + //Log::info($url); + } + + private function sendAnalytics($data) + { + $data = json_encode($data); + $curl = curl_init(); + + $opts = [ + CURLOPT_URL => GOOGLE_ANALYITCS_URL, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => 'POST', + CURLOPT_POSTFIELDS => $data, + ]; + + curl_setopt_array($curl, $opts); + $response = curl_exec($curl); + curl_close($curl); + } +} diff --git a/app/Listeners/CreditListener.php b/app/Listeners/CreditListener.php index bed71a47f59d..3a76288a1b95 100644 --- a/app/Listeners/CreditListener.php +++ b/app/Listeners/CreditListener.php @@ -1,4 +1,4 @@ -invoice; $account = Auth::user()->account; diff --git a/app/Listeners/NotificationListener.php b/app/Listeners/NotificationListener.php index 29a190eeb712..87a685944919 100644 --- a/app/Listeners/NotificationListener.php +++ b/app/Listeners/NotificationListener.php @@ -1,4 +1,4 @@ - array( + PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY, + PLAN_TERM_YEARLY => PLAN_PRICE_PRO_YEARLY, + ), + PLAN_ENTERPRISE => array( + PLAN_TERM_MONTHLY => PLAN_PRICE_ENTERPRISE_MONTHLY, + PLAN_TERM_YEARLY => PLAN_PRICE_ENTERPRISE_YEARLY, + ), + ); protected $presenter = 'App\Ninja\Presenters\AccountPresenter'; protected $dates = ['deleted_at']; @@ -55,6 +68,7 @@ class Account extends Eloquent ACCOUNT_PRODUCTS, ACCOUNT_NOTIFICATIONS, ACCOUNT_IMPORT_EXPORT, + ACCOUNT_MANAGEMENT, ]; public static $advancedSettings = [ @@ -174,6 +188,11 @@ class Account extends Eloquent return $this->hasMany('App\Models\Payment','account_id','id')->withTrashed(); } + public function company() + { + return $this->belongsTo('App\Models\Company'); + } + public function setIndustryIdAttribute($value) { $this->attributes['industry_id'] = $value ?: null; @@ -193,7 +212,9 @@ class Account extends Eloquent public function isGatewayConfigured($gatewayId = 0) { - $this->load('account_gateways'); + if ( ! $this->relationLoaded('account_gateways')) { + $this->load('account_gateways'); + } if ($gatewayId) { return $this->getGatewayConfig($gatewayId) != false; @@ -222,7 +243,7 @@ class Account extends Eloquent return $this->name; } - $this->load('users'); + //$this->load('users'); $user = $this->users()->first(); return $user->getDisplayName(); @@ -384,32 +405,82 @@ class Account extends Eloquent public function hasLogo() { - return file_exists($this->getLogoFullPath()); + if($this->logo == ''){ + $this->calculateLogoDetails(); + } + + return !empty($this->logo); + } + + public function getLogoDisk(){ + return Storage::disk(env('LOGO_FILESYSTEM', 'logos')); + } + + protected function calculateLogoDetails(){ + $disk = $this->getLogoDisk(); + + if($disk->exists($this->account_key.'.png')){ + $this->logo = $this->account_key.'.png'; + } else if($disk->exists($this->account_key.'.jpg')) { + $this->logo = $this->account_key.'.jpg'; + } + + if(!empty($this->logo)){ + $image = imagecreatefromstring($disk->get($this->logo)); + $this->logo_width = imagesx($image); + $this->logo_height = imagesy($image); + $this->logo_size = $disk->size($this->logo); + } else { + $this->logo = null; + } + $this->save(); } - public function getLogoPath() + public function getLogoRaw(){ + if(!$this->hasLogo()){ + return null; + } + + $disk = $this->getLogoDisk(); + return $disk->get($this->logo); + } + + public function getLogoURL($cachebuster = false) { - $fileName = 'logo/' . $this->account_key; - - return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg'; + if(!$this->hasLogo()){ + return null; + } + + $disk = $this->getLogoDisk(); + $adapter = $disk->getAdapter(); + + if($adapter instanceof \League\Flysystem\Adapter\Local) { + // Stored locally + $logo_url = str_replace(public_path(), url('/'), $adapter->applyPathPrefix($this->logo), $count); + + if ($cachebuster) { + $logo_url .= '?no_cache='.time(); + } + + if($count == 1){ + return str_replace(DIRECTORY_SEPARATOR, '/', $logo_url); + } + } + + return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); } - public function getLogoFullPath() + public function getPrimaryUser() { - $fileName = public_path() . '/logo/' . $this->account_key; - - return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg'; + return $this->users() + ->orderBy('id') + ->first(); } - public function getLogoURL() - { - return SITE_URL . '/' . $this->getLogoPath(); - } - - public function getToken($name) + public function getToken($userId, $name) { foreach ($this->account_tokens as $token) { - if ($token->name === $name) { + if ($token->user_id == $userId && $token->name === $name) { return $token->token; } } @@ -419,24 +490,20 @@ class Account extends Eloquent public function getLogoWidth() { - $path = $this->getLogoFullPath(); - if (!file_exists($path)) { - return 0; + if(!$this->hasLogo()){ + return null; } - list($width, $height) = getimagesize($path); - return $width; + return $this->logo_width; } public function getLogoHeight() { - $path = $this->getLogoFullPath(); - if (!file_exists($path)) { - return 0; + if(!$this->hasLogo()){ + return null; } - list($width, $height) = getimagesize($path); - return $height; + return $this->logo_height; } public function createInvoice($entityType = ENTITY_INVOICE, $clientId = null) @@ -475,7 +542,7 @@ class Account extends Eloquent public function getNumberPrefix($isQuote) { - if ( ! $this->isPro()) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return ''; } @@ -484,7 +551,7 @@ class Account extends Eloquent public function hasNumberPattern($isQuote) { - if ( ! $this->isPro()) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return false; } @@ -510,7 +577,7 @@ class Account extends Eloquent $replace = [date('Y')]; $search[] = '{$counter}'; - $replace[] = str_pad($this->getCounter($invoice->is_quote), 4, '0', STR_PAD_LEFT); + $replace[] = str_pad($this->getCounter($invoice->is_quote), $this->invoice_number_padding, '0', STR_PAD_LEFT); if (strstr($pattern, '{$userId}')) { $search[] = '{$userId}'; @@ -576,7 +643,7 @@ class Account extends Eloquent // confirm the invoice number isn't already taken do { - $number = $prefix . str_pad($counter, 4, '0', STR_PAD_LEFT); + $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); $counter++; $counterOffset++; @@ -604,7 +671,7 @@ class Account extends Eloquent $default = $this->invoice_number_counter; $actual = Utils::parseInt($invoice->invoice_number); - if ( ! $this->isPro() && $default != $actual) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $default != $actual) { $this->invoice_number_counter = $actual + 1; } else { $this->invoice_number_counter += 1; @@ -725,17 +792,84 @@ class Account extends Eloquent return $this->account_key === NINJA_ACCOUNT_KEY; } - public function startTrial() + public function startTrial($plan) { if ( ! Utils::isNinja()) { return; } - $this->pro_plan_trial = date_create()->format('Y-m-d'); - $this->save(); + $this->company->trial_plan = $plan; + $this->company->trial_started = date_create()->format('Y-m-d'); + $this->company->save(); } - public function isPro() + public function hasFeature($feature) + { + if (Utils::isNinjaDev()) { + return true; + } + + $planDetails = $this->getPlanDetails(); + $selfHost = !Utils::isNinjaProd(); + + if (!$selfHost && function_exists('ninja_account_features')) { + $result = ninja_account_features($this, $feature); + + if ($result != null) { + return $result; + } + } + + switch ($feature) { + // Pro + case FEATURE_CUSTOMIZE_INVOICE_DESIGN: + case FEATURE_REMOVE_CREATED_BY: + case FEATURE_DIFFERENT_DESIGNS: + case FEATURE_EMAIL_TEMPLATES_REMINDERS: + case FEATURE_INVOICE_SETTINGS: + case FEATURE_CUSTOM_EMAILS: + case FEATURE_PDF_ATTACHMENT: + case FEATURE_MORE_INVOICE_DESIGNS: + case FEATURE_QUOTES: + case FEATURE_REPORTS: + case FEATURE_API: + case FEATURE_CLIENT_PORTAL_PASSWORD: + case FEATURE_CUSTOM_URL: + return $selfHost || !empty($planDetails); + + // Pro; No trial allowed, unless they're trialing enterprise with an active pro plan + case FEATURE_MORE_CLIENTS: + return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false))); + + // White Label + case FEATURE_WHITE_LABEL: + if ($this->isNinjaAccount() || (!$selfHost && $planDetails && !$planDetails['expires'])) { + return false; + } + // Fallthrough + case FEATURE_CLIENT_PORTAL_CSS: + return !empty($planDetails);// A plan is required even for self-hosted users + + // Enterprise; No Trial allowed; grandfathered for old pro users + case FEATURE_USERS:// Grandfathered for old Pro users + if($planDetails && $planDetails['trial']) { + // Do they have a non-trial plan? + $planDetails = $this->getPlanDetails(false, false); + } + + return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE)); + + // Enterprise; No Trial allowed + case FEATURE_DOCUMENTS: + case FEATURE_USER_PERMISSIONS: + return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial']; + + default: + return false; + } + } + + public function isPro(&$plan_details = null) { if (!Utils::isNinjaProd()) { return true; @@ -745,14 +879,113 @@ class Account extends Eloquent return true; } - $datePaid = $this->pro_plan_paid; - $trialStart = $this->pro_plan_trial; + $plan_details = $this->getPlanDetails(); + + return !empty($plan_details); + } - if ($datePaid == NINJA_DATE) { + public function isEnterprise(&$plan_details = null) + { + if (!Utils::isNinjaProd()) { return true; } - return Utils::withinPastTwoWeeks($trialStart) || Utils::withinPastYear($datePaid); + if ($this->isNinjaAccount()) { + return true; + } + + $plan_details = $this->getPlanDetails(); + + return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE; + } + + public function getPlanDetails($include_inactive = false, $include_trial = true) + { + if (!$this->company) { + return null; + } + + $plan = $this->company->plan; + $trial_plan = $this->company->trial_plan; + + if(!$plan && (!$trial_plan || !$include_trial)) { + return null; + } + + $trial_active = false; + if ($trial_plan && $include_trial) { + $trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started); + $trial_expires = clone $trial_started; + $trial_expires->modify('+2 weeks'); + + if ($trial_expires >= date_create()) { + $trial_active = true; + } + } + + $plan_active = false; + if ($plan) { + if ($this->company->plan_expires == null) { + $plan_active = true; + $plan_expires = false; + } else { + $plan_expires = DateTime::createFromFormat('Y-m-d', $this->company->plan_expires); + if ($plan_expires >= date_create()) { + $plan_active = true; + } + } + } + + if (!$include_inactive && !$plan_active && !$trial_active) { + return null; + } + + // Should we show plan details or trial details? + if (($plan && !$trial_plan) || !$include_trial) { + $use_plan = true; + } elseif (!$plan && $trial_plan) { + $use_plan = false; + } else { + // There is both a plan and a trial + if (!empty($plan_active) && empty($trial_active)) { + $use_plan = true; + } elseif (empty($plan_active) && !empty($trial_active)) { + $use_plan = false; + } elseif (!empty($plan_active) && !empty($trial_active)) { + // Both are active; use whichever is a better plan + if ($plan == PLAN_ENTERPRISE) { + $use_plan = true; + } elseif ($trial_plan == PLAN_ENTERPRISE) { + $use_plan = false; + } else { + // They're both the same; show the plan + $use_plan = true; + } + } else { + // Neither are active; use whichever expired most recently + $use_plan = $plan_expires >= $trial_expires; + } + } + + if ($use_plan) { + return array( + 'trial' => false, + 'plan' => $plan, + 'started' => DateTime::createFromFormat('Y-m-d', $this->company->plan_started), + 'expires' => $plan_expires, + 'paid' => DateTime::createFromFormat('Y-m-d', $this->company->plan_paid), + 'term' => $this->company->plan_term, + 'active' => $plan_active, + ); + } else { + return array( + 'trial' => true, + 'plan' => $trial_plan, + 'started' => $trial_started, + 'expires' => $trial_expires, + 'active' => $trial_active, + ); + } } public function isTrial() @@ -760,35 +993,54 @@ class Account extends Eloquent if (!Utils::isNinjaProd()) { return false; } + + $plan_details = $this->getPlanDetails(); - if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { - return false; - } - - return Utils::withinPastTwoWeeks($this->pro_plan_trial); + return $plan_details && $plan_details['trial']; } - public function isEligibleForTrial() + public function isEligibleForTrial($plan = null) { - return ! $this->pro_plan_trial || $this->pro_plan_trial == '0000-00-00'; + if (!$this->company->trial_plan) { + if ($plan) { + return $plan == PLAN_PRO || $plan == PLAN_ENTERPRISE; + } else { + return array(PLAN_PRO, PLAN_ENTERPRISE); + } + } + + if ($this->company->trial_plan == PLAN_PRO) { + if ($plan) { + return $plan != PLAN_PRO; + } else { + return array(PLAN_ENTERPRISE); + } + } + + return false; } public function getCountTrialDaysLeft() { - $interval = Utils::getInterval($this->pro_plan_trial); + $planDetails = $this->getPlanDetails(true); - return $interval ? 14 - $interval->d : 0; + if(!$planDetails || !$planDetails['trial']) { + return 0; + } + + $today = new DateTime('now'); + $interval = $today->diff($planDetails['expires']); + + return $interval ? $interval->d : 0; } public function getRenewalDate() { - if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $this->pro_plan_paid); - $date->modify('+1 year'); + $planDetails = $this->getPlanDetails(); + + if ($planDetails) { + $date = $planDetails['expires']; $date = max($date, date_create()); - } elseif ($this->isTrial()) { - $date = date_create(); - $date->modify('+'.$this->getCountTrialDaysLeft().' day'); } else { $date = date_create(); } @@ -796,31 +1048,13 @@ class Account extends Eloquent return $date->format('Y-m-d'); } - public function isWhiteLabel() - { - if ($this->isNinjaAccount()) { - return false; - } - - if (Utils::isNinjaProd()) { - return self::isPro() && $this->pro_plan_paid != NINJA_DATE; - } else { - if ($this->pro_plan_paid == NINJA_DATE) { - return true; - } - - return Utils::withinPastYear($this->pro_plan_paid); - } - } - public function getLogoSize() { - if (!$this->hasLogo()) { - return 0; + if(!$this->hasLogo()){ + return null; } - $filename = $this->getLogoFullPath(); - return round(File::size($filename) / 1000); + return round($this->logo_size / 1000); } public function isLogoTooLarge() @@ -890,7 +1124,7 @@ class Account extends Eloquent public function getEmailSubject($entityType) { - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { $field = "email_subject_{$entityType}"; $value = $this->$field; @@ -910,7 +1144,7 @@ class Account extends Eloquent $template = "
\$client,

"; - if ($this->isPro() && $this->email_design_id != EMAIL_DESIGN_PLAIN) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS) && $this->email_design_id != EMAIL_DESIGN_PLAIN) { $template .= "
" . trans("texts.{$entityType}_message_button", ['amount' => '$amount']) . "

" . "
\$viewButton

"; } else { @@ -929,7 +1163,7 @@ class Account extends Eloquent { $template = false; - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { $field = "email_template_{$entityType}"; $template = $this->$field; } @@ -942,13 +1176,18 @@ class Account extends Eloquent return str_replace('/>', ' />', $template); } + public function getTemplateView($view = '') + { + return $this->getEmailDesignId() == EMAIL_DESIGN_PLAIN ? $view : 'design' . $this->getEmailDesignId(); + } + public function getEmailFooter() { if ($this->email_footer) { // Add line breaks if HTML isn't already being used return strip_tags($this->email_footer) == $this->email_footer ? nl2br($this->email_footer) : $this->email_footer; } else { - return "

" . trans('texts.email_signature') . "\n
\$account

"; + return "

" . trans('texts.email_signature') . "\n
\$account

"; } } @@ -1025,7 +1264,7 @@ class Account extends Eloquent public function showCustomField($field, $entity = false) { - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return $this->$field ? true : false; } @@ -1041,18 +1280,18 @@ class Account extends Eloquent public function attatchPDF() { - return $this->isPro() && $this->pdf_email_attachment; + return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment; } public function getEmailDesignId() { - return $this->isPro() ? $this->email_design_id : EMAIL_DESIGN_PLAIN; + return $this->hasFeature(FEATURE_CUSTOM_EMAILS) ? $this->email_design_id : EMAIL_DESIGN_PLAIN; } public function clientViewCSS(){ - $css = null; + $css = ''; - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $bodyFont = $this->getBodyFontCss(); $headerFont = $this->getHeaderFontCss(); @@ -1060,27 +1299,15 @@ class Account extends Eloquent if ($headerFont != $bodyFont) { $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}'; } - - if ((Utils::isNinja() && $this->isPro()) || $this->isWhiteLabel()) { - // For self-hosted users, a white-label license is required for custom CSS - $css .= $this->client_view_css; - } + } + if ($this->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { + // For self-hosted users, a white-label license is required for custom CSS + $css .= $this->client_view_css; } return $css; } - public function hasLargeFont() - { - foreach (['chinese', 'japanese'] as $language) { - if (stripos($this->getBodyFontName(), $language) || stripos($this->getHeaderFontName(), $language)) { - return true; - } - } - - return false; - } - public function getFontsUrl($protocol = ''){ $bodyFont = $this->getHeaderFontId(); $headerFont = $this->getBodyFontId(); @@ -1097,11 +1324,11 @@ class Account extends Eloquent } public function getHeaderFontId() { - return ($this->isPro() && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; + return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; } public function getBodyFontId() { - return ($this->isPro() && $this->body_font_id) ? $this->body_font_id : DEFAULT_BODY_FONT; + return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->body_font_id) ? $this->body_font_id : DEFAULT_BODY_FONT; } public function getHeaderFontName(){ diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index a4027db6ef36..5e951285349e 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -60,13 +60,7 @@ class AccountGateway extends EntityModel public function getConfigField($field) { - $config = $this->getConfig(); - - if (!$field || !property_exists($config, $field)) { - return false; - } - - return $config->$field; + return object_get($this->getConfig(), $field, false); } public function getPublishableStripeKey() diff --git a/app/Models/AccountToken.php b/app/Models/AccountToken.php index dd9a98800535..87728b37016e 100644 --- a/app/Models/AccountToken.php +++ b/app/Models/AccountToken.php @@ -16,4 +16,9 @@ class AccountToken extends EntityModel { return $this->belongsTo('App\Models\Account'); } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } } diff --git a/app/Models/Client.php b/app/Models/Client.php index 92232647117c..6d2b44fbac90 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -146,7 +146,7 @@ class Client extends EntityModel public function addContact($data, $isPrimary = false) { - $publicId = isset($data['public_id']) ? $data['public_id'] : false; + $publicId = isset($data['public_id']) ? $data['public_id'] : (isset($data['id']) ? $data['id'] : false); if ($publicId && $publicId != '-1') { $contact = Contact::scope($publicId)->firstOrFail(); @@ -155,7 +155,7 @@ class Client extends EntityModel $contact->send_invoice = true; } - if (!Utils::isPro() || $this->account->enable_portal_password){ + if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->account->enable_portal_password){ if(!empty($data['password']) && $data['password']!='-%unchanged%-'){ $contact->password = bcrypt($data['password']); } else if(empty($data['password'])){ @@ -199,6 +199,13 @@ class Client extends EntityModel return $this->name; } + public function getPrimaryContact() + { + return $this->contacts() + ->whereIsPrimary(true) + ->first(); + } + public function getDisplayName() { if ($this->name) { @@ -256,13 +263,17 @@ class Client extends EntityModel public function getGatewayToken() { - $this->account->load('account_gateways'); + $account = $this->account; + + if ( ! $account->relationLoaded('account_gateways')) { + $account->load('account_gateways'); + } - if (!count($this->account->account_gateways)) { + if (!count($account->account_gateways)) { return false; } - $accountGateway = $this->account->getGatewayConfig(GATEWAY_STRIPE); + $accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE); if (!$accountGateway) { return false; diff --git a/app/Models/Company.php b/app/Models/Company.php new file mode 100644 index 000000000000..1345db9e4bfb --- /dev/null +++ b/app/Models/Company.php @@ -0,0 +1,21 @@ +hasMany('App\Models\Account'); + } + + public function payment() + { + return $this->belongsTo('App\Models\Payment'); + } +} diff --git a/app/Models/Document.php b/app/Models/Document.php new file mode 100644 index 000000000000..6d9c24857143 --- /dev/null +++ b/app/Models/Document.php @@ -0,0 +1,249 @@ + 'jpeg', + 'tif' => 'tiff', + ); + + public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts + 'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain', + 'application/zip', 'application/msword', + 'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint', + ); + + public static $types = array( + 'png' => array( + 'mime' => 'image/png', + ), + 'ai' => array( + 'mime' => 'application/postscript', + ), + 'svg' => array( + 'mime' => 'image/svg+xml', + ), + 'jpeg' => array( + 'mime' => 'image/jpeg', + ), + 'tiff' => array( + 'mime' => 'image/tiff', + ), + 'pdf' => array( + 'mime' => 'application/pdf', + ), + 'gif' => array( + 'mime' => 'image/gif', + ), + 'psd' => array( + 'mime' => 'image/vnd.adobe.photoshop', + ), + 'txt' => array( + 'mime' => 'text/plain', + ), + 'zip' => array( + 'mime' => 'application/zip', + ), + 'doc' => array( + 'mime' => 'application/msword', + ), + 'xls' => array( + 'mime' => 'application/vnd.ms-excel', + ), + 'ppt' => array( + 'mime' => 'application/vnd.ms-powerpoint', + ), + 'xlsx' => array( + 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ), + 'docx' => array( + 'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ), + 'pptx' => array( + 'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ), + ); + + public function fill(array $attributes) + { + parent::fill($attributes); + + if(empty($this->attributes['disk'])){ + $this->attributes['disk'] = env('DOCUMENT_FILESYSTEM', 'documents'); + } + + return $this; + } + + public function account() + { + return $this->belongsTo('App\Models\Account'); + } + + public function user() + { + return $this->belongsTo('App\Models\User'); + } + + public function expense() + { + return $this->belongsTo('App\Models\Expense')->withTrashed(); + } + + public function invoice() + { + return $this->belongsTo('App\Models\Invoice')->withTrashed(); + } + + public function getDisk(){ + return Storage::disk(!empty($this->disk)?$this->disk:env('DOCUMENT_FILESYSTEM', 'documents')); + } + + public function setDiskAttribute($value) + { + $this->attributes['disk'] = $value?$value:env('DOCUMENT_FILESYSTEM', 'documents'); + } + + public function getDirectUrl(){ + return static::getDirectFileUrl($this->path, $this->getDisk()); + } + + public function getDirectPreviewUrl(){ + return $this->preview?static::getDirectFileUrl($this->preview, $this->getDisk(), true):null; + } + + public static function getDirectFileUrl($path, $disk, $prioritizeSpeed = false){ + $adapter = $disk->getAdapter(); + $fullPath = $adapter->applyPathPrefix($path); + + if($adapter instanceof \League\Flysystem\AwsS3v3\AwsS3Adapter) { + $client = $adapter->getClient(); + $command = $client->getCommand('GetObject', [ + 'Bucket' => $adapter->getBucket(), + 'Key' => $fullPath + ]); + + return (string) $client->createPresignedRequest($command, '+10 minutes')->getUri(); + } else if (!$prioritizeSpeed // Rackspace temp URLs are slow, so we don't use them for previews + && $adapter instanceof \League\Flysystem\Rackspace\RackspaceAdapter) { + $secret = env('RACKSPACE_TEMP_URL_SECRET'); + if($secret){ + $object = $adapter->getContainer()->getObject($fullPath); + + if(env('RACKSPACE_TEMP_URL_SECRET_SET')){ + // Go ahead and set the secret too + $object->getService()->getAccount()->setTempUrlSecret($secret); + } + + $url = $object->getUrl(); + $expiry = strtotime('+10 minutes'); + $urlPath = urldecode($url->getPath()); + $body = sprintf("%s\n%d\n%s", 'GET', $expiry, $urlPath); + $hash = hash_hmac('sha1', $body, $secret); + return sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry); + } + } + + return null; + } + + public function getRaw(){ + $disk = $this->getDisk(); + + return $disk->get($this->path); + } + + public function getStream(){ + $disk = $this->getDisk(); + + return $disk->readStream($this->path); + } + + public function getRawPreview(){ + $disk = $this->getDisk(); + + return $disk->get($this->preview); + } + + public function getUrl(){ + return url('documents/'.$this->public_id.'/'.$this->name); + } + + public function getClientUrl($invitation){ + return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name); + } + + public function isPDFEmbeddable(){ + return $this->type == 'jpeg' || $this->type == 'png' || $this->preview; + } + + public function getVFSJSUrl(){ + if(!$this->isPDFEmbeddable())return null; + return url('documents/js/'.$this->public_id.'/'.$this->name.'.js'); + } + + public function getClientVFSJSUrl(){ + if(!$this->isPDFEmbeddable())return null; + return url('client/documents/js/'.$this->public_id.'/'.$this->name.'.js'); + } + + public function getPreviewUrl(){ + return $this->preview?url('documents/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null; + } + + public function toArray() + { + $array = parent::toArray(); + + if(empty($this->visible) || in_array('url', $this->visible))$array['url'] = $this->getUrl(); + if(empty($this->visible) || in_array('preview_url', $this->visible))$array['preview_url'] = $this->getPreviewUrl(); + + return $array; + } + + public function cloneDocument(){ + $document = Document::createNew($this); + $document->path = $this->path; + $document->preview = $this->preview; + $document->name = $this->name; + $document->type = $this->type; + $document->disk = $this->disk; + $document->hash = $this->hash; + $document->size = $this->size; + $document->width = $this->width; + $document->height = $this->height; + + return $document; + } +} + +Document::deleted(function ($document) { + $same_path_count = DB::table('documents') + ->where('documents.account_id', '=', $document->account_id) + ->where('documents.path', '=', $document->path) + ->where('documents.disk', '=', $document->disk) + ->count(); + + if(!$same_path_count){ + $document->getDisk()->delete($document->path); + } + + if($document->preview){ + $same_preview_count = DB::table('documents') + ->where('documents.account_id', '=', $document->account_id) + ->where('documents.preview', '=', $document->preview) + ->where('documents.disk', '=', $document->disk) + ->count(); + if(!$same_preview_count){ + $document->getDisk()->delete($document->preview); + } + } + +}); \ No newline at end of file diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index aa6544e52f8b..95e85e6acef4 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -24,9 +24,14 @@ class EntityModel extends Eloquent Utils::fatalError(); } - $lastEntity = $className::withTrashed() - ->scope(false, $entity->account_id) - ->orderBy('public_id', 'DESC') + if(method_exists($className, 'withTrashed')){ + $lastEntity = $className::withTrashed() + ->scope(false, $entity->account_id); + } else { + $lastEntity = $className::scope(false, $entity->account_id); + } + + $lastEntity = $lastEntity->orderBy('public_id', 'DESC') ->first(); if ($lastEntity) { @@ -96,6 +101,16 @@ class EntityModel extends Eloquent return $this->getName(); } + public static function getClassName($entityType) + { + return 'App\\Models\\' . ucwords(Utils::toCamelCase($entityType)); + } + + public static function getTransformerName($entityType) + { + return 'App\\Ninja\\Transformers\\' . ucwords(Utils::toCamelCase($entityType)) . 'Transformer'; + } + public function setNullValues() { foreach ($this->fillable as $field) { @@ -113,56 +128,4 @@ class EntityModel extends Eloquent $name = $parts[count($parts)-1]; return strtolower($name) . '_id'; } - - public static function canCreate() { - return Auth::user()->hasPermission('create_all'); - } - - public function canEdit() { - return static::canEditItem($this); - } - - public static function canEditItem($item) { - return Auth::user()->hasPermission('edit_all') || (isset($item->user_id) && Auth::user()->id == $item->user_id); - } - - public static function canEditItemById($item_id) { - if(Auth::user()->hasPermission('edit_all')) { - return true; - } - - return static::whereId($item_id)->first()->user_id == Auth::user()->id; - } - - public static function canEditItemByOwner($user_id) { - if(Auth::user()->hasPermission('edit_all')) { - return true; - } - - return Auth::user()->id == $user_id; - } - - public function canView() { - return static::canViewItem($this); - } - - public static function canViewItem($item) { - return Auth::user()->hasPermission('view_all') || (isset($item->user_id) && Auth::user()->id == $item->user_id); - } - - public static function canViewItemById($item_id) { - if(Auth::user()->hasPermission('view_all')) { - return true; - } - - return static::whereId($item_id)->first()->user_id == Auth::user()->id; - } - - public static function canViewItemByOwner($user_id) { - if(Auth::user()->hasPermission('view_all')) { - return true; - } - - return Auth::user()->id == $user_id; - } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 2d1b8041d7ee..316491a5356b 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -53,6 +53,11 @@ class Expense extends EntityModel return $this->belongsTo('App\Models\Invoice')->withTrashed(); } + public function documents() + { + return $this->hasMany('App\Models\Document')->orderBy('id'); + } + public function getName() { if($this->expense_number) @@ -80,6 +85,20 @@ class Expense extends EntityModel { return $this->invoice_currency_id != $this->expense_currency_id; } + + public function convertedAmount() + { + return round($this->amount * $this->exchange_rate, 2); + } + + public function toArray() + { + $array = parent::toArray(); + + if(empty($this->visible) || in_array('converted_amount', $this->visible))$array['converted_amount'] = $this->convertedAmount(); + + return $array; + } } Expense::creating(function ($expense) { diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 7abd074a08b4..becd0197c93f 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -29,7 +29,9 @@ class Invitation extends EntityModel return $this->belongsTo('App\Models\Account'); } - public function getLink($type = 'view') + // If we're getting the link for PhantomJS to generate the PDF + // we need to make sure it's served from our site + public function getLink($type = 'view', $forceOnsite = false) { if (!$this->account) { $this->load('account'); @@ -38,8 +40,8 @@ class Invitation extends EntityModel $url = SITE_URL; $iframe_url = $this->account->iframe_url; - if ($this->account->isPro()) { - if ($iframe_url) { + if ($this->account->hasFeature(FEATURE_CUSTOM_URL)) { + if ($iframe_url && !$forceOnsite) { return "{$iframe_url}?{$this->invitation_key}"; } elseif ($this->account->subdomain) { $url = Utils::replaceSubdomain($url, $this->account->subdomain); diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 340ce419b757..5c006ecd9eaa 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -2,6 +2,7 @@ use Utils; use DateTime; +use URL; use Illuminate\Database\Eloquent\SoftDeletes; use Laracasts\Presenter\PresentableTrait; use App\Models\BalanceAffecting; @@ -24,6 +25,13 @@ class Invoice extends EntityModel implements BalanceAffecting protected $presenter = 'App\Ninja\Presenters\InvoicePresenter'; protected $dates = ['deleted_at']; + protected $fillable = [ + 'tax_name1', + 'tax_rate1', + 'tax_name2', + 'tax_rate2', + ]; + protected $casts = [ 'is_recurring' => 'boolean', 'has_tasks' => 'boolean', @@ -175,6 +183,11 @@ class Invoice extends EntityModel implements BalanceAffecting return $this->hasMany('App\Models\InvoiceItem')->orderBy('id'); } + public function documents() + { + return $this->hasMany('App\Models\Document')->orderBy('id'); + } + public function invoice_status() { return $this->belongsTo('App\Models\InvoiceStatus'); @@ -215,6 +228,12 @@ class Invoice extends EntityModel implements BalanceAffecting return $this->hasMany('App\Models\Expense','invoice_id','id')->withTrashed(); } + public function scopeInvoices($query) + { + return $query->where('is_quote', '=', false) + ->where('is_recurring', '=', false); + } + public function markInvitationsSent($notify = false) { foreach ($this->invitations as $invitation) { @@ -385,14 +404,18 @@ class Invoice extends EntityModel implements BalanceAffecting 'amount', 'balance', 'invoice_items', + 'documents', + 'expenses', 'client', - 'tax_name', - 'tax_rate', + 'tax_name1', + 'tax_rate1', + 'tax_name2', + 'tax_rate2', 'account', 'invoice_design', 'invoice_design_id', 'invoice_fonts', - 'is_pro', + 'features', 'is_quote', 'custom_value1', 'custom_value2', @@ -419,6 +442,7 @@ class Invoice extends EntityModel implements BalanceAffecting 'contacts', 'country', 'currency_id', + 'country_id', 'custom_value1', 'custom_value2', ]); @@ -457,6 +481,8 @@ class Invoice extends EntityModel implements BalanceAffecting 'custom_invoice_text_label2', 'custom_invoice_item_label1', 'custom_invoice_item_label2', + 'invoice_embed_documents', + 'page_size', ]); foreach ($this->invoice_items as $invoiceItem) { @@ -467,8 +493,10 @@ class Invoice extends EntityModel implements BalanceAffecting 'custom_value2', 'cost', 'qty', - 'tax_name', - 'tax_rate', + 'tax_name1', + 'tax_rate1', + 'tax_name2', + 'tax_rate2', ]); } @@ -481,6 +509,26 @@ class Invoice extends EntityModel implements BalanceAffecting ]); } + foreach ($this->documents as $document) { + $document->setVisible([ + 'public_id', + 'name', + ]); + } + + foreach ($this->expenses as $expense) { + $expense->setVisible([ + 'documents', + ]); + + foreach ($expense->documents as $document) { + $document->setVisible([ + 'public_id', + 'name', + ]); + } + } + return $this; } @@ -749,9 +797,8 @@ class Invoice extends EntityModel implements BalanceAffecting } $invitation = $this->invitations[0]; - $link = $invitation->getLink(); + $link = $invitation->getLink('view', true); $key = env('PHANTOMJS_CLOUD_KEY'); - $curl = curl_init(); if (Utils::isNinjaDev()) { $link = env('TEST_LINK'); @@ -814,53 +861,79 @@ class Invoice extends EntityModel implements BalanceAffecting return $total; } + // if $calculatePaid is true we'll loop through each payment to + // determine the sum, otherwise we'll use the cached paid_to_date amount public function getTaxes($calculatePaid = false) { $taxes = []; $taxable = $this->getTaxable(); - - if ($this->tax_rate && $this->tax_name) { - $taxAmount = $taxable * ($this->tax_rate / 100); - $taxAmount = round($taxAmount, 2); + $paidAmount = $this->getAmountPaid($calculatePaid); + + if ($this->tax_name1) { + $invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2); + $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; + $this->calculateTax($taxes, $this->tax_name1, $this->tax_rate1, $invoiceTaxAmount, $invoicePaidAmount); + } - if ($taxAmount) { - $taxes[$this->tax_name.$this->tax_rate] = [ - 'name' => $this->tax_name, - 'rate' => $this->tax_rate, - 'amount' => $taxAmount, - 'paid' => round($this->getAmountPaid($calculatePaid) / $this->amount * $taxAmount, 2) - ]; - } + if ($this->tax_name2) { + $invoiceTaxAmount = round($taxable * ($this->tax_rate2 / 100), 2); + $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; + $this->calculateTax($taxes, $this->tax_name2, $this->tax_rate2, $invoiceTaxAmount, $invoicePaidAmount); } foreach ($this->invoice_items as $invoiceItem) { - if ( ! $invoiceItem->tax_rate || ! $invoiceItem->tax_name) { - continue; + $itemTaxAmount = $this->getItemTaxable($invoiceItem, $taxable); + + if ($invoiceItem->tax_name1) { + $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2); + $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; + $this->calculateTax($taxes, $invoiceItem->tax_name1, $invoiceItem->tax_rate1, $itemTaxAmount, $itemPaidAmount); } - $taxAmount = $this->getItemTaxable($invoiceItem, $taxable); - $taxAmount = $taxable * ($invoiceItem->tax_rate / 100); - $taxAmount = round($taxAmount, 2); - - if ($taxAmount) { - $key = $invoiceItem->tax_name.$invoiceItem->tax_rate; - - if ( ! isset($taxes[$key])) { - $taxes[$key] = [ - 'amount' => 0, - 'paid' => 0 - ]; - } - - $taxes[$key]['amount'] += $taxAmount; - $taxes[$key]['paid'] += $this->amount && $taxAmount ? round($this->getAmountPaid($calculatePaid) / $this->amount * $taxAmount, 2) : 0; - $taxes[$key]['name'] = $invoiceItem->tax_name; - $taxes[$key]['rate'] = $invoiceItem->tax_rate; + if ($invoiceItem->tax_name2) { + $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate2 / 100), 2); + $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; + $this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount); } } - + return $taxes; } + + private function calculateTax(&$taxes, $name, $rate, $amount, $paid) + { + if ( ! $amount) { + return; + } + + $amount = round($amount, 2); + $paid = round($paid, 2); + $key = $rate . ' ' . $name; + + if ( ! isset($taxes[$key])) { + $taxes[$key] = [ + 'name' => $name, + 'rate' => $rate+0, + 'amount' => 0, + 'paid' => 0 + ]; + } + + $taxes[$key]['amount'] += $amount; + $taxes[$key]['paid'] += $paid; + } + + public function hasDocuments(){ + if(count($this->documents))return true; + return $this->hasExpenseDocuments(); + } + + public function hasExpenseDocuments(){ + foreach($this->expenses as $expense){ + if(count($expense->documents))return true; + } + return false; + } } Invoice::creating(function ($invoice) { diff --git a/app/Models/InvoiceDesign.php b/app/Models/InvoiceDesign.php index 2b53ac6383c7..51f15b035ad6 100644 --- a/app/Models/InvoiceDesign.php +++ b/app/Models/InvoiceDesign.php @@ -1,4 +1,4 @@ -belongsTo('App\Models\Invoice'); } + public function user() + { + return $this->belongsTo('App\Models\User')->withTrashed(); + } + public function product() { return $this->belongsTo('App\Models\Product'); diff --git a/app/Models/Product.php b/app/Models/Product.php index 4098c67063d2..05944c9fff94 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -8,6 +8,14 @@ class Product extends EntityModel use SoftDeletes; protected $dates = ['deleted_at']; + protected $fillable = [ + 'product_key', + 'notes', + 'cost', + 'qty', + 'default_tax_rate_id', + ]; + public function getEntityType() { return ENTITY_PRODUCT; @@ -18,12 +26,13 @@ class Product extends EntityModel return Product::scope()->where('product_key', '=', $key)->first(); } + public function user() + { + return $this->belongsTo('App\Models\User')->withTrashed(); + } + public function default_tax_rate() { return $this->belongsTo('App\Models\TaxRate'); } - - public function canEdit() { - return Auth::user()->hasPermission('admin'); - } } diff --git a/app/Models/TaxRate.php b/app/Models/TaxRate.php index cf0a576a8f0d..384ccf933b36 100644 --- a/app/Models/TaxRate.php +++ b/app/Models/TaxRate.php @@ -17,8 +17,9 @@ class TaxRate extends EntityModel { return ENTITY_TAX_RATE; } - - public function canEdit() { - return Auth::user()->hasPermission('admin'); + + public function user() + { + return $this->belongsTo('App\Models\User')->withTrashed(); } } diff --git a/app/Models/User.php b/app/Models/User.php index 1b2ae7815a36..71069d25821c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,20 +7,22 @@ use App\Libraries\Utils; use App\Events\UserSettingsChanged; use App\Events\UserSignedUp; use Illuminate\Auth\Authenticatable; +use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Database\Eloquent\Model; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; +use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Database\Eloquent\SoftDeletes; -class User extends Model implements AuthenticatableContract, CanResetPasswordContract { +class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract { public static $all_permissions = array( 'create_all' => 0b0001, 'view_all' => 0b0010, 'edit_all' => 0b0100, ); - use Authenticatable, CanResetPassword; + use Authenticatable, Authorizable, CanResetPassword; /** * The database table used by the model. @@ -112,9 +114,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isPro(); } + public function hasFeature($feature) + { + return $this->account->hasFeature($feature); + } + public function isPaidPro() { - return $this->isPro() && ! $this->isTrial(); + return $this->isPro($accountDetails) && !$accountDetails['trial']; } public function isTrial() @@ -122,14 +129,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isTrial(); } - public function isEligibleForTrial() + public function isEligibleForTrial($plan = null) { - return $this->account->isEligibleForTrial(); + return $this->account->isEligibleForTrial($plan); } public function maxInvoiceDesignId() { - return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); + return $this->hasFeature(FEATURE_MORE_INVOICE_DESIGNS) ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); } public function getDisplayName() @@ -173,7 +180,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumClients() { - if ($this->isPro() && ! $this->isTrial()) { + if ($this->hasFeature(FEATURE_MORE_CLIENTS)) { return MAX_NUM_CLIENTS_PRO; } @@ -186,7 +193,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumVendors() { - if ($this->isPro() && ! $this->isTrial()) { + if ($this->hasFeature(FEATURE_MORE_CLIENTS)) { return MAX_NUM_VENDORS_PRO; } @@ -321,6 +328,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return false; } + + public function owns($entity) { + return !empty($entity->user_id) && $entity->user_id == $this->id; + } } User::updating(function ($user) { diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 573a581e4774..6fcd10f14092 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -95,7 +95,7 @@ class Vendor extends EntityModel return $this->hasMany('App\Models\Payment'); } - public function vendorContacts() + public function vendor_contacts() { return $this->hasMany('App\Models\VendorContact'); } @@ -143,7 +143,7 @@ class Vendor extends EntityModel $contact->fill($data); $contact->is_primary = $isPrimary; - return $this->vendorContacts()->save($contact); + return $this->vendor_contacts()->save($contact); } public function getRoute() diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index e526300a5dd9..fbebd0c5fdfd 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -1,16 +1,13 @@ templateService = $templateService; + } + public function sendInvoice(Invoice $invoice, $reminder = false, $pdfString = false) { $invoice->load('invitations', 'client.language', 'account'); @@ -59,9 +62,32 @@ class ContactMailer extends Mailer if ($account->attatchPDF() && !$pdfString) { $pdfString = $invoice->getPDFString(); } + + $documentStrings = array(); + if ($account->document_email_attachment && $invoice->hasDocuments()) { + $documents = $invoice->documents; + + foreach($invoice->expenses as $expense){ + $documents = $documents->merge($expense->documents); + } + + $documents = $documents->sortBy('size'); + + $size = 0; + $maxSize = MAX_EMAIL_DOCUMENTS_SIZE * 1000; + foreach($documents as $document){ + $size += $document->size; + if($size > $maxSize)break; + + $documentStrings[] = array( + 'name' => $document->name, + 'data' => $document->getRaw(), + ); + } + } foreach ($invoice->invitations as $invitation) { - $response = $this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString); + $response = $this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString, $documentStrings); if ($response === true) { $sent = true; } @@ -80,7 +106,7 @@ class ContactMailer extends Mailer return $response; } - private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString) + private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString, $documentStrings) { $client = $invoice->client; $account = $invoice->account; @@ -111,7 +137,7 @@ class ContactMailer extends Mailer 'amount' => $invoice->getRequestedAmount() ]; - if (empty($invitation->contact->password) && $account->isPro() && $account->enable_portal_password && $account->send_portal_password) { + if (empty($invitation->contact->password) && $account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $account->enable_portal_password && $account->send_portal_password) { // The contact needs a password $variables['password'] = $password = $this->generatePassword(); $invitation->contact->password = bcrypt($password); @@ -119,7 +145,7 @@ class ContactMailer extends Mailer } $data = [ - 'body' => $this->processVariables($body, $variables), + 'body' => $this->templateService->processVariables($body, $variables), 'link' => $invitation->getLink(), 'entityType' => $invoice->getEntityType(), 'invoiceId' => $invoice->id, @@ -127,6 +153,7 @@ class ContactMailer extends Mailer 'account' => $account, 'client' => $client, 'invoice' => $invoice, + 'documents' => $documentStrings, ]; if ($account->attatchPDF()) { @@ -134,14 +161,9 @@ class ContactMailer extends Mailer $data['pdfFileName'] = $invoice->getFileName(); } - $subject = $this->processVariables($subject, $variables); + $subject = $this->templateService->processVariables($subject, $variables); $fromEmail = $user->email; - - if ($account->getEmailDesignId() == EMAIL_DESIGN_PLAIN) { - $view = ENTITY_INVOICE; - } else { - $view = 'design' . ($account->getEmailDesignId() - 1); - } + $view = $account->getTemplateView(ENTITY_INVOICE); $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data); @@ -204,7 +226,7 @@ class ContactMailer extends Mailer ]; $data = [ - 'body' => $this->processVariables($emailTemplate, $variables), + 'body' => $this->templateService->processVariables($emailTemplate, $variables), 'link' => $invitation->getLink(), 'invoice' => $invoice, 'client' => $client, @@ -218,14 +240,10 @@ class ContactMailer extends Mailer $data['pdfFileName'] = $invoice->getFileName(); } - $subject = $this->processVariables($emailSubject, $variables); + $subject = $this->templateService->processVariables($emailSubject, $variables); $data['invoice_id'] = $payment->invoice->id; - if ($account->getEmailDesignId() == EMAIL_DESIGN_PLAIN) { - $view = 'payment_confirmation'; - } else { - $view = 'design' . ($account->getEmailDesignId() - 1); - } + $view = $account->getTemplateView('payment_confirmation'); if ($user->email && $contact->email) { $this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data); @@ -255,60 +273,4 @@ class ContactMailer extends Mailer $this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } - - private function processVariables($template, $data) - { - $account = $data['account']; - $client = $data['client']; - $invitation = $data['invitation']; - $invoice = $invitation->invoice; - $passwordHTML = isset($data['password'])?'

'.trans('texts.password').': '.$data['password'].'

':false; - - $variables = [ - '$footer' => $account->getEmailFooter(), - '$client' => $client->getDisplayName(), - '$account' => $account->getDisplayName(), - '$dueDate' => $account->formatDate($invoice->due_date), - '$invoiceDate' => $account->formatDate($invoice->invoice_date), - '$contact' => $invitation->contact->getDisplayName(), - '$firstName' => $invitation->contact->first_name, - '$amount' => $account->formatMoney($data['amount'], $client), - '$invoice' => $invoice->invoice_number, - '$quote' => $invoice->invoice_number, - '$link' => $invitation->getLink(), - '$password' => $passwordHTML, - '$viewLink' => $invitation->getLink().'$password', - '$viewButton' => Form::emailViewButton($invitation->getLink(), $invoice->getEntityType()).'$password', - '$paymentLink' => $invitation->getLink('payment').'$password', - '$paymentButton' => Form::emailPaymentButton($invitation->getLink('payment')).'$password', - '$customClient1' => $account->custom_client_label1, - '$customClient2' => $account->custom_client_label2, - '$customInvoice1' => $account->custom_invoice_text_label1, - '$customInvoice2' => $account->custom_invoice_text_label2, - ]; - - // Add variables for available payment types - foreach (Gateway::$paymentTypes as $type) { - $camelType = Gateway::getPaymentTypeName($type); - $type = Utils::toSnakeCase($camelType); - $variables["\${$camelType}Link"] = $invitation->getLink('payment') . "/{$type}"; - $variables["\${$camelType}Button"] = Form::emailPaymentButton($invitation->getLink('payment') . "/{$type}"); - } - - $includesPasswordPlaceholder = strpos($template, '$password') !== false; - - $str = str_replace(array_keys($variables), array_values($variables), $template); - - if(!$includesPasswordPlaceholder && $passwordHTML){ - $pos = strrpos($str, '$password'); - if($pos !== false) - { - $str = substr_replace($str, $passwordHTML, $pos, 9/* length of "$password" */); - } - } - $str = str_replace('$password', '', $str); - $str = autolink($str, 100); - - return $str; - } } diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index 7afcc2548c93..d655e4945e31 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -44,6 +44,13 @@ class Mailer if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) { $message->attachData($data['pdfString'], $data['pdfFileName']); } + + // Attach documents to the email + if(!empty($data['documents'])){ + foreach($data['documents'] as $document){ + $message->attachData($document['data'], $document['name']); + } + } }); return $this->handleSuccess($response, $data); diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php index 6b66080ded85..1980480a2f53 100644 --- a/app/Ninja/Presenters/ExpensePresenter.php +++ b/app/Ninja/Presenters/ExpensePresenter.php @@ -16,14 +16,9 @@ class ExpensePresenter extends Presenter { return Utils::fromSqlDate($this->entity->expense_date); } - public function converted_amount() - { - return round($this->entity->amount * $this->entity->exchange_rate, 2); - } - public function invoiced_amount() { - return $this->entity->invoice_id ? $this->converted_amount() : 0; + return $this->entity->invoice_id ? $this->entity->convertedAmount() : 0; } public function link() diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php index ebb3297d5c1a..36a2ce2acbd6 100644 --- a/app/Ninja/Presenters/InvoicePresenter.php +++ b/app/Ninja/Presenters/InvoicePresenter.php @@ -18,7 +18,7 @@ class InvoicePresenter extends Presenter { public function balanceDueLabel() { - if ($this->entity->partial) { + if ($this->entity->partial > 0) { return 'partial_due'; } elseif ($this->entity->is_quote) { return 'total'; diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 0e08fbba5d06..38753dde9b9a 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -17,6 +17,7 @@ use App\Models\Client; use App\Models\Language; use App\Models\Contact; use App\Models\Account; +use App\Models\Company; use App\Models\User; use App\Models\UserAccount; use App\Models\AccountToken; @@ -25,9 +26,13 @@ class AccountRepository { public function create($firstName = '', $lastName = '', $email = '', $password = '') { + $company = new Company(); + $company->save(); + $account = new Account(); $account->ip = Request::getClientIp(); $account->account_key = str_random(RANDOM_KEY_LENGTH); + $account->company_id = $company->id; // Track referal code if ($referralCode = Session::get(SESSION_REFERRAL_CODE)) { @@ -70,17 +75,19 @@ class AccountRepository return $account; } - public function getSearchData($account) + public function getSearchData($user) { - $data = $this->getAccountSearchData($account); + $data = $this->getAccountSearchData($user); - $data['navigation'] = $this->getNavigationSearchData(); + $data['navigation'] = $user->is_admin ? $this->getNavigationSearchData() : []; return $data; } - private function getAccountSearchData($account) + private function getAccountSearchData($user) { + $account = $user->account; + $data = [ 'clients' => [], 'contacts' => [], @@ -95,11 +102,19 @@ class AccountRepository if ($account->custom_client_label2) { $data[$account->custom_client_label2] = []; } - - $clients = Client::scope() - ->with('contacts', 'invoices') - ->get(); - + + if ($user->hasPermission('view_all')) { + $clients = Client::scope() + ->with('contacts', 'invoices') + ->get(); + } else { + $clients = Client::scope() + ->where('user_id', '=', $user->id) + ->with(['contacts', 'invoices' => function($query) use ($user) { + $query->where('user_id', '=', $user->id); + }])->get(); + } + foreach ($clients as $client) { if ($client->name) { $data['clients'][] = [ @@ -162,6 +177,7 @@ class AccountRepository ENTITY_QUOTE, ENTITY_TASK, ENTITY_EXPENSE, + ENTITY_VENDOR, ENTITY_RECURRING_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT @@ -178,15 +194,22 @@ class AccountRepository ]; } - $features[] = ['dashboard', '/dashboard']; - $features[] = ['customize_design', '/settings/customize_design']; - $features[] = ['new_tax_rate', '/tax_rates/create']; - $features[] = ['new_product', '/products/create']; - $features[] = ['new_user', '/users/create']; - $features[] = ['custom_fields', '/settings/invoice_settings']; + $features = array_merge($features, [ + ['dashboard', '/dashboard'], + ['customize_design', '/settings/customize_design'], + ['new_tax_rate', '/tax_rates/create'], + ['new_product', '/products/create'], + ['new_user', '/users/create'], + ['custom_fields', '/settings/invoice_settings'], + ['invoice_number', '/settings/invoice_settings'], + ]); $settings = array_merge(Account::$basicSettings, Account::$advancedSettings); + if ( ! Utils::isNinjaProd()) { + $settings[] = ACCOUNT_SYSTEM_SETTINGS; + } + foreach ($settings as $setting) { $features[] = [ $setting, @@ -205,21 +228,23 @@ class AccountRepository return $data; } - public function enableProPlan() + public function enablePlan($plan = PLAN_PRO, $term = PLAN_TERM_MONTHLY, $credit = 0, $pending_monthly = false) { - if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { - return false; - } - $account = Auth::user()->account; $client = $this->getNinjaClient($account); - $invitation = $this->createNinjaInvoice($client, $account); + $invitation = $this->createNinjaInvoice($client, $account, $plan, $term, $credit, $pending_monthly); return $invitation; } - public function createNinjaInvoice($client, $clientAccount) + public function createNinjaInvoice($client, $clientAccount, $plan = PLAN_PRO, $term = PLAN_TERM_MONTHLY, $credit = 0, $pending_monthly = false) { + if ($credit < 0) { + $credit = 0; + } + + $plan_cost = Account::$plan_prices[$plan][$term]; + $account = $this->getNinjaAccount(); $lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first(); $publicId = $lastInvoice ? ($lastInvoice->public_id + 1) : 1; @@ -230,19 +255,39 @@ class AccountRepository $invoice->client_id = $client->id; $invoice->invoice_number = $account->getNextInvoiceNumber($invoice); $invoice->invoice_date = $clientAccount->getRenewalDate(); - $invoice->amount = PRO_PLAN_PRICE; - $invoice->balance = PRO_PLAN_PRICE; + $invoice->amount = $invoice->balance = $plan_cost - $credit; $invoice->save(); - $item = new InvoiceItem(); - $item->account_id = $account->id; - $item->user_id = $account->users()->first()->id; - $item->public_id = $publicId; + if ($credit) { + $credit_item = InvoiceItem::createNew($invoice); + $credit_item->qty = 1; + $credit_item->cost = -$credit; + $credit_item->notes = trans('texts.plan_credit_description'); + $credit_item->product_key = trans('texts.plan_credit_product'); + $invoice->invoice_items()->save($credit_item); + } + + $item = InvoiceItem::createNew($invoice); $item->qty = 1; - $item->cost = PRO_PLAN_PRICE; - $item->notes = trans('texts.pro_plan_description'); - $item->product_key = trans('texts.pro_plan_product'); + $item->cost = $plan_cost; + $item->notes = trans("texts.{$plan}_plan_{$term}_description"); + + // Don't change this without updating the regex in PaymentService->createPayment() + $item->product_key = 'Plan - '.ucfirst($plan).' ('.ucfirst($term).')'; $invoice->invoice_items()->save($item); + + if ($pending_monthly) { + $term_end = $term == PLAN_MONTHLY ? date_create('+1 month') : date_create('+1 year'); + $pending_monthly_item = InvoiceItem::createNew($invoice); + $item->qty = 1; + $pending_monthly_item->cost = 0; + $pending_monthly_item->notes = trans("texts.plan_pending_monthly", array('date', Utils::dateToString($term_end))); + + // Don't change this without updating the text in PaymentService->createPayment() + $pending_monthly_item->product_key = 'Pending Monthly'; + $invoice->invoice_items()->save($pending_monthly_item); + } + $invitation = new Invitation(); $invitation->account_id = $account->id; @@ -298,28 +343,42 @@ class AccountRepository { $account->load('users'); $ninjaAccount = $this->getNinjaAccount(); - $client = Client::whereAccountId($ninjaAccount->id)->wherePublicId($account->id)->first(); + $ninjaUser = $ninjaAccount->getPrimaryUser(); + $client = Client::whereAccountId($ninjaAccount->id) + ->wherePublicId($account->id) + ->first(); + $clientExists = $client ? true : false; if (!$client) { $client = new Client(); $client->public_id = $account->id; - $client->user_id = $ninjaAccount->users()->first()->id; + $client->account_id = $ninjaAccount->id; + $client->user_id = $ninjaUser->id; $client->currency_id = 1; - foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id'] as $field) { - $client->$field = $account->$field; - } - $ninjaAccount->clients()->save($client); + } + + foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) { + $client->$field = $account->$field; + } + + $client->save(); + if ($clientExists) { + $contact = $client->getPrimaryContact(); + } else { $contact = new Contact(); - $contact->user_id = $ninjaAccount->users()->first()->id; + $contact->user_id = $ninjaUser->id; $contact->account_id = $ninjaAccount->id; $contact->public_id = $account->id; $contact->is_primary = true; - foreach (['first_name', 'last_name', 'email', 'phone'] as $field) { - $contact->$field = $account->users()->first()->$field; - } - $client->contacts()->save($contact); } + + $user = $account->getPrimaryUser(); + foreach (['first_name', 'last_name', 'email', 'phone'] as $field) { + $contact->$field = $user->$field; + } + + $client->contacts()->save($contact); return $client; } @@ -355,7 +414,7 @@ class AccountRepository $user->last_name = $lastName; $user->registered = true; - $user->account->startTrial(); + $user->account->startTrial(PLAN_PRO); } $user->oauth_provider_id = $providerId; @@ -471,11 +530,11 @@ class AccountRepository $item = new stdClass(); $item->id = $record->id; $item->user_id = $user->id; + $item->public_id = $user->public_id; $item->user_name = $user->getDisplayName(); $item->account_id = $user->account->id; $item->account_name = $user->account->getDisplayName(); - $item->pro_plan_paid = $user->account->pro_plan_paid; - $item->logo_path = $user->account->hasLogo() ? $user->account->getLogoPath() : null; + $item->logo_url = $user->account->hasLogo() ? $user->account->getLogoUrl() : null; $data[] = $item; } @@ -487,43 +546,6 @@ class AccountRepository return self::prepareUsersData($record); } - public function syncAccounts($userId, $proPlanPaid) { - $users = self::loadAccounts($userId); - self::syncUserAccounts($users, $proPlanPaid); - } - - public function syncUserAccounts($users, $proPlanPaid = false) { - if (!$users) { - return; - } - - if (!$proPlanPaid) { - foreach ($users as $user) { - if ($user->pro_plan_paid && $user->pro_plan_paid != '0000-00-00') { - $proPlanPaid = $user->pro_plan_paid; - break; - } - } - } - - if (!$proPlanPaid) { - return; - } - - $accountIds = []; - foreach ($users as $user) { - if ($user->pro_plan_paid != $proPlanPaid) { - $accountIds[] = $user->account_id; - } - } - - if (count($accountIds)) { - DB::table('accounts') - ->whereIn('id', $accountIds) - ->update(['pro_plan_paid' => $proPlanPaid]); - } - } - public function associateAccounts($userId1, $userId2) { $record = self::findUserAccounts($userId1, $userId2); @@ -542,8 +564,59 @@ class AccountRepository $record->save(); - $users = self::prepareUsersData($record); - self::syncUserAccounts($users); + $users = $this->getUserAccounts($record); + + // Pick the primary user + foreach ($users as $user) { + if (!$user->public_id) { + $useAsPrimary = false; + if(empty($primaryUser)) { + $useAsPrimary = true; + } + + $planDetails = $user->account->getPlanDetails(false, false); + $planLevel = 0; + + if ($planDetails) { + $planLevel = 1; + if ($planDetails['plan'] == PLAN_ENTERPRISE) { + $planLevel = 2; + } + + if (!$useAsPrimary && ( + $planLevel > $primaryUserPlanLevel + || ($planLevel == $primaryUserPlanLevel && $planDetails['expires'] > $primaryUserPlanExpires) + )) { + $useAsPrimary = true; + } + } + + if ($useAsPrimary) { + $primaryUser = $user; + $primaryUserPlanLevel = $planLevel; + if ($planDetails) { + $primaryUserPlanExpires = $planDetails['expires']; + } + } + } + } + + // Merge other companies into the primary user's company + if (!empty($primaryUser)) { + foreach ($users as $user) { + if ($user == $primaryUser || $user->public_id) { + continue; + } + + if ($user->account->company_id != $primaryUser->account->company_id) { + foreach ($user->account->company->accounts as $account) { + $account->company_id = $primaryUser->account->company_id; + $account->save(); + } + $user->account->company->forceDelete(); + } + } + } return $users; } @@ -563,6 +636,15 @@ class AccountRepository $userAccount->removeUserId($userId); $userAccount->save(); } + + $user = User::whereId($userId)->first(); + + if (!$user->public_id && $user->account->company->accounts->count() > 1) { + $company = Company::create(); + $company->save(); + $user->account->company_id = $company->id; + $user->account->save(); + } } public function findWithReminders() @@ -586,7 +668,7 @@ class AccountRepository { $name = trim($name) ?: 'TOKEN'; $users = $this->findUsers($user); - + foreach ($users as $user) { if ($token = AccountToken::whereUserId($user->id)->whereName($name)->first()) { continue; diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index 9df81663a5e5..83f08cd97a59 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -66,16 +66,19 @@ class ClientRepository extends BaseRepository return $query; } - public function save($data) + public function save($data, $client = null) { $publicId = isset($data['public_id']) ? $data['public_id'] : false; - if (!$publicId || $publicId == '-1') { + if ($client) { + // do nothing + } elseif (!$publicId || $publicId == '-1') { $client = Client::createNew(); } else { $client = Client::scope($publicId)->with('contacts')->firstOrFail(); + \Log::warning('Entity not set in client repo save'); } - + // convert currency code to id if (isset($data['currency_code'])) { $currencyCode = strtolower($data['currency_code']); @@ -100,6 +103,11 @@ class ClientRepository extends BaseRepository $contacts = isset($data['contact']) ? [$data['contact']] : $data['contacts']; $contactIds = []; + // If the primary is set ensure it's listed first + usort($contacts, function ($left, $right) { + return (isset($right['is_primary']) ? $right['is_primary'] : 1) - (isset($left['is_primary']) ? $left['is_primary'] : 0); + }); + foreach ($contacts as $contact) { $contact = $client->addContact($contact, $first); $contactIds[] = $contact->public_id; diff --git a/app/Ninja/Repositories/CreditRepository.php b/app/Ninja/Repositories/CreditRepository.php index 9803b4628af8..068707ace02b 100644 --- a/app/Ninja/Repositories/CreditRepository.php +++ b/app/Ninja/Repositories/CreditRepository.php @@ -27,7 +27,7 @@ class CreditRepository extends BaseRepository DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), 'credits.public_id', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'credits.amount', @@ -59,12 +59,15 @@ class CreditRepository extends BaseRepository return $query; } - public function save($input) + public function save($input, $credit = null) { $publicId = isset($data['public_id']) ? $data['public_id'] : false; - - if ($publicId) { + + if ($credit) { + // do nothing + } elseif ($publicId) { $credit = Credit::scope($publicId)->firstOrFail(); + \Log::warning('Entity not set in credit repo save'); } else { $credit = Credit::createNew(); } diff --git a/app/Ninja/Repositories/DocumentRepository.php b/app/Ninja/Repositories/DocumentRepository.php new file mode 100644 index 000000000000..094b4848ebe0 --- /dev/null +++ b/app/Ninja/Repositories/DocumentRepository.php @@ -0,0 +1,228 @@ +with('user') + ->get(); + } + + public function find() + { + $accountid = \Auth::user()->account_id; + $query = DB::table('clients') + ->join('accounts', 'accounts.id', '=', 'clients.account_id') + ->leftjoin('clients', 'clients.id', '=', 'clients.client_id') + /*->leftJoin('expenses', 'expenses.id', '=', 'clients.expense_id') + ->leftJoin('invoices', 'invoices.id', '=', 'clients.invoice_id')*/ + ->where('documents.account_id', '=', $accountid) + /*->where('vendors.deleted_at', '=', null) + ->where('clients.deleted_at', '=', null)*/ + ->select( + 'documents.account_id', + 'documents.path', + 'documents.deleted_at', + 'documents.size', + 'documents.width', + 'documents.height', + 'documents.id', + 'documents.is_deleted', + 'documents.public_id', + 'documents.invoice_id', + 'documents.expense_id', + 'documents.user_id', + 'invoices.public_id as invoice_public_id', + 'invoices.user_id as invoice_user_id', + 'expenses.public_id as expense_public_id', + 'expenses.user_id as expense_user_id' + ); + + return $query; + } + + public function upload($uploaded, &$doc_array=null) + { + $extension = strtolower($uploaded->getClientOriginalExtension()); + if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ + $documentType = Document::$extraExtensions[$extension]; + } + else{ + $documentType = $extension; + } + + if(empty(Document::$types[$documentType])){ + return 'Unsupported file type'; + } + + $documentTypeData = Document::$types[$documentType]; + + $filePath = $uploaded->path(); + $name = $uploaded->getClientOriginalName(); + $size = filesize($filePath); + + if($size/1000 > MAX_DOCUMENT_SIZE){ + return 'File too large'; + } + + + + $hash = sha1_file($filePath); + $filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType; + + $document = Document::createNew(); + $disk = $document->getDisk(); + if(!$disk->exists($filename)){// Have we already stored the same file + $stream = fopen($filePath, 'r'); + $disk->getDriver()->putStream($filename, $stream, ['mimetype'=>$documentTypeData['mime']]); + fclose($stream); + } + + // This is an image; check if we need to create a preview + if(in_array($documentType, array('jpeg','png','gif','bmp','tiff','psd'))){ + $makePreview = false; + $imageSize = getimagesize($filePath); + $width = $imageSize[0]; + $height = $imageSize[1]; + $imgManagerConfig = array(); + if(in_array($documentType, array('gif','bmp','tiff','psd'))){ + // Needs to be converted + $makePreview = true; + } else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){ + $makePreview = true; + } + + if(in_array($documentType,array('bmp','tiff','psd'))){ + if(!class_exists('Imagick')){ + // Cant't read this + $makePreview = false; + } else { + $imgManagerConfig['driver'] = 'imagick'; + } + } + + if($makePreview){ + $previewType = 'jpeg'; + if(in_array($documentType, array('png','gif','tiff','psd'))){ + // Has transparency + $previewType = 'png'; + } + + $document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType.'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType; + if(!$disk->exists($document->preview)){ + // We haven't created a preview yet + $imgManager = new ImageManager($imgManagerConfig); + + $img = $imgManager->make($filePath); + + if($width <= DOCUMENT_PREVIEW_SIZE && $height <= DOCUMENT_PREVIEW_SIZE){ + $previewWidth = $width; + $previewHeight = $height; + } else if($width > $height) { + $previewWidth = DOCUMENT_PREVIEW_SIZE; + $previewHeight = $height * DOCUMENT_PREVIEW_SIZE / $width; + } else { + $previewHeight = DOCUMENT_PREVIEW_SIZE; + $previewWidth = $width * DOCUMENT_PREVIEW_SIZE / $height; + } + + $img->resize($previewWidth, $previewHeight); + + $previewContent = (string) $img->encode($previewType); + $disk->put($document->preview, $previewContent); + $base64 = base64_encode($previewContent); + } + else{ + $base64 = base64_encode($disk->get($document->preview)); + } + }else{ + $base64 = base64_encode(file_get_contents($filePath)); + } + } + + $document->path = $filename; + $document->type = $documentType; + $document->size = $size; + $document->hash = $hash; + $document->name = substr($name, -255); + + if(!empty($imageSize)){ + $document->width = $imageSize[0]; + $document->height = $imageSize[1]; + } + + $document->save(); + $doc_array = $document->toArray(); + + if(!empty($base64)){ + $mime = Document::$types[!empty($previewType)?$previewType:$documentType]['mime']; + $doc_array['base64'] = 'data:'.$mime.';base64,'.$base64; + } + + return $document; + } + + public function getClientDatatable($contactId, $entityType, $search) + { + + $query = DB::table('invitations') + ->join('accounts', 'accounts.id', '=', 'invitations.account_id') + ->join('invoices', 'invoices.id', '=', 'invitations.invoice_id') + ->join('documents', 'documents.invoice_id', '=', 'invitations.invoice_id') + ->join('clients', 'clients.id', '=', 'invoices.client_id') + ->where('invitations.contact_id', '=', $contactId) + ->where('invitations.deleted_at', '=', null) + ->where('invoices.is_deleted', '=', false) + ->where('clients.deleted_at', '=', null) + ->where('invoices.is_recurring', '=', false) + // This needs to be a setting to also hide the activity on the dashboard page + //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) + ->select( + 'invitations.invitation_key', + 'invoices.invoice_number', + 'documents.name', + 'documents.public_id', + 'documents.created_at', + 'documents.size' + ); + + $table = \Datatable::query($query) + ->addColumn('invoice_number', function ($model) { + return link_to( + '/view/'.$model->invitation_key, + $model->invoice_number + )->toHtml(); + }) + ->addColumn('name', function ($model) { + return link_to( + '/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name, + $model->name, + ['target'=>'_blank'] + )->toHtml(); + }) + ->addColumn('document_date', function ($model) { + return Utils::fromSqlDate($model->created_at); + }) + ->addColumn('document_size', function ($model) { + return Form::human_filesize($model->size); + }); + + return $table->make(); + } +} diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index 49988435e598..442af74bd32b 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -1,20 +1,29 @@ -documentRepo = $documentRepo; + } + public function all() { return Expense::scope() @@ -87,7 +96,7 @@ class ExpenseRepository extends BaseRepository 'vendors.name as vendor_name', 'vendors.public_id as vendor_public_id', 'vendors.user_id as vendor_user_id', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'contacts.first_name', @@ -113,12 +122,15 @@ class ExpenseRepository extends BaseRepository return $query; } - public function save($input) + public function save($input, $expense = null) { $publicId = isset($input['public_id']) ? $input['public_id'] : false; - if ($publicId) { + if ($expense) { + // do nothing + } elseif ($publicId) { $expense = Expense::scope($publicId)->firstOrFail(); + \Log::warning('Entity not set in expense repo save'); } else { $expense = Expense::createNew(); } @@ -144,9 +156,46 @@ class ExpenseRepository extends BaseRepository $rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1; $expense->exchange_rate = round($rate, 4); $expense->amount = round(Utils::parseFloat($input['amount']), 2); - + $expense->save(); + // Documents + $document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();; + foreach ($document_ids as $document_id){ + $document = Document::scope($document_id)->first(); + if($document && Auth::user()->can('edit', $document)){ + $document->invoice_id = null; + $document->expense_id = $expense->id; + $document->save(); + } + } + + if(!empty($input['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){ + // Fallback upload + $doc_errors = array(); + foreach($input['documents'] as $upload){ + $result = $this->documentRepo->upload($upload); + if(is_string($result)){ + $doc_errors[] = $result; + } + else{ + $result->expense_id = $expense->id; + $result->save(); + $document_ids[] = $result->public_id; + } + } + if(!empty($doc_errors)){ + Session::flash('error', implode('
',array_map('htmlentities',$doc_errors))); + } + } + + foreach ($expense->documents as $document){ + if(!in_array($document->public_id, $document_ids)){ + // Not checking permissions; deleting a document is just editing the invoice + $document->delete(); + } + } + return $expense; } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 67c0bab31080..91ffddfea138 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -1,25 +1,31 @@ -documentRepo = $documentRepo; $this->paymentService = $paymentService; } @@ -52,7 +58,7 @@ class InvoiceRepository extends BaseRepository 'clients.user_id as client_user_id', 'invoice_number', 'invoice_status_id', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'invoices.public_id', 'invoices.amount', 'invoices.balance', @@ -109,7 +115,7 @@ class InvoiceRepository extends BaseRepository DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), 'clients.public_id as client_public_id', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'invoices.public_id', 'invoices.amount', 'frequencies.name as frequency', @@ -119,7 +125,8 @@ class InvoiceRepository extends BaseRepository 'contacts.last_name', 'contacts.email', 'invoices.deleted_at', - 'invoices.is_deleted' + 'invoices.is_deleted', + 'invoices.user_id' ); if ($clientPublicId) { @@ -146,11 +153,14 @@ class InvoiceRepository extends BaseRepository ->join('accounts', 'accounts.id', '=', 'invitations.account_id') ->join('invoices', 'invoices.id', '=', 'invitations.invoice_id') ->join('clients', 'clients.id', '=', 'invoices.client_id') + ->join('contacts', 'contacts.client_id', '=', 'clients.id') ->where('invitations.contact_id', '=', $contactId) ->where('invitations.deleted_at', '=', null) ->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE) ->where('invoices.is_deleted', '=', false) ->where('clients.deleted_at', '=', null) + ->where('contacts.deleted_at', '=', null) + ->where('contacts.is_primary', '=', true) ->where('invoices.is_recurring', '=', false) // This needs to be a setting to also hide the activity on the dashboard page //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) @@ -163,7 +173,7 @@ class InvoiceRepository extends BaseRepository 'invoices.balance as balance', 'invoices.due_date', 'clients.public_id as client_public_id', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'invoices.public_id', 'invoices.amount', 'invoices.start_date', @@ -191,14 +201,16 @@ class InvoiceRepository extends BaseRepository ->make(); } - public function save($data, $checkSubPermissions = false) + public function save($data, $invoice = null) { $account = \Auth::user()->account; $publicId = isset($data['public_id']) ? $data['public_id'] : false; $isNew = !$publicId || $publicId == '-1'; - if ($isNew) { + if ($invoice) { + // do nothing + } elseif ($isNew) { $entityType = ENTITY_INVOICE; if (isset($data['is_recurring']) && filter_var($data['is_recurring'], FILTER_VALIDATE_BOOLEAN)) { $entityType = ENTITY_RECURRING_INVOICE; @@ -214,8 +226,11 @@ class InvoiceRepository extends BaseRepository } } else { $invoice = Invoice::scope($publicId)->firstOrFail(); + \Log::warning('Entity not set in invoice repo save'); } + $invoice->fill($data); + if ((isset($data['set_default_terms']) && $data['set_default_terms']) || (isset($data['set_default_footer']) && $data['set_default_footer'])) { if (isset($data['set_default_terms']) && $data['set_default_terms']) { @@ -277,14 +292,23 @@ class InvoiceRepository extends BaseRepository $invoice->end_date = null; } - $invoice->terms = (isset($data['terms']) && trim($data['terms'])) ? trim($data['terms']) : (!$publicId && $account->invoice_terms ? $account->invoice_terms : ''); + if (isset($data['terms']) && trim($data['terms'])) { + $invoice->terms = trim($data['terms']); + } elseif ($isNew && $account->{"{$entityType}_terms"}) { + $invoice->terms = $account->{"{$entityType}_terms"}; + } else { + $invoice->terms = ''; + } + $invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : ''); $invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null; - // process date variables - $invoice->terms = Utils::processVariables($invoice->terms); - $invoice->invoice_footer = Utils::processVariables($invoice->invoice_footer); - $invoice->public_notes = Utils::processVariables($invoice->public_notes); + // process date variables if not recurring + if(!$invoice->is_recurring) { + $invoice->terms = Utils::processVariables($invoice->terms); + $invoice->invoice_footer = Utils::processVariables($invoice->invoice_footer); + $invoice->public_notes = Utils::processVariables($invoice->public_notes); + } if (isset($data['po_number'])) { $invoice->po_number = trim($data['po_number']); @@ -292,12 +316,10 @@ class InvoiceRepository extends BaseRepository $invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id; - if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) { - $invoice->tax_rate = Utils::parseFloat($data['tax_rate']); - $invoice->tax_name = trim($data['tax_name']); - } else { - $invoice->tax_rate = 0; - $invoice->tax_name = ''; + // provide backwards compatability + if (isset($data['tax_name']) && isset($data['tax_rate'])) { + $data['tax_name1'] = $data['tax_name']; + $data['tax_rate1'] = $data['tax_rate']; } $total = 0; @@ -318,20 +340,24 @@ class InvoiceRepository extends BaseRepository foreach ($data['invoice_items'] as $item) { $item = (array) $item; - if (isset($item['tax_rate']) && Utils::parseFloat($item['tax_rate']) > 0) { - $invoiceItemCost = round(Utils::parseFloat($item['cost']), 2); - $invoiceItemQty = round(Utils::parseFloat($item['qty']), 2); - $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate']); - $lineTotal = $invoiceItemCost * $invoiceItemQty; + $invoiceItemCost = round(Utils::parseFloat($item['cost']), 2); + $invoiceItemQty = round(Utils::parseFloat($item['qty']), 2); + $lineTotal = $invoiceItemCost * $invoiceItemQty; - if ($invoice->discount > 0) { - if ($invoice->is_amount_discount) { - $lineTotal -= round(($lineTotal/$total) * $invoice->discount, 2); - } else { - $lineTotal -= round($lineTotal * ($invoice->discount/100), 2); - } + if ($invoice->discount > 0) { + if ($invoice->is_amount_discount) { + $lineTotal -= round(($lineTotal/$total) * $invoice->discount, 2); + } else { + $lineTotal -= round($lineTotal * ($invoice->discount/100), 2); } + } + if (isset($item['tax_rate1']) && Utils::parseFloat($item['tax_rate1']) > 0) { + $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']); + $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); + } + if (isset($item['tax_rate2']) && Utils::parseFloat($item['tax_rate2']) > 0) { + $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']); $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); } } @@ -373,8 +399,9 @@ class InvoiceRepository extends BaseRepository $total += $invoice->custom_value2; } - $total += $total * $invoice->tax_rate / 100; - $total = round($total, 2); + $taxAmount1 = round($total * $invoice->tax_rate1 / 100, 2); + $taxAmount2 = round($total * $invoice->tax_rate2 / 100, 2); + $total = round($total + $taxAmount1 + $taxAmount2, 2); $total += $itemTax; // custom fields not charged taxes @@ -397,6 +424,53 @@ class InvoiceRepository extends BaseRepository if ($publicId) { $invoice->invoice_items()->forceDelete(); } + + $document_ids = !empty($data['document_ids'])?array_map('intval', $data['document_ids']):array();; + foreach ($document_ids as $document_id){ + $document = Document::scope($document_id)->first(); + if($document && Auth::user()->can('edit', $document)){ + + if($document->invoice_id && $document->invoice_id != $invoice->id){ + // From a clone + $document = $document->cloneDocument(); + $document_ids[] = $document->public_id;// Don't remove this document + } + + $document->invoice_id = $invoice->id; + $document->expense_id = null; + $document->save(); + } + } + + if(!empty($data['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){ + // Fallback upload + $doc_errors = array(); + foreach($data['documents'] as $upload){ + $result = $this->documentRepo->upload($upload); + if(is_string($result)){ + $doc_errors[] = $result; + } + else{ + $result->invoice_id = $invoice->id; + $result->save(); + $document_ids[] = $result->public_id; + } + } + if(!empty($doc_errors)){ + Session::flash('error', implode('
',array_map('htmlentities',$doc_errors))); + } + } + + foreach ($invoice->documents as $document){ + if(!in_array($document->public_id, $document_ids)){ + // Removed + // Not checking permissions; deleting a document is just editing the invoice + if($document->invoice_id == $invoice->id){ + // Make sure the document isn't on a clone + $document->delete(); + } + } + } foreach ($data['invoice_items'] as $item) { $item = (array) $item; @@ -407,7 +481,7 @@ class InvoiceRepository extends BaseRepository $task = false; if (isset($item['task_public_id']) && $item['task_public_id']) { $task = Task::scope($item['task_public_id'])->where('invoice_id', '=', null)->firstOrFail(); - if(!$checkSubPermissions || $task->canEdit()){ + if(Auth::user()->can('edit', $task)){ $task->invoice_id = $invoice->id; $task->client_id = $invoice->client_id; $task->save(); @@ -417,7 +491,7 @@ class InvoiceRepository extends BaseRepository $expense = false; if (isset($item['expense_public_id']) && $item['expense_public_id']) { $expense = Expense::scope($item['expense_public_id'])->where('invoice_id', '=', null)->firstOrFail(); - if(!$checkSubPermissions || $expense->canEdit()){ + if(Auth::user()->can('edit', $expense)){ $expense->invoice_id = $invoice->id; $expense->client_id = $invoice->client_id; $expense->save(); @@ -428,7 +502,7 @@ class InvoiceRepository extends BaseRepository if (\Auth::user()->account->update_products && ! strtotime($productKey)) { $product = Product::findProductByKey($productKey); if (!$product) { - if(!$checkSubPermissions || Product::canCreate()){ + if (Auth::user()->can('create', ENTITY_PRODUCT)) { $product = Product::createNew(); $product->product_key = trim($item['product_key']); } @@ -436,7 +510,7 @@ class InvoiceRepository extends BaseRepository $product = null; } } - if($product && (!$checkSubPermissions || $product->canEdit())){ + if ($product && (Auth::user()->can('edit', $product))) { $product->notes = ($task || $expense) ? '' : $item['notes']; $product->cost = $expense ? 0 : $item['cost']; $product->save(); @@ -450,7 +524,6 @@ class InvoiceRepository extends BaseRepository $invoiceItem->notes = trim($invoice->is_recurring ? $item['notes'] : Utils::processVariables($item['notes'])); $invoiceItem->cost = Utils::parseFloat($item['cost']); $invoiceItem->qty = Utils::parseFloat($item['qty']); - $invoiceItem->tax_rate = 0; if (isset($item['custom_value1'])) { $invoiceItem->custom_value1 = $item['custom_value1']; @@ -459,11 +532,14 @@ class InvoiceRepository extends BaseRepository $invoiceItem->custom_value2 = $item['custom_value2']; } - if (isset($item['tax_rate']) && isset($item['tax_name']) && $item['tax_name']) { - $invoiceItem['tax_rate'] = Utils::parseFloat($item['tax_rate']); - $invoiceItem['tax_name'] = trim($item['tax_name']); + // provide backwards compatability + if (isset($item['tax_name']) && isset($item['tax_rate'])) { + $item['tax_name1'] = $item['tax_name']; + $item['tax_rate1'] = $item['tax_rate']; } + $invoiceItem->fill($item); + $invoice->invoice_items()->save($invoiceItem); } @@ -494,14 +570,13 @@ class InvoiceRepository extends BaseRepository } } $clone->invoice_number = $invoiceNumber ?: $account->getNextInvoiceNumber($clone); + $clone->invoice_date = date_create()->format('Y-m-d'); foreach ([ 'client_id', 'discount', 'is_amount_discount', - 'invoice_date', 'po_number', - 'due_date', 'is_recurring', 'frequency_id', 'start_date', @@ -510,8 +585,10 @@ class InvoiceRepository extends BaseRepository 'invoice_footer', 'public_notes', 'invoice_design_id', - 'tax_name', - 'tax_rate', + 'tax_name1', + 'tax_rate1', + 'tax_name2', + 'tax_rate2', 'amount', 'is_quote', 'custom_value1', @@ -545,14 +622,22 @@ class InvoiceRepository extends BaseRepository 'notes', 'cost', 'qty', - 'tax_name', - 'tax_rate', ] as $field) { + 'tax_name1', + 'tax_rate1', + 'tax_name2', + 'tax_rate2', + ] as $field) { $cloneItem->$field = $item->$field; } $clone->invoice_items()->save($cloneItem); } + foreach ($invoice->documents as $document) { + $cloneDocument = $document->cloneDocument(); + $invoice->documents()->save($cloneDocument); + } + foreach ($invoice->invitations as $invitation) { $cloneInvitation = Invitation::createNew($invoice); $cloneInvitation->contact_id = $invitation->contact_id; @@ -581,7 +666,7 @@ class InvoiceRepository extends BaseRepository return false; } - $invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country'); + $invoice->load('user', 'invoice_items', 'documents', 'invoice_design', 'account.country', 'client.contacts', 'client.country'); $client = $invoice->client; if (!$client || $client->is_deleted) { @@ -632,15 +717,17 @@ class InvoiceRepository extends BaseRepository $invoice->public_notes = Utils::processVariables($recurInvoice->public_notes); $invoice->terms = Utils::processVariables($recurInvoice->terms); $invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer); - $invoice->tax_name = $recurInvoice->tax_name; - $invoice->tax_rate = $recurInvoice->tax_rate; + $invoice->tax_name1 = $recurInvoice->tax_name1; + $invoice->tax_rate1 = $recurInvoice->tax_rate1; + $invoice->tax_name2 = $recurInvoice->tax_name2; + $invoice->tax_rate2 = $recurInvoice->tax_rate2; $invoice->invoice_design_id = $recurInvoice->invoice_design_id; $invoice->custom_value1 = $recurInvoice->custom_value1 ?: 0; $invoice->custom_value2 = $recurInvoice->custom_value2 ?: 0; $invoice->custom_taxes1 = $recurInvoice->custom_taxes1 ?: 0; $invoice->custom_taxes2 = $recurInvoice->custom_taxes2 ?: 0; - $invoice->custom_text_value1 = $recurInvoice->custom_text_value1; - $invoice->custom_text_value2 = $recurInvoice->custom_text_value2; + $invoice->custom_text_value1 = Utils::processVariables($recurInvoice->custom_text_value1); + $invoice->custom_text_value2 = Utils::processVariables($recurInvoice->custom_text_value2); $invoice->is_amount_discount = $recurInvoice->is_amount_discount; $invoice->due_date = $recurInvoice->getDueDate(); $invoice->save(); @@ -652,11 +739,20 @@ class InvoiceRepository extends BaseRepository $item->cost = $recurItem->cost; $item->notes = Utils::processVariables($recurItem->notes); $item->product_key = Utils::processVariables($recurItem->product_key); - $item->tax_name = $recurItem->tax_name; - $item->tax_rate = $recurItem->tax_rate; + $item->tax_name1 = $recurItem->tax_name1; + $item->tax_rate1 = $recurItem->tax_rate1; + $item->tax_name2 = $recurItem->tax_name2; + $item->tax_rate2 = $recurItem->tax_rate2; + $item->custom_value1 = Utils::processVariables($recurItem->custom_value1); + $item->custom_value2 = Utils::processVariables($recurItem->custom_value2); $invoice->invoice_items()->save($item); } + foreach ($recurInvoice->documents as $recurDocument) { + $document = $recurDocument->cloneDocument(); + $invoice->documents()->save($document); + } + foreach ($recurInvoice->invitations as $recurInvitation) { $invitation = Invitation::createNew($recurInvitation); $invitation->contact_id = $recurInvitation->contact_id; diff --git a/app/Ninja/Repositories/NinjaRepository.php b/app/Ninja/Repositories/NinjaRepository.php index 3f9c1fa4f19e..d1dc7abede6e 100644 --- a/app/Ninja/Repositories/NinjaRepository.php +++ b/app/Ninja/Repositories/NinjaRepository.php @@ -4,7 +4,7 @@ use App\Models\Account; class NinjaRepository { - public function updateProPlanPaid($clientPublicId, $proPlanPaid) + public function updatePlanDetails($clientPublicId, $data) { $account = Account::whereId($clientPublicId)->first(); @@ -12,7 +12,13 @@ class NinjaRepository return; } - $account->pro_plan_paid = $proPlanPaid; - $account->save(); + $company = $account->company; + $company->plan = !empty($data['plan']) && $data['plan'] != PLAN_FREE?$data['plan']:null; + $company->plan_term = !empty($data['plan_term'])?$data['plan_term']:null; + $company->plan_paid = !empty($data['plan_paid'])?$data['plan_paid']:null; + $company->plan_started = !empty($data['plan_started'])?$data['plan_started']:null; + $company->plan_expires = !empty($data['plan_expires'])?$data['plan_expires']:null; + + $company->save(); } } diff --git a/app/Ninja/Repositories/PaymentRepository.php b/app/Ninja/Repositories/PaymentRepository.php index a027cb62aedb..dd99fccdfdd4 100644 --- a/app/Ninja/Repositories/PaymentRepository.php +++ b/app/Ninja/Repositories/PaymentRepository.php @@ -34,7 +34,7 @@ class PaymentRepository extends BaseRepository DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), 'payments.transaction_reference', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'payments.amount', @@ -64,7 +64,14 @@ class PaymentRepository extends BaseRepository if ($filter) { $query->where(function ($query) use ($filter) { - $query->where('clients.name', 'like', '%'.$filter.'%'); + $query->where('clients.name', 'like', '%'.$filter.'%') + ->orWhere('invoices.invoice_number', 'like', '%'.$filter.'%') + ->orWhere('payments.transaction_reference', 'like', '%'.$filter.'%') + ->orWhere('gateways.name', 'like', '%'.$filter.'%') + ->orWhere('payment_types.name', 'like', '%'.$filter.'%') + ->orWhere('contacts.first_name', 'like', '%'.$filter.'%') + ->orWhere('contacts.last_name', 'like', '%'.$filter.'%') + ->orWhere('contacts.email', 'like', '%'.$filter.'%'); }); } @@ -94,7 +101,7 @@ class PaymentRepository extends BaseRepository 'invitations.invitation_key', 'payments.public_id', 'payments.transaction_reference', - 'clients.name as client_name', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', @@ -116,12 +123,15 @@ class PaymentRepository extends BaseRepository return $query; } - public function save($input) + public function save($input, $payment = null) { $publicId = isset($input['public_id']) ? $input['public_id'] : false; - if ($publicId) { + if ($payment) { + // do nothing + } elseif ($publicId) { $payment = Payment::scope($publicId)->firstOrFail(); + \Log::warning('Entity not set in payment repo save'); } else { $payment = Payment::createNew(); } diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php index 417b49f23640..eb0e7383e9e5 100644 --- a/app/Ninja/Repositories/ProductRepository.php +++ b/app/Ninja/Repositories/ProductRepository.php @@ -1,6 +1,7 @@ firstOrFail(); + \Log::warning('Entity not set in product repo save'); + } else { + $product = Product::createNew(); + } + + $product->fill($data); + $product->save(); + + return $product; + } + } \ No newline at end of file diff --git a/app/Ninja/Repositories/ReferralRepository.php b/app/Ninja/Repositories/ReferralRepository.php index c847f3386b39..f96475c72ddc 100644 --- a/app/Ninja/Repositories/ReferralRepository.php +++ b/app/Ninja/Repositories/ReferralRepository.php @@ -1,31 +1,32 @@ where('referral_user_id', $userId) - ->get(['id', 'pro_plan_paid']); + $accounts = Account::where('referral_user_id', $userId); $counts = [ 'free' => 0, - 'pro' => 0 + 'pro' => 0, + 'enterprise' => 0 ]; foreach ($accounts as $account) { $counts['free']++; - if (Utils::withinPastYear($account->pro_plan_paid)) { + $plan = $account->getPlanDetails(false, false); + + if ($plan) { $counts['pro']++; + if ($plan['plan'] == PLAN_ENTERPRISE) { + $counts['enterprise']++; + } } } return $counts; } - - - } \ No newline at end of file diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php index a7655a8abd9e..3a5eca04a112 100644 --- a/app/Ninja/Repositories/TaskRepository.php +++ b/app/Ninja/Repositories/TaskRepository.php @@ -25,7 +25,7 @@ class TaskRepository ->where('clients.deleted_at', '=', null) ->select( 'tasks.public_id', - 'clients.name as client_name', + \DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'contacts.first_name', @@ -64,24 +64,13 @@ class TaskRepository return $query; } - public function getErrors($input) + public function save($publicId, $data, $task = null) { - $rules = [ - 'time_log' => 'time_log', - ]; - $validator = \Validator::make($input, $rules); - - if ($validator->fails()) { - return $validator; - } - - return false; - } - - public function save($publicId, $data) - { - if ($publicId) { + if ($task) { + // do nothing + } elseif ($publicId) { $task = Task::scope($publicId)->firstOrFail(); + \Log::warning('Entity not set in task repo save'); } else { $task = Task::createNew(); } diff --git a/app/Ninja/Repositories/TaxRateRepository.php b/app/Ninja/Repositories/TaxRateRepository.php index 2e325100a016..8f1ad7f6550f 100644 --- a/app/Ninja/Repositories/TaxRateRepository.php +++ b/app/Ninja/Repositories/TaxRateRepository.php @@ -20,14 +20,15 @@ class TaxRateRepository extends BaseRepository ->select('tax_rates.public_id', 'tax_rates.name', 'tax_rates.rate', 'tax_rates.deleted_at'); } - public function save($data, $taxRate = false) + public function save($data, $taxRate = null) { - if ( ! $taxRate) { - if (isset($data['public_id'])) { - $taxRate = TaxRate::scope($data['public_id'])->firstOrFail(); - } else { - $taxRate = TaxRate::createNew(); - } + if ($taxRate) { + // do nothing + } elseif (isset($data['public_id'])) { + $taxRate = TaxRate::scope($data['public_id'])->firstOrFail(); + \Log::warning('Entity not set in tax rate repo save'); + } else { + $taxRate = TaxRate::createNew(); } $taxRate->fill($data); diff --git a/app/Ninja/Repositories/TokenRepository.php b/app/Ninja/Repositories/TokenRepository.php index 5237eb7a0369..af0bbb6533d0 100644 --- a/app/Ninja/Repositories/TokenRepository.php +++ b/app/Ninja/Repositories/TokenRepository.php @@ -13,10 +13,10 @@ class TokenRepository extends BaseRepository return 'App\Models\AccountToken'; } - public function find($accountId) + public function find($userId) { $query = DB::table('account_tokens') - ->where('account_tokens.account_id', '=', $accountId); + ->where('account_tokens.user_id', '=', $userId); if (!Session::get('show_trash:token')) { $query->where('account_tokens.deleted_at', '=', null); diff --git a/app/Ninja/Repositories/VendorRepository.php b/app/Ninja/Repositories/VendorRepository.php index df885f62e12e..82e1bfdd18f1 100644 --- a/app/Ninja/Repositories/VendorRepository.php +++ b/app/Ninja/Repositories/VendorRepository.php @@ -16,7 +16,7 @@ class VendorRepository extends BaseRepository public function all() { return Vendor::scope() - ->with('user', 'vendorcontacts', 'country') + ->with('user', 'vendor_contacts', 'country') ->withTrashed() ->where('is_deleted', '=', false) ->get(); @@ -62,21 +62,24 @@ class VendorRepository extends BaseRepository return $query; } - public function save($data) + public function save($data, $vendor = null) { $publicId = isset($data['public_id']) ? $data['public_id'] : false; - if (!$publicId || $publicId == '-1') { + if ($vendor) { + // do nothing + } elseif (!$publicId || $publicId == '-1') { $vendor = Vendor::createNew(); } else { - $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail(); + $vendor = Vendor::scope($publicId)->with('vendor_contacts')->firstOrFail(); + \Log::warning('Entity not set in vendor repo save'); } $vendor->fill($data); $vendor->save(); $first = true; - $vendorcontacts = isset($data['vendorcontact']) ? [$data['vendorcontact']] : $data['vendorcontacts']; + $vendorcontacts = isset($data['vendor_contact']) ? [$data['vendor_contact']] : $data['vendor_contacts']; foreach ($vendorcontacts as $vendorcontact) { $vendorcontact = $vendor->addVendorContact($vendorcontact, $first); diff --git a/app/Ninja/Transformers/AccountTransformer.php b/app/Ninja/Transformers/AccountTransformer.php index 6a1c32f30e09..f56635bad2b1 100644 --- a/app/Ninja/Transformers/AccountTransformer.php +++ b/app/Ninja/Transformers/AccountTransformer.php @@ -14,12 +14,12 @@ class AccountTransformer extends EntityTransformer 'users', 'products', 'taxRates', - 'payments' ]; protected $availableIncludes = [ 'clients', 'invoices', + 'payments', ]; public function includeUsers(Account $account) diff --git a/app/Ninja/Transformers/ClientTransformer.php b/app/Ninja/Transformers/ClientTransformer.php index f7d786a2ee25..ea282f74ceca 100644 --- a/app/Ninja/Transformers/ClientTransformer.php +++ b/app/Ninja/Transformers/ClientTransformer.php @@ -47,7 +47,7 @@ class ClientTransformer extends EntityTransformer protected $availableIncludes = [ 'invoices', 'credits', - 'expenses', + //'expenses', ]; public function includeContacts(Client $client) @@ -58,7 +58,7 @@ class ClientTransformer extends EntityTransformer public function includeInvoices(Client $client) { - $transformer = new InvoiceTransformer($this->account, $this->serializer); + $transformer = new InvoiceTransformer($this->account, $this->serializer, $client); return $this->includeCollection($client->invoices, $transformer, ENTITY_INVOICE); } @@ -77,13 +77,11 @@ class ClientTransformer extends EntityTransformer public function transform(Client $client) { - return [ + return array_merge($this->getDefaults($client), [ 'id' => (int) $client->public_id, 'name' => $client->name, 'balance' => (float) $client->balance, 'paid_to_date' => (float) $client->paid_to_date, - 'user_id' => (int) $client->user->public_id + 1, - 'account_key' => $this->account->account_key, 'updated_at' => $this->getTimestamp($client->updated_at), 'archived_at' => $this->getTimestamp($client->deleted_at), 'address1' => $client->address1, @@ -106,6 +104,6 @@ class ClientTransformer extends EntityTransformer 'currency_id' => (int) $client->currency_id, 'custom_value1' => $client->custom_value1, 'custom_value2' => $client->custom_value2, - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/ContactTransformer.php b/app/Ninja/Transformers/ContactTransformer.php index e404b73660d4..68172e156c60 100644 --- a/app/Ninja/Transformers/ContactTransformer.php +++ b/app/Ninja/Transformers/ContactTransformer.php @@ -8,7 +8,7 @@ class ContactTransformer extends EntityTransformer { public function transform(Contact $contact) { - return [ + return array_merge($this->getDefaults($contact), [ 'id' => (int) $contact->public_id, 'first_name' => $contact->first_name, 'last_name' => $contact->last_name, @@ -18,8 +18,7 @@ class ContactTransformer extends EntityTransformer 'is_primary' => (bool) $contact->is_primary, 'phone' => $contact->phone, 'last_login' => $contact->last_login, - 'account_key' => $this->account->account_key, 'send_invoice' => (bool) $contact->send_invoice, - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/CreditTransformer.php b/app/Ninja/Transformers/CreditTransformer.php index a33185d2ff39..39ce5ff1d8f3 100644 --- a/app/Ninja/Transformers/CreditTransformer.php +++ b/app/Ninja/Transformers/CreditTransformer.php @@ -8,17 +8,16 @@ class CreditTransformer extends EntityTransformer { public function transform(Credit $credit) { - return [ + return array_merge($this->getDefaults($credit), [ 'id' => (int) $credit->public_id, 'amount' => (float) $credit->amount, 'balance' => (float) $credit->balance, 'updated_at' => $this->getTimestamp($credit->updated_at), 'archived_at' => $this->getTimestamp($credit->deleted_at), 'is_deleted' => (bool) $credit->is_deleted, - 'account_key' => $this->account->account_key, 'credit_date' => $credit->credit_date, 'credit_number' => $credit->credit_number, 'private_notes' => $credit->private_notes, - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/DocumentTransformer.php b/app/Ninja/Transformers/DocumentTransformer.php new file mode 100644 index 000000000000..4cbfa0619193 --- /dev/null +++ b/app/Ninja/Transformers/DocumentTransformer.php @@ -0,0 +1,19 @@ +getDefaults($document), [ + 'id' => (int) $document->public_id, + 'name' => $document->name, + 'type' => $document->type, + 'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null, + 'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null, + ]); + } +} \ No newline at end of file diff --git a/app/Ninja/Transformers/EntityTransformer.php b/app/Ninja/Transformers/EntityTransformer.php index ea0dba263d97..d2d02bd0b6ee 100644 --- a/app/Ninja/Transformers/EntityTransformer.php +++ b/app/Ninja/Transformers/EntityTransformer.php @@ -1,5 +1,6 @@ getTimestamp() : null; } + + public function getDefaultIncludes() + { + return $this->defaultIncludes; + } + + protected function getDefaults($entity) + { + $data = [ + 'account_key' => $this->account->account_key, + 'is_owner' => (bool) Auth::user()->owns($entity), + ]; + + if ($entity->relationLoaded('user')) { + $data['user_id'] = (int) $entity->user->public_id + 1; + } + + return $data; + } } diff --git a/app/Ninja/Transformers/ExpenseTransformer.php b/app/Ninja/Transformers/ExpenseTransformer.php index a6d358d15766..46c334cb3e55 100644 --- a/app/Ninja/Transformers/ExpenseTransformer.php +++ b/app/Ninja/Transformers/ExpenseTransformer.php @@ -6,10 +6,16 @@ use League\Fractal; class ExpenseTransformer extends EntityTransformer { + public function __construct($account = null, $serializer = null, $client = null) + { + parent::__construct($account, $serializer); + + $this->client = $client; + } + public function transform(Expense $expense) { - - return [ + return array_merge($this->getDefaults($expense), [ 'id' => (int) $expense->public_id, 'private_notes' => $expense->private_notes, 'public_notes' => $expense->public_notes, @@ -19,15 +25,14 @@ class ExpenseTransformer extends EntityTransformer 'transaction_id' => $expense->transaction_id, 'bank_id' => $expense->bank_id, 'expense_currency_id' => (int) $expense->expense_currency_id, - 'account_key' => $this->account->account_key, 'amount' => (float) $expense->amount, 'expense_date' => $expense->expense_date, 'exchange_rate' => (float) $expense->exchange_rate, 'invoice_currency_id' => (int) $expense->invoice_currency_id, 'is_deleted' => (bool) $expense->is_deleted, - 'client_id' => isset($expense->client->public_id) ? (int) $expense->client->public_id : null, + 'client_id' => $this->client ? $this->client->public_id : (isset($expense->client->public_id) ? (int) $expense->client->public_id : null), 'invoice_id' => isset($expense->invoice->public_id) ? (int) $expense->invoice->public_id : null, 'vendor_id' => isset($expense->vendor->public_id) ? (int) $expense->vendor->public_id : null, - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/InvoiceItemTransformer.php b/app/Ninja/Transformers/InvoiceItemTransformer.php index 66d9fe137dd8..080234f35e1b 100644 --- a/app/Ninja/Transformers/InvoiceItemTransformer.php +++ b/app/Ninja/Transformers/InvoiceItemTransformer.php @@ -8,19 +8,19 @@ class InvoiceItemTransformer extends EntityTransformer { public function transform(InvoiceItem $item) { - return [ + return array_merge($this->getDefaults($item), [ 'id' => (int) $item->public_id, 'product_key' => $item->product_key, - 'account_key' => $this->account->account_key, - 'user_id' => (int) $item->user_id, 'updated_at' => $this->getTimestamp($item->updated_at), 'archived_at' => $this->getTimestamp($item->deleted_at), 'product_key' => $item->product_key, 'notes' => $item->notes, 'cost' => (float) $item->cost, 'qty' => (float) $item->qty, - 'tax_name' => $item->tax_name, - 'tax_rate' => (float) $item->tax_rate - ]; + 'tax_name1' => $item->tax_name1 ? $item->tax_name1 : '', + 'tax_rate1' => (float) $item->tax_rate1, + 'tax_name2' => $item->tax_name2 ? $item->tax_name1 : '', + 'tax_rate2' => (float) $item->tax_rate2, + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php index b67635386d8e..c866f1213540 100644 --- a/app/Ninja/Transformers/InvoiceTransformer.php +++ b/app/Ninja/Transformers/InvoiceTransformer.php @@ -28,9 +28,16 @@ class InvoiceTransformer extends EntityTransformer 'invitations', 'payments', 'client', - 'expenses', + //'expenses', ]; + public function __construct($account = null, $serializer = null, $client = null) + { + parent::__construct($account, $serializer); + + $this->client = $client; + } + public function includeInvoiceItems(Invoice $invoice) { $transformer = new InvoiceItemTransformer($this->account, $this->serializer); @@ -45,7 +52,7 @@ class InvoiceTransformer extends EntityTransformer public function includePayments(Invoice $invoice) { - $transformer = new PaymentTransformer($this->account, $this->serializer); + $transformer = new PaymentTransformer($this->account, $this->serializer, $invoice); return $this->includeCollection($invoice->payments, $transformer, ENTITY_PAYMENT); } @@ -64,11 +71,11 @@ class InvoiceTransformer extends EntityTransformer public function transform(Invoice $invoice) { - return [ + return array_merge($this->getDefaults($invoice), [ 'id' => (int) $invoice->public_id, 'amount' => (float) $invoice->amount, 'balance' => (float) $invoice->balance, - 'client_id' => (int) $invoice->client->public_id, + 'client_id' => (int) ($this->client ? $this->client->public_id : $invoice->client->public_id), 'invoice_status_id' => (int) $invoice->invoice_status_id, 'updated_at' => $this->getTimestamp($invoice->updated_at), 'archived_at' => $this->getTimestamp($invoice->deleted_at), @@ -87,8 +94,10 @@ class InvoiceTransformer extends EntityTransformer 'end_date' => $invoice->end_date, 'last_sent_date' => $invoice->last_sent_date, 'recurring_invoice_id' => (int) $invoice->recurring_invoice_id, - 'tax_name' => $invoice->tax_name, - 'tax_rate' => (float) $invoice->tax_rate, + 'tax_name1' => $invoice->tax_name1 ? $invoice->tax_name1 : '', + 'tax_rate1' => (float) $invoice->tax_rate1, + 'tax_name2' => $invoice->tax_name2 ? $invoice->tax_name2 : '', + 'tax_rate2' => (float) $invoice->tax_rate2, 'amount' => (float) $invoice->amount, 'balance' => (float) $invoice->balance, 'is_amount_discount' => (bool) $invoice->is_amount_discount, @@ -96,8 +105,6 @@ class InvoiceTransformer extends EntityTransformer 'partial' => (float) $invoice->partial, 'has_tasks' => (bool) $invoice->has_tasks, 'auto_bill' => (bool) $invoice->auto_bill, - 'account_key' => $this->account->account_key, - 'user_id' => (int) $invoice->user->public_id + 1, 'custom_value1' => (float) $invoice->custom_value1, 'custom_value2' => (float) $invoice->custom_value2, 'custom_taxes1' => (bool) $invoice->custom_taxes1, @@ -106,6 +113,6 @@ class InvoiceTransformer extends EntityTransformer 'quote_invoice_id' => (int) $invoice->quote_invoice_id, 'custom_text_value1' => $invoice->custom_text_value1, 'custom_text_value2' => $invoice->custom_text_value2, - ]; + ]); } } diff --git a/app/Ninja/Transformers/PaymentTransformer.php b/app/Ninja/Transformers/PaymentTransformer.php index a1de09cd6ff4..c4e4328cb845 100644 --- a/app/Ninja/Transformers/PaymentTransformer.php +++ b/app/Ninja/Transformers/PaymentTransformer.php @@ -26,10 +26,11 @@ class PaymentTransformer extends EntityTransformer ]; - public function __construct(Account $account) + public function __construct($account = null, $serializer = null, $invoice = null) { - parent::__construct($account); - + parent::__construct($account, $serializer); + + $this->invoice = $invoice; } public function includeInvoice(Payment $payment) @@ -46,18 +47,16 @@ class PaymentTransformer extends EntityTransformer public function transform(Payment $payment) { - return [ + return array_merge($this->getDefaults($payment), [ 'id' => (int) $payment->public_id, 'amount' => (float) $payment->amount, - 'account_key' => $this->account->account_key, - 'user_id' => (int) $payment->user->public_id + 1, 'transaction_reference' => $payment->transaction_reference, 'payment_date' => $payment->payment_date, 'updated_at' => $this->getTimestamp($payment->updated_at), 'archived_at' => $this->getTimestamp($payment->deleted_at), 'is_deleted' => (bool) $payment->is_deleted, 'payment_type_id' => (int) $payment->payment_type_id, - 'invoice_id' => (int) $payment->invoice->public_id, - ]; + 'invoice_id' => (int) ($this->invoice ? $this->invoice->public_id : $payment->invoice->public_id), + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/ProductTransformer.php b/app/Ninja/Transformers/ProductTransformer.php index 34fbcf7f18ff..309305815fb0 100644 --- a/app/Ninja/Transformers/ProductTransformer.php +++ b/app/Ninja/Transformers/ProductTransformer.php @@ -7,16 +7,15 @@ class ProductTransformer extends EntityTransformer { public function transform(Product $product) { - return [ + return array_merge($this->getDefaults($product), [ 'id' => (int) $product->public_id, 'product_key' => $product->product_key, 'notes' => $product->notes, 'cost' => $product->cost, 'qty' => $product->qty, - 'account_key' =>$this->account->account_key, 'default_tax_rate_id' =>$product->default_tax_rate_id, 'updated_at' =>$this->getTimestamp($product->updated_at), 'archived_at' => $this->getTimestamp($product->deleted_at), - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/TaskTransformer.php b/app/Ninja/Transformers/TaskTransformer.php index 908a8118aaea..7bfc474a72f2 100644 --- a/app/Ninja/Transformers/TaskTransformer.php +++ b/app/Ninja/Transformers/TaskTransformer.php @@ -39,12 +39,10 @@ class TaskTransformer extends EntityTransformer public function transform(Task $task) { - return [ + return array_merge($this->getDefaults($task), [ 'id' => (int) $task->public_id, - 'account_key' => $this->account->account_key, - 'user_id' => (int) $task->user->public_id + 1, 'description' => $task->description, 'duration' => $task->getDuration() - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/TaxRateTransformer.php b/app/Ninja/Transformers/TaxRateTransformer.php index f7d307bf7bb4..a0c5aab539a0 100644 --- a/app/Ninja/Transformers/TaxRateTransformer.php +++ b/app/Ninja/Transformers/TaxRateTransformer.php @@ -21,13 +21,12 @@ class TaxRateTransformer extends EntityTransformer public function transform(TaxRate $taxRate) { - return [ + return array_merge($this->getDefaults($taxRate), [ 'id' => (int) $taxRate->public_id, 'name' => $taxRate->name, 'rate' => (float) $taxRate->rate, 'updated_at' => $this->getTimestamp($taxRate->updated_at), 'archived_at' => $this->getTimestamp($taxRate->deleted_at), - 'account_key' => $this->account->account_key, - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/UserAccountTransformer.php b/app/Ninja/Transformers/UserAccountTransformer.php index bc49a96c546a..e914a25c663d 100644 --- a/app/Ninja/Transformers/UserAccountTransformer.php +++ b/app/Ninja/Transformers/UserAccountTransformer.php @@ -32,7 +32,7 @@ class UserAccountTransformer extends EntityTransformer return [ 'account_key' => $user->account->account_key, 'name' => $user->account->present()->name, - 'token' => $user->account->getToken($this->tokenName), + 'token' => $user->account->getToken($user->id, $this->tokenName), 'default_url' => SITE_URL ]; } diff --git a/app/Ninja/Transformers/VendorContactTransformer.php b/app/Ninja/Transformers/VendorContactTransformer.php index 3b75aee53a28..f277964cec28 100644 --- a/app/Ninja/Transformers/VendorContactTransformer.php +++ b/app/Ninja/Transformers/VendorContactTransformer.php @@ -8,7 +8,7 @@ class VendorContactTransformer extends EntityTransformer { public function transform(VendorContact $contact) { - return [ + return array_merge($this->getDefaults($contact), [ 'id' => (int) $contact->public_id, 'first_name' => $contact->first_name, 'last_name' => $contact->last_name, @@ -17,7 +17,6 @@ class VendorContactTransformer extends EntityTransformer 'archived_at' => $this->getTimestamp($contact->deleted_at), 'is_primary' => (bool) $contact->is_primary, 'phone' => $contact->phone, - 'account_key' => $this->account->account_key, - ]; + ]); } } \ No newline at end of file diff --git a/app/Ninja/Transformers/VendorTransformer.php b/app/Ninja/Transformers/VendorTransformer.php index f0b8fd0415f0..df8dc5ea3461 100644 --- a/app/Ninja/Transformers/VendorTransformer.php +++ b/app/Ninja/Transformers/VendorTransformer.php @@ -35,16 +35,19 @@ class VendorTransformer extends EntityTransformer * @SWG\Property(property="id_number", type="string", example="123456") */ + protected $defaultIncludes = [ + 'vendor_contacts', + ]; + protected $availableIncludes = [ - 'vendorContacts', 'invoices', - 'expenses', + //'expenses', ]; public function includeVendorContacts(Vendor $vendor) { $transformer = new VendorContactTransformer($this->account, $this->serializer); - return $this->includeCollection($vendor->vendorContacts, $transformer, ENTITY_CONTACT); + return $this->includeCollection($vendor->vendor_contacts, $transformer, ENTITY_CONTACT); } public function includeInvoices(Vendor $vendor) @@ -61,13 +64,11 @@ class VendorTransformer extends EntityTransformer public function transform(Vendor $vendor) { - return [ + return array_merge($this->getDefaults($vendor), [ 'id' => (int) $vendor->public_id, 'name' => $vendor->name, 'balance' => (float) $vendor->balance, 'paid_to_date' => (float) $vendor->paid_to_date, - 'user_id' => (int) $vendor->user->public_id + 1, - 'account_key' => $this->account->account_key, 'updated_at' => $this->getTimestamp($vendor->updated_at), 'archived_at' => $this->getTimestamp($vendor->deleted_at), 'address1' => $vendor->address1, @@ -84,6 +85,6 @@ class VendorTransformer extends EntityTransformer 'vat_number' => $vendor->vat_number, 'id_number' => $vendor->id_number, 'currency_id' => (int) $vendor->currency_id - ]; + ]); } } \ No newline at end of file diff --git a/app/Policies/AccountGatewayPolicy.php b/app/Policies/AccountGatewayPolicy.php new file mode 100644 index 000000000000..1e98caf04e92 --- /dev/null +++ b/app/Policies/AccountGatewayPolicy.php @@ -0,0 +1,13 @@ +hasPermission('admin'); + } + + public static function create($user) { + return $user->hasPermission('admin'); + } +} \ No newline at end of file diff --git a/app/Policies/BankAccountPolicy.php b/app/Policies/BankAccountPolicy.php new file mode 100644 index 000000000000..fa5eeff84856 --- /dev/null +++ b/app/Policies/BankAccountPolicy.php @@ -0,0 +1,13 @@ +hasPermission('admin'); + } + + public static function create($user) { + return $user->hasPermission('admin'); + } +} \ No newline at end of file diff --git a/app/Policies/ClientPolicy.php b/app/Policies/ClientPolicy.php new file mode 100644 index 000000000000..4610c139fbfa --- /dev/null +++ b/app/Policies/ClientPolicy.php @@ -0,0 +1,5 @@ +hasPermission('view_all'))return true; + if($document->expense){ + if($document->expense->invoice)return $user->can('view', $document->expense->invoice); + return $user->can('view', $document->expense); + } + if($document->invoice)return $user->can('view', $document->invoice); + + return $user->owns($item); + } +} \ No newline at end of file diff --git a/app/Policies/EntityPolicy.php b/app/Policies/EntityPolicy.php new file mode 100644 index 000000000000..4c5e8ded9d07 --- /dev/null +++ b/app/Policies/EntityPolicy.php @@ -0,0 +1,33 @@ +hasPermission('create_all'); + } + + public static function edit($user, $item) { + return $user->hasPermission('edit_all') || $user->owns($item); + } + + public static function view($user, $item) { + return $user->hasPermission('view_all') || $user->owns($item); + } + + public static function viewByOwner($user, $ownerUserId) { + return $user->hasPermission('view_all') || $user->id == $ownerUserId; + } + + public static function editByOwner($user, $ownerUserId) { + return $user->hasPermission('edit_all') || $user->id == $ownerUserId; + } +} \ No newline at end of file diff --git a/app/Policies/ExpensePolicy.php b/app/Policies/ExpensePolicy.php new file mode 100644 index 000000000000..4fdac4d627aa --- /dev/null +++ b/app/Policies/ExpensePolicy.php @@ -0,0 +1,5 @@ +hasPermission('admin'); + } + + public static function create($user) { + return $user->hasPermission('admin'); + } +} \ No newline at end of file diff --git a/app/Policies/ProductPolicy.php b/app/Policies/ProductPolicy.php new file mode 100644 index 000000000000..897fe7404a03 --- /dev/null +++ b/app/Policies/ProductPolicy.php @@ -0,0 +1,13 @@ +hasPermission('admin'); + } + + public static function create($user) { + return $user->hasPermission('admin'); + } +} \ No newline at end of file diff --git a/app/Policies/TaskPolicy.php b/app/Policies/TaskPolicy.php new file mode 100644 index 000000000000..b1fbe2902974 --- /dev/null +++ b/app/Policies/TaskPolicy.php @@ -0,0 +1,5 @@ +hasPermission('admin'); + } + + public static function create($user) { + return $user->hasPermission('admin'); + } +} \ No newline at end of file diff --git a/app/Policies/TokenPolicy.php b/app/Policies/TokenPolicy.php new file mode 100644 index 000000000000..36e37e313a69 --- /dev/null +++ b/app/Policies/TokenPolicy.php @@ -0,0 +1,13 @@ +hasPermission('admin'); + } + + public static function create($user) { + return $user->hasPermission('admin'); + } +} \ No newline at end of file diff --git a/app/Policies/VendorPolicy.php b/app/Policies/VendorPolicy.php new file mode 100644 index 000000000000..681cdcb50240 --- /dev/null +++ b/app/Policies/VendorPolicy.php @@ -0,0 +1,5 @@ + '.trans("texts.$types").''; $items = []; - if(Auth::user()->hasPermission('create_all')){ - $items[] = '

  • '.trans("texts.new_$type").'
  • '; - } + if($user->can('create', $type))$items[] = '
  • '.trans("texts.new_$type").'
  • '; if ($type == ENTITY_INVOICE) { if(!empty($items))$items[] = '
  • '; $items[] = '
  • '.trans("texts.recurring_invoices").'
  • '; - if(Invoice::canCreate())$items[] = '
  • '.trans("texts.new_recurring_invoice").'
  • '; - if (Auth::user()->isPro()) { + if($user->can('create', ENTITY_INVOICE))$items[] = '
  • '.trans("texts.new_recurring_invoice").'
  • '; + if ($user->hasFeature(FEATURE_QUOTES)) { $items[] = '
  • '; $items[] = '
  • '.trans("texts.quotes").'
  • '; - if(Invoice::canCreate())$items[] = '
  • '.trans("texts.new_quote").'
  • '; + if($user->can('create', ENTITY_INVOICE))$items[] = '
  • '.trans("texts.new_quote").'
  • '; } } else if ($type == ENTITY_CLIENT) { if(!empty($items))$items[] = '
  • '; $items[] = '
  • '.trans("texts.credits").'
  • '; - if(Credit::canCreate())$items[] = '
  • '.trans("texts.new_credit").'
  • '; + if($user->can('create', ENTITY_CREDIT))$items[] = '
  • '.trans("texts.new_credit").'
  • '; } else if ($type == ENTITY_EXPENSE) { if(!empty($items))$items[] = '
  • '; $items[] = '
  • '.trans("texts.vendors").'
  • '; - if(Vendor::canCreate())$items[] = '
  • '.trans("texts.new_vendor").'
  • '; + if($user->can('create', ENTITY_VENDOR))$items[] = '
  • '.trans("texts.new_vendor").'
  • '; } if(!empty($items)){ @@ -152,6 +158,13 @@ class AppServiceProvider extends ServiceProvider { return $str . ''; }); + Form::macro('human_filesize', function($bytes, $decimals = 1) { + $size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB'); + $factor = floor((strlen($bytes) - 1) / 3); + if($factor == 0)$decimals=0;// There aren't fractional bytes + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor]; + }); + Validator::extend('positive', function($attribute, $value, $parameters) { return Utils::parseFloat($value) >= 0; }); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 000000000000..227a1d6df65c --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,46 @@ + \App\Policies\ClientPolicy::class, + \App\Models\Credit::class => \App\Policies\CreditPolicy::class, + \App\Models\Document::class => \App\Policies\DocumentPolicy::class, + \App\Models\Expense::class => \App\Policies\ExpensePolicy::class, + \App\Models\Invoice::class => \App\Policies\InvoicePolicy::class, + \App\Models\Payment::class => \App\Policies\PaymentPolicy::class, + \App\Models\Task::class => \App\Policies\TaskPolicy::class, + \App\Models\Vendor::class => \App\Policies\VendorPolicy::class, + \App\Models\Product::class => \App\Policies\ProductPolicy::class, + \App\Models\TaxRate::class => \App\Policies\TaxRatePolicy::class, + \App\Models\AccountGateway::class => \App\Policies\AccountGatewayPolicy::class, + \App\Models\Token::class => \App\Policies\TokenPolicy::class, + \App\Models\BankAccount::class => \App\Policies\BankAccountPolicy::class, + \App\Models\PaymentTerm::class => \App\Policies\PaymentTermPolicy::class, + ]; + + /** + * Register any application authentication / authorization services. + * + * @param \Illuminate\Contracts\Auth\Access\Gate $gate + * @return void + */ + public function boot(GateContract $gate) + { + foreach (get_class_methods(new \App\Policies\GenericEntityPolicy) as $method) { + $gate->define($method, "App\Policies\GenericEntityPolicy@{$method}"); + } + + $this->registerPolicies($gate); + } +} \ No newline at end of file diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index bb4e94d79546..45c4a789ca22 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -98,6 +98,7 @@ class EventServiceProvider extends ServiceProvider { 'App\Listeners\SubscriptionListener@createdPayment', 'App\Listeners\InvoiceListener@createdPayment', 'App\Listeners\NotificationListener@createdPayment', + 'App\Listeners\AnalyticsListener@trackRevenue', ], 'App\Events\PaymentWasArchived' => [ 'App\Listeners\ActivityListener@archivedPayment', diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index b68b06482a04..6bbbc729b246 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -1,5 +1,6 @@ getRepo()->findByPublicIdsWithTrashed($ids); foreach ($entities as $entity) { - if($entity->canEdit()){ + if(Auth::user()->can('edit', $entity)){ $this->getRepo()->$action($entity); } } diff --git a/app/Services/ClientService.php b/app/Services/ClientService.php index 662fc8eab9e8..96357991c519 100644 --- a/app/Services/ClientService.php +++ b/app/Services/ClientService.php @@ -30,13 +30,13 @@ class ClientService extends BaseService return $this->clientRepo; } - public function save($data) + public function save($data, $client = null) { - if (Auth::user()->account->isNinjaAccount() && isset($data['pro_plan_paid'])) { - $this->ninjaRepo->updateProPlanPaid($data['public_id'], $data['pro_plan_paid']); + if (Auth::user()->account->isNinjaAccount() && isset($data['plan'])) { + $this->ninjaRepo->updatePlanDetails($data['public_id'], $data); } - return $this->clientRepo->save($data); + return $this->clientRepo->save($data, $client); } public function getDatatable($search) @@ -101,13 +101,14 @@ class ClientService extends BaseService return URL::to("clients/{$model->public_id}/edit"); }, function ($model) { - return Client::canEditItem($model); + return Auth::user()->can('editByOwner', [ENTITY_CLIENT, $model->user_id]); } ], [ '--divider--', function(){return false;}, function ($model) { - return Client::canEditItem($model) && (Task::canCreate() || Invoice::canCreate()); + $user = Auth::user(); + return $user->can('editByOwner', [ENTITY_CLIENT, $model->user_id]) && ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)); } ], [ @@ -116,7 +117,7 @@ class ClientService extends BaseService return URL::to("tasks/create/{$model->public_id}"); }, function ($model) { - return Task::canCreate(); + return Auth::user()->can('create', ENTITY_TASK); } ], [ @@ -125,7 +126,7 @@ class ClientService extends BaseService return URL::to("invoices/create/{$model->public_id}"); }, function ($model) { - return Invoice::canCreate(); + return Auth::user()->can('create', ENTITY_INVOICE); } ], [ @@ -134,13 +135,14 @@ class ClientService extends BaseService return URL::to("quotes/create/{$model->public_id}"); }, function ($model) { - return Auth::user()->isPro() && Invoice::canCreate(); + return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_INVOICE); } ], [ '--divider--', function(){return false;}, function ($model) { - return (Task::canCreate() || Invoice::canCreate()) && (Payment::canCreate() || Credit::canCreate() || Expense::canCreate()); + $user = Auth::user(); + return ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)) && ($user->can('create', ENTITY_PAYMENT) || $user->can('create', ENTITY_CREDIT) || $user->can('create', ENTITY_EXPENSE)); } ], [ @@ -149,7 +151,7 @@ class ClientService extends BaseService return URL::to("payments/create/{$model->public_id}"); }, function ($model) { - return Payment::canCreate(); + return Auth::user()->can('create', ENTITY_PAYMENT); } ], [ @@ -158,7 +160,7 @@ class ClientService extends BaseService return URL::to("credits/create/{$model->public_id}"); }, function ($model) { - return Credit::canCreate(); + return Auth::user()->can('create', ENTITY_CREDIT); } ], [ @@ -167,7 +169,7 @@ class ClientService extends BaseService return URL::to("expenses/create/0/{$model->public_id}"); }, function ($model) { - return Expense::canCreate(); + return Auth::user()->can('create', ENTITY_EXPENSE); } ] ]; diff --git a/app/Services/CreditService.php b/app/Services/CreditService.php index 2e9220ad0544..54ef659f05f9 100644 --- a/app/Services/CreditService.php +++ b/app/Services/CreditService.php @@ -47,7 +47,7 @@ class CreditService extends BaseService [ 'client_name', function ($model) { - if(!Client::canViewItemByOwner($model->client_user_id)){ + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ return Utils::getClientDisplayName($model); } @@ -91,7 +91,7 @@ class CreditService extends BaseService return URL::to("payments/create/{$model->client_public_id}") . '?paymentTypeId=1'; }, function ($model) { - return Payment::canCreate(); + return Auth::user()->can('create', ENTITY_PAYMENT); } ] ]; diff --git a/app/Services/ExpenseService.php b/app/Services/ExpenseService.php index 2fc2afbc84b1..671648ea32a5 100644 --- a/app/Services/ExpenseService.php +++ b/app/Services/ExpenseService.php @@ -28,7 +28,7 @@ class ExpenseService extends BaseService return $this->expenseRepo; } - public function save($data) + public function save($data, $expense = null) { if (isset($data['client_id']) && $data['client_id']) { $data['client_id'] = Client::getPrivateId($data['client_id']); @@ -38,7 +38,7 @@ class ExpenseService extends BaseService $data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']); } - return $this->expenseRepo->save($data); + return $this->expenseRepo->save($data, $expense); } public function getDatatable($search) @@ -70,7 +70,7 @@ class ExpenseService extends BaseService function ($model) { if ($model->vendor_public_id) { - if(!Vendor::canViewItemByOwner($model->vendor_user_id)){ + if(!Auth::user()->can('viewByOwner', [ENTITY_VENDOR, $model->vendor_user_id])){ return $model->vendor_name; } @@ -85,7 +85,7 @@ class ExpenseService extends BaseService function ($model) { if ($model->client_public_id) { - if(!Client::canViewItemByOwner($model->client_user_id)){ + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ return Utils::getClientDisplayName($model); } @@ -98,7 +98,7 @@ class ExpenseService extends BaseService [ 'expense_date', function ($model) { - if(!Expense::canEditItemByOwner($model->user_id)){ + if(!Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id])){ return Utils::fromSqlDate($model->expense_date); } @@ -172,7 +172,7 @@ class ExpenseService extends BaseService return URL::to("expenses/{$model->public_id}/edit") ; }, function ($model) { - return Expense::canEditItem($model); + return Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id]); } ], [ @@ -181,7 +181,7 @@ class ExpenseService extends BaseService return URL::to("/invoices/{$model->invoice_public_id}/edit"); }, function ($model) { - return $model->invoice_public_id && Invoice::canEditItemByOwner($model->invoice_user_id); + return $model->invoice_public_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]); } ], [ @@ -190,7 +190,7 @@ class ExpenseService extends BaseService return "javascript:invoiceEntity({$model->public_id})"; }, function ($model) { - return ! $model->invoice_id && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Invoice::canCreate(); + return ! $model->invoice_id && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE); } ], ]; diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index c7c0be72acc0..edbee8caf84c 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -30,26 +30,23 @@ class InvoiceService extends BaseService return $this->invoiceRepo; } - public function save($data, $checkSubPermissions = false) + public function save($data, $invoice = null) { if (isset($data['client'])) { - $can_save_client = !$checkSubPermissions; - if(!$can_save_client){ - if(empty($data['client']['public_id']) || $data['client']['public_id']=='-1'){ - $can_save_client = Client::canCreate(); - } - else{ - $can_save_client = Client::wherePublicId($data['client']['public_id'])->first()->canEdit(); - } - } - - if($can_save_client){ + $canSaveClient = false; + $clientPublicId = array_get($data, 'client.public_id') ?: array_get($data, 'client.id'); + if (empty($clientPublicId) || $clientPublicId == '-1') { + $canSaveClient = Auth::user()->can('create', ENTITY_CLIENT); + } else { + $canSaveClient = Auth::user()->can('edit', Client::scope($clientPublicId)->first()); + } + if ($canSaveClient) { $client = $this->clientRepo->save($data['client']); $data['client_id'] = $client->id; } } - $invoice = $this->invoiceRepo->save($data, $checkSubPermissions); + $invoice = $this->invoiceRepo->save($data, $invoice); $client = $invoice->client; $client->load('contacts'); @@ -100,7 +97,7 @@ class InvoiceService extends BaseService return null; } - if ($account->auto_convert_quote || ! $account->isPro()) { + if ($account->auto_convert_quote || ! $account->hasFeature(FEATURE_QUOTES)) { $invoice = $this->convertQuote($quote, $invitation); event(new QuoteInvitationWasApproved($quote, $invoice, $invitation)); @@ -137,7 +134,7 @@ class InvoiceService extends BaseService [ 'invoice_number', function ($model) use ($entityType) { - if(!Invoice::canEditItem($model)){ + if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id])){ return $model->invoice_number; } @@ -147,7 +144,7 @@ class InvoiceService extends BaseService [ 'client_name', function ($model) { - if(!Client::canViewItemByOwner($model->client_user_id)){ + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ return Utils::getClientDisplayName($model); } return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); @@ -202,7 +199,7 @@ class InvoiceService extends BaseService return URL::to("{$entityType}s/{$model->public_id}/edit"); }, function ($model) { - return Invoice::canEditItem($model); + return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } ], [ @@ -211,7 +208,7 @@ class InvoiceService extends BaseService return URL::to("{$entityType}s/{$model->public_id}/clone"); }, function ($model) { - return Invoice::canCreate(); + return Auth::user()->can('create', ENTITY_INVOICE); } ], [ @@ -223,7 +220,7 @@ class InvoiceService extends BaseService [ '--divider--', function(){return false;}, function ($model) { - return Invoice::canEditItem($model) || Payment::canCreate(); + return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]) || Auth::user()->can('create', ENTITY_PAYMENT); } ], [ @@ -232,7 +229,7 @@ class InvoiceService extends BaseService return "javascript:markEntity({$model->public_id})"; }, function ($model) { - return $model->invoice_status_id < INVOICE_STATUS_SENT && Invoice::canEditItem($model); + return $model->invoice_status_id < INVOICE_STATUS_SENT && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } ], [ @@ -241,7 +238,7 @@ class InvoiceService extends BaseService return URL::to("payments/create/{$model->client_public_id}/{$model->public_id}"); }, function ($model) use ($entityType) { - return $entityType == ENTITY_INVOICE && $model->balance > 0 && Payment::canCreate(); + return $entityType == ENTITY_INVOICE && $model->balance > 0 && Auth::user()->can('create', ENTITY_PAYMENT); } ], [ @@ -250,7 +247,7 @@ class InvoiceService extends BaseService return URL::to("quotes/{$model->quote_id}/edit"); }, function ($model) use ($entityType) { - return $entityType == ENTITY_INVOICE && $model->quote_id && Invoice::canEditItem($model); + return $entityType == ENTITY_INVOICE && $model->quote_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } ], [ @@ -259,7 +256,7 @@ class InvoiceService extends BaseService return URL::to("invoices/{$model->quote_invoice_id}/edit"); }, function ($model) use ($entityType) { - return $entityType == ENTITY_QUOTE && $model->quote_invoice_id && Invoice::canEditItem($model); + return $entityType == ENTITY_QUOTE && $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } ], [ @@ -268,7 +265,7 @@ class InvoiceService extends BaseService return "javascript:convertEntity({$model->public_id})"; }, function ($model) use ($entityType) { - return $entityType == ENTITY_QUOTE && ! $model->quote_invoice_id && Invoice::canEditItem($model); + return $entityType == ENTITY_QUOTE && ! $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } ] ]; diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 3bf26d8ba206..23b5f4e8f167 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -39,18 +39,7 @@ class PaymentService extends BaseService public function createGateway($accountGateway) { $gateway = Omnipay::create($accountGateway->gateway->provider); - $config = $accountGateway->getConfig(); - - foreach ($config as $key => $val) { - if (!$val) { - continue; - } - - $function = "set".ucfirst($key); - if (method_exists($gateway, $function)) { - $gateway->$function($val); - } - } + $gateway->initialize((array) $accountGateway->getConfig()); if ($accountGateway->isGateway(GATEWAY_DWOLLA)) { if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { @@ -105,12 +94,17 @@ class PaymentService extends BaseService $data = [ 'firstName' => $input['first_name'], 'lastName' => $input['last_name'], + 'email' => $input['email'], 'number' => isset($input['card_number']) ? $input['card_number'] : null, 'expiryMonth' => isset($input['expiration_month']) ? $input['expiration_month'] : null, 'expiryYear' => isset($input['expiration_year']) ? $input['expiration_year'] : null, - 'cvv' => isset($input['cvv']) ? $input['cvv'] : '', ]; - + + // allow space until there's a setting to disable + if (isset($input['cvv']) && $input['cvv'] != ' ') { + $data['cvv'] = $input['cvv']; + } + if (isset($input['country_id'])) { $country = Country::find($input['country_id']); @@ -215,17 +209,6 @@ class PaymentService extends BaseService { $invoice = $invitation->invoice; - // enable pro plan for hosted users - if ($invoice->account->account_key == NINJA_ACCOUNT_KEY && $invoice->amount == PRO_PLAN_PRICE) { - $account = Account::with('users')->find($invoice->client->public_id); - $account->pro_plan_paid = $account->getRenewalDate(); - $account->save(); - - // sync pro accounts - $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; @@ -235,13 +218,66 @@ class PaymentService extends BaseService $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(); + // enable pro plan for hosted users + if ($invoice->account->account_key == NINJA_ACCOUNT_KEY) { + foreach ($invoice->invoice_items as $invoice_item) { + // Hacky, but invoices don't have meta fields to allow us to store this easily + if (1 == preg_match('/^Plan - (.+) \((.+)\)$/', $invoice_item->product_key, $matches)) { + $plan = strtolower($matches[1]); + $term = strtolower($matches[2]); + } elseif ($invoice_item->product_key == 'Pending Monthly') { + $pending_monthly = true; + } + } + + if (!empty($plan)) { + $account = Account::with('users')->find($invoice->client->public_id); + + if( + $account->company->plan != $plan + || DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create('-7 days') + ) { + // Either this is a different plan, or the subscription expired more than a week ago + // Reset any grandfathering + $account->company->plan_started = date_create()->format('Y-m-d'); + } + + if ( + $account->company->plan == $plan + && $account->company->plan_term == $term + && DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create() + ) { + // This is a renewal; mark it paid as of when this term expires + $account->company->plan_paid = $account->company->plan_expires; + } else { + $account->company->plan_paid = date_create()->format('Y-m-d'); + } + + $account->company->payment_id = $payment->id; + $account->company->plan = $plan; + $account->company->plan_term = $term; + $account->company->plan_expires = DateTime::createFromFormat('Y-m-d', $account->company->plan_paid) + ->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d'); + + if (!empty($pending_monthly)) { + $account->company->pending_plan = $plan; + $account->company->pending_term = PLAN_TERM_MONTHLY; + } else { + $account->company->pending_plan = null; + $account->company->pending_term = null; + } + + $account->company->save(); + } + } + return $payment; } @@ -302,7 +338,7 @@ class PaymentService extends BaseService [ 'invoice_number', function ($model) { - if(!Invoice::canEditItemByOwner($model->invoice_user_id)){ + if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){ return $model->invoice_number; } @@ -312,7 +348,7 @@ class PaymentService extends BaseService [ 'client_name', function ($model) { - if(!Client::canViewItemByOwner($model->client_user_id)){ + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ return Utils::getClientDisplayName($model); } @@ -356,7 +392,7 @@ class PaymentService extends BaseService return URL::to("payments/{$model->public_id}/edit"); }, function ($model) { - return Payment::canEditItem($model); + return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]); } ] ]; diff --git a/app/Services/RecurringInvoiceService.php b/app/Services/RecurringInvoiceService.php index 2786ccfc6869..b003455abd6a 100644 --- a/app/Services/RecurringInvoiceService.php +++ b/app/Services/RecurringInvoiceService.php @@ -74,7 +74,7 @@ class RecurringInvoiceService extends BaseService return URL::to("invoices/{$model->public_id}/edit"); }, function ($model) { - return Invoice::canEditItem($model); + return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } ] ]; diff --git a/app/Services/TaskService.php b/app/Services/TaskService.php index 70e7e22c7c88..e07793b2f85c 100644 --- a/app/Services/TaskService.php +++ b/app/Services/TaskService.php @@ -49,7 +49,7 @@ class TaskService extends BaseService [ 'client_name', function ($model) { - if(!Client::canViewItemByOwner($model->client_user_id)){ + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ return Utils::getClientDisplayName($model); } @@ -93,7 +93,7 @@ class TaskService extends BaseService return URL::to('tasks/'.$model->public_id.'/edit'); }, function ($model) { - return (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Task::canEditItem($model); + return (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]); } ], [ @@ -102,7 +102,7 @@ class TaskService extends BaseService return URL::to("/invoices/{$model->invoice_public_id}/edit"); }, function ($model) { - return $model->invoice_number && Invoice::canEditItemByOwner($model->invoice_user_id); + return $model->invoice_number && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]); } ], [ @@ -111,7 +111,7 @@ class TaskService extends BaseService return "javascript:stopTask({$model->public_id})"; }, function ($model) { - return $model->is_running && Task::canEditItem($model); + return $model->is_running && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]); } ], [ @@ -120,7 +120,7 @@ class TaskService extends BaseService return "javascript:invoiceEntity({$model->public_id})"; }, function ($model) { - return ! $model->invoice_number && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Invoice::canCreate(); + return ! $model->invoice_number && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE); } ] ]; diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php new file mode 100644 index 000000000000..5a41c705352d --- /dev/null +++ b/app/Services/TemplateService.php @@ -0,0 +1,80 @@ +invoice; + $passwordHTML = isset($data['password'])?'

    '.trans('texts.password').': '.$data['password'].'

    ':false; + $documentsHTML = ''; + + if ($account->hasFeature(FEATURE_DOCUMENTS) && $invoice->hasDocuments()) { + $documentsHTML .= trans('texts.email_documents_header').'

    '; + } + + $variables = [ + '$footer' => $account->getEmailFooter(), + '$client' => $client->getDisplayName(), + '$account' => $account->getDisplayName(), + '$dueDate' => $account->formatDate($invoice->due_date), + '$invoiceDate' => $account->formatDate($invoice->invoice_date), + '$contact' => $invitation->contact->getDisplayName(), + '$firstName' => $invitation->contact->first_name, + '$amount' => $account->formatMoney($data['amount'], $client), + '$invoice' => $invoice->invoice_number, + '$quote' => $invoice->invoice_number, + '$link' => $invitation->getLink(), + '$password' => $passwordHTML, + '$viewLink' => $invitation->getLink().'$password', + '$viewButton' => Form::emailViewButton($invitation->getLink(), $invoice->getEntityType()).'$password', + '$paymentLink' => $invitation->getLink('payment').'$password', + '$paymentButton' => Form::emailPaymentButton($invitation->getLink('payment')).'$password', + '$customClient1' => $account->custom_client_label1, + '$customClient2' => $account->custom_client_label2, + '$customInvoice1' => $account->custom_invoice_text_label1, + '$customInvoice2' => $account->custom_invoice_text_label2, + '$documents' => $documentsHTML, + ]; + + // Add variables for available payment types + foreach (Gateway::$paymentTypes as $type) { + $camelType = Gateway::getPaymentTypeName($type); + $type = Utils::toSnakeCase($camelType); + $variables["\${$camelType}Link"] = $invitation->getLink('payment') . "/{$type}"; + $variables["\${$camelType}Button"] = Form::emailPaymentButton($invitation->getLink('payment') . "/{$type}"); + } + + $includesPasswordPlaceholder = strpos($template, '$password') !== false; + + $str = str_replace(array_keys($variables), array_values($variables), $template); + + if (!$includesPasswordPlaceholder && $passwordHTML) { + $pos = strrpos($str, '$password'); + if ($pos !== false) + { + $str = substr_replace($str, $passwordHTML, $pos, 9/* length of "$password" */); + } + } + $str = str_replace('$password', '', $str); + $str = autolink($str, 100); + + return $str; + } +} \ No newline at end of file diff --git a/app/Services/TokenService.php b/app/Services/TokenService.php index 8b428f0eb199..092f3995d3d7 100644 --- a/app/Services/TokenService.php +++ b/app/Services/TokenService.php @@ -27,9 +27,9 @@ class TokenService extends BaseService } */ - public function getDatatable($accountId) + public function getDatatable($userId) { - $query = $this->tokenRepo->find($accountId); + $query = $this->tokenRepo->find($userId); return $this->createDatatable(ENTITY_TOKEN, $query, false); } diff --git a/app/Services/VendorService.php b/app/Services/VendorService.php index 6f9b4420d772..41f5fd4664bb 100644 --- a/app/Services/VendorService.php +++ b/app/Services/VendorService.php @@ -26,13 +26,13 @@ class VendorService extends BaseService return $this->vendorRepo; } - public function save($data) + public function save($data, $vendor = null) { - if (Auth::user()->account->isNinjaAccount() && isset($data['pro_plan_paid'])) { - $this->ninjaRepo->updateProPlanPaid($data['public_id'], $data['pro_plan_paid']); + if (Auth::user()->account->isNinjaAccount() && isset($data['plan'])) { + $this->ninjaRepo->updatePlanDetails($data['public_id'], $data); } - return $this->vendorRepo->save($data); + return $this->vendorRepo->save($data, $vendor); } public function getDatatable($search) @@ -91,13 +91,13 @@ class VendorService extends BaseService return URL::to("vendors/{$model->public_id}/edit"); }, function ($model) { - return Vendor::canEditItem($model); + return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]); } ], [ '--divider--', function(){return false;}, function ($model) { - return Vendor::canEditItem($model) && Expense::canCreate(); + return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]) && Auth::user()->can('create', ENTITY_EXPENSE); } ], @@ -107,7 +107,7 @@ class VendorService extends BaseService return URL::to("expenses/create/{$model->public_id}"); }, function ($model) { - return Expense::canCreate(); + return Auth::user()->can('create', ENTITY_EXPENSE); } ] ]; diff --git a/bower.json b/bower.json index c395df671586..5d4c3a6c1026 100644 --- a/bower.json +++ b/bower.json @@ -26,7 +26,8 @@ "quill": "~0.20.0", "datetimepicker": "~2.4.5", "stacktrace-js": "~1.0.1", - "fuse.js": "~2.0.2" + "fuse.js": "~2.0.2", + "dropzone": "~4.3.0" }, "resolutions": { "jquery": "~1.11" diff --git a/composer.json b/composer.json index 3d349c74fd9e..9bb49326b1f8 100644 --- a/composer.json +++ b/composer.json @@ -1,119 +1,125 @@ { - "name": "hillelcoren/invoice-ninja", - "description": "An open-source invoicing site built with Laravel", - "keywords": ["invoice", "laravel"], - "license": "Attribution Assurance License", - "authors": [ - { - "name": "Hillel Coren", - "email": "hillelcoren@gmail.com" - } - ], - "require": { - "turbo124/laravel-push-notification": "dev-laravel5", + "name": "hillelcoren/invoice-ninja", + "description": "An open-source invoicing site built with Laravel", + "keywords": [ + "invoice", + "laravel" + ], + "license": "Attribution Assurance License", + "authors": [ + { + "name": "Hillel Coren", + "email": "hillelcoren@gmail.com" + } + ], + "require": { + "turbo124/laravel-push-notification": "dev-laravel5", "omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525", "omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248", "omnipay/gocardless": "dev-master", "omnipay/stripe": "2.3.0", - "laravel/framework": "5.2.*", + "laravel/framework": "5.2.*", "laravelcollective/html": "5.2.*", "laravelcollective/bus": "5.2.*", "symfony/css-selector": "~3.0", - "patricktalmadge/bootstrapper": "5.5.x", - "anahkiasen/former": "4.0.*@dev", - "barryvdh/laravel-debugbar": "~2.0", - "chumper/datatable": "dev-develop#04ef2bf", - "omnipay/omnipay": "~2.3.0", - "intervention/image": "dev-master", - "webpatser/laravel-countries": "dev-master", - "barryvdh/laravel-ide-helper": "dev-master", - "doctrine/dbal": "2.5.x", - "jsanc623/phpbenchtime": "2.x", - "lokielse/omnipay-alipay": "dev-master", - "coatesap/omnipay-datacash": "~2.0", - "mfauveau/omnipay-pacnet": "~2.0", - "coatesap/omnipay-paymentsense": "2.0.0", - "coatesap/omnipay-realex": "~2.0", - "fruitcakestudio/omnipay-sisow": "~2.0", - "alfaproject/omnipay-skrill": "dev-master", - "omnipay/bitpay": "dev-master", - "guzzlehttp/guzzle": "~6.0", - "wildbit/laravel-postmark-provider": "3.0", - "Dwolla/omnipay-dwolla": "dev-master", - "laravel/socialite": "~2.0", - "simshaun/recurr": "dev-master", - "league/fractal": "0.13.*", - "agmscode/omnipay-agms": "~1.0", - "samvaughton/omnipay-barclays-epdq": "~2.0", - "cardgate/omnipay-cardgate": "~2.0", - "fotografde/omnipay-checkoutcom": "~2.0", - "meebio/omnipay-creditcall": "dev-master", - "dioscouri/omnipay-cybersource": "dev-master", - "dercoder/omnipay-ecopayz": "~1.0", - "andreas22/omnipay-fasapay": "1.*", - "delatbabel/omnipay-fatzebra": "dev-master", - "vink/omnipay-komoju": "~1.0", - "incube8/omnipay-multicards": "dev-master", - "descubraomundo/omnipay-pagarme": "dev-master", - "dercoder/omnipay-paysafecard": "dev-master", - "softcommerce/omnipay-paytrace": "~1.0", - "meebio/omnipay-secure-trading": "dev-master", - "justinbusschau/omnipay-secpay": "~2.0", - "labs7in0/omnipay-wechat": "dev-master", - "collizo4sky/omnipay-wepay": "~1.0", - "laracasts/presenter": "dev-master", - "jlapp/swaggervel": "master-dev", - "maatwebsite/excel": "~2.0", - "ezyang/htmlpurifier": "~v4.7", - "cerdic/css-tidy": "~v1.5", - "asgrim/ofxparser": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "~4.0", - "phpspec/phpspec": "~2.1", - "codeception/codeception": "*", - "codeception/c3": "~2.0", - "fzaninotto/faker": "^1.5", + "patricktalmadge/bootstrapper": "5.5.x", + "anahkiasen/former": "4.0.*@dev", + "barryvdh/laravel-debugbar": "~2.0", + "chumper/datatable": "dev-develop#04ef2bf", + "omnipay/omnipay": "~2.3", + "intervention/image": "dev-master", + "webpatser/laravel-countries": "dev-master", + "barryvdh/laravel-ide-helper": "dev-master", + "doctrine/dbal": "2.5.x", + "jsanc623/phpbenchtime": "2.x", + "lokielse/omnipay-alipay": "dev-master", + "coatesap/omnipay-datacash": "~2.0", + "mfauveau/omnipay-pacnet": "~2.0", + "coatesap/omnipay-paymentsense": "2.0.0", + "coatesap/omnipay-realex": "~2.0", + "fruitcakestudio/omnipay-sisow": "~2.0", + "alfaproject/omnipay-skrill": "dev-master", + "omnipay/bitpay": "dev-master", + "guzzlehttp/guzzle": "~6.0", + "wildbit/laravel-postmark-provider": "3.0", + "Dwolla/omnipay-dwolla": "dev-master", + "laravel/socialite": "~2.0", + "simshaun/recurr": "dev-master", + "league/fractal": "0.13.*", + "agmscode/omnipay-agms": "~1.0", + "samvaughton/omnipay-barclays-epdq": "~2.0", + "cardgate/omnipay-cardgate": "~2.0", + "fotografde/omnipay-checkoutcom": "~2.0", + "meebio/omnipay-creditcall": "dev-master", + "dioscouri/omnipay-cybersource": "dev-master", + "dercoder/omnipay-ecopayz": "~1.0", + "andreas22/omnipay-fasapay": "1.*", + "delatbabel/omnipay-fatzebra": "dev-master", + "vink/omnipay-komoju": "~1.0", + "incube8/omnipay-multicards": "dev-master", + "descubraomundo/omnipay-pagarme": "dev-master", + "dercoder/omnipay-paysafecard": "dev-master", + "softcommerce/omnipay-paytrace": "~1.0", + "meebio/omnipay-secure-trading": "dev-master", + "justinbusschau/omnipay-secpay": "~2.0", + "labs7in0/omnipay-wechat": "dev-master", + "collizo4sky/omnipay-wepay": "~1.0", + "laracasts/presenter": "dev-master", + "jlapp/swaggervel": "master-dev", + "maatwebsite/excel": "~2.0", + "ezyang/htmlpurifier": "~v4.7", + "cerdic/css-tidy": "~v1.5", + "asgrim/ofxparser": "^1.1", + "league/flysystem-aws-s3-v3": "~1.0", + "league/flysystem-rackspace": "~1.0", + "barracudanetworks/archivestream-php": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "phpspec/phpspec": "~2.1", + "codeception/codeception": "*", + "codeception/c3": "~2.0", + "fzaninotto/faker": "^1.5", "symfony/dom-crawler": "~3.0" - }, - "autoload": { - "classmap": [ - "app/Console/Commands", - "app/Libraries", - "app/Http/Controllers", - "app/Models", - "app/Ninja", - "app/Ninja/Repositories", - "database" - ], - "psr-4": { - "App\\": "app/" - }, + }, + "autoload": { + "classmap": [ + "app/Console/Commands", + "app/Libraries", + "app/Http/Controllers", + "app/Models", + "app/Ninja", + "app/Ninja/Repositories", + "database" + ], + "psr-4": { + "App\\": "app/" + }, "files": [ "app/Libraries/lib_autolink.php", "app/Libraries/OFX.php" ] - }, - "autoload-dev": { - "classmap": [ - "tests/TestCase.php" - ] - }, - "scripts": { - "post-install-cmd": [ - "php artisan clear-compiled", - "php artisan optimize" - ], - "post-update-cmd": [ - "php artisan clear-compiled", - "php artisan optimize" - ], - "post-create-project-cmd": [ - "php -r \"copy('.env.example', '.env');\"", - "php artisan key:generate" - ] - }, - "config": { - "preferred-install": "dist" - } -} + }, + "autoload-dev": { + "classmap": [ + "tests/TestCase.php" + ] + }, + "scripts": { + "post-install-cmd": [ + "php artisan clear-compiled", + "php artisan optimize" + ], + "post-update-cmd": [ + "php artisan clear-compiled", + "php artisan optimize" + ], + "post-create-project-cmd": [ + "php -r \"copy('.env.example', '.env');\"", + "php artisan key:generate" + ] + }, + "config": { + "preferred-install": "dist" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index f47bad86a156..4dd6f3db14ed 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ce6df956642de67a38a4a22775eaeff3", - "content-hash": "fcd326b2ee271a65f719ba87f57a6c14", + "hash": "cf82d2ddb25cb1a7d6b4867bcc8692b8", + "content-hash": "481a95753b873249aebceb99e7426421", "packages": [ { "name": "agmscode/omnipay-agms", @@ -127,7 +127,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/formers/former/zipball/d97f907741323b390f43954a90a227921ecc6b96", + "url": "https://api.github.com/repos/formers/former/zipball/78ae8c65b1f8134e2db1c9491c251c03638823ca", "reference": "d97f907741323b390f43954a90a227921ecc6b96", "shasum": "" }, @@ -321,6 +321,126 @@ ], "time": "2015-12-11 11:08:57" }, + { + "name": "aws/aws-sdk-php", + "version": "3.17.1", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "f8c0cc9357e10896a5c57104f2c79d1b727d97d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f8c0cc9357e10896a5c57104f2c79d1b727d97d0", + "reference": "f8c0cc9357e10896a5c57104f2c79d1b727d97d0", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~5.3|~6.0.1|~6.1", + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "~1.0", + "mtdowling/jmespath.php": "~2.2", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-json": "*", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "nette/neon": "^2.3", + "phpunit/phpunit": "~4.0|~5.0", + "psr/cache": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2016-03-22 19:19:22" + }, + { + "name": "barracudanetworks/archivestream-php", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/barracudanetworks/ArchiveStream-php.git", + "reference": "9a81c7de7f0cd5ea2150fc3dc00f1c43178362b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barracudanetworks/ArchiveStream-php/zipball/9a81c7de7f0cd5ea2150fc3dc00f1c43178362b6", + "reference": "9a81c7de7f0cd5ea2150fc3dc00f1c43178362b6", + "shasum": "" + }, + "require": { + "ext-gmp": "*", + "ext-mbstring": "*", + "php": ">=5.1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Barracuda\\ArchiveStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A library for dynamically streaming dynamic tar or zip files without the need to have the complete file stored on the server.", + "homepage": "https://github.com/barracudanetworks/ArchiveStream-php", + "keywords": [ + "archive", + "php", + "stream", + "tar", + "zip" + ], + "time": "2016-01-07 06:02:26" + }, { "name": "barryvdh/laravel-debugbar", "version": "v2.2.0", @@ -3056,6 +3176,100 @@ ], "time": "2016-03-14 21:54:11" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/595e24678bf78f8107ebc9355d8376ae0eb712c6", + "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.0.0", + "league/flysystem": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3v3\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for the AWS S3 SDK v3.x", + "time": "2015-11-19 08:44:16" + }, + { + "name": "league/flysystem-rackspace", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-rackspace.git", + "reference": "ba877e837f5dce60e78a0555de37eb9bfc7dd6b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-rackspace/zipball/ba877e837f5dce60e78a0555de37eb9bfc7dd6b9", + "reference": "ba877e837f5dce60e78a0555de37eb9bfc7dd6b9", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "php": ">=5.4.0", + "rackspace/php-opencloud": "~1.16" + }, + "require-dev": { + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\Rackspace\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for Rackspace", + "time": "2016-03-11 12:13:42" + }, { "name": "league/fractal", "version": "0.13.0", @@ -3585,6 +3799,33 @@ ], "time": "2015-07-14 19:53:54" }, + { + "name": "mikemccabe/json-patch-php", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/mikemccabe/json-patch-php.git", + "reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikemccabe/json-patch-php/zipball/b3af30a6aec7f6467c773cd49b2d974a70f7c0d4", + "reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "mikemccabe\\JsonPatch\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Produce and apply json-patch objects", + "time": "2015-01-05 21:19:54" + }, { "name": "monolog/monolog", "version": "1.18.1", @@ -3707,6 +3948,61 @@ ], "time": "2016-01-26 21:23:30" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "192f93e43c2c97acde7694993ab171b3de284093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/192f93e43c2c97acde7694993ab171b3de284093", + "reference": "192f93e43c2c97acde7694993ab171b3de284093", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2016-01-05 18:25:05" + }, { "name": "nesbot/carbon", "version": "1.21.0", @@ -3933,7 +4229,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-bitpay/zipball/cf813f1d5436a1d2f942d3df6666695d1e2b5280", + "url": "https://api.github.com/repos/thephpleague/omnipay-bitpay/zipball/9cadfb7955bd361d1a00ac8f0570aee4c05c6bb4", "reference": "cf813f1d5436a1d2f942d3df6666695d1e2b5280", "shasum": "" }, @@ -5906,6 +6202,63 @@ ], "time": "2016-03-09 05:03:14" }, + { + "name": "rackspace/php-opencloud", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/rackspace/php-opencloud.git", + "reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/d6b71feed7f9e7a4b52e0240a79f06473ba69c8c", + "reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c", + "shasum": "" + }, + "require": { + "guzzle/guzzle": "~3.8", + "mikemccabe/json-patch-php": "~0.1", + "php": ">=5.4", + "psr/log": "~1.0" + }, + "require-dev": { + "apigen/apigen": "~4.0", + "fabpot/php-cs-fixer": "1.0.*@dev", + "jakub-onderka/php-parallel-lint": "0.*", + "phpspec/prophecy": "~1.4", + "phpunit/phpunit": "4.3.*", + "satooshi/php-coveralls": "0.6.*@dev" + }, + "type": "library", + "autoload": { + "psr-0": { + "OpenCloud": [ + "lib/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Jamie Hannaford", + "email": "jamie.hannaford@rackspace.com", + "homepage": "https://github.com/jamiehannaford" + } + ], + "description": "PHP SDK for Rackspace/OpenStack APIs", + "keywords": [ + "Openstack", + "nova", + "opencloud", + "rackspace", + "swift" + ], + "time": "2016-01-29 10:34:57" + }, { "name": "samvaughton/omnipay-barclays-epdq", "version": "2.2.0", @@ -9547,4 +9900,4 @@ "prefer-lowest": false, "platform": [], "platform-dev": [] -} +} \ No newline at end of file diff --git a/config/app.php b/config/app.php index c86362a9afea..4a75f65f9e5f 100644 --- a/config/app.php +++ b/config/app.php @@ -157,6 +157,7 @@ return [ /* * Application Service Providers... */ + 'App\Providers\AuthServiceProvider', 'App\Providers\AppServiceProvider', //'App\Providers\BusServiceProvider', 'App\Providers\ConfigServiceProvider', @@ -194,6 +195,7 @@ return [ 'Eloquent' => 'Illuminate\Database\Eloquent\Model', 'Event' => 'Illuminate\Support\Facades\Event', 'File' => 'Illuminate\Support\Facades\File', + 'Gate' => 'Illuminate\Support\Facades\Gate', 'Hash' => 'Illuminate\Support\Facades\Hash', 'Input' => 'Illuminate\Support\Facades\Input', 'Lang' => 'Illuminate\Support\Facades\Lang', diff --git a/config/filesystems.php b/config/filesystems.php index 0221fa70dbe1..da16e0e16766 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -47,23 +47,33 @@ return [ 'driver' => 'local', 'root' => storage_path().'/app', ], + + 'logos' => [ + 'driver' => 'local', + 'root' => env('LOGO_PATH', public_path().'/logo'), + ], + + 'documents' => [ + 'driver' => 'local', + 'root' => storage_path().'/documents', + ], 's3' => [ 'driver' => 's3', - 'key' => 'your-key', - 'secret' => 'your-secret', - 'region' => 'your-region', - 'bucket' => 'your-bucket', + 'key' => env('S3_KEY', ''), + 'secret' => env('S3_SECRET', ''), + 'region' => env('S3_REGION', 'us-east-1'), + 'bucket' => env('S3_BUCKET', ''), ], 'rackspace' => [ 'driver' => 'rackspace', - 'username' => 'your-username', - 'key' => 'your-key', - 'container' => 'your-container', - 'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/', - 'region' => 'IAD', - 'url_type' => 'publicURL' + 'username' => env('RACKSPACE_USERNAME', ''), + 'key' => env('RACKSPACE_KEY', ''), + 'container' => env('RACKSPACE_CONTAINER', ''), + 'endpoint' => env('RACKSPACE_ENDPOINT', 'https://identity.api.rackspacecloud.com/v2.0/'), + 'region' => env('RACKSPACE_REGION', 'IAD'), + 'url_type' => env('RACKSPACE_URL_TYPE', 'publicURL') ], ], diff --git a/config/swaggervel.php b/config/swaggervel.php index 341b47ed830a..037cf930096a 100644 --- a/config/swaggervel.php +++ b/config/swaggervel.php @@ -21,6 +21,13 @@ return array( */ 'doc-route' => 'docs', + /* + |-------------------------------------------------------------------------- + | Relative path to access swagger ui. + |-------------------------------------------------------------------------- + */ + 'api-docs-route' => 'api-docs', + /* |-------------------------------------------------------------------------- | Absolute path to directory containing the swagger annotations are stored. diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php new file mode 100644 index 000000000000..018da7b2357b --- /dev/null +++ b/database/factories/ModelFactory.php @@ -0,0 +1,48 @@ +define(Contact::class, function (Faker\Generator $faker) { + return [ + 'client_id' => function() { + return factory(Client::class)->create()->id; + }, + 'user_id' => 1, + 'account_id' => 1, + 'public_id' => Contact::count() + 1, + 'is_primary' => true, + 'send_invoice' => true, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $faker->safeEmail, + 'phone' => $faker->phoneNumber, + ]; +}); + +$factory->define(Client::class, function (Faker\Generator $faker) { + return [ + 'user_id' => 1, + 'account_id' => 1, + 'public_id' => Client::count() + 1, + 'name' => $faker->name, + 'address1' => $faker->streetAddress, + 'address2' => $faker->secondaryAddress, + 'city' => $faker->city, + 'state' => $faker->state, + 'postal_code' => $faker->postcode, + 'country_id' => Country::all()->random()->id, + ]; +}); \ No newline at end of file diff --git a/database/migrations/2016_03_22_168362_add_documents.php b/database/migrations/2016_03_22_168362_add_documents.php new file mode 100644 index 000000000000..3d0a44f20c25 --- /dev/null +++ b/database/migrations/2016_03_22_168362_add_documents.php @@ -0,0 +1,71 @@ +string('logo')->nullable()->default(null); + $table->unsignedInteger('logo_width'); + $table->unsignedInteger('logo_height'); + $table->unsignedInteger('logo_size'); + $table->boolean('invoice_embed_documents')->default(1); + $table->boolean('document_email_attachment')->default(1); + }); + + DB::table('accounts')->update(array('logo' => '')); + Schema::dropIfExists('documents'); + Schema::create('documents', function($t) + { + $t->increments('id'); + $t->unsignedInteger('public_id')->nullable(); + $t->unsignedInteger('account_id'); + $t->unsignedInteger('user_id'); + $t->unsignedInteger('invoice_id')->nullable(); + $t->unsignedInteger('expense_id')->nullable(); + $t->string('path'); + $t->string('preview'); + $t->string('name'); + $t->string('type'); + $t->string('disk'); + $t->string('hash', 40); + $t->unsignedInteger('size'); + $t->unsignedInteger('width')->nullable(); + $t->unsignedInteger('height')->nullable(); + + $t->timestamps(); + + $t->foreign('account_id')->references('id')->on('accounts'); + $t->foreign('user_id')->references('id')->on('users'); + $t->foreign('invoice_id')->references('id')->on('invoices'); + $t->foreign('expense_id')->references('id')->on('expenses'); + + + $t->unique( array('account_id','public_id') ); + }); + } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function($table) { + $table->dropColumn('logo'); + $table->dropColumn('logo_width'); + $table->dropColumn('logo_height'); + $table->dropColumn('logo_size'); + $table->dropColumn('invoice_embed_documents'); + $table->dropColumn('document_email_attachment'); + }); + + Schema::dropIfExists('documents'); + } +} diff --git a/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php b/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php new file mode 100644 index 000000000000..4c2d8a29b931 --- /dev/null +++ b/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php @@ -0,0 +1,68 @@ +decimal('tax_rate', 13, 3)->change(); + }); + + Schema::table('invoice_items', function($table) { + $table->decimal('tax_rate', 13, 3)->change(); + }); + + Schema::table('invoices', function($table) { + $table->renameColumn('tax_rate', 'tax_rate1'); + $table->renameColumn('tax_name', 'tax_name1'); + $table->string('tax_name2')->nullable(); + $table->decimal('tax_rate2', 13, 3); + }); + + Schema::table('invoice_items', function($table) { + $table->renameColumn('tax_rate', 'tax_rate1'); + $table->renameColumn('tax_name', 'tax_name1'); + $table->string('tax_name2')->nullable(); + $table->decimal('tax_rate2', 13, 3); + }); + + Schema::table('accounts', function($table) { + $table->boolean('enable_client_portal_dashboard')->default(true); + }); + } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('invoices', function($table) { + $table->decimal('tax_rate1', 13, 2)->change(); + $table->renameColumn('tax_rate1', 'tax_rate'); + $table->renameColumn('tax_name1', 'tax_name'); + $table->dropColumn('tax_name2'); + $table->dropColumn('tax_rate2'); + }); + + Schema::table('invoice_items', function($table) { + $table->decimal('tax_rate1', 13, 2)->change(); + $table->renameColumn('tax_rate1', 'tax_rate'); + $table->renameColumn('tax_name1', 'tax_name'); + $table->dropColumn('tax_name2'); + $table->dropColumn('tax_rate2'); + }); + + Schema::table('accounts', function($table) { + $table->dropColumn('enable_client_portal_dashboard'); + }); + } +} \ No newline at end of file diff --git a/database/migrations/2016_04_16_103943_enterprise_plan.php b/database/migrations/2016_04_16_103943_enterprise_plan.php new file mode 100644 index 000000000000..8a3a63717367 --- /dev/null +++ b/database/migrations/2016_04_16_103943_enterprise_plan.php @@ -0,0 +1,232 @@ +increments('id'); + + $table->enum('plan', array('pro', 'enterprise', 'white_label'))->nullable(); + $table->enum('plan_term', array('month', 'year'))->nullable(); + $table->date('plan_started')->nullable(); + $table->date('plan_paid')->nullable(); + $table->date('plan_expires')->nullable(); + + $table->unsignedInteger('payment_id')->nullable(); + $table->foreign('payment_id')->references('id')->on('payments'); + + $table->date('trial_started')->nullable(); + $table->enum('trial_plan', array('pro', 'enterprise'))->nullable(); + + $table->enum('pending_plan', array('pro', 'enterprise', 'free'))->nullable(); + $table->enum('pending_term', array('month', 'year'))->nullable(); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + if (!Schema::hasColumn('accounts', 'company_id')) { + Schema::table('accounts', function($table) + { + $table->unsignedInteger('company_id')->nullable(); + $table->foreign('company_id')->references('id')->on('companies'); + }); + } + + $single_account_ids = \DB::table('users') + ->leftJoin('user_accounts', function ($join) { + $join->on('user_accounts.user_id1', '=', 'users.id'); + $join->orOn('user_accounts.user_id2', '=', 'users.id'); + $join->orOn('user_accounts.user_id3', '=', 'users.id'); + $join->orOn('user_accounts.user_id4', '=', 'users.id'); + $join->orOn('user_accounts.user_id5', '=', 'users.id'); + }) + ->leftJoin('accounts', 'accounts.id', '=', 'users.account_id') + ->whereNull('user_accounts.id') + ->whereNull('accounts.company_id') + ->where(function ($query) { + $query->whereNull('users.public_id'); + $query->orWhere('users.public_id', '=', 0); + }) + ->lists('users.account_id'); + + if (count($single_account_ids)) { + foreach (Account::find($single_account_ids) as $account) { + $this->upAccounts($account); + $this->checkTimeout($timeout, $startTime); + } + } + + $group_accounts = \DB::select( + 'SELECT u1.account_id as account1, u2.account_id as account2, u3.account_id as account3, u4.account_id as account4, u5.account_id as account5 FROM `user_accounts` + LEFT JOIN users u1 ON (u1.public_id IS NULL OR u1.public_id = 0) AND user_accounts.user_id1 = u1.id + LEFT JOIN users u2 ON (u2.public_id IS NULL OR u2.public_id = 0) AND user_accounts.user_id2 = u2.id + LEFT JOIN users u3 ON (u3.public_id IS NULL OR u3.public_id = 0) AND user_accounts.user_id3 = u3.id + LEFT JOIN users u4 ON (u4.public_id IS NULL OR u4.public_id = 0) AND user_accounts.user_id4 = u4.id + LEFT JOIN users u5 ON (u5.public_id IS NULL OR u5.public_id = 0) AND user_accounts.user_id5 = u5.id + LEFT JOIN accounts a1 ON a1.id = u1.account_id + LEFT JOIN accounts a2 ON a2.id = u2.account_id + LEFT JOIN accounts a3 ON a3.id = u3.account_id + LEFT JOIN accounts a4 ON a4.id = u4.account_id + LEFT JOIN accounts a5 ON a5.id = u5.account_id + WHERE (a1.id IS NOT NULL AND a1.company_id IS NULL) + OR (a2.id IS NOT NULL AND a2.company_id IS NULL) + OR (a3.id IS NOT NULL AND a3.company_id IS NULL) + OR (a4.id IS NOT NULL AND a4.company_id IS NULL) + OR (a5.id IS NOT NULL AND a5.company_id IS NULL)'); + + if (count($group_accounts)) { + foreach ($group_accounts as $group_account) { + $this->upAccounts(null, Account::find(get_object_vars($group_account))); + $this->checkTimeout($timeout, $startTime); + } + } + + if (Schema::hasColumn('accounts', 'pro_plan_paid')) { + Schema::table('accounts', function($table) + { + $table->dropColumn('pro_plan_paid'); + $table->dropColumn('pro_plan_trial'); + }); + } + } + + private function upAccounts($primaryAccount, $otherAccounts = array()) { + if(!$primaryAccount) { + $primaryAccount = $otherAccounts->first(); + } + + if (empty($primaryAccount)) { + return; + } + + $company = Company::create(); + if ($primaryAccount->pro_plan_paid && $primaryAccount->pro_plan_paid != '0000-00-00') { + $company->plan = 'pro'; + $company->plan_term = 'year'; + $company->plan_started = $primaryAccount->pro_plan_paid; + $company->plan_paid = $primaryAccount->pro_plan_paid; + + $expires = DateTime::createFromFormat('Y-m-d', $primaryAccount->pro_plan_paid); + $expires->modify('+1 year'); + $expires = $expires->format('Y-m-d'); + + // check for self host white label licenses + if (!Utils::isNinjaProd()) { + if ($company->plan_paid) { + $company->plan = 'white_label'; + // old ones were unlimited, new ones are yearly + if ($company->plan_paid == NINJA_DATE) { + $company->plan_term = null; + } else { + $company->plan_term = PLAN_TERM_YEARLY; + $company->plan_expires = $expires; + } + } + } elseif ($company->plan_paid != NINJA_DATE) { + $company->plan_expires = $expires; + } + } + + if ($primaryAccount->pro_plan_trial && $primaryAccount->pro_plan_trial != '0000-00-00') { + $company->trial_started = $primaryAccount->pro_plan_trial; + $company->trial_plan = 'pro'; + } + + $company->save(); + + $primaryAccount->company_id = $company->id; + $primaryAccount->save(); + + if (!empty($otherAccounts)) { + foreach ($otherAccounts as $account) { + if ($account && $account->id != $primaryAccount->id) { + $account->company_id = $company->id; + $account->save(); + } + } + } + } + + protected function checkTimeout($timeout, $startTime) { + if (time() - $startTime >= $timeout) { + exit('Migration reached time limit; please run again to continue'); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $timeout = ini_get('max_execution_time'); + if ($timeout == 0) { + $timeout = 600; + } + $timeout = max($timeout - 10, $timeout * .9); + $startTime = time(); + + if (!Schema::hasColumn('accounts', 'pro_plan_paid')) { + Schema::table('accounts', function($table) + { + $table->date('pro_plan_paid')->nullable(); + $table->date('pro_plan_trial')->nullable(); + }); + } + + $company_ids = \DB::table('companies') + ->leftJoin('accounts', 'accounts.company_id', '=', 'companies.id') + ->whereNull('accounts.pro_plan_paid') + ->whereNull('accounts.pro_plan_trial') + ->where(function ($query) { + $query->whereNotNull('companies.plan_paid'); + $query->orWhereNotNull('companies.trial_started'); + }) + ->lists('companies.id'); + + $company_ids = array_unique($company_ids); + + if (count($company_ids)) { + foreach (Company::find($company_ids) as $company) { + foreach ($company->accounts as $account) { + $account->pro_plan_paid = $company->plan_paid; + $account->pro_plan_trial = $company->trial_started; + $account->save(); + } + $this->checkTimeout($timeout, $startTime); + } + } + + if (Schema::hasColumn('accounts', 'company_id')) { + Schema::table('accounts', function($table) + { + $table->dropForeign('accounts_company_id_foreign'); + $table->dropColumn('company_id'); + }); + } + + Schema::dropIfExists('companies'); + } +} \ No newline at end of file diff --git a/database/migrations/2016_04_18_174135_add_page_size.php b/database/migrations/2016_04_18_174135_add_page_size.php new file mode 100644 index 000000000000..0094c84335b5 --- /dev/null +++ b/database/migrations/2016_04_18_174135_add_page_size.php @@ -0,0 +1,73 @@ +string('page_size')->default('A4'); + $table->boolean('live_preview')->default(true); + $table->smallInteger('invoice_number_padding')->default(4); + }); + + Schema::table('fonts', function ($table) { + $table->dropColumn('is_early_access'); + }); + + Schema::create('expense_categories', function($table) + { + $table->increments('id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('account_id')->index(); + $table->timestamps(); + $table->softDeletes(); + + $table->string('name')->nullable(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + $table->unsignedInteger('public_id')->index(); + $table->unique( array('account_id','public_id') ); + }); + + Schema::table('expenses', function ($table) { + $table->unsignedInteger('expense_category_id')->nullable()->index(); + + $table->foreign('expense_category_id')->references('id')->on('expense_categories')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function ($table) { + $table->dropColumn('page_size'); + $table->dropColumn('live_preview'); + $table->dropColumn('invoice_number_padding'); + }); + + Schema::table('fonts', function ($table) { + $table->boolean('is_early_access'); + }); + + Schema::table('expenses', function ($table) { + $table->dropForeign('expenses_expense_category_id_foreign'); + $table->dropColumn('expense_category_id'); + }); + + Schema::dropIfExists('expense_categories'); + } +} diff --git a/database/seeds/CurrenciesSeeder.php b/database/seeds/CurrenciesSeeder.php index 9a8304b181f6..3b44d55d8d7f 100644 --- a/database/seeds/CurrenciesSeeder.php +++ b/database/seeds/CurrenciesSeeder.php @@ -8,6 +8,7 @@ class CurrenciesSeeder extends Seeder { Eloquent::unguard(); + // http://www.localeplanet.com/icu/currency.html $currencies = [ ['name' => 'US Dollar', 'code' => 'USD', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['name' => 'Pound Sterling', 'code' => 'GBP', 'symbol' => '£', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], @@ -55,6 +56,8 @@ class CurrenciesSeeder extends Seeder ['name' => 'Saudi Riyal', 'code' => 'SAR', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['name' => 'Japanese Yen', 'code' => 'JPY', 'symbol' => '¥', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['name' => 'Maldivian Rufiyaa', 'code' => 'MVR', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['name' => 'Costa Rican Colón', 'code' => 'CRC', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['name' => 'Pakistani Rupee', 'code' => 'PKR', 'symbol' => 'Rs ', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], ]; foreach ($currencies as $currency) { diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index 8791f30e71fb..d7f38d3e1e53 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -23,5 +23,6 @@ class DatabaseSeeder extends Seeder $this->call('DateFormatsSeeder'); $this->call('InvoiceDesignsSeeder'); $this->call('PaymentTermsSeeder'); + $this->call('LanguageSeeder'); } } diff --git a/database/seeds/FontsSeeder.php b/database/seeds/FontsSeeder.php index 6f27ffa0be02..de67f441ced5 100644 --- a/database/seeds/FontsSeeder.php +++ b/database/seeds/FontsSeeder.php @@ -246,7 +246,6 @@ class FontsSeeder extends Seeder foreach ($fonts as $font) { if (!DB::table('fonts')->where('name', '=', $font['name'])->get()) { - $font['is_early_access'] = false; Font::create($font); } } diff --git a/database/seeds/LanguageSeeder.php b/database/seeds/LanguageSeeder.php new file mode 100644 index 000000000000..5fc9569d5cef --- /dev/null +++ b/database/seeds/LanguageSeeder.php @@ -0,0 +1,42 @@ + 'English', 'locale' => 'en'], + ['name' => 'Italian', 'locale' => 'it'], + ['name' => 'German', 'locale' => 'de'], + ['name' => 'French', 'locale' => 'fr'], + ['name' => 'Brazilian Portuguese', 'locale' => 'pt_BR'], + ['name' => 'Dutch', 'locale' => 'nl'], + ['name' => 'Spanish', 'locale' => 'es'], + ['name' => 'Norwegian', 'locale' => 'nb_NO'], + ['name' => 'Danish', 'locale' => 'da'], + ['name' => 'Japanese', 'locale' => 'ja'], + ['name' => 'Swedish', 'locale' => 'sv'], + ['name' => 'Spanish - Spain', 'locale' => 'es_ES'], + ['name' => 'French - Canada', 'locale' => 'fr_CA'], + ['name' => 'Lithuanian', 'locale' => 'lt'], + ['name' => 'Polish', 'locale' => 'pl'], + ['name' => 'Czech', 'locale' => 'cs'], + ]; + + foreach ($languages as $language) { + $record = Language::whereLocale($language['locale'])->first(); + if ($record) { + $record->name = $language['name']; + $record->save(); + } else { + Language::create($language); + } + } + + Eloquent::reguard(); + } +} diff --git a/database/seeds/UpdateSeeder.php b/database/seeds/UpdateSeeder.php new file mode 100644 index 000000000000..d445811de506 --- /dev/null +++ b/database/seeds/UpdateSeeder.php @@ -0,0 +1,24 @@ +command->info('Running UpdateSeeder...'); + + $this->call('PaymentLibrariesSeeder'); + $this->call('FontsSeeder'); + $this->call('BanksSeeder'); + $this->call('InvoiceStatusSeeder'); + $this->call('CurrenciesSeeder'); + $this->call('DateFormatsSeeder'); + $this->call('InvoiceDesignsSeeder'); + $this->call('PaymentTermsSeeder'); + $this->call('LanguageSeeder'); + } +} diff --git a/database/seeds/UserTableSeeder.php b/database/seeds/UserTableSeeder.php index 2f27b9479304..dbaba660b75c 100644 --- a/database/seeds/UserTableSeeder.php +++ b/database/seeds/UserTableSeeder.php @@ -1,8 +1,13 @@ 'Test Account', + 'name' => $faker->name, + 'address1' => $faker->streetAddress, + 'address2' => $faker->secondaryAddress, + 'city' => $faker->city, + 'state' => $faker->state, + 'postal_code' => $faker->postcode, + 'country_id' => Country::all()->random()->id, 'account_key' => str_random(RANDOM_KEY_LENGTH), + 'invoice_terms' => $faker->text($faker->numberBetween(50, 300)), + 'work_phone' => $faker->phoneNumber, + 'work_email' => $faker->safeEmail, + 'invoice_design_id' => min(InvoiceDesign::all()->random()->id, 10), + 'header_font_id' => min(Font::all()->random()->id, 17), + 'body_font_id' => min(Font::all()->random()->id, 17), + 'primary_color' => $faker->hexcolor, 'timezone_id' => 1, + 'company_id' => $company->id, ]); User::create([ @@ -26,6 +48,8 @@ class UserTableSeeder extends Seeder 'password' => Hash::make(TEST_PASSWORD), 'registered' => true, 'confirmed' => true, + 'notify_sent' => false, + 'notify_paid' => false, ]); Affiliate::create([ diff --git a/public/.htaccess b/public/.htaccess index c9a6c555964e..ab45908df822 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -14,6 +14,6 @@ RewriteRule ^ index.php [L] # In case of running InvoiceNinja in a Subdomain like invoiceninja.example.com, - # you have to enablel the following line: + # you have to enable the following line: # RewriteBase / diff --git a/public/built.js b/public/built.js index 9fccf73e0bff..8cd3a1e70e03 100644 --- a/public/built.js +++ b/public/built.js @@ -27170,6 +27170,8 @@ d[b]="undefined"!==f.getType(g)?g:f.visitModel(j,c,a);break;default:d[b]=c(j,a.p !function(a){a.fn.datepicker.dates.no={days:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],daysShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],daysMin:["Sø","Ma","Ti","On","To","Fr","Lø"],months:["Januar","Februar","Mars","April","Mai","Juni","Juli","August","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Des"],today:"I dag",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); !function(a){a.fn.datepicker.dates.es={days:["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado","Domingo"],daysShort:["Dom","Lun","Mar","Mié","Jue","Vie","Sáb","Dom"],daysMin:["Do","Lu","Ma","Mi","Ju","Vi","Sa","Do"],months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],monthsShort:["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"],today:"Hoy",clear:"Borrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); !function(a){a.fn.datepicker.dates.sv={days:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag","Söndag"],daysShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör","Sön"],daysMin:["Sö","Må","Ti","On","To","Fr","Lö","Sö"],months:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"Idag",format:"yyyy-mm-dd",weekStart:1,clear:"Rensa"}}(jQuery); +(function(){var a,b,c,d,e,f,g,h,i=[].slice,j={}.hasOwnProperty,k=function(a,b){function c(){this.constructor=a}for(var d in b)j.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a};g=function(){},b=function(){function a(){}return a.prototype.addEventListener=a.prototype.on,a.prototype.on=function(a,b){return this._callbacks=this._callbacks||{},this._callbacks[a]||(this._callbacks[a]=[]),this._callbacks[a].push(b),this},a.prototype.emit=function(){var a,b,c,d,e,f;if(d=arguments[0],a=2<=arguments.length?i.call(arguments,1):[],this._callbacks=this._callbacks||{},c=this._callbacks[d])for(e=0,f=c.length;f>e;e++)b=c[e],b.apply(this,a);return this},a.prototype.removeListener=a.prototype.off,a.prototype.removeAllListeners=a.prototype.off,a.prototype.removeEventListener=a.prototype.off,a.prototype.off=function(a,b){var c,d,e,f,g;if(!this._callbacks||0===arguments.length)return this._callbacks={},this;if(d=this._callbacks[a],!d)return this;if(1===arguments.length)return delete this._callbacks[a],this;for(e=f=0,g=d.length;g>f;e=++f)if(c=d[e],c===b){d.splice(e,1);break}return this},a}(),a=function(a){function c(a,b){var e,f,g;if(this.element=a,this.version=c.version,this.defaultOptions.previewTemplate=this.defaultOptions.previewTemplate.replace(/\n*/g,""),this.clickableElements=[],this.listeners=[],this.files=[],"string"==typeof this.element&&(this.element=document.querySelector(this.element)),!this.element||null==this.element.nodeType)throw new Error("Invalid dropzone element.");if(this.element.dropzone)throw new Error("Dropzone already attached.");if(c.instances.push(this),this.element.dropzone=this,e=null!=(g=c.optionsForElement(this.element))?g:{},this.options=d({},this.defaultOptions,e,null!=b?b:{}),this.options.forceFallback||!c.isBrowserSupported())return this.options.fallback.call(this);if(null==this.options.url&&(this.options.url=this.element.getAttribute("action")),!this.options.url)throw new Error("No URL provided.");if(this.options.acceptedFiles&&this.options.acceptedMimeTypes)throw new Error("You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated.");this.options.acceptedMimeTypes&&(this.options.acceptedFiles=this.options.acceptedMimeTypes,delete this.options.acceptedMimeTypes),this.options.method=this.options.method.toUpperCase(),(f=this.getExistingFallback())&&f.parentNode&&f.parentNode.removeChild(f),this.options.previewsContainer!==!1&&(this.previewsContainer=this.options.previewsContainer?c.getElement(this.options.previewsContainer,"previewsContainer"):this.element),this.options.clickable&&(this.clickableElements=this.options.clickable===!0?[this.element]:c.getElements(this.options.clickable,"clickable")),this.init()}var d,e;return k(c,a),c.prototype.Emitter=b,c.prototype.events=["drop","dragstart","dragend","dragenter","dragover","dragleave","addedfile","addedfiles","removedfile","thumbnail","error","errormultiple","processing","processingmultiple","uploadprogress","totaluploadprogress","sending","sendingmultiple","success","successmultiple","canceled","canceledmultiple","complete","completemultiple","reset","maxfilesexceeded","maxfilesreached","queuecomplete"],c.prototype.defaultOptions={url:null,method:"post",withCredentials:!1,parallelUploads:2,uploadMultiple:!1,maxFilesize:256,paramName:"file",createImageThumbnails:!0,maxThumbnailFilesize:10,thumbnailWidth:120,thumbnailHeight:120,filesizeBase:1e3,maxFiles:null,params:{},clickable:!0,ignoreHiddenFiles:!0,acceptedFiles:null,acceptedMimeTypes:null,autoProcessQueue:!0,autoQueue:!0,addRemoveLinks:!1,previewsContainer:null,hiddenInputContainer:"body",capture:null,renameFilename:null,dictDefaultMessage:"Drop files here to upload",dictFallbackMessage:"Your browser does not support drag'n'drop file uploads.",dictFallbackText:"Please use the fallback form below to upload your files like in the olden days.",dictFileTooBig:"File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.",dictInvalidFileType:"You can't upload files of this type.",dictResponseError:"Server responded with {{statusCode}} code.",dictCancelUpload:"Cancel upload",dictCancelUploadConfirmation:"Are you sure you want to cancel this upload?",dictRemoveFile:"Remove file",dictRemoveFileConfirmation:null,dictMaxFilesExceeded:"You can not upload any more files.",accept:function(a,b){return b()},init:function(){return g},forceFallback:!1,fallback:function(){var a,b,d,e,f,g;for(this.element.className=""+this.element.className+" dz-browser-not-supported",g=this.element.getElementsByTagName("div"),e=0,f=g.length;f>e;e++)a=g[e],/(^| )dz-message($| )/.test(a.className)&&(b=a,a.className="dz-message");return b||(b=c.createElement('
    '),this.element.appendChild(b)),d=b.getElementsByTagName("span")[0],d&&(null!=d.textContent?d.textContent=this.options.dictFallbackMessage:null!=d.innerText&&(d.innerText=this.options.dictFallbackMessage)),this.element.appendChild(this.getFallbackForm())},resize:function(a){var b,c,d;return b={srcX:0,srcY:0,srcWidth:a.width,srcHeight:a.height},c=a.width/a.height,b.optWidth=this.options.thumbnailWidth,b.optHeight=this.options.thumbnailHeight,null==b.optWidth&&null==b.optHeight?(b.optWidth=b.srcWidth,b.optHeight=b.srcHeight):null==b.optWidth?b.optWidth=c*b.optHeight:null==b.optHeight&&(b.optHeight=1/c*b.optWidth),d=b.optWidth/b.optHeight,a.heightd?(b.srcHeight=a.height,b.srcWidth=b.srcHeight*d):(b.srcWidth=a.width,b.srcHeight=b.srcWidth/d),b.srcX=(a.width-b.srcWidth)/2,b.srcY=(a.height-b.srcHeight)/2,b},drop:function(){return this.element.classList.remove("dz-drag-hover")},dragstart:g,dragend:function(){return this.element.classList.remove("dz-drag-hover")},dragenter:function(){return this.element.classList.add("dz-drag-hover")},dragover:function(){return this.element.classList.add("dz-drag-hover")},dragleave:function(){return this.element.classList.remove("dz-drag-hover")},paste:g,reset:function(){return this.element.classList.remove("dz-started")},addedfile:function(a){var b,d,e,f,g,h,i,j,k,l,m,n,o;if(this.element===this.previewsContainer&&this.element.classList.add("dz-started"),this.previewsContainer){for(a.previewElement=c.createElement(this.options.previewTemplate.trim()),a.previewTemplate=a.previewElement,this.previewsContainer.appendChild(a.previewElement),l=a.previewElement.querySelectorAll("[data-dz-name]"),f=0,i=l.length;i>f;f++)b=l[f],b.textContent=this._renameFilename(a.name);for(m=a.previewElement.querySelectorAll("[data-dz-size]"),g=0,j=m.length;j>g;g++)b=m[g],b.innerHTML=this.filesize(a.size);for(this.options.addRemoveLinks&&(a._removeLink=c.createElement(''+this.options.dictRemoveFile+""),a.previewElement.appendChild(a._removeLink)),d=function(b){return function(d){return d.preventDefault(),d.stopPropagation(),a.status===c.UPLOADING?c.confirm(b.options.dictCancelUploadConfirmation,function(){return b.removeFile(a)}):b.options.dictRemoveFileConfirmation?c.confirm(b.options.dictRemoveFileConfirmation,function(){return b.removeFile(a)}):b.removeFile(a)}}(this),n=a.previewElement.querySelectorAll("[data-dz-remove]"),o=[],h=0,k=n.length;k>h;h++)e=n[h],o.push(e.addEventListener("click",d));return o}},removedfile:function(a){var b;return a.previewElement&&null!=(b=a.previewElement)&&b.parentNode.removeChild(a.previewElement),this._updateMaxFilesReachedClass()},thumbnail:function(a,b){var c,d,e,f;if(a.previewElement){for(a.previewElement.classList.remove("dz-file-preview"),f=a.previewElement.querySelectorAll("[data-dz-thumbnail]"),d=0,e=f.length;e>d;d++)c=f[d],c.alt=a.name,c.src=b;return setTimeout(function(){return function(){return a.previewElement.classList.add("dz-image-preview")}}(this),1)}},error:function(a,b){var c,d,e,f,g;if(a.previewElement){for(a.previewElement.classList.add("dz-error"),"String"!=typeof b&&b.error&&(b=b.error),f=a.previewElement.querySelectorAll("[data-dz-errormessage]"),g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.textContent=b);return g}},errormultiple:g,processing:function(a){return a.previewElement&&(a.previewElement.classList.add("dz-processing"),a._removeLink)?a._removeLink.textContent=this.options.dictCancelUpload:void 0},processingmultiple:g,uploadprogress:function(a,b){var c,d,e,f,g;if(a.previewElement){for(f=a.previewElement.querySelectorAll("[data-dz-uploadprogress]"),g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push("PROGRESS"===c.nodeName?c.value=b:c.style.width=""+b+"%");return g}},totaluploadprogress:g,sending:g,sendingmultiple:g,success:function(a){return a.previewElement?a.previewElement.classList.add("dz-success"):void 0},successmultiple:g,canceled:function(a){return this.emit("error",a,"Upload canceled.")},canceledmultiple:g,complete:function(a){return a._removeLink&&(a._removeLink.textContent=this.options.dictRemoveFile),a.previewElement?a.previewElement.classList.add("dz-complete"):void 0},completemultiple:g,maxfilesexceeded:g,maxfilesreached:g,queuecomplete:g,addedfiles:g,previewTemplate:'
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n \n Check\n \n \n \n \n \n
    \n
    \n \n Error\n \n \n \n \n \n \n \n
    \n
    '},d=function(){var a,b,c,d,e,f,g;for(d=arguments[0],c=2<=arguments.length?i.call(arguments,1):[],f=0,g=c.length;g>f;f++){b=c[f];for(a in b)e=b[a],d[a]=e}return d},c.prototype.getAcceptedFiles=function(){var a,b,c,d,e;for(d=this.files,e=[],b=0,c=d.length;c>b;b++)a=d[b],a.accepted&&e.push(a);return e},c.prototype.getRejectedFiles=function(){var a,b,c,d,e;for(d=this.files,e=[],b=0,c=d.length;c>b;b++)a=d[b],a.accepted||e.push(a);return e},c.prototype.getFilesWithStatus=function(a){var b,c,d,e,f;for(e=this.files,f=[],c=0,d=e.length;d>c;c++)b=e[c],b.status===a&&f.push(b);return f},c.prototype.getQueuedFiles=function(){return this.getFilesWithStatus(c.QUEUED)},c.prototype.getUploadingFiles=function(){return this.getFilesWithStatus(c.UPLOADING)},c.prototype.getAddedFiles=function(){return this.getFilesWithStatus(c.ADDED)},c.prototype.getActiveFiles=function(){var a,b,d,e,f;for(e=this.files,f=[],b=0,d=e.length;d>b;b++)a=e[b],(a.status===c.UPLOADING||a.status===c.QUEUED)&&f.push(a);return f},c.prototype.init=function(){var a,b,d,e,f,g,h;for("form"===this.element.tagName&&this.element.setAttribute("enctype","multipart/form-data"),this.element.classList.contains("dropzone")&&!this.element.querySelector(".dz-message")&&this.element.appendChild(c.createElement('
    '+this.options.dictDefaultMessage+"
    ")),this.clickableElements.length&&(d=function(a){return function(){return a.hiddenFileInput&&a.hiddenFileInput.parentNode.removeChild(a.hiddenFileInput),a.hiddenFileInput=document.createElement("input"),a.hiddenFileInput.setAttribute("type","file"),(null==a.options.maxFiles||a.options.maxFiles>1)&&a.hiddenFileInput.setAttribute("multiple","multiple"),a.hiddenFileInput.className="dz-hidden-input",null!=a.options.acceptedFiles&&a.hiddenFileInput.setAttribute("accept",a.options.acceptedFiles),null!=a.options.capture&&a.hiddenFileInput.setAttribute("capture",a.options.capture),a.hiddenFileInput.style.visibility="hidden",a.hiddenFileInput.style.position="absolute",a.hiddenFileInput.style.top="0",a.hiddenFileInput.style.left="0",a.hiddenFileInput.style.height="0",a.hiddenFileInput.style.width="0",document.querySelector(a.options.hiddenInputContainer).appendChild(a.hiddenFileInput),a.hiddenFileInput.addEventListener("change",function(){var b,c,e,f;if(c=a.hiddenFileInput.files,c.length)for(e=0,f=c.length;f>e;e++)b=c[e],a.addFile(b);return a.emit("addedfiles",c),d()})}}(this))(),this.URL=null!=(g=window.URL)?g:window.webkitURL,h=this.events,e=0,f=h.length;f>e;e++)a=h[e],this.on(a,this.options[a]);return this.on("uploadprogress",function(a){return function(){return a.updateTotalUploadProgress()}}(this)),this.on("removedfile",function(a){return function(){return a.updateTotalUploadProgress()}}(this)),this.on("canceled",function(a){return function(b){return a.emit("complete",b)}}(this)),this.on("complete",function(a){return function(){return 0===a.getAddedFiles().length&&0===a.getUploadingFiles().length&&0===a.getQueuedFiles().length?setTimeout(function(){return a.emit("queuecomplete")},0):void 0}}(this)),b=function(a){return a.stopPropagation(),a.preventDefault?a.preventDefault():a.returnValue=!1},this.listeners=[{element:this.element,events:{dragstart:function(a){return function(b){return a.emit("dragstart",b)}}(this),dragenter:function(a){return function(c){return b(c),a.emit("dragenter",c)}}(this),dragover:function(a){return function(c){var d;try{d=c.dataTransfer.effectAllowed}catch(e){}return c.dataTransfer.dropEffect="move"===d||"linkMove"===d?"move":"copy",b(c),a.emit("dragover",c)}}(this),dragleave:function(a){return function(b){return a.emit("dragleave",b)}}(this),drop:function(a){return function(c){return b(c),a.drop(c)}}(this),dragend:function(a){return function(b){return a.emit("dragend",b)}}(this)}}],this.clickableElements.forEach(function(a){return function(b){return a.listeners.push({element:b,events:{click:function(d){return(b!==a.element||d.target===a.element||c.elementInside(d.target,a.element.querySelector(".dz-message")))&&a.hiddenFileInput.click(),!0}}})}}(this)),this.enable(),this.options.init.call(this)},c.prototype.destroy=function(){var a;return this.disable(),this.removeAllFiles(!0),(null!=(a=this.hiddenFileInput)?a.parentNode:void 0)&&(this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput),this.hiddenFileInput=null),delete this.element.dropzone,c.instances.splice(c.instances.indexOf(this),1)},c.prototype.updateTotalUploadProgress=function(){var a,b,c,d,e,f,g,h;if(d=0,c=0,a=this.getActiveFiles(),a.length){for(h=this.getActiveFiles(),f=0,g=h.length;g>f;f++)b=h[f],d+=b.upload.bytesSent,c+=b.upload.total;e=100*d/c}else e=100;return this.emit("totaluploadprogress",e,c,d)},c.prototype._getParamName=function(a){return"function"==typeof this.options.paramName?this.options.paramName(a):""+this.options.paramName+(this.options.uploadMultiple?"["+a+"]":"")},c.prototype._renameFilename=function(a){return"function"!=typeof this.options.renameFilename?a:this.options.renameFilename(a)},c.prototype.getFallbackForm=function(){var a,b,d,e;return(a=this.getExistingFallback())?a:(d='
    ',this.options.dictFallbackText&&(d+="

    "+this.options.dictFallbackText+"

    "),d+='
    ',b=c.createElement(d),"FORM"!==this.element.tagName?(e=c.createElement('
    '),e.appendChild(b)):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=e?e:b)},c.prototype.getExistingFallback=function(){var a,b,c,d,e,f;for(b=function(a){var b,c,d;for(c=0,d=a.length;d>c;c++)if(b=a[c],/(^| )fallback($| )/.test(b.className))return b},f=["div","form"],d=0,e=f.length;e>d;d++)if(c=f[d],a=b(this.element.getElementsByTagName(c)))return a},c.prototype.setupEventListeners=function(){var a,b,c,d,e,f,g;for(f=this.listeners,g=[],d=0,e=f.length;e>d;d++)a=f[d],g.push(function(){var d,e;d=a.events,e=[];for(b in d)c=d[b],e.push(a.element.addEventListener(b,c,!1));return e}());return g},c.prototype.removeEventListeners=function(){var a,b,c,d,e,f,g;for(f=this.listeners,g=[],d=0,e=f.length;e>d;d++)a=f[d],g.push(function(){var d,e;d=a.events,e=[];for(b in d)c=d[b],e.push(a.element.removeEventListener(b,c,!1));return e}());return g},c.prototype.disable=function(){var a,b,c,d,e;for(this.clickableElements.forEach(function(a){return a.classList.remove("dz-clickable")}),this.removeEventListeners(),d=this.files,e=[],b=0,c=d.length;c>b;b++)a=d[b],e.push(this.cancelUpload(a));return e},c.prototype.enable=function(){return this.clickableElements.forEach(function(a){return a.classList.add("dz-clickable")}),this.setupEventListeners()},c.prototype.filesize=function(a){var b,c,d,e,f,g,h,i;if(d=0,e="b",a>0){for(g=["TB","GB","MB","KB","b"],c=h=0,i=g.length;i>h;c=++h)if(f=g[c],b=Math.pow(this.options.filesizeBase,4-c)/10,a>=b){d=a/Math.pow(this.options.filesizeBase,4-c),e=f;break}d=Math.round(10*d)/10}return""+d+" "+e},c.prototype._updateMaxFilesReachedClass=function(){return null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(this.getAcceptedFiles().length===this.options.maxFiles&&this.emit("maxfilesreached",this.files),this.element.classList.add("dz-max-files-reached")):this.element.classList.remove("dz-max-files-reached")},c.prototype.drop=function(a){var b,c;a.dataTransfer&&(this.emit("drop",a),b=a.dataTransfer.files,this.emit("addedfiles",b),b.length&&(c=a.dataTransfer.items,c&&c.length&&null!=c[0].webkitGetAsEntry?this._addFilesFromItems(c):this.handleFiles(b)))},c.prototype.paste=function(a){var b,c;if(null!=(null!=a&&null!=(c=a.clipboardData)?c.items:void 0))return this.emit("paste",a),b=a.clipboardData.items,b.length?this._addFilesFromItems(b):void 0},c.prototype.handleFiles=function(a){var b,c,d,e;for(e=[],c=0,d=a.length;d>c;c++)b=a[c],e.push(this.addFile(b));return e},c.prototype._addFilesFromItems=function(a){var b,c,d,e,f;for(f=[],d=0,e=a.length;e>d;d++)c=a[d],f.push(null!=c.webkitGetAsEntry&&(b=c.webkitGetAsEntry())?b.isFile?this.addFile(c.getAsFile()):b.isDirectory?this._addFilesFromDirectory(b,b.name):void 0:null!=c.getAsFile?null==c.kind||"file"===c.kind?this.addFile(c.getAsFile()):void 0:void 0);return f},c.prototype._addFilesFromDirectory=function(a,b){var c,d,e;return c=a.createReader(),d=function(a){return"undefined"!=typeof console&&null!==console&&"function"==typeof console.log?console.log(a):void 0},(e=function(a){return function(){return c.readEntries(function(c){var d,f,g;if(c.length>0){for(f=0,g=c.length;g>f;f++)d=c[f],d.isFile?d.file(function(c){return a.options.ignoreHiddenFiles&&"."===c.name.substring(0,1)?void 0:(c.fullPath=""+b+"/"+c.name,a.addFile(c))}):d.isDirectory&&a._addFilesFromDirectory(d,""+b+"/"+d.name);e()}return null},d)}}(this))()},c.prototype.accept=function(a,b){return a.size>1024*this.options.maxFilesize*1024?b(this.options.dictFileTooBig.replace("{{filesize}}",Math.round(a.size/1024/10.24)/100).replace("{{maxFilesize}}",this.options.maxFilesize)):c.isValidFile(a,this.options.acceptedFiles)?null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(b(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}",this.options.maxFiles)),this.emit("maxfilesexceeded",a)):this.options.accept.call(this,a,b):b(this.options.dictInvalidFileType)},c.prototype.addFile=function(a){return a.upload={progress:0,total:a.size,bytesSent:0},this.files.push(a),a.status=c.ADDED,this.emit("addedfile",a),this._enqueueThumbnail(a),this.accept(a,function(b){return function(c){return c?(a.accepted=!1,b._errorProcessing([a],c)):(a.accepted=!0,b.options.autoQueue&&b.enqueueFile(a)),b._updateMaxFilesReachedClass()}}(this))},c.prototype.enqueueFiles=function(a){var b,c,d;for(c=0,d=a.length;d>c;c++)b=a[c],this.enqueueFile(b);return null},c.prototype.enqueueFile=function(a){if(a.status!==c.ADDED||a.accepted!==!0)throw new Error("This file can't be queued because it has already been processed or was rejected.");return a.status=c.QUEUED,this.options.autoProcessQueue?setTimeout(function(a){return function(){return a.processQueue()}}(this),0):void 0},c.prototype._thumbnailQueue=[],c.prototype._processingThumbnail=!1,c.prototype._enqueueThumbnail=function(a){return this.options.createImageThumbnails&&a.type.match(/image.*/)&&a.size<=1024*this.options.maxThumbnailFilesize*1024?(this._thumbnailQueue.push(a),setTimeout(function(a){return function(){return a._processThumbnailQueue()}}(this),0)):void 0},c.prototype._processThumbnailQueue=function(){return this._processingThumbnail||0===this._thumbnailQueue.length?void 0:(this._processingThumbnail=!0,this.createThumbnail(this._thumbnailQueue.shift(),function(a){return function(){return a._processingThumbnail=!1,a._processThumbnailQueue()}}(this)))},c.prototype.removeFile=function(a){return a.status===c.UPLOADING&&this.cancelUpload(a),this.files=h(this.files,a),this.emit("removedfile",a),0===this.files.length?this.emit("reset"):void 0},c.prototype.removeAllFiles=function(a){var b,d,e,f;for(null==a&&(a=!1),f=this.files.slice(),d=0,e=f.length;e>d;d++)b=f[d],(b.status!==c.UPLOADING||a)&&this.removeFile(b);return null},c.prototype.createThumbnail=function(a,b){var c;return c=new FileReader,c.onload=function(d){return function(){return"image/svg+xml"===a.type?(d.emit("thumbnail",a,c.result),void(null!=b&&b())):d.createThumbnailFromUrl(a,c.result,b)}}(this),c.readAsDataURL(a)},c.prototype.createThumbnailFromUrl=function(a,b,c,d){var e;return e=document.createElement("img"),d&&(e.crossOrigin=d),e.onload=function(b){return function(){var d,g,h,i,j,k,l,m;return a.width=e.width,a.height=e.height,h=b.options.resize.call(b,a),null==h.trgWidth&&(h.trgWidth=h.optWidth),null==h.trgHeight&&(h.trgHeight=h.optHeight),d=document.createElement("canvas"),g=d.getContext("2d"),d.width=h.trgWidth,d.height=h.trgHeight,f(g,e,null!=(j=h.srcX)?j:0,null!=(k=h.srcY)?k:0,h.srcWidth,h.srcHeight,null!=(l=h.trgX)?l:0,null!=(m=h.trgY)?m:0,h.trgWidth,h.trgHeight),i=d.toDataURL("image/png"),b.emit("thumbnail",a,i),null!=c?c():void 0}}(this),null!=c&&(e.onerror=c),e.src=b},c.prototype.processQueue=function(){var a,b,c,d;if(b=this.options.parallelUploads,c=this.getUploadingFiles().length,a=c,!(c>=b)&&(d=this.getQueuedFiles(),d.length>0)){if(this.options.uploadMultiple)return this.processFiles(d.slice(0,b-c));for(;b>a;){if(!d.length)return;this.processFile(d.shift()),a++}}},c.prototype.processFile=function(a){return this.processFiles([a])},c.prototype.processFiles=function(a){var b,d,e;for(d=0,e=a.length;e>d;d++)b=a[d],b.processing=!0,b.status=c.UPLOADING,this.emit("processing",b);return this.options.uploadMultiple&&this.emit("processingmultiple",a),this.uploadFiles(a)},c.prototype._getFilesWithXhr=function(a){var b,c;return c=function(){var c,d,e,f;for(e=this.files,f=[],c=0,d=e.length;d>c;c++)b=e[c],b.xhr===a&&f.push(b);return f}.call(this)},c.prototype.cancelUpload=function(a){var b,d,e,f,g,h,i;if(a.status===c.UPLOADING){for(d=this._getFilesWithXhr(a.xhr),e=0,g=d.length;g>e;e++)b=d[e],b.status=c.CANCELED;for(a.xhr.abort(),f=0,h=d.length;h>f;f++)b=d[f],this.emit("canceled",b);this.options.uploadMultiple&&this.emit("canceledmultiple",d)}else((i=a.status)===c.ADDED||i===c.QUEUED)&&(a.status=c.CANCELED,this.emit("canceled",a),this.options.uploadMultiple&&this.emit("canceledmultiple",[a]));return this.options.autoProcessQueue?this.processQueue():void 0},e=function(){var a,b;return b=arguments[0],a=2<=arguments.length?i.call(arguments,1):[],"function"==typeof b?b.apply(this,a):b},c.prototype.uploadFile=function(a){return this.uploadFiles([a])},c.prototype.uploadFiles=function(a){var b,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L;for(w=new XMLHttpRequest,x=0,B=a.length;B>x;x++)b=a[x],b.xhr=w;p=e(this.options.method,a),u=e(this.options.url,a),w.open(p,u,!0),w.withCredentials=!!this.options.withCredentials,s=null,g=function(c){return function(){var d,e,f;for(f=[],d=0,e=a.length;e>d;d++)b=a[d],f.push(c._errorProcessing(a,s||c.options.dictResponseError.replace("{{statusCode}}",w.status),w));return f}}(this),t=function(c){return function(d){var e,f,g,h,i,j,k,l,m;if(null!=d)for(f=100*d.loaded/d.total,g=0,j=a.length;j>g;g++)b=a[g],b.upload={progress:f,total:d.total,bytesSent:d.loaded};else{for(e=!0,f=100,h=0,k=a.length;k>h;h++)b=a[h],(100!==b.upload.progress||b.upload.bytesSent!==b.upload.total)&&(e=!1),b.upload.progress=f,b.upload.bytesSent=b.upload.total;if(e)return}for(m=[],i=0,l=a.length;l>i;i++)b=a[i],m.push(c.emit("uploadprogress",b,f,b.upload.bytesSent));return m}}(this),w.onload=function(b){return function(d){var e;if(a[0].status!==c.CANCELED&&4===w.readyState){if(s=w.responseText,w.getResponseHeader("content-type")&&~w.getResponseHeader("content-type").indexOf("application/json"))try{s=JSON.parse(s)}catch(f){d=f,s="Invalid JSON response from server."}return t(),200<=(e=w.status)&&300>e?b._finished(a,s,d):g()}}}(this),w.onerror=function(){return function(){return a[0].status!==c.CANCELED?g():void 0}}(this),r=null!=(G=w.upload)?G:w,r.onprogress=t,j={Accept:"application/json","Cache-Control":"no-cache","X-Requested-With":"XMLHttpRequest"},this.options.headers&&d(j,this.options.headers);for(h in j)i=j[h],i&&w.setRequestHeader(h,i);if(f=new FormData,this.options.params){H=this.options.params;for(o in H)v=H[o],f.append(o,v)}for(y=0,C=a.length;C>y;y++)b=a[y],this.emit("sending",b,w,f);if(this.options.uploadMultiple&&this.emit("sendingmultiple",a,w,f),"FORM"===this.element.tagName)for(I=this.element.querySelectorAll("input, textarea, select, button"),z=0,D=I.length;D>z;z++)if(l=I[z],m=l.getAttribute("name"),n=l.getAttribute("type"),"SELECT"===l.tagName&&l.hasAttribute("multiple"))for(J=l.options,A=0,E=J.length;E>A;A++)q=J[A],q.selected&&f.append(m,q.value);else(!n||"checkbox"!==(K=n.toLowerCase())&&"radio"!==K||l.checked)&&f.append(m,l.value);for(k=F=0,L=a.length-1;L>=0?L>=F:F>=L;k=L>=0?++F:--F)f.append(this._getParamName(k),a[k],this._renameFilename(a[k].name));return this.submitRequest(w,f,a)},c.prototype.submitRequest=function(a,b){return a.send(b)},c.prototype._finished=function(a,b,d){var e,f,g;for(f=0,g=a.length;g>f;f++)e=a[f],e.status=c.SUCCESS,this.emit("success",e,b,d),this.emit("complete",e);return this.options.uploadMultiple&&(this.emit("successmultiple",a,b,d),this.emit("completemultiple",a)),this.options.autoProcessQueue?this.processQueue():void 0},c.prototype._errorProcessing=function(a,b,d){var e,f,g;for(f=0,g=a.length;g>f;f++)e=a[f],e.status=c.ERROR,this.emit("error",e,b,d),this.emit("complete",e);return this.options.uploadMultiple&&(this.emit("errormultiple",a,b,d),this.emit("completemultiple",a)),this.options.autoProcessQueue?this.processQueue():void 0},c}(b),a.version="4.3.0",a.options={},a.optionsForElement=function(b){return b.getAttribute("id")?a.options[c(b.getAttribute("id"))]:void 0},a.instances=[],a.forElement=function(a){if("string"==typeof a&&(a=document.querySelector(a)),null==(null!=a?a.dropzone:void 0))throw new Error("No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone.");return a.dropzone},a.autoDiscover=!0,a.discover=function(){var b,c,d,e,f,g;for(document.querySelectorAll?d=document.querySelectorAll(".dropzone"):(d=[],b=function(a){var b,c,e,f;for(f=[],c=0,e=a.length;e>c;c++)b=a[c],f.push(/(^| )dropzone($| )/.test(b.className)?d.push(b):void 0);return f},b(document.getElementsByTagName("div")),b(document.getElementsByTagName("form"))),g=[],e=0,f=d.length;f>e;e++)c=d[e],g.push(a.optionsForElement(c)!==!1?new a(c):void 0);return g},a.blacklistedBrowsers=[/opera.*Macintosh.*version\/12/i],a.isBrowserSupported=function(){var b,c,d,e,f;if(b=!0,window.File&&window.FileReader&&window.FileList&&window.Blob&&window.FormData&&document.querySelector)if("classList"in document.createElement("a"))for(f=a.blacklistedBrowsers,d=0,e=f.length;e>d;d++)c=f[d],c.test(navigator.userAgent)&&(b=!1);else b=!1;else b=!1;return b},h=function(a,b){var c,d,e,f;for(f=[],d=0,e=a.length;e>d;d++)c=a[d],c!==b&&f.push(c);return f},c=function(a){return a.replace(/[\-_](\w)/g,function(a){return a.charAt(1).toUpperCase()})},a.createElement=function(a){var b;return b=document.createElement("div"),b.innerHTML=a,b.childNodes[0]},a.elementInside=function(a,b){if(a===b)return!0;for(;a=a.parentNode;)if(a===b)return!0;return!1},a.getElement=function(a,b){var c;if("string"==typeof a?c=document.querySelector(a):null!=a.nodeType&&(c=a),null==c)throw new Error("Invalid `"+b+"` option provided. Please provide a CSS selector or a plain HTML element.");return c},a.getElements=function(a,b){var c,d,e,f,g,h,i,j;if(a instanceof Array){e=[];try{for(f=0,h=a.length;h>f;f++)d=a[f],e.push(this.getElement(d,b))}catch(k){c=k,e=null}}else if("string"==typeof a)for(e=[],j=document.querySelectorAll(a),g=0,i=j.length;i>g;g++)d=j[g],e.push(d);else null!=a.nodeType&&(e=[a]);if(null==e||!e.length)throw new Error("Invalid `"+b+"` option provided. Please provide a CSS selector, a plain HTML element or a list of those.");return e},a.confirm=function(a,b,c){return window.confirm(a)?b():null!=c?c():void 0},a.isValidFile=function(a,b){var c,d,e,f,g;if(!b)return!0;for(b=b.split(","),d=a.type,c=d.replace(/\/.*$/,""),f=0,g=b.length;g>f;f++)if(e=b[f],e=e.trim(),"."===e.charAt(0)){if(-1!==a.name.toLowerCase().indexOf(e.toLowerCase(),a.name.length-e.length))return!0}else if(/\/\*$/.test(e)){if(c===e.replace(/\/.*$/,""))return!0 +}else if(d===e)return!0;return!1},"undefined"!=typeof jQuery&&null!==jQuery&&(jQuery.fn.dropzone=function(b){return this.each(function(){return new a(this,b)})}),"undefined"!=typeof module&&null!==module?module.exports=a:window.Dropzone=a,a.ADDED="added",a.QUEUED="queued",a.ACCEPTED=a.QUEUED,a.UPLOADING="uploading",a.PROCESSING=a.UPLOADING,a.CANCELED="canceled",a.ERROR="error",a.SUCCESS="success",e=function(a){var b,c,d,e,f,g,h,i,j,k;for(h=a.naturalWidth,g=a.naturalHeight,c=document.createElement("canvas"),c.width=1,c.height=g,d=c.getContext("2d"),d.drawImage(a,0,0),e=d.getImageData(0,0,1,g).data,k=0,f=g,i=g;i>k;)b=e[4*(i-1)+3],0===b?f=i:k=i,i=f+k>>1;return j=i/g,0===j?1:j},f=function(a,b,c,d,f,g,h,i,j,k){var l;return l=e(b),a.drawImage(b,c,d,f,g,h,i,j,k/l)},d=function(a,b){var c,d,e,f,g,h,i,j,k;if(e=!1,k=!0,d=a.document,j=d.documentElement,c=d.addEventListener?"addEventListener":"attachEvent",i=d.addEventListener?"removeEventListener":"detachEvent",h=d.addEventListener?"":"on",f=function(c){return"readystatechange"!==c.type||"complete"===d.readyState?(("load"===c.type?a:d)[i](h+c.type,f,!1),!e&&(e=!0)?b.call(a,c.type||c):void 0):void 0},g=function(){var a;try{j.doScroll("left")}catch(b){return a=b,void setTimeout(g,50)}return f("poll")},"complete"!==d.readyState){if(d.createEventObject&&j.doScroll){try{k=!a.frameElement}catch(l){}k&&g()}return d[c](h+"DOMContentLoaded",f,!1),d[c](h+"readystatechange",f,!1),a[c](h+"load",f,!1)}},a._autoDiscoverFunction=function(){return a.autoDiscover?a.discover():void 0},d(window,a._autoDiscoverFunction)}).call(this); /*! * typeahead.js 0.11.1 * https://github.com/twitter/typeahead.js @@ -30482,6 +30484,11 @@ function calculateAmounts(invoice) { var hasTaxes = false; var taxes = {}; invoice.has_product_key = false; + + // Bold designs currently breaks w/o the product column + if (invoice.invoice_design_id == 2) { + invoice.has_product_key = true; + } // sum line item for (var i=0; i 0 && field == 'balance_due') { field = 'partial_due'; } else if (invoice.is_quote) { field = field.replace('invoice', 'quote'); @@ -31237,13 +31254,13 @@ NINJA.notesAndTerms = function(invoice) var data = []; if (invoice.public_notes) { - data.push({stack:[{text: invoice.public_notes, style: ['notes']}]}); + data.push({stack:[{text: invoice.is_recurring ? processVariables(invoice.public_notes) : invoice.public_notes, style: ['notes']}]}); data.push({text:' '}); } if (invoice.terms) { data.push({text:invoiceLabels.terms, style: ['termsLabel']}); - data.push({stack:[{text: invoice.terms, style: ['terms']}]}); + data.push({stack:[{text: invoice.is_recurring ? processVariables(invoice.terms) : invoice.terms, style: ['terms']}]}); } return NINJA.prepareDataList(data, 'notesAndTerms'); @@ -31260,16 +31277,16 @@ NINJA.invoiceColumns = function(invoice) columns.push("*") - if (invoice.is_pro && account.custom_invoice_item_label1) { + if (invoice.features.invoice_settings && account.custom_invoice_item_label1) { columns.push("10%"); } - if (invoice.is_pro && account.custom_invoice_item_label2) { + if (invoice.features.invoice_settings && account.custom_invoice_item_label2) { columns.push("10%"); } var count = 3; if (account.hide_quantity == '1') { - count--; + count -= 2; } if (account.show_item_taxes == '1') { count++; @@ -31283,10 +31300,16 @@ NINJA.invoiceColumns = function(invoice) NINJA.invoiceFooter = function(invoice) { - if (!invoice.is_pro && invoice.invoice_design_id == 3) { - return invoice.invoice_footer ? invoice.invoice_footer.substring(0, 200) : ' '; + var footer = invoice.invoice_footer; + + if (invoice.is_recurring) { + footer = processVariables(footer); + } + + if (!invoice.features.invoice_settings && invoice.invoice_design_id == 3) { + return footer ? footer.substring(0, 200) : ' '; } else { - return invoice.invoice_footer || ' '; + return footer || ' '; } } @@ -31315,16 +31338,15 @@ NINJA.invoiceLines = function(invoice) { grid[0].push({text: invoiceLabels.description, style: ['tableHeader', 'descriptionTableHeader']}); - if (invoice.is_pro && account.custom_invoice_item_label1) { + if (invoice.features.invoice_settings && account.custom_invoice_item_label1) { grid[0].push({text: account.custom_invoice_item_label1, style: ['tableHeader', 'custom1TableHeader']}); } - if (invoice.is_pro && account.custom_invoice_item_label2) { + if (invoice.features.invoice_ettings && account.custom_invoice_item_label2) { grid[0].push({text: account.custom_invoice_item_label2, style: ['tableHeader', 'custom2TableHeader']}); - } - - grid[0].push({text: invoiceLabels.unit_cost, style: ['tableHeader', 'costTableHeader']}); + } if (!hideQuantity) { + grid[0].push({text: invoiceLabels.unit_cost, style: ['tableHeader', 'costTableHeader']}); grid[0].push({text: invoiceLabels.quantity, style: ['tableHeader', 'qtyTableHeader']}); } if (showItemTaxes) { @@ -31341,13 +31363,17 @@ NINJA.invoiceLines = function(invoice) { var qty = NINJA.parseFloat(item.qty) ? roundToTwo(NINJA.parseFloat(item.qty)) + '' : ''; var notes = item.notes; var productKey = item.product_key; - var tax = ''; + var tax1 = ''; + var tax2 = ''; + var custom_value1 = item.custom_value1; + var custom_value2 = item.custom_value2; if (showItemTaxes) { - if (item.tax && parseFloat(item.tax.rate)) { - tax = parseFloat(item.tax.rate); - } else if (item.tax_rate && parseFloat(item.tax_rate)) { - tax = parseFloat(item.tax_rate); + if (item.tax_name1) { + tax1 = parseFloat(item.tax_rate1); + } + if (item.tax_name2) { + tax2 = parseFloat(item.tax_rate2); } } @@ -31362,6 +31388,8 @@ NINJA.invoiceLines = function(invoice) { if (invoice.is_recurring) { notes = processVariables(notes); productKey = processVariables(productKey); + custom_value1 = processVariables(item.custom_value1); + custom_value2 = processVariables(item.custom_value2); } var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); @@ -31373,18 +31401,28 @@ NINJA.invoiceLines = function(invoice) { row.push({style:["productKey", rowStyle], text:productKey || ' '}); // product key can be blank when selecting from a datalist } row.push({style:["notes", rowStyle], stack:[{text:notes || ' '}]}); - if (invoice.is_pro && account.custom_invoice_item_label1) { - row.push({style:["customValue1", rowStyle], text:item.custom_value1 || ' '}); + if (invoice.features.invoice_settings && account.custom_invoice_item_label1) { + row.push({style:["customValue1", rowStyle], text:custom_value1 || ' '}); } - if (invoice.is_pro && account.custom_invoice_item_label2) { - row.push({style:["customValue2", rowStyle], text:item.custom_value2 || ' '}); + if (invoice.features.invoice_settings && account.custom_invoice_item_label2) { + row.push({style:["customValue2", rowStyle], text:custom_value2 || ' '}); } - row.push({style:["cost", rowStyle], text:cost}); if (!hideQuantity) { + row.push({style:["cost", rowStyle], text:cost}); row.push({style:["quantity", rowStyle], text:qty || ' '}); } if (showItemTaxes) { - row.push({style:["tax", rowStyle], text:tax ? (tax.toString() + '%') : ' '}); + var str = ' '; + if (tax1) { + str += tax1.toString() + '%'; + } + if (tax2) { + if (tax1) { + str += ' '; + } + str += tax2.toString() + '%'; + } + row.push({style:["tax", rowStyle], text:str}); } row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '}); @@ -31394,6 +31432,39 @@ NINJA.invoiceLines = function(invoice) { return NINJA.prepareDataTable(grid, 'invoiceItems'); } +NINJA.invoiceDocuments = function(invoice) { + if(!invoice.account.invoice_embed_documents)return[]; + var stack = []; + var stackItem = null; + + var j = 0; + for (var i = 0; i < invoice.documents.length; i++)addDoc(invoice.documents[i]); + + if(invoice.expenses){ + for (var i = 0; i < invoice.expenses.length; i++) { + var expense = invoice.expenses[i]; + for (var i = 0; i < expense.documents.length; i++)addDoc(expense.documents[i]); + } + } + + function addDoc(document){ + var path = document.base64; + + if(!path)path = 'docs/'+document.public_id+'/'+document.name; + if(path && (window.pdfMake.vfs[path] || document.base64)){ + // Only embed if we actually have an image for it + if(j%3==0){ + stackItem = {columns:[]}; + stack.push(stackItem); + } + stackItem.columns.push({stack:[{image:path,style:'invoiceDocument',fit:[150,150]}], width:175}) + j++; + } + } + + return stack.length?{stack:stack}:[]; +} + NINJA.subtotals = function(invoice, hideBalance) { if (!invoice) { @@ -31417,15 +31488,19 @@ NINJA.subtotals = function(invoice, hideBalance) for (var key in invoice.item_taxes) { if (invoice.item_taxes.hasOwnProperty(key)) { - var taxRate = invoice.item_taxes[key]; + var taxRate = invoice.item_taxes[key]; var taxStr = taxRate.name + ' ' + (taxRate.rate*1).toString() + '%'; data.push([{text: taxStr}, {text: formatMoneyInvoice(taxRate.amount, invoice)}]); } } - if (invoice.tax && invoice.tax.name || invoice.tax_name) { - var taxStr = invoice.tax_name + ' ' + (invoice.tax_rate*1).toString() + '%'; - data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount, invoice)}]); + if (invoice.tax_amount1) { + var taxStr = invoice.tax_name1 + ' ' + (invoice.tax_rate1*1).toString() + '%'; + data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount1, invoice)}]); + } + if (invoice.tax_amount2) { + var taxStr = invoice.tax_name2 + ' ' + (invoice.tax_rate2*1).toString() + '%'; + data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount2, invoice)}]); } if (NINJA.parseFloat(invoice.custom_value1) && invoice.custom_taxes1 != '1') { @@ -31496,7 +31571,7 @@ NINJA.accountAddress = function(invoice) { {text: account.country ? account.country.name : ''}, ]; - if (invoice.is_pro) { + if (invoice.features.invoice_settings) { data.push({text: invoice.account.custom_value1 ? invoice.account.custom_label1 + ' ' + invoice.account.custom_value1 : false}); data.push({text: invoice.account.custom_value2 ? invoice.account.custom_label2 + ' ' + invoice.account.custom_value2 : false}); } @@ -31521,42 +31596,35 @@ NINJA.invoiceDetails = function(invoice) { ], [ {text: (invoice.is_quote ? invoiceLabels.valid_until : invoiceLabels.due_date)}, - {text: invoice.due_date} + {text: invoice.is_recurring ? false : invoice.due_date} ] ]; if (invoice.custom_text_value1) { data.push([ {text: invoice.account.custom_invoice_text_label1}, - {text: invoice.custom_text_value1} + {text: invoice.is_recurring ? processVariables(invoice.custom_text_value1) : invoice.custom_text_value1} ]) } if (invoice.custom_text_value2) { data.push([ {text: invoice.account.custom_invoice_text_label2}, - {text: invoice.custom_text_value2} + {text: invoice.is_recurring ? processVariables(invoice.custom_text_value2) : invoice.custom_text_value2} ]) } - var isPartial = NINJA.parseFloat(invoice.partial); - - if (NINJA.parseFloat(invoice.balance) < NINJA.parseFloat(invoice.amount)) { - data.push([ - {text: invoiceLabels.balance_due}, - {text: formatMoneyInvoice(invoice.amount, invoice)} - ]); - } else if (isPartial) { - data.push([ - {text: invoiceLabels.balance_due}, - {text: formatMoneyInvoice(invoice.total_amount, invoice)} - ]); - } - data.push([ - {text: isPartial ? invoiceLabels.partial_due : invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']}, - {text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['invoiceDetailBalanceDue']} + {text: invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']}, + {text: formatMoneyInvoice(invoice.total_amount, invoice), style: ['invoiceDetailBalanceDue']} ]) + if (NINJA.parseFloat(invoice.partial)) { + data.push([ + {text: invoiceLabels.partial_due, style: ['invoiceDetailBalanceDueLabel']}, + {text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['invoiceDetailBalanceDue']} + ]) + } + return NINJA.prepareDataPairs(data, 'invoiceDetails'); } diff --git a/public/css/built.css b/public/css/built.css index 8e2782ebbffc..3c7c53828f1c 100644 --- a/public/css/built.css +++ b/public/css/built.css @@ -1464,6 +1464,8 @@ fieldset[disabled] .datepicker table tr td span.active.disabled:hover.active { margin-right: -5px; } +@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,0.3);background:white;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:0.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:white}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-ms-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,0.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,0.8);background-color:rgba(255,255,255,0.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,0.4);padding:0 0.4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05, 1.05);-moz-transform:scale(1.05, 1.05);-ms-transform:scale(1.05, 1.05);-o-transform:scale(1.05, 1.05);transform:scale(1.05, 1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;-o-transition:all 0.2s linear;transition:all 0.2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity 0.4s ease-in;-moz-transition:opacity 0.4s ease-in;-ms-transition:opacity 0.4s ease-in;-o-transition:opacity 0.4s ease-in;transition:opacity 0.4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,0.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom, #666, #444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width 300ms ease-in-out;-moz-transition:width 300ms ease-in-out;-ms-transition:width 300ms ease-in-out;-o-transition:width 300ms ease-in-out;transition:width 300ms ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity 0.3s ease;-moz-transition:opacity 0.3s ease;-ms-transition:opacity 0.3s ease;-o-transition:opacity 0.3s ease;transition:opacity 0.3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom, #be2626, #a92222);padding:0.5em 1.2em;color:white}.dropzone .dz-preview .dz-error-message:after{content:'';position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626} + /*** Spectrum Colorpicker v1.3.4 https://github.com/bgrins/spectrum @@ -2530,13 +2532,22 @@ font-weight: bold; } .navbar, -.panel-default, ul.dropdown-menu, -.twitter-typeahead .tt-menu, +.twitter-typeahead .tt-menu { + x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + box-shadow: 0 0 10px 2px rgba(0,0,0,.05); +} + +.twitter-typeahead .tt-menu { + overflow-x: hidden; +} + +.panel-default, canvas { - x-moz-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - x-webkit-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - box-shadow: 0 0 1px 1px rgba(0,0,0,.05); + border: 1px solid; + border-color: #e5e6e9 #dfe0e4 #d0d1d5; + border-radius: 3px; } .navbar .active > a { @@ -3188,4 +3199,43 @@ td.right { div.panel-body div.panel-body { padding-bottom: 0px; +} + +/* Attached Documents */ +#document-upload { + border:1px solid #ebe7e7; + background:#f9f9f9 !important; + border-radius:3px; + padding:20px; +} + +.invoice-table #document-upload{ + width:500px; +} + +#document-upload .dropzone{ + background:none; + border:none; + padding:0; +} + +.dropzone .dz-preview.dz-image-preview{ + background:none; +} + +.dropzone .dz-preview .dz-image{ + border-radius:5px!important; +} + +.dropzone .dz-preview.dz-image-preview .dz-image img{ + object-fit: cover; + width: 100%; + height: 100%; +} + +.dropzone .fallback-doc{ + display:none; +} +.dropzone.dz-browser-not-supported .fallback-doc{ + display:block; } \ No newline at end of file diff --git a/public/css/built.public.css b/public/css/built.public.css index 3e91dc0ed193..2a02c2db94b6 100644 --- a/public/css/built.public.css +++ b/public/css/built.public.css @@ -805,9 +805,9 @@ html { } .navbar { - x-moz-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - x-webkit-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - box-shadow: 0 0 1px 1px rgba(0,0,0,.05); + x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + box-shadow: 0 0 10px 2px rgba(0,0,0,.05); } #footer { diff --git a/public/css/public.style.css b/public/css/public.style.css index 95b158cb9b62..b1f91c25739f 100644 --- a/public/css/public.style.css +++ b/public/css/public.style.css @@ -22,9 +22,9 @@ html { } .navbar { - x-moz-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - x-webkit-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - box-shadow: 0 0 1px 1px rgba(0,0,0,.05); + x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + box-shadow: 0 0 10px 2px rgba(0,0,0,.05); } #footer { diff --git a/public/css/style.css b/public/css/style.css index 2cf0cdceca24..801141c03cc9 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -403,13 +403,22 @@ font-weight: bold; } .navbar, -.panel-default, ul.dropdown-menu, -.twitter-typeahead .tt-menu, +.twitter-typeahead .tt-menu { + x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + box-shadow: 0 0 10px 2px rgba(0,0,0,.05); +} + +.twitter-typeahead .tt-menu { + overflow-x: hidden; +} + +.panel-default, canvas { - x-moz-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - x-webkit-box-shadow: 0 0 1px 1px rgba(0,0,0,.05); - box-shadow: 0 0 1px 1px rgba(0,0,0,.05); + border: 1px solid; + border-color: #e5e6e9 #dfe0e4 #d0d1d5; + border-radius: 3px; } .navbar .active > a { @@ -1061,4 +1070,43 @@ td.right { div.panel-body div.panel-body { padding-bottom: 0px; +} + +/* Attached Documents */ +#document-upload { + border:1px solid #ebe7e7; + background:#f9f9f9 !important; + border-radius:3px; + padding:20px; +} + +.invoice-table #document-upload{ + width:500px; +} + +#document-upload .dropzone{ + background:none; + border:none; + padding:0; +} + +.dropzone .dz-preview.dz-image-preview{ + background:none; +} + +.dropzone .dz-preview .dz-image{ + border-radius:5px!important; +} + +.dropzone .dz-preview.dz-image-preview .dz-image img{ + object-fit: cover; + width: 100%; + height: 100%; +} + +.dropzone .fallback-doc{ + display:none; +} +.dropzone.dz-browser-not-supported .fallback-doc{ + display:block; } \ No newline at end of file diff --git a/public/js/compatibility.js b/public/js/compatibility.js index 8ca68137fb5e..c417d7185921 100644 --- a/public/js/compatibility.js +++ b/public/js/compatibility.js @@ -1,5 +1,3 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,9 +23,10 @@ if (typeof PDFJS === 'undefined') { } // Checking if the typed arrays are supported +// Support: iOS<6.0 (subarray), IE<10, Android<4.0 (function checkTypedArrayCompatibility() { if (typeof Uint8Array !== 'undefined') { - // some mobile versions do not support subarray (e.g. safari 5 / iOS) + // Support: iOS<6.0 if (typeof Uint8Array.prototype.subarray === 'undefined') { Uint8Array.prototype.subarray = function subarray(start, end) { return new Uint8Array(this.slice(start, end)); @@ -37,10 +36,10 @@ if (typeof PDFJS === 'undefined') { }; } - // some mobile version might not support Float64Array - if (typeof Float64Array === 'undefined') + // Support: Android<4.1 + if (typeof Float64Array === 'undefined') { window.Float64Array = Float32Array; - + } return; } @@ -49,23 +48,26 @@ if (typeof PDFJS === 'undefined') { } function setArrayOffset(array, offset) { - if (arguments.length < 2) + if (arguments.length < 2) { offset = 0; - for (var i = 0, n = array.length; i < n; ++i, ++offset) + } + for (var i = 0, n = array.length; i < n; ++i, ++offset) { this[offset] = array[i] & 0xFF; + } } function TypedArray(arg1) { - var result; + var result, i, n; if (typeof arg1 === 'number') { result = []; - for (var i = 0; i < arg1; ++i) + for (i = 0; i < arg1; ++i) { result[i] = 0; + } } else if ('slice' in arg1) { result = arg1.slice(0); } else { result = []; - for (var i = 0, n = arg1.length; i < n; ++i) { + for (i = 0, n = arg1.length; i < n; ++i) { result[i] = arg1[i]; } } @@ -75,13 +77,14 @@ if (typeof PDFJS === 'undefined') { result.byteLength = result.length; result.set = setArrayOffset; - if (typeof arg1 === 'object' && arg1.buffer) + if (typeof arg1 === 'object' && arg1.buffer) { result.buffer = arg1.buffer; - + } return result; } window.Uint8Array = TypedArray; + window.Int8Array = TypedArray; // we don't need support for set, byteLength for 32-bit array // so we can use the TypedArray as well @@ -93,25 +96,15 @@ if (typeof PDFJS === 'undefined') { })(); // URL = URL || webkitURL +// Support: Safari<7, Android 4.2+ (function normalizeURLObject() { if (!window.URL) { window.URL = window.webkitURL; } })(); -// Object.create() ? -(function checkObjectCreateCompatibility() { - if (typeof Object.create !== 'undefined') - return; - - Object.create = function objectCreate(proto) { - function Constructor() {} - Constructor.prototype = proto; - return new Constructor(); - }; -})(); - -// Object.defineProperty() ? +// Object.defineProperty()? +// Support: Android<4.0, Safari<5.1 (function checkObjectDefinePropertyCompatibility() { if (typeof Object.defineProperty !== 'undefined') { var definePropertyPossible = true; @@ -127,15 +120,19 @@ if (typeof PDFJS === 'undefined') { } catch (e) { definePropertyPossible = false; } - if (definePropertyPossible) return; + if (definePropertyPossible) { + return; + } } Object.defineProperty = function objectDefineProperty(obj, name, def) { delete obj[name]; - if ('get' in def) + if ('get' in def) { obj.__defineGetter__(name, def['get']); - if ('set' in def) + } + if ('set' in def) { obj.__defineSetter__(name, def['set']); + } if ('value' in def) { obj.__defineSetter__(name, function objectDefinePropertySetter(value) { this.__defineGetter__(name, function objectDefinePropertyGetter() { @@ -148,105 +145,77 @@ if (typeof PDFJS === 'undefined') { }; })(); -// Object.keys() ? -(function checkObjectKeysCompatibility() { - if (typeof Object.keys !== 'undefined') - return; - Object.keys = function objectKeys(obj) { - var result = []; - for (var i in obj) { - if (obj.hasOwnProperty(i)) - result.push(i); - } - return result; - }; -})(); - -// No readAsArrayBuffer ? -(function checkFileReaderReadAsArrayBuffer() { - if (typeof FileReader === 'undefined') - return; // FileReader is not implemented - var frPrototype = FileReader.prototype; - // Older versions of Firefox might not have readAsArrayBuffer - if ('readAsArrayBuffer' in frPrototype) - return; // readAsArrayBuffer is implemented - Object.defineProperty(frPrototype, 'readAsArrayBuffer', { - value: function fileReaderReadAsArrayBuffer(blob) { - var fileReader = new FileReader(); - var originalReader = this; - fileReader.onload = function fileReaderOnload(evt) { - var data = evt.target.result; - var buffer = new ArrayBuffer(data.length); - var uint8Array = new Uint8Array(buffer); - - for (var i = 0, ii = data.length; i < ii; i++) - uint8Array[i] = data.charCodeAt(i); - - Object.defineProperty(originalReader, 'result', { - value: buffer, - enumerable: true, - writable: false, - configurable: true - }); - - var event = document.createEvent('HTMLEvents'); - event.initEvent('load', false, false); - originalReader.dispatchEvent(event); - }; - fileReader.readAsBinaryString(blob); - } - }); -})(); - -// No XMLHttpRequest.response ? +// No XMLHttpRequest#response? +// Support: IE<11, Android <4.0 (function checkXMLHttpRequestResponseCompatibility() { var xhrPrototype = XMLHttpRequest.prototype; - if (!('overrideMimeType' in xhrPrototype)) { + var xhr = new XMLHttpRequest(); + if (!('overrideMimeType' in xhr)) { // IE10 might have response, but not overrideMimeType + // Support: IE10 Object.defineProperty(xhrPrototype, 'overrideMimeType', { value: function xmlHttpRequestOverrideMimeType(mimeType) {} }); } - if ('response' in xhrPrototype || - 'mozResponseArrayBuffer' in xhrPrototype || - 'mozResponse' in xhrPrototype || - 'responseArrayBuffer' in xhrPrototype) + if ('responseType' in xhr) { return; - // IE9 ? + } + + // The worker will be using XHR, so we can save time and disable worker. + PDFJS.disableWorker = true; + + Object.defineProperty(xhrPrototype, 'responseType', { + get: function xmlHttpRequestGetResponseType() { + return this._responseType || 'text'; + }, + set: function xmlHttpRequestSetResponseType(value) { + if (value === 'text' || value === 'arraybuffer') { + this._responseType = value; + if (value === 'arraybuffer' && + typeof this.overrideMimeType === 'function') { + this.overrideMimeType('text/plain; charset=x-user-defined'); + } + } + } + }); + + // Support: IE9 if (typeof VBArray !== 'undefined') { Object.defineProperty(xhrPrototype, 'response', { get: function xmlHttpRequestResponseGet() { - return new Uint8Array(new VBArray(this.responseBody).toArray()); + if (this.responseType === 'arraybuffer') { + return new Uint8Array(new VBArray(this.responseBody).toArray()); + } else { + return this.responseText; + } } }); return; } - // other browsers - function responseTypeSetter() { - // will be only called to set "arraybuffer" - this.overrideMimeType('text/plain; charset=x-user-defined'); - } - if (typeof xhrPrototype.overrideMimeType === 'function') { - Object.defineProperty(xhrPrototype, 'responseType', - { set: responseTypeSetter }); - } - function responseGetter() { - var text = this.responseText; - var i, n = text.length; - var result = new Uint8Array(n); - for (i = 0; i < n; ++i) - result[i] = text.charCodeAt(i) & 0xFF; - return result; - } - Object.defineProperty(xhrPrototype, 'response', { get: responseGetter }); + Object.defineProperty(xhrPrototype, 'response', { + get: function xmlHttpRequestResponseGet() { + if (this.responseType !== 'arraybuffer') { + return this.responseText; + } + var text = this.responseText; + var i, n = text.length; + var result = new Uint8Array(n); + for (i = 0; i < n; ++i) { + result[i] = text.charCodeAt(i) & 0xFF; + } + return result.buffer; + } + }); })(); // window.btoa (base64 encode function) ? +// Support: IE<10 (function checkWindowBtoaCompatibility() { - if ('btoa' in window) + if ('btoa' in window) { return; + } var digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; @@ -268,17 +237,21 @@ if (typeof PDFJS === 'undefined') { }; })(); -// window.atob (base64 encode function) ? +// window.atob (base64 encode function)? +// Support: IE<10 (function checkWindowAtobCompatibility() { - if ('atob' in window) + if ('atob' in window) { return; + } // https://github.com/davidchambers/Base64.js var digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; window.atob = function (input) { input = input.replace(/=+$/, ''); - if (input.length % 4 == 1) throw new Error('bad atob input'); + if (input.length % 4 === 1) { + throw new Error('bad atob input'); + } for ( // initialize result and counters var bc = 0, bs, buffer, idx = 0, output = ''; @@ -298,15 +271,17 @@ if (typeof PDFJS === 'undefined') { }; })(); -// Function.prototype.bind ? +// Function.prototype.bind? +// Support: Android<4.0, iOS<6.0 (function checkFunctionPrototypeBindCompatibility() { - if (typeof Function.prototype.bind !== 'undefined') + if (typeof Function.prototype.bind !== 'undefined') { return; + } Function.prototype.bind = function functionPrototypeBind(obj) { var fn = this, headArgs = Array.prototype.slice.call(arguments, 1); var bound = function functionPrototypeBindBound() { - var args = Array.prototype.concat.apply(headArgs, arguments); + var args = headArgs.concat(Array.prototype.slice.call(arguments)); return fn.apply(obj, args); }; return bound; @@ -314,23 +289,29 @@ if (typeof PDFJS === 'undefined') { })(); // HTMLElement dataset property +// Support: IE<11, Safari<5.1, Android<4.0 (function checkDatasetProperty() { var div = document.createElement('div'); - if ('dataset' in div) + if ('dataset' in div) { return; // dataset property exists + } Object.defineProperty(HTMLElement.prototype, 'dataset', { get: function() { - if (this._dataset) + if (this._dataset) { return this._dataset; + } var dataset = {}; for (var j = 0, jj = this.attributes.length; j < jj; j++) { var attribute = this.attributes[j]; - if (attribute.name.substring(0, 5) != 'data-') + if (attribute.name.substring(0, 5) !== 'data-') { continue; + } var key = attribute.name.substring(5).replace(/\-([a-z])/g, - function(all, ch) { return ch.toUpperCase(); }); + function(all, ch) { + return ch.toUpperCase(); + }); dataset[key] = attribute.value; } @@ -346,20 +327,26 @@ if (typeof PDFJS === 'undefined') { })(); // HTMLElement classList property +// Support: IE<10, Android<4.0, iOS<5.0 (function checkClassListProperty() { var div = document.createElement('div'); - if ('classList' in div) + if ('classList' in div) { return; // classList property exists + } function changeList(element, itemName, add, remove) { var s = element.className || ''; var list = s.split(/\s+/g); - if (list[0] === '') list.shift(); + if (list[0] === '') { + list.shift(); + } var index = list.indexOf(itemName); - if (index < 0 && add) + if (index < 0 && add) { list.push(itemName); - if (index >= 0 && remove) + } + if (index >= 0 && remove) { list.splice(index, 1); + } element.className = list.join(' '); return (index >= 0); } @@ -381,8 +368,9 @@ if (typeof PDFJS === 'undefined') { Object.defineProperty(HTMLElement.prototype, 'classList', { get: function() { - if (this._classList) + if (this._classList) { return this._classList; + } var classList = Object.create(classListPrototype, { element: { @@ -403,6 +391,9 @@ if (typeof PDFJS === 'undefined') { })(); // Check console compatibility +// In older IE versions the console object is not available +// unless console is open. +// Support: IE<10 (function checkConsoleCompatibility() { if (!('console' in window)) { window.console = { @@ -425,6 +416,7 @@ if (typeof PDFJS === 'undefined') { })(); // Check onclick compatibility in Opera +// Support: Opera<15 (function checkOnClickCompatibility() { // workaround for reported Opera bug DSK-354448: // onclick fires on disabled buttons with opaque content @@ -436,30 +428,34 @@ if (typeof PDFJS === 'undefined') { function isDisabled(node) { return node.disabled || (node.parentNode && isDisabled(node.parentNode)); } - if (navigator.userAgent.indexOf('Opera') != -1) { + if (navigator.userAgent.indexOf('Opera') !== -1) { // use browser detection since we cannot feature-check this bug document.addEventListener('click', ignoreIfTargetDisabled, true); } })(); +// Checks if possible to use URL.createObjectURL() +// Support: IE +(function checkOnBlobSupport() { + // sometimes IE loosing the data created with createObjectURL(), see #3977 + if (navigator.userAgent.indexOf('Trident') >= 0) { + PDFJS.disableCreateObjectURL = true; + } +})(); + // Checks if navigator.language is supported (function checkNavigatorLanguage() { - if ('language' in navigator) + if ('language' in navigator) { return; - Object.defineProperty(navigator, 'language', { - get: function navigatorLanguage() { - var language = navigator.userLanguage || 'en-US'; - return language.substring(0, 2).toLowerCase() + - language.substring(2).toUpperCase(); - }, - enumerable: true - }); + } + PDFJS.locale = navigator.userLanguage || 'en-US'; })(); (function checkRangeRequests() { // Safari has issues with cached range requests see: // https://github.com/mozilla/pdf.js/issues/3260 // Last tested with version 6.0.4. + // Support: Safari 6.0+ var isSafari = Object.prototype.toString.call( window.HTMLElement).indexOf('Constructor') > 0; @@ -467,17 +463,131 @@ if (typeof PDFJS === 'undefined') { // https://github.com/mozilla/pdf.js/issues/3381. // Make sure that we only match webkit-based Android browsers, // since Firefox/Fennec works as expected. + // Support: Android<3.0 var regex = /Android\s[0-2][^\d]/; var isOldAndroid = regex.test(navigator.userAgent); - if (isSafari || isOldAndroid) { + // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318 + var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent); + + if (isSafari || isOldAndroid || isChromeWithRangeBug) { PDFJS.disableRange = true; + PDFJS.disableStream = true; } })(); // Check if the browser supports manipulation of the history. +// Support: IE<10, Android<4.2 (function checkHistoryManipulation() { - if (!window.history.pushState) { + // Android 2.x has so buggy pushState support that it was removed in + // Android 3.0 and restored as late as in Android 4.2. + // Support: Android 2.x + if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) { PDFJS.disableHistory = true; } })(); + +// Support: IE<11, Chrome<21, Android<4.4, Safari<6 +(function checkSetPresenceInImageData() { + // IE < 11 will use window.CanvasPixelArray which lacks set function. + if (window.CanvasPixelArray) { + if (typeof window.CanvasPixelArray.prototype.set !== 'function') { + window.CanvasPixelArray.prototype.set = function(arr) { + for (var i = 0, ii = this.length; i < ii; i++) { + this[i] = arr[i]; + } + }; + } + } else { + // Old Chrome and Android use an inaccessible CanvasPixelArray prototype. + // Because we cannot feature detect it, we rely on user agent parsing. + var polyfill = false, versionMatch; + if (navigator.userAgent.indexOf('Chrom') >= 0) { + versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + // Chrome < 21 lacks the set function. + polyfill = versionMatch && parseInt(versionMatch[2]) < 21; + } else if (navigator.userAgent.indexOf('Android') >= 0) { + // Android < 4.4 lacks the set function. + // Android >= 4.4 will contain Chrome in the user agent, + // thus pass the Chrome check above and not reach this block. + polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent); + } else if (navigator.userAgent.indexOf('Safari') >= 0) { + versionMatch = navigator.userAgent. + match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//); + // Safari < 6 lacks the set function. + polyfill = versionMatch && parseInt(versionMatch[1]) < 6; + } + + if (polyfill) { + var contextPrototype = window.CanvasRenderingContext2D.prototype; + var createImageData = contextPrototype.createImageData; + contextPrototype.createImageData = function(w, h) { + var imageData = createImageData.call(this, w, h); + imageData.data.set = function(arr) { + for (var i = 0, ii = this.length; i < ii; i++) { + this[i] = arr[i]; + } + }; + return imageData; + }; + // this closure will be kept referenced, so clear its vars + contextPrototype = null; + } + } +})(); + +// Support: IE<10, Android<4.0, iOS +(function checkRequestAnimationFrame() { + function fakeRequestAnimationFrame(callback) { + window.setTimeout(callback, 20); + } + + var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + if (isIOS) { + // requestAnimationFrame on iOS is broken, replacing with fake one. + window.requestAnimationFrame = fakeRequestAnimationFrame; + return; + } + if ('requestAnimationFrame' in window) { + return; + } + window.requestAnimationFrame = + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + fakeRequestAnimationFrame; +})(); + +(function checkCanvasSizeLimitation() { + var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + var isAndroid = /Android/g.test(navigator.userAgent); + if (isIOS || isAndroid) { + // 5MP + PDFJS.maxCanvasPixels = 5242880; + } +})(); + +// Disable fullscreen support for certain problematic configurations. +// Support: IE11+ (when embedded). +(function checkFullscreenSupport() { + var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 && + window.parent !== window); + if (isEmbeddedIE) { + PDFJS.disableFullscreen = true; + } +})(); + +// Provides document.currentScript support +// Support: IE, Chrome<29. +(function checkCurrentScript() { + if ('currentScript' in document) { + return; + } + Object.defineProperty(document, 'currentScript', { + get: function () { + var scripts = document.getElementsByTagName('script'); + return scripts[scripts.length - 1]; + }, + enumerable: true, + configurable: true + }); +})(); \ No newline at end of file diff --git a/public/js/pdf.pdfmake.js b/public/js/pdf.pdfmake.js index f8f30f4b546f..8adf2e642481 100644 --- a/public/js/pdf.pdfmake.js +++ b/public/js/pdf.pdfmake.js @@ -55,7 +55,7 @@ function GetPdfMake(invoice, javascript, callback) { } // determine whether or not to show the header/footer - if (invoice.is_pro) { + if (invoice.features.customize_invoice_design) { if (key === 'header') { return function(page, pages) { return page === 1 || invoice.account.all_pages_header == '1' ? val : ''; @@ -86,7 +86,7 @@ function GetPdfMake(invoice, javascript, callback) { // Add ninja logo to the footer var dd = JSON.parse(javascript, jsonCallBack); var designId = invoice.invoice_design_id; - if (!invoice.is_pro) { + if (!invoice.features.remove_created_by) { if (designId == NINJA.TEMPLATES.CLEAN || designId == NINJA.TEMPLATES.NORMAL) { dd.footer.columns.push({image: logoImages.imageLogo1, alignment: 'right', width: 130, margin: [0, 0, 0, 0]}) } else if (designId == NINJA.TEMPLATES.BOLD) { @@ -96,7 +96,8 @@ function GetPdfMake(invoice, javascript, callback) { } } - + // set page size + dd.pageSize = invoice.account.page_size; pdfMake.fonts = {} fonts = window.invoiceFonts || invoice.invoice_fonts; @@ -109,11 +110,12 @@ function GetPdfMake(invoice, javascript, callback) { function addFont(font){ if(window.ninjaFontVfs[font.folder]){ + folder = 'fonts/'+font.folder; pdfMake.fonts[font.name] = { - normal: font.folder+'/'+font.normal, - italics: font.folder+'/'+font.italics, - bold: font.folder+'/'+font.bold, - bolditalics: font.folder+'/'+font.bolditalics + normal: folder+'/'+font.normal, + italics: folder+'/'+font.italics, + bold: folder+'/'+font.bold, + bolditalics: folder+'/'+font.bolditalics } } } @@ -144,6 +146,7 @@ NINJA.decodeJavascript = function(invoice, javascript) 'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16, 'invoiceLineItems': NINJA.invoiceLines(invoice), 'invoiceLineItemColumns': NINJA.invoiceColumns(invoice), + 'invoiceDocuments' : NINJA.invoiceDocuments(invoice), 'quantityWidth': NINJA.quantityWidth(invoice), 'taxWidth': NINJA.taxWidth(invoice), 'clientDetails': NINJA.clientDetails(invoice), @@ -189,7 +192,7 @@ NINJA.decodeJavascript = function(invoice, javascript) field = toSnakeCase(field); var value = getDescendantProp(invoice, field); if (match.indexOf('?') < 0 || value) { - if (invoice.partial && field == 'balance_due') { + if (invoice.partial > 0 && field == 'balance_due') { field = 'partial_due'; } else if (invoice.is_quote) { field = field.replace('invoice', 'quote'); @@ -244,13 +247,13 @@ NINJA.notesAndTerms = function(invoice) var data = []; if (invoice.public_notes) { - data.push({stack:[{text: invoice.public_notes, style: ['notes']}]}); + data.push({stack:[{text: invoice.is_recurring ? processVariables(invoice.public_notes) : invoice.public_notes, style: ['notes']}]}); data.push({text:' '}); } if (invoice.terms) { data.push({text:invoiceLabels.terms, style: ['termsLabel']}); - data.push({stack:[{text: invoice.terms, style: ['terms']}]}); + data.push({stack:[{text: invoice.is_recurring ? processVariables(invoice.terms) : invoice.terms, style: ['terms']}]}); } return NINJA.prepareDataList(data, 'notesAndTerms'); @@ -267,16 +270,16 @@ NINJA.invoiceColumns = function(invoice) columns.push("*") - if (invoice.is_pro && account.custom_invoice_item_label1) { + if (invoice.features.invoice_settings && account.custom_invoice_item_label1) { columns.push("10%"); } - if (invoice.is_pro && account.custom_invoice_item_label2) { + if (invoice.features.invoice_settings && account.custom_invoice_item_label2) { columns.push("10%"); } var count = 3; if (account.hide_quantity == '1') { - count--; + count -= 2; } if (account.show_item_taxes == '1') { count++; @@ -290,10 +293,16 @@ NINJA.invoiceColumns = function(invoice) NINJA.invoiceFooter = function(invoice) { - if (!invoice.is_pro && invoice.invoice_design_id == 3) { - return invoice.invoice_footer ? invoice.invoice_footer.substring(0, 200) : ' '; + var footer = invoice.invoice_footer; + + if (invoice.is_recurring) { + footer = processVariables(footer); + } + + if (!invoice.features.invoice_settings && invoice.invoice_design_id == 3) { + return footer ? footer.substring(0, 200) : ' '; } else { - return invoice.invoice_footer || ' '; + return footer || ' '; } } @@ -322,16 +331,15 @@ NINJA.invoiceLines = function(invoice) { grid[0].push({text: invoiceLabels.description, style: ['tableHeader', 'descriptionTableHeader']}); - if (invoice.is_pro && account.custom_invoice_item_label1) { + if (invoice.features.invoice_settings && account.custom_invoice_item_label1) { grid[0].push({text: account.custom_invoice_item_label1, style: ['tableHeader', 'custom1TableHeader']}); } - if (invoice.is_pro && account.custom_invoice_item_label2) { + if (invoice.features.invoice_ettings && account.custom_invoice_item_label2) { grid[0].push({text: account.custom_invoice_item_label2, style: ['tableHeader', 'custom2TableHeader']}); - } - - grid[0].push({text: invoiceLabels.unit_cost, style: ['tableHeader', 'costTableHeader']}); + } if (!hideQuantity) { + grid[0].push({text: invoiceLabels.unit_cost, style: ['tableHeader', 'costTableHeader']}); grid[0].push({text: invoiceLabels.quantity, style: ['tableHeader', 'qtyTableHeader']}); } if (showItemTaxes) { @@ -348,13 +356,17 @@ NINJA.invoiceLines = function(invoice) { var qty = NINJA.parseFloat(item.qty) ? roundToTwo(NINJA.parseFloat(item.qty)) + '' : ''; var notes = item.notes; var productKey = item.product_key; - var tax = ''; + var tax1 = ''; + var tax2 = ''; + var custom_value1 = item.custom_value1; + var custom_value2 = item.custom_value2; if (showItemTaxes) { - if (item.tax && parseFloat(item.tax.rate)) { - tax = parseFloat(item.tax.rate); - } else if (item.tax_rate && parseFloat(item.tax_rate)) { - tax = parseFloat(item.tax_rate); + if (item.tax_name1) { + tax1 = parseFloat(item.tax_rate1); + } + if (item.tax_name2) { + tax2 = parseFloat(item.tax_rate2); } } @@ -369,6 +381,8 @@ NINJA.invoiceLines = function(invoice) { if (invoice.is_recurring) { notes = processVariables(notes); productKey = processVariables(productKey); + custom_value1 = processVariables(item.custom_value1); + custom_value2 = processVariables(item.custom_value2); } var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); @@ -380,18 +394,28 @@ NINJA.invoiceLines = function(invoice) { row.push({style:["productKey", rowStyle], text:productKey || ' '}); // product key can be blank when selecting from a datalist } row.push({style:["notes", rowStyle], stack:[{text:notes || ' '}]}); - if (invoice.is_pro && account.custom_invoice_item_label1) { - row.push({style:["customValue1", rowStyle], text:item.custom_value1 || ' '}); + if (invoice.features.invoice_settings && account.custom_invoice_item_label1) { + row.push({style:["customValue1", rowStyle], text:custom_value1 || ' '}); } - if (invoice.is_pro && account.custom_invoice_item_label2) { - row.push({style:["customValue2", rowStyle], text:item.custom_value2 || ' '}); + if (invoice.features.invoice_settings && account.custom_invoice_item_label2) { + row.push({style:["customValue2", rowStyle], text:custom_value2 || ' '}); } - row.push({style:["cost", rowStyle], text:cost}); if (!hideQuantity) { + row.push({style:["cost", rowStyle], text:cost}); row.push({style:["quantity", rowStyle], text:qty || ' '}); } if (showItemTaxes) { - row.push({style:["tax", rowStyle], text:tax ? (tax.toString() + '%') : ' '}); + var str = ' '; + if (tax1) { + str += tax1.toString() + '%'; + } + if (tax2) { + if (tax1) { + str += ' '; + } + str += tax2.toString() + '%'; + } + row.push({style:["tax", rowStyle], text:str}); } row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '}); @@ -401,6 +425,39 @@ NINJA.invoiceLines = function(invoice) { return NINJA.prepareDataTable(grid, 'invoiceItems'); } +NINJA.invoiceDocuments = function(invoice) { + if(!invoice.account.invoice_embed_documents)return[]; + var stack = []; + var stackItem = null; + + var j = 0; + for (var i = 0; i < invoice.documents.length; i++)addDoc(invoice.documents[i]); + + if(invoice.expenses){ + for (var i = 0; i < invoice.expenses.length; i++) { + var expense = invoice.expenses[i]; + for (var i = 0; i < expense.documents.length; i++)addDoc(expense.documents[i]); + } + } + + function addDoc(document){ + var path = document.base64; + + if(!path)path = 'docs/'+document.public_id+'/'+document.name; + if(path && (window.pdfMake.vfs[path] || document.base64)){ + // Only embed if we actually have an image for it + if(j%3==0){ + stackItem = {columns:[]}; + stack.push(stackItem); + } + stackItem.columns.push({stack:[{image:path,style:'invoiceDocument',fit:[150,150]}], width:175}) + j++; + } + } + + return stack.length?{stack:stack}:[]; +} + NINJA.subtotals = function(invoice, hideBalance) { if (!invoice) { @@ -424,15 +481,19 @@ NINJA.subtotals = function(invoice, hideBalance) for (var key in invoice.item_taxes) { if (invoice.item_taxes.hasOwnProperty(key)) { - var taxRate = invoice.item_taxes[key]; + var taxRate = invoice.item_taxes[key]; var taxStr = taxRate.name + ' ' + (taxRate.rate*1).toString() + '%'; data.push([{text: taxStr}, {text: formatMoneyInvoice(taxRate.amount, invoice)}]); } } - if (invoice.tax && invoice.tax.name || invoice.tax_name) { - var taxStr = invoice.tax_name + ' ' + (invoice.tax_rate*1).toString() + '%'; - data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount, invoice)}]); + if (invoice.tax_amount1) { + var taxStr = invoice.tax_name1 + ' ' + (invoice.tax_rate1*1).toString() + '%'; + data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount1, invoice)}]); + } + if (invoice.tax_amount2) { + var taxStr = invoice.tax_name2 + ' ' + (invoice.tax_rate2*1).toString() + '%'; + data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount2, invoice)}]); } if (NINJA.parseFloat(invoice.custom_value1) && invoice.custom_taxes1 != '1') { @@ -503,7 +564,7 @@ NINJA.accountAddress = function(invoice) { {text: account.country ? account.country.name : ''}, ]; - if (invoice.is_pro) { + if (invoice.features.invoice_settings) { data.push({text: invoice.account.custom_value1 ? invoice.account.custom_label1 + ' ' + invoice.account.custom_value1 : false}); data.push({text: invoice.account.custom_value2 ? invoice.account.custom_label2 + ' ' + invoice.account.custom_value2 : false}); } @@ -528,42 +589,35 @@ NINJA.invoiceDetails = function(invoice) { ], [ {text: (invoice.is_quote ? invoiceLabels.valid_until : invoiceLabels.due_date)}, - {text: invoice.due_date} + {text: invoice.is_recurring ? false : invoice.due_date} ] ]; if (invoice.custom_text_value1) { data.push([ {text: invoice.account.custom_invoice_text_label1}, - {text: invoice.custom_text_value1} + {text: invoice.is_recurring ? processVariables(invoice.custom_text_value1) : invoice.custom_text_value1} ]) } if (invoice.custom_text_value2) { data.push([ {text: invoice.account.custom_invoice_text_label2}, - {text: invoice.custom_text_value2} + {text: invoice.is_recurring ? processVariables(invoice.custom_text_value2) : invoice.custom_text_value2} ]) } - var isPartial = NINJA.parseFloat(invoice.partial); - - if (NINJA.parseFloat(invoice.balance) < NINJA.parseFloat(invoice.amount)) { - data.push([ - {text: invoiceLabels.balance_due}, - {text: formatMoneyInvoice(invoice.amount, invoice)} - ]); - } else if (isPartial) { - data.push([ - {text: invoiceLabels.balance_due}, - {text: formatMoneyInvoice(invoice.total_amount, invoice)} - ]); - } - data.push([ - {text: isPartial ? invoiceLabels.partial_due : invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']}, - {text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['invoiceDetailBalanceDue']} + {text: invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']}, + {text: formatMoneyInvoice(invoice.total_amount, invoice), style: ['invoiceDetailBalanceDue']} ]) + if (NINJA.parseFloat(invoice.partial)) { + data.push([ + {text: invoiceLabels.partial_due, style: ['invoiceDetailBalanceDueLabel']}, + {text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['invoiceDetailBalanceDue']} + ]) + } + return NINJA.prepareDataPairs(data, 'invoiceDetails'); } diff --git a/public/js/script.js b/public/js/script.js index c447905f81b9..a76e6d37cd0e 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -590,6 +590,11 @@ function calculateAmounts(invoice) { var hasTaxes = false; var taxes = {}; invoice.has_product_key = false; + + // Bold designs currently breaks w/o the product column + if (invoice.invoice_design_id == 2) { + invoice.has_product_key = true; + } // sum line item for (var i=0; i= 0 && remove) + } + if (index >= 0 && remove) { list.splice(index, 1); + } element.className = list.join(' '); return (index >= 0); } @@ -7802,8 +7789,9 @@ if (typeof PDFJS === 'undefined') { Object.defineProperty(HTMLElement.prototype, 'classList', { get: function() { - if (this._classList) + if (this._classList) { return this._classList; + } var classList = Object.create(classListPrototype, { element: { @@ -7824,6 +7812,9 @@ if (typeof PDFJS === 'undefined') { })(); // Check console compatibility +// In older IE versions the console object is not available +// unless console is open. +// Support: IE<10 (function checkConsoleCompatibility() { if (!('console' in window)) { window.console = { @@ -7846,6 +7837,7 @@ if (typeof PDFJS === 'undefined') { })(); // Check onclick compatibility in Opera +// Support: Opera<15 (function checkOnClickCompatibility() { // workaround for reported Opera bug DSK-354448: // onclick fires on disabled buttons with opaque content @@ -7857,30 +7849,34 @@ if (typeof PDFJS === 'undefined') { function isDisabled(node) { return node.disabled || (node.parentNode && isDisabled(node.parentNode)); } - if (navigator.userAgent.indexOf('Opera') != -1) { + if (navigator.userAgent.indexOf('Opera') !== -1) { // use browser detection since we cannot feature-check this bug document.addEventListener('click', ignoreIfTargetDisabled, true); } })(); +// Checks if possible to use URL.createObjectURL() +// Support: IE +(function checkOnBlobSupport() { + // sometimes IE loosing the data created with createObjectURL(), see #3977 + if (navigator.userAgent.indexOf('Trident') >= 0) { + PDFJS.disableCreateObjectURL = true; + } +})(); + // Checks if navigator.language is supported (function checkNavigatorLanguage() { - if ('language' in navigator) + if ('language' in navigator) { return; - Object.defineProperty(navigator, 'language', { - get: function navigatorLanguage() { - var language = navigator.userLanguage || 'en-US'; - return language.substring(0, 2).toLowerCase() + - language.substring(2).toUpperCase(); - }, - enumerable: true - }); + } + PDFJS.locale = navigator.userLanguage || 'en-US'; })(); (function checkRangeRequests() { // Safari has issues with cached range requests see: // https://github.com/mozilla/pdf.js/issues/3260 // Last tested with version 6.0.4. + // Support: Safari 6.0+ var isSafari = Object.prototype.toString.call( window.HTMLElement).indexOf('Constructor') > 0; @@ -7888,21 +7884,134 @@ if (typeof PDFJS === 'undefined') { // https://github.com/mozilla/pdf.js/issues/3381. // Make sure that we only match webkit-based Android browsers, // since Firefox/Fennec works as expected. + // Support: Android<3.0 var regex = /Android\s[0-2][^\d]/; var isOldAndroid = regex.test(navigator.userAgent); - if (isSafari || isOldAndroid) { + // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318 + var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent); + + if (isSafari || isOldAndroid || isChromeWithRangeBug) { PDFJS.disableRange = true; + PDFJS.disableStream = true; } })(); // Check if the browser supports manipulation of the history. +// Support: IE<10, Android<4.2 (function checkHistoryManipulation() { - if (!window.history.pushState) { + // Android 2.x has so buggy pushState support that it was removed in + // Android 3.0 and restored as late as in Android 4.2. + // Support: Android 2.x + if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) { PDFJS.disableHistory = true; } })(); +// Support: IE<11, Chrome<21, Android<4.4, Safari<6 +(function checkSetPresenceInImageData() { + // IE < 11 will use window.CanvasPixelArray which lacks set function. + if (window.CanvasPixelArray) { + if (typeof window.CanvasPixelArray.prototype.set !== 'function') { + window.CanvasPixelArray.prototype.set = function(arr) { + for (var i = 0, ii = this.length; i < ii; i++) { + this[i] = arr[i]; + } + }; + } + } else { + // Old Chrome and Android use an inaccessible CanvasPixelArray prototype. + // Because we cannot feature detect it, we rely on user agent parsing. + var polyfill = false, versionMatch; + if (navigator.userAgent.indexOf('Chrom') >= 0) { + versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + // Chrome < 21 lacks the set function. + polyfill = versionMatch && parseInt(versionMatch[2]) < 21; + } else if (navigator.userAgent.indexOf('Android') >= 0) { + // Android < 4.4 lacks the set function. + // Android >= 4.4 will contain Chrome in the user agent, + // thus pass the Chrome check above and not reach this block. + polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent); + } else if (navigator.userAgent.indexOf('Safari') >= 0) { + versionMatch = navigator.userAgent. + match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//); + // Safari < 6 lacks the set function. + polyfill = versionMatch && parseInt(versionMatch[1]) < 6; + } + + if (polyfill) { + var contextPrototype = window.CanvasRenderingContext2D.prototype; + var createImageData = contextPrototype.createImageData; + contextPrototype.createImageData = function(w, h) { + var imageData = createImageData.call(this, w, h); + imageData.data.set = function(arr) { + for (var i = 0, ii = this.length; i < ii; i++) { + this[i] = arr[i]; + } + }; + return imageData; + }; + // this closure will be kept referenced, so clear its vars + contextPrototype = null; + } + } +})(); + +// Support: IE<10, Android<4.0, iOS +(function checkRequestAnimationFrame() { + function fakeRequestAnimationFrame(callback) { + window.setTimeout(callback, 20); + } + + var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + if (isIOS) { + // requestAnimationFrame on iOS is broken, replacing with fake one. + window.requestAnimationFrame = fakeRequestAnimationFrame; + return; + } + if ('requestAnimationFrame' in window) { + return; + } + window.requestAnimationFrame = + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + fakeRequestAnimationFrame; +})(); + +(function checkCanvasSizeLimitation() { + var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + var isAndroid = /Android/g.test(navigator.userAgent); + if (isIOS || isAndroid) { + // 5MP + PDFJS.maxCanvasPixels = 5242880; + } +})(); + +// Disable fullscreen support for certain problematic configurations. +// Support: IE11+ (when embedded). +(function checkFullscreenSupport() { + var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 && + window.parent !== window); + if (isEmbeddedIE) { + PDFJS.disableFullscreen = true; + } +})(); + +// Provides document.currentScript support +// Support: IE, Chrome<29. +(function checkCurrentScript() { + if ('currentScript' in document) { + return; + } + Object.defineProperty(document, 'currentScript', { + get: function () { + var scripts = document.getElementsByTagName('script'); + return scripts[scripts.length - 1]; + }, + enumerable: true, + configurable: true + }); +})(); !function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){(function(e){t.exports=e.pdfMake=n(1)}).call(e,function(){return this}())},function(t,e,n){(function(e){"use strict";function r(t,e,n){this.docDefinition=t,this.fonts=e||s,this.vfs=n}var i=n(6),o=n(105),a=o.saveAs,s={Roboto:{normal:"Roboto-Regular.ttf",bold:"Roboto-Medium.ttf",italics:"Roboto-Italic.ttf",bolditalics:"Roboto-Italic.ttf"}};r.prototype._createDoc=function(t,n){var r=new i(this.fonts);r.fs.bindFS(this.vfs);var o,a=r.createPdfKitDocument(this.docDefinition,t),s=[];a.on("data",function(t){s.push(t)}),a.on("end",function(){o=e.concat(s),n(o,a._pdfMakePages)}),a.end()},r.prototype._getPages=function(t,e){if(!e)throw"getBuffer is an async method and needs a callback argument";this._createDoc(t,function(t,n){e(n)})},r.prototype.open=function(t){var e=window.open("","_blank");try{this.getDataUrl(function(t){e.location.href=t})}catch(n){throw e.close(),n}},r.prototype.print=function(){this.getDataUrl(function(t){var e=document.createElement("iframe");e.style.position="absolute",e.style.left="-99999px",e.src=t,e.onload=function(){function t(){document.body.removeChild(e),document.removeEventListener("click",t)}document.addEventListener("click",t,!1)},document.body.appendChild(e)},{autoPrint:!0})},r.prototype.download=function(t,e){"function"==typeof t&&(e=t,t=null),t=t||"file.pdf",this.getBuffer(function(n){var r;try{r=new Blob([n],{type:"application/pdf"})}catch(i){if("InvalidStateError"==i.name){var o=new Uint8Array(n);r=new Blob([o.buffer],{type:"application/pdf"})}}if(!r)throw"Could not generate blob";a(r,t),"function"==typeof e&&e()})},r.prototype.getBase64=function(t,e){if(!t)throw"getBase64 is an async method and needs a callback argument";this._createDoc(e,function(e){t(e.toString("base64"))})},r.prototype.getDataUrl=function(t,e){if(!t)throw"getDataUrl is an async method and needs a callback argument";this._createDoc(e,function(e){t("data:application/pdf;base64,"+e.toString("base64"))})},r.prototype.getBuffer=function(t,e){if(!t)throw"getBuffer is an async method and needs a callback argument";this._createDoc(e,function(e){t(e)})},t.exports={createPdf:function(t){return new r(t,window.pdfMake.fonts,window.pdfMake.vfs)}}}).call(e,n(2).Buffer)},function(t,e,n){(function(t,r){function i(){function t(){}try{var e=new Uint8Array(1);return e.foo=function(){return 42},e.constructor=t,42===e.foo()&&e.constructor===t&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(n){return!1}}function o(){return t.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function t(e){return this instanceof t?(this.length=0,this.parent=void 0,"number"==typeof e?a(this,e):"string"==typeof e?s(this,e,arguments.length>1?arguments[1]:"utf8"):h(this,e)):arguments.length>1?new t(e,arguments[1]):new t(e)}function a(e,n){if(e=g(e,0>n?0:0|v(n)),!t.TYPED_ARRAY_SUPPORT)for(var r=0;n>r;r++)e[r]=0;return e}function s(t,e,n){("string"!=typeof n||""===n)&&(n="utf8");var r=0|y(e,n);return t=g(t,r),t.write(e,n),t}function h(e,n){if(t.isBuffer(n))return u(e,n);if(V(n))return c(e,n);if(null==n)throw new TypeError("must start with number, buffer, array or string");if("undefined"!=typeof ArrayBuffer){if(n.buffer instanceof ArrayBuffer)return l(e,n);if(n instanceof ArrayBuffer)return f(e,n)}return n.length?d(e,n):p(e,n)}function u(t,e){var n=0|v(e.length);return t=g(t,n),e.copy(t,0,0,n),t}function c(t,e){var n=0|v(e.length);t=g(t,n);for(var r=0;n>r;r+=1)t[r]=255&e[r];return t}function l(t,e){var n=0|v(e.length);t=g(t,n);for(var r=0;n>r;r+=1)t[r]=255&e[r];return t}function f(e,n){return t.TYPED_ARRAY_SUPPORT?(n.byteLength,e=t._augment(new Uint8Array(n))):e=l(e,new Uint8Array(n)),e}function d(t,e){var n=0|v(e.length);t=g(t,n);for(var r=0;n>r;r+=1)t[r]=255&e[r];return t}function p(t,e){var n,r=0;"Buffer"===e.type&&V(e.data)&&(n=e.data,r=0|v(n.length)),t=g(t,r);for(var i=0;r>i;i+=1)t[i]=255&n[i];return t}function g(e,n){t.TYPED_ARRAY_SUPPORT?(e=t._augment(new Uint8Array(n)),e.__proto__=t.prototype):(e.length=n,e._isBuffer=!0);var r=0!==n&&n<=t.poolSize>>>1;return r&&(e.parent=$),e}function v(t){if(t>=o())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+o().toString(16)+" bytes");return 0|t}function m(e,n){if(!(this instanceof m))return new m(e,n);var r=new t(e,n);return delete r.parent,r}function y(t,e){"string"!=typeof t&&(t=""+t);var n=t.length;if(0===n)return 0;for(var r=!1;;)switch(e){case"ascii":case"binary":case"raw":case"raws":return n;case"utf8":case"utf-8":return H(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return Y(t).length;default:if(r)return H(t).length;e=(""+e).toLowerCase(),r=!0}}function _(t,e,n){var r=!1;if(e=0|e,n=void 0===n||n===1/0?this.length:0|n,t||(t="utf8"),0>e&&(e=0),n>this.length&&(n=this.length),e>=n)return"";for(;;)switch(t){case"hex":return T(this,e,n);case"utf8":case"utf-8":return I(this,e,n);case"ascii":return L(this,e,n);case"binary":return R(this,e,n);case"base64":return C(this,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return B(this,e,n);default:if(r)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),r=!0}}function w(t,e,n,r){n=Number(n)||0;var i=t.length-n;r?(r=Number(r),r>i&&(r=i)):r=i;var o=e.length;if(o%2!==0)throw new Error("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;r>a;a++){var s=parseInt(e.substr(2*a,2),16);if(isNaN(s))throw new Error("Invalid hex string");t[n+a]=s}return a}function b(t,e,n,r){return q(H(e,t.length-n),t,n,r)}function x(t,e,n,r){return q(Z(e),t,n,r)}function S(t,e,n,r){return x(t,e,n,r)}function k(t,e,n,r){return q(Y(e),t,n,r)}function E(t,e,n,r){return q(G(e,t.length-n),t,n,r)}function C(t,e,n){return 0===e&&n===t.length?K.fromByteArray(t):K.fromByteArray(t.slice(e,n))}function I(t,e,n){n=Math.min(t.length,n);for(var r=[],i=e;n>i;){var o=t[i],a=null,s=o>239?4:o>223?3:o>191?2:1;if(n>=i+s){var h,u,c,l;switch(s){case 1:128>o&&(a=o);break;case 2:h=t[i+1],128===(192&h)&&(l=(31&o)<<6|63&h,l>127&&(a=l));break;case 3:h=t[i+1],u=t[i+2],128===(192&h)&&128===(192&u)&&(l=(15&o)<<12|(63&h)<<6|63&u,l>2047&&(55296>l||l>57343)&&(a=l));break;case 4:h=t[i+1],u=t[i+2],c=t[i+3],128===(192&h)&&128===(192&u)&&128===(192&c)&&(l=(15&o)<<18|(63&h)<<12|(63&u)<<6|63&c,l>65535&&1114112>l&&(a=l))}}null===a?(a=65533,s=1):a>65535&&(a-=65536,r.push(a>>>10&1023|55296),a=56320|1023&a),r.push(a),i+=s}return A(r)}function A(t){var e=t.length;if(J>=e)return String.fromCharCode.apply(String,t);for(var n="",r=0;e>r;)n+=String.fromCharCode.apply(String,t.slice(r,r+=J));return n}function L(t,e,n){var r="";n=Math.min(t.length,n);for(var i=e;n>i;i++)r+=String.fromCharCode(127&t[i]);return r}function R(t,e,n){var r="";n=Math.min(t.length,n);for(var i=e;n>i;i++)r+=String.fromCharCode(t[i]);return r}function T(t,e,n){var r=t.length;(!e||0>e)&&(e=0),(!n||0>n||n>r)&&(n=r);for(var i="",o=e;n>o;o++)i+=j(t[o]);return i}function B(t,e,n){for(var r=t.slice(e,n),i="",o=0;ot)throw new RangeError("offset is not uint");if(t+e>n)throw new RangeError("Trying to access beyond buffer length")}function M(e,n,r,i,o,a){if(!t.isBuffer(e))throw new TypeError("buffer must be a Buffer instance");if(n>o||a>n)throw new RangeError("value is out of bounds");if(r+i>e.length)throw new RangeError("index out of range")}function D(t,e,n,r){0>e&&(e=65535+e+1);for(var i=0,o=Math.min(t.length-n,2);o>i;i++)t[n+i]=(e&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function U(t,e,n,r){0>e&&(e=4294967295+e+1);for(var i=0,o=Math.min(t.length-n,4);o>i;i++)t[n+i]=e>>>8*(r?i:3-i)&255}function P(t,e,n,r,i,o){if(e>i||o>e)throw new RangeError("value is out of bounds");if(n+r>t.length)throw new RangeError("index out of range");if(0>n)throw new RangeError("index out of range")}function z(t,e,n,r,i){return i||P(t,e,n,4,3.4028234663852886e38,-3.4028234663852886e38),X.write(t,e,n,r,23,4),n+4}function F(t,e,n,r,i){return i||P(t,e,n,8,1.7976931348623157e308,-1.7976931348623157e308),X.write(t,e,n,r,52,8),n+8}function W(t){if(t=N(t).replace(tt,""),t.length<2)return"";for(;t.length%4!==0;)t+="=";return t}function N(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function j(t){return 16>t?"0"+t.toString(16):t.toString(16)}function H(t,e){e=e||1/0;for(var n,r=t.length,i=null,o=[],a=0;r>a;a++){if(n=t.charCodeAt(a),n>55295&&57344>n){if(!i){if(n>56319){(e-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(e-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(56320>n){(e-=3)>-1&&o.push(239,191,189),i=n;continue}n=i-55296<<10|n-56320|65536}else i&&(e-=3)>-1&&o.push(239,191,189);if(i=null,128>n){if((e-=1)<0)break;o.push(n)}else if(2048>n){if((e-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(65536>n){if((e-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(1114112>n))throw new Error("Invalid code point");if((e-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function Z(t){for(var e=[],n=0;n>8,i=n%256,o.push(i),o.push(r);return o}function Y(t){return K.toByteArray(W(t))}function q(t,e,n,r){for(var i=0;r>i&&!(i+n>=e.length||i>=t.length);i++)e[i+n]=t[i];return i}/*! * The buffer module from node.js, for the browser. * @@ -7926,7 +8035,12 @@ if(window.ninjaFontVfs)ninjaLoadFontVfs(); function ninjaLoadFontVfs(){ jQuery.each(window.ninjaFontVfs, function(font, files){ jQuery.each(files, function(filename, file){ - window.pdfMake.vfs[font+'/'+filename] = file; + window.pdfMake.vfs['fonts/'+font+'/'+filename] = file; }); }) +} +function ninjaAddVFSDoc(name,content){ + window.pdfMake.vfs['docs/'+name] = content; + if(window.refreshPDF)refreshPDF(true); + jQuery(document).trigger('ninjaVFSDocAdded'); } \ No newline at end of file diff --git a/readme.md b/readme.md index c8fc462f01f4..54057e1a3782 100644 --- a/readme.md +++ b/readme.md @@ -3,15 +3,11 @@

    # Invoice Ninja -### [https://www.invoiceninja.com](https://www.invoiceninja.com) +### [http://www.invoiceninja.org](http://www.invoiceninja.org) [![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=master)](https://travis-ci.org/invoiceninja/invoiceninja) [![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Note: we've recently updated this branch to Laravel 5.2. If you're upgrading here are some things to note -* Make sure to run composer install -* If there are any strings with spaces in your .env file you'll need to enclose them in quotes to prevent error class log not found. - ### Affiliates Programs * Referral program (we pay you): $100 per signup paid over 3 years - [Learn more](https://www.invoiceninja.com/referral-program/) * White-label reseller (you pay us): 10% of revenue with a $100 sign up fee diff --git a/resources/lang/cs/auth.php b/resources/lang/cs/auth.php new file mode 100644 index 000000000000..aec1bc4c0596 --- /dev/null +++ b/resources/lang/cs/auth.php @@ -0,0 +1,19 @@ + 'Tyto přihlašovací údajě neodpovídají žadnému záznamu.', + 'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds vteřin.', + +]; diff --git a/resources/lang/cs/pagination.php b/resources/lang/cs/pagination.php new file mode 100644 index 000000000000..0931d77fabfb --- /dev/null +++ b/resources/lang/cs/pagination.php @@ -0,0 +1,19 @@ + '« předchozí', + 'next' => 'další »', + +]; diff --git a/resources/lang/cs/passwords.php b/resources/lang/cs/passwords.php new file mode 100644 index 000000000000..271d609e96da --- /dev/null +++ b/resources/lang/cs/passwords.php @@ -0,0 +1,22 @@ + 'Heslo musí obsahovat alespoň 6 znaků a musí odpovídat.', + 'reset' => 'Heslo bylo obnoveno!', + 'sent' => 'Upomínka ke změně hesla byla odeslána!', + 'token' => 'Klíč pro obnovu hesla je nesprávný.', + 'user' => 'Nepodařilo se najít uživatele s touto e-mailovou adresou.', + +]; diff --git a/resources/lang/cs/texts.php b/resources/lang/cs/texts.php new file mode 100644 index 000000000000..97771446b40e --- /dev/null +++ b/resources/lang/cs/texts.php @@ -0,0 +1,1194 @@ + 'Organizace', + 'name' => 'Jméno', + 'website' => 'Web', + 'work_phone' => 'Telefon', + 'address' => 'Adresa', + 'address1' => 'Ulice', + 'address2' => 'Pokoj', + 'city' => 'Město', + 'state' => 'Oblast', + 'postal_code' => 'PSČ', + 'country_id' => 'Země', + 'contacts' => 'Kontakty', + 'first_name' => 'Jméno', + 'last_name' => 'Příjmení', + 'phone' => 'Telefon', + 'email' => 'Email', + 'additional_info' => 'Další info', + 'payment_terms' => 'Platební podmínky', + 'currency_id' => 'Měna', + 'size_id' => 'Velikost firmy', + 'industry_id' => 'Odvětví', + 'private_notes' => 'Soukromé poznámky', + 'invoice' => 'Faktura', + 'client' => 'Klient', + 'invoice_date' => 'Datum vystavení', + 'due_date' => 'Datum splatnosti', + 'invoice_number' => 'Číslo faktury', + 'invoice_number_short' => 'Faktura #', + 'po_number' => 'PO číslo', + 'po_number_short' => 'PO #', + 'frequency_id' => 'Jak často', + 'discount' => 'Sleva', + 'taxes' => 'Daně', + 'tax' => 'Daň', + 'item' => 'Položka', + 'description' => 'Popis', + 'unit_cost' => 'Jednotková cena', + 'quantity' => 'Množství', + 'line_total' => 'Celkem', + 'subtotal' => 'Mezisoučet', + 'paid_to_date' => 'Zaplaceno ke dni', + 'balance_due' => 'Zbývající zůstatek', + 'invoice_design_id' => 'Design', + 'terms' => 'Podmínky', + 'your_invoice' => 'Vaše faktura', + 'remove_contact' => 'Odstranit kontakt', + 'add_contact' => 'Přidat kontakt', + 'create_new_client' => 'Vytvořit nového klienta', + 'edit_client_details' => 'Editovat detaily klienta', + 'enable' => 'Umožnit', + 'learn_more' => 'Viz více', + 'manage_rates' => 'Spravovat sazby', + 'note_to_client' => 'Poznámka ke klientovi', + 'invoice_terms' => 'Fakturační podmínky', + 'save_as_default_terms' => 'Uložit jako výchozí podmínky', + 'download_pdf' => 'Stáhnout PDF', + 'pay_now' => 'Zaplatit nyní', + 'save_invoice' => 'Uložit fakturu', + 'clone_invoice' => 'Zduplikovat fakturu', + 'archive_invoice' => 'Archivovat fakturu', + 'delete_invoice' => 'Smazat fakturu', + 'email_invoice' => 'Poslat fakturu emailem', + 'enter_payment' => 'Zadat platbu', + 'tax_rates' => 'Sazby daně', + 'rate' => 'Sazba', + 'settings' => 'Nastavení', + 'enable_invoice_tax' => 'Umožnit nastavit daň na faktuře', + 'enable_line_item_tax' => 'Umožnit nastavitdaně u položek', + 'dashboard' => 'Dashboard', + 'clients' => 'Klienti', + 'invoices' => 'Faktury', + 'payments' => 'Platby', + 'credits' => 'Kredity', + 'history' => 'Historie', + 'search' => 'Vyhledat', + 'sign_up' => 'Zaregistrovat se', + 'guest' => 'Host', + 'company_details' => 'Detaily firmy', + 'online_payments' => 'Online platby', + 'notifications' => 'Emailové notifikace', + 'import_export' => 'Import | Export | Zrušit', + 'done' => 'Hotovo', + 'save' => 'Uložit', + 'create' => 'Vytvořit', + 'upload' => 'Nahrát', + 'import' => 'Import', + 'download' => 'Stažení', + 'cancel' => 'Zrušení', + 'close' => 'Zavřít', + 'provide_email' => 'Prosím zadejte platnou adresu', + 'powered_by' => 'Vytvořeno', + 'no_items' => 'Žádné položky', + 'recurring_invoices' => 'Pravidelné faktury', + 'recurring_help' => '

    Posílejte klientům stejné faktury týdně, dvakrát za měsíc, měsíčně, čtvrtletně nebo ročně.

    +

    Použijte :MONTH, :QUARTER or :YEAR pro dynamicky měnící se datumy. Funguje zde i základní matematika například, jako :MONTH-1.

    +

    Příklady dynamických proměnných na faktuře:

    +
      +
    • "Členství v posilovně za :MONTH" => "Členství v posilovně za červenec"
    • +
    • ":YEAR+1 roční předplatné" => "2015 roční předplatné"
    • +
    • "Platba za :QUARTER+1" => "Platba za Q2"
    • +
    ', + 'in_total_revenue' => 'v celkových příjmech', + 'billed_client' => 'klient s fakturací', + 'billed_clients' => 'klienti s fakturací', + 'active_client' => 'aktivní klient', + 'active_clients' => 'aktivní klienti', + 'invoices_past_due' => 'Faktury po splatnosti', + 'upcoming_invoices' => 'Budoucí faktury', + 'average_invoice' => 'Průměrná faktura', + 'archive' => 'Archivovat', + 'delete' => 'Smazat', + 'archive_client' => 'Archivovat klienta', + 'delete_client' => 'Smazat klienta', + 'archive_payment' => 'Archivovat platbu', + 'delete_payment' => 'Smazat platbu', + 'archive_credit' => 'Archivovat kredit', + 'delete_credit' => 'Smazat kredit', + 'show_archived_deleted' => 'Zobrazit archivované/smazané', + 'filter' => 'Filtr', + 'new_client' => 'Nový klient', + 'new_invoice' => 'Nová faktura', + 'new_payment' => 'Nová platba', + 'new_credit' => 'Nový kredit', + 'contact' => 'Kontakt', + 'date_created' => 'Datum vytvoření', + 'last_login' => 'Poslední přihlášení', + 'balance' => 'Zůstatek', + 'action' => 'Akce', + 'status' => 'Status', + 'invoice_total' => 'Faktury celkem', + 'frequency' => 'Frekvence', + 'start_date' => 'Datum počátku', + 'end_date' => 'Datum konce', + 'transaction_reference' => 'Odkaz na transakci', + 'method' => 'Metoda', + 'payment_amount' => 'Částka k platbě', + 'payment_date' => 'Datum platby', + 'credit_amount' => 'Částka kreditu', + 'credit_balance' => 'Zůstatek kreditu', + 'credit_date' => 'Datum kreditu', + 'empty_table' => 'Nejsou dostupná žádná data', + 'select' => 'Zvolit', + 'edit_client' => 'Editovat klienta', + 'edit_invoice' => 'Editovat fakturu', + 'create_invoice' => 'Vytvořit fakturu', + 'enter_credit' => 'Zadat kredit', + 'last_logged_in' => 'Poslední přihlášení', + 'details' => 'Detaily', + 'standing' => 'Trvající', + 'credit' => 'Kredit', + 'activity' => 'Aktivity', + 'date' => 'Datum', + 'message' => 'Vzkaz', + 'adjustment' => 'Úprava', + 'are_you_sure' => 'Jste si jisti?', + 'payment_type_id' => 'Typ platby', + 'amount' => 'Částka', + 'work_email' => 'Email', + 'language_id' => 'Jazyk', + 'timezone_id' => 'Časová zóna', + 'date_format_id' => 'Formát datumu', + 'datetime_format_id' => 'Formátu času a datumu', + 'users' => 'Uživatelé', + 'localization' => 'Lokalizace', + 'remove_logo' => 'Odstranit logo', + 'logo_help' => 'Podporujeme: JPEG, GIF a PNG', + 'payment_gateway' => 'Platební brána', + 'gateway_id' => 'brána', + 'email_notifications' => 'Emailové notifikace', + 'email_sent' => 'Odeslat email pokud je faktura odeslaná', + 'email_viewed' => 'Odeslat email pokud je faktura prohlédnuta', + 'email_paid' => 'Odeslat email pokud je faktura zaplacena', + 'site_updates' => 'Změny na webu', + 'custom_messages' => 'Volitelné vzkazy', + 'default_email_footer' => 'Nastavit výchozí podpis do emailu', + 'select_file' => 'Prosím zvolte soubor', + 'first_row_headers' => 'Použít první řádku jako záhlaví', + 'column' => 'Sloupec', + 'sample' => 'Vzorek', + 'import_to' => 'Importovat do', + 'client_will_create' => 'klient bude vytvořen', + 'clients_will_create' => 'klienti budou vytvořeni', + 'email_settings' => 'Nastavení emailu', + 'client_view_styling' => 'Úprava klientského zobrazení', + 'pdf_email_attachment' => 'Připojit PDF', + 'custom_css' => 'Volitelné CSS', + 'import_clients' => 'Importovat klientská data', + 'csv_file' => 'CSV soubor', + 'export_clients' => 'Exportovat klienty', + 'created_client' => 'Úspěšně vytvořen klient', + 'created_clients' => 'Úspěšně vytvořeno :count klientů', + 'updated_settings' => 'Úspěšně změněno nastavení', + 'removed_logo' => 'Úspěšně odtraněno logo', + 'sent_message' => 'Úspěšně odeslán vzkaz', + 'invoice_error' => 'Ujistěte se, že máte zvoleného klienta a opravte případné chyby', + 'limit_clients' => 'Omlouváme se , to přesahuje limit :count počtu klientů', + 'payment_error' => 'Nastala chyba během zpracování Vaší platby. Zkuste to prosím později znovu.', + 'registration_required' => 'Pro odeslání faktury se zeregistrujte', + 'confirmation_required' => 'Prosím potvrďte vaši emailovou adresu', + 'updated_client' => 'Úspěšně změněn klient', + 'created_client' => 'Úspěšně vytvořen klient', + 'archived_client' => 'Úspěšně archivován klient', + 'archived_clients' => 'Úspěšně archivováno :count klientů', + 'deleted_client' => 'Úspěšně smazán klient', + 'deleted_clients' => 'Úspěšně smazáno :count klientů', + 'updated_invoice' => 'Úspěšně změněna faktura', + 'created_invoice' => 'Úspěšně vytvořena faktura', + 'cloned_invoice' => 'Úspěšně zduplikována faktura', + 'emailed_invoice' => 'Úspěšně odeslána faktura', + 'and_created_client' => 'a vytvořen klient', + 'archived_invoice' => 'Úspěšně archivována faktura', + 'archived_invoices' => 'Úspěšně archivováno :count faktur', + 'deleted_invoice' => 'Úspěšně smazána faktura', + 'deleted_invoices' => 'Úspěšně smazáno :count faktur', + 'created_payment' => 'Úspěšně vytvořena platba', + 'created_payments' => 'Úspěšně vytvořeno :count plateb', + 'archived_payment' => 'Úspěšně archivována platba', + 'archived_payments' => 'Úspěšně archivováno :count plateb', + 'deleted_payment' => 'Úspěšně smazána platba', + 'deleted_payments' => 'Úspěšně smazáno :count plateb', + 'applied_payment' => 'Úspěšně aplikována platba', + 'created_credit' => 'Úspěšně vytvořen kredit', + 'archived_credit' => 'Úspěšně archivován kredit', + 'archived_credits' => 'Úspěšně archivován :count kreditů', + 'deleted_credit' => 'Úspěšně smazán kredit', + 'deleted_credits' => 'Úspěšně smazáno :count kreditů', + 'imported_file' => 'Úspěšně importován soubor', + 'updated_vendor' => 'Úspěšně změněn dodavatel', + 'created_vendor' => 'Úspěšně vytvořen dodavatel', + 'archived_vendor' => 'Úspěšně archivován dodavatel', + 'archived_vendors' => 'Úspěšně archivováno :count dodavatelů', + 'deleted_vendor' => 'Úspěšně smazán dodavatel', + 'deleted_vendors' => 'Úspěšně smazán :count dodavatel', + 'confirmation_subject' => 'Invoice Ninja účet - ověření', + 'confirmation_header' => 'Ověření účtu', + 'confirmation_message' => 'Prosím klikněte na odkaz níže pro potvrzení Vašeho účtu.', + 'invoice_subject' => 'Nová faktura :invoice od :account', + 'invoice_message' => 'Pro zobrazení faktury na :amount, klikněte na odkaz níže.', + 'payment_subject' => 'Platba obdržena', + 'payment_message' => 'Děkujeme za Vaši platbu :amount.', + 'email_salutation' => 'Vážený(á) :name,', + 'email_signature' => 'S pozdravem,', + 'email_from' => 'Invoice Ninja Tým', + 'invoice_link_message' => 'Pro zobrazení faktury klikněte na odkaz níže:', + 'notification_invoice_paid_subject' => 'Faktura :invoice byla zaplacena :client', + 'notification_invoice_sent_subject' => 'Faktura :invoice byla odeslána :client', + 'notification_invoice_viewed_subject' => 'Faktura :invoice byla zobrazena :client', + 'notification_invoice_paid' => 'Platba :amount byla odeslána :client na fakturu :invoice.', + 'notification_invoice_sent' => 'Klientovi :client byla odeslána faktura :invoice na :amount.', + 'notification_invoice_viewed' => 'Klientovi :client se zobrazila faktura :invoice na :amount.', + 'reset_password' => 'Resetovat své heslo můžete kliknutím na následující tlačítko:', + 'secure_payment' => 'Bezpečná platba', + 'card_number' => 'Číslo karty', + 'expiration_month' => 'Expirace měsíc', + 'expiration_year' => 'Expirace rok', + 'cvv' => 'CVV kód', + 'logout' => 'Odhlásit se', + 'sign_up_to_save' => 'Zaregistrujte se pro uložení své práce', + 'agree_to_terms' => 'Souhlasím s podmínkami Ninja :terms', + 'terms_of_service' => 'Obchodní podmínky', + 'email_taken' => 'Tento email už byl registrován', + 'working' => 'Pracuji', + 'success' => 'Úspěšně', + 'success_message' => 'Úspěšně jste se zaregistroval(a)! Prosím klikněte pak na odkaz, který Vám přijde pro ověření do Vašeho emailu.', + 'erase_data' => 'Toto kompletně vymaže navždy Vaše data.', + 'password' => 'Heslo', + 'pro_plan_product' => 'Profi plán', + 'pro_plan_description' => 'Roční využití Invoice Ninja Profi plánu.', + 'pro_plan_success' => 'Děkujeme za použití Invoice Ninja\'s Profi plánu!

     
    + Další kroky

    Faktura k úhradě Vám byla zaslána na email spojený s tímto účtem. + Pro povolení všech skvělých profi vlastností si prosím přečtěte instrukce pro provedení platby na zaslané faktuře + pro roční využívání Profi plánu.

    + Nemůžete fakturu najít? Potřebujete pomoc? Rádi Vám pomůžeme na + -- emailu contact@invoiceninja.com', + 'unsaved_changes' => 'Máte neuložené změny', + 'custom_fields' => 'Volitelná pole', + 'company_fields' => 'Pole pro firmu', + 'client_fields' => 'Pole pro klienta', + 'field_label' => 'Popiska pole', + 'field_value' => 'Hodnota pole', + 'edit' => 'Editovat', + 'set_name' => 'Nastavit jméno vaší firmy', + 'view_as_recipient' => 'Vidět jako příjemce', + 'product_library' => 'Katalog produktů', + 'product' => 'Produkt', + 'products' => 'Katalog produktů', + 'fill_products' => 'Automaticky vyplnit produkty', + 'fill_products_help' => 'Výběr produktu automaticky vyplní popis a náklady', + 'update_products' => 'Automaticky měnit produkty', + 'update_products_help' => 'Změna na faktuře automaticky promítne do katalogu produktů', + 'create_product' => 'Přidat produkt', + 'edit_product' => 'Editovat produkt', + 'archive_product' => 'Archivovat produkt', + 'updated_product' => 'Úspěšně změněn produkt', + 'created_product' => 'Úspěšně vytvořen produkt', + 'archived_product' => 'Úspěšně archivován produkt', + 'pro_plan_custom_fields' => ':link pro nastavení volitelných polí připojením k Profi plánu', + 'advanced_settings' => 'Pokročilé nastavení', + 'pro_plan_advanced_settings' => ':link pro nastavení pokročilých nastavení připojením k Profi plánu', + 'invoice_design' => 'Vzhled faktur', + 'specify_colors' => 'Určete barvy', + 'specify_colors_label' => 'Zvolit barvy použité ve faktuře', + 'chart_builder' => 'Generování grafů', + 'ninja_email_footer' => 'Použijte :site k fakturaci Vašim klientům a nechte si platit online zdarma!', + 'go_pro' => 'Přejít na Profi', + 'quote' => 'Nabídka', + 'quotes' => 'Nabídky', + 'quote_number' => 'Číslo nabídky', + 'quote_number_short' => 'Nabídka #', + 'quote_date' => 'Datum nabídky', + 'quote_total' => 'Nabídek celkem', + 'your_quote' => 'Vaše nabídky', + 'total' => 'Celkem', + 'clone' => 'Duplikovat', + 'new_quote' => 'Nová nabídka', + 'create_quote' => 'Vytvořit nabídku', + 'edit_quote' => 'Editovat nabídku', + 'archive_quote' => 'Archivovat nabídku', + 'delete_quote' => 'Smazat nabídku', + 'save_quote' => 'Uložit nabídku', + 'email_quote' => 'Odeslat nabídku emailem', + 'clone_quote' => 'Duplikovat nabídku', + 'convert_to_invoice' => 'Změnit na fakturu', + 'view_invoice' => 'Zobrazit fakturu', + 'view_client' => 'Zobrazit klienta', + 'view_quote' => 'Zobrazit nabídku', + 'updated_quote' => 'Úspěšně změněna nabídka', + 'created_quote' => 'Úspěšně vytvořena nabídka', + 'cloned_quote' => 'Úspěšně zduplikována nabídka', + 'emailed_quote' => 'Úspěšně odeslána nabídka', + 'archived_quote' => 'Úspěšně archivována nabídka', + 'archived_quotes' => 'Úspěšně archiváno :count nabídek', + 'deleted_quote' => 'Úspěšně smazána nabídka', + 'deleted_quotes' => 'Úspěšně smazáno :count nabídek', + 'converted_to_invoice' => 'Úspěšně změněna nabídka na fakturu', + 'quote_subject' => 'Nová nabídka $quote od :account', + 'quote_message' => 'Pro zobrazení nabídky na :amount, klikněte na odkaz níže.', + 'quote_link_message' => 'Pro zobrazení nabídky pro vašeho klienta klikněte na odkaz níže:', + 'notification_quote_sent_subject' => 'Nabídka :invoice byla odeslána :client', + 'notification_quote_viewed_subject' => 'Nabídka :invoice byla zobrazena :client', + 'notification_quote_sent' => 'Klientovi :client byla emailem odeslána nabídka :invoice na :amount.', + 'notification_quote_viewed' => 'Klientovi :client se zobrazila nabídka :invoice na :amount.', + 'session_expired' => 'Vaše přihlášení expirovalo.', + 'invoice_fields' => 'Pole faktury', + 'invoice_options' => 'Možnosti faktury', + 'hide_quantity' => 'Skrýt množství', + 'hide_quantity_help' => 'Pokud množství v řádcích je vždy 1, tak můžete faktury zjednodušit vynecháním tohoto pole.', + 'hide_paid_to_date' => 'Skrýt Zaplaceno ke dni', + 'hide_paid_to_date_help' => 'Pouze zobrazte "Zaplaceno ke dni" na faktuře jakmile přijde platba.', + 'charge_taxes' => 'Použít daně', + 'user_management' => 'Správa uživatelů', + 'add_user' => 'Přidat uživatele', + 'send_invite' => 'Poslat pozvánku', + 'sent_invite' => 'Pozvánka úspěšně odeslána', + 'updated_user' => 'Uživatel úspěšně změněn', + 'invitation_message' => 'Byl(a) jste pozván(a) :invitor. ', + 'register_to_add_user' => 'Prosím zaregistrujte se jako uživatel', + 'user_state' => 'Stav', + 'edit_user' => 'Editovat uživatele', + 'delete_user' => 'Smazat uživatele', + 'active' => 'Aktivní', + 'pending' => 'Nevyřízený', + 'deleted_user' => 'Uživatel úspěšně smazán', + 'confirm_email_invoice' => 'Jste si jistí, že chcete poslat tuto fakturu emailem?', + 'confirm_email_quote' => 'Jste si jistí, že chcete poslat tuto nabídku emailem?', + 'confirm_recurring_email_invoice' => 'Jste si jistí, že chcete poslat tuto fakturu emailem?', + 'cancel_account' => 'Zrušit účet', + 'cancel_account_message' => 'Varování: Tento krok smaže navždy všechny vaše data, není zde žádná cesta zpět.', + 'go_back' => 'Jít zpět', + 'data_visualizations' => 'Vizualizace dat', + 'sample_data' => 'Zobrazit vzorová data', + 'hide' => 'Skrýt', + 'new_version_available' => 'Nová verze :releases_link je k dispozici. Nyní používáte :user_version, poslední je :latest_version', + 'invoice_settings' => 'Nastavení faktury', + 'invoice_number_prefix' => 'Nastavení prefixu čísla faktury', + 'invoice_number_counter' => 'Číselná řada faktur', + 'quote_number_prefix' => 'Prefix čísla nabídky', + 'quote_number_counter' => 'Číselná řada nabídek', + 'share_invoice_counter' => 'Sdílet číselnou řadu faktur', + 'invoice_issued_to' => 'Faktura vystavena', + 'invalid_counter' => 'Pro případný konflikt si raději nastavte prefix pro faktury nebo nabídky', + 'mark_sent' => 'Značka odesláno', + 'gateway_help_1' => ':link zaregistrovat se na Authorize.net.', + 'gateway_help_2' => ':link zaregistrovat se na Authorize.net.', + 'gateway_help_17' => ':link získat PayPal API signature.', + 'gateway_help_27' => ':link zaregistrovat se na TwoCheckout.', + 'more_designs' => 'Více vzhledů', + 'more_designs_title' => 'Další vzhledy faktur', + 'more_designs_cloud_header' => 'Přejděte na Profi plán pro více vzhledů faktur', + 'more_designs_cloud_text' => '', + 'more_designs_self_host_text' => '', + 'buy' => 'Buy', + 'bought_designs' => 'Další vzhledy faktur přidány', + 'sent' => 'sent', + 'vat_number' => 'DIČ', + 'timesheets' => 'Časové výkazy', + 'payment_title' => 'Zadejte Vaší fakturační adresu a informace o platební kartě', + 'payment_cvv' => '*To jsou 3-4 čísla na zadní straně Vaší karty', + 'payment_footer1' => '*Fakturační adresa musí sedět s tou uvedenou u platební karty.', + 'payment_footer2' => '*Prosím kliněte na "Zaplatit nyní " jenom jednou - transkace může trvat až 1 minutu.', + 'id_number' => 'Číslo ID', + 'white_label_link' => 'White label', + 'white_label_header' => 'White Label', + 'bought_white_label' => 'Úspěšně nastavena white label licence', + 'white_labeled' => 'White labeled', + 'restore' => 'Obnovit', + 'restore_invoice' => 'Obnovit fakturu', + 'restore_quote' => 'Obnovit nabídku', + 'restore_client' => 'Obnovit klienta', + 'restore_credit' => 'Obnovit kredit', + 'restore_payment' => 'Obnovit platbu', + 'restored_invoice' => 'Faktura úspěšně obnovena', + 'restored_quote' => 'Nabídka úspěšně obnovena', + 'restored_client' => 'Klient úspěšně obnoven', + 'restored_payment' => 'Platba úspěšně obnovena', + 'restored_credit' => 'Kredit úspěšně obnoven', + 'reason_for_canceling' => 'Když nám řeknete proč odcházíte, pomůžete nám zlepšit náš web. Děkujeme.', + 'discount_percent' => 'Procento', + 'discount_amount' => 'Částka', + 'invoice_history' => 'Historie faktur', + 'quote_history' => 'Historie nabídek', + 'current_version' => 'Současná verze', + 'select_versiony' => 'Zvolte verzi', + 'view_history' => 'Zobrazit historii', + 'edit_payment' => 'Editovat platbu', + 'updated_payment' => 'Platba úspěšně změněna', + 'deleted' => 'Smazáno', + 'restore_user' => 'Obnovit uživatele', + 'restored_user' => 'Uživatel úspěšně obnoven', + 'show_deleted_users' => 'Zobrazit smazané uživatele', + 'email_templates' => 'Emailové šablony', + 'invoice_email' => 'Email pro fakturu', + 'payment_email' => 'Email pro platbu', + 'quote_email' => 'Email pro nabídku', + 'reset_all' => 'Resetovat vše', + 'approve' => 'Schválit', + 'token_billing_type_id' => 'Token účtování', + 'token_billing_help' => 'Umožňuje ukládat platební karty ve vaší platební bráně a zatížit je platbou později.', + 'token_billing_1' => 'Vypnuto', + 'token_billing_2' => 'Opt-in - checkbox je zobrazen nezaškrtnutý', + 'token_billing_3' => 'Opt-out - je zobrazen zaškrtnutý', + 'token_billing_4' => 'Vždy', + 'token_billing_checkbox' => 'Ukládat detaily platební karty', + 'view_in_stripe' => 'Zobrazit ve Stripe', + 'use_card_on_file' => 'Použít uloženou kartu', + 'edit_payment_details' => 'Editovat platební údaje', + 'token_billing' => 'Ukládat platební údaje', + 'token_billing_secure' => 'Data jsou bezpečně uložena u :stripe_link', + 'support' => 'Popdora', + 'contact_information' => 'Kontaktní informace', + '256_encryption' => '256-Bitové šifrování', + 'amount_due' => 'Částka k platbě', + 'billing_address' => 'Fakturační adresa', + 'billing_method' => 'Způsob fakturace', + 'order_overview' => 'Přehled objednávky', + 'match_address' => '*Adresa musí odpovídat té uvedené na platební kartě.', + 'click_once' => '*Prosím klikněte na "Zaplatit nyní" pouze jednou - transkace může trvat až 1 minutu.', + 'invoice_footer' => 'Patička faktury', + 'save_as_default_footer' => 'Uložit jako výchozí patičku', + 'token_management' => 'Správa tokenů', + 'tokens' => 'Tokeny', + 'add_token' => 'Přidat token', + 'show_deleted_tokens' => 'Zobrazit smazané tokeny', + 'deleted_token' => 'Token úspěšně smazán', + 'created_token' => 'Token úspěšně vytvořen', + 'updated_token' => 'Token úspěšně změněn', + 'edit_token' => 'Editovat token', + 'delete_token' => 'Smazat Token', + 'token' => 'Token', + 'add_gateway' => 'Přidat platební bránu', + 'delete_gateway' => 'Smazat platební bránu', + 'edit_gateway' => 'Editovat bránu', + 'updated_gateway' => 'Brána úspěšně změněna', + 'created_gateway' => 'Brána úspěšně vytvořena', + 'deleted_gateway' => 'Brána úspěšně smazána', + 'pay_with_paypal' => 'PayPal', + 'pay_with_card' => 'Platební karty', + 'change_password' => 'Změnit heslo', + 'current_password' => 'Současné heslo', + 'new_password' => 'Nové heslo', + 'confirm_password' => 'Potvrdit heslo', + 'password_error_incorrect' => 'Současné heslo je neplatné.', + 'password_error_invalid' => 'Nové heslo je neplatné.', + 'updated_password' => 'Heslo úspěšně změněno', + 'api_tokens' => 'API Tokeny', + 'users_and_tokens' => 'Uživatelé & Tokeny', + 'account_login' => 'Přihlášení k účtu', + 'recover_password' => 'Obnovit vaše heslo', + 'forgot_password' => 'Zapomněli jste heslo?', + 'email_address' => 'Email', + 'lets_go' => 'Jdeme na to', + 'password_recovery' => 'Obnovení hesla', + 'send_email' => 'Poslat email', + 'set_password' => 'Nastavit heslo', + 'converted' => 'Zkonvertováno', + 'email_approved' => 'Odeslat email po schválení nabídky', + 'notification_quote_approved_subject' => 'Nabídka :invoice byla schválena :client', + 'notification_quote_approved' => 'Klient :client schválil nabídku :invoice na :amount.', + 'resend_confirmation' => 'Znovu poslat potvrzovací email', + 'confirmation_resent' => 'Potvrzení bylo odesláno emailem', + 'gateway_help_42' => ':link zaregistrujte se na BitPay.
    Poznámka: použijte Legacy API Key, nikoliv API token.', + 'payment_type_credit_card' => 'Platební karty', + 'payment_type_paypal' => 'PayPal', + 'payment_type_bitcoin' => 'Bitcoin', + 'knowledge_base' => 'Knowledge Base', + 'partial' => 'Část', + 'partial_remaining' => ':partial z :balance', + 'more_fields' => 'Více polí', + 'less_fields' => 'Méně polí', + 'client_name' => 'Jméno klienta', + 'pdf_settings' => 'Nastavení PDF', + 'product_settings' => 'Nastavení produktu', + 'auto_wrap' => 'Automatické zalomení řádky', + 'duplicate_post' => 'Varování: předchozí stránka byla odeslána dvakrát. Druhé odeslání bylo ignorováno.', + 'view_documentation' => 'Zobrazit dokumentaci', + 'app_title' => 'Open source online fakrurace', + 'app_description' => 'Invoice Ninja je bezplatné open-source řešení pro fakturaci a účtování zákazníkům. + S Invoice Ninja, můžete jednoduše vytvářet a posílat hezké faktury z jakéhokoli zařízení, které má přístup na web. Vaši klienti si mohou faktury + vytisknout, stáhnout jako PDF nebo Vám rovnou online zaplatit.', + 'rows' => 'řádky', + 'www' => 'www', + 'logo' => 'Logo', + 'subdomain' => 'subdoména', + 'provide_name_or_email' => 'Prosím zadejte jméno nebo email', + 'charts_and_reports' => 'Grafy & Reporty', + 'chart' => 'Graf', + 'report' => 'Report', + 'group_by' => 'Seskupené podle', + 'paid' => 'Zaplacené', + 'enable_report' => 'Report', + 'enable_chart' => 'Graf', + 'totals' => 'Celkem', + 'run' => 'Běh', + 'export' => 'Export', + 'documentation' => 'Dokumentace', + 'zapier' => 'Zapier', + 'recurring' => 'Pravidelné', + 'last_invoice_sent' => 'Poslední faktura byla odeslána :date', + 'processed_updates' => 'Změna úspěšně provedena', + 'tasks' => 'Úlohy', + 'new_task' => 'Nový úloha', + 'start_time' => 'Počáteční čas', + 'created_task' => 'Úloha úspěšně vytvořena', + 'updated_task' => 'Úloha úspěšně změněna', + 'edit_task' => 'Editovat úlohu', + 'archive_task' => 'Archivovat úlohu', + 'restore_task' => 'Obnovit úlohu', + 'delete_task' => 'Smazat úlohu', + 'stop_task' => 'Zastavit úlohu', + 'time' => 'Čas', + 'start' => 'Začátek', + 'stop' => 'Konec', + 'now' => 'Nyní', + 'timer' => 'Časovač', + 'manual' => 'Manuální', + 'date_and_time' => 'Datum & Čas', + 'second' => 'vteřina', + 'seconds' => 'vteřin', + 'minute' => 'minuta', + 'minutes' => 'minut', + 'hour' => 'hodina', + 'hours' => 'hodiny', + 'task_details' => 'Detaily úlohy', + 'duration' => 'Trvání', + 'end_time' => 'Čas konce', + 'end' => 'Konec', + 'invoiced' => 'Fakturováno', + 'logged' => 'Přihlášen', + 'running' => 'Bežící', + 'task_error_multiple_clients' => 'Úloha nemůže být přiřazena různým klientům', + 'task_error_running' => 'Prosím zatavte napřed běžící úlohy', + 'task_error_invoiced' => 'Úlohy byly vyfakturovány', + 'restored_task' => 'Úloha úspěšně obnovena', + 'archived_task' => 'Úloha úspěšně archivována', + 'archived_tasks' => 'Úspěšně archivováno :count úloh', + 'deleted_task' => 'Úloha úspěšně smazána', + 'deleted_tasks' => 'Úspěšně smazáno :count úloh', + 'create_task' => 'Vytvořit úlohu', + 'stopped_task' => 'Úloha úspěšně zastavena', + 'invoice_task' => 'Fakturační úloha', + 'invoice_labels' => 'Fakturační popisky', + 'prefix' => 'Prefix', + 'counter' => 'Počítadlo', + 'payment_type_dwolla' => 'Dwolla', + 'gateway_help_43' => ':link zaregistrujte se na Dwolla', + 'partial_value' => 'Musí být větší než nula a méně než součet', + 'more_actions' => 'Více akcí', + 'pro_plan_title' => 'NINJA PROFI', + 'pro_plan_call_to_action' => 'Upgradujte nyní!', + 'pro_plan_feature1' => 'Neomezené množství klientů', + 'pro_plan_feature2' => 'Přístup k 10 nádherným šablonám faktur', + 'pro_plan_feature3' => 'Volitelné URLs - "vaseznacka.InvoiceNinja.com"', + 'pro_plan_feature4' => 'Odstranit "Vytvořeno Invoice Ninja"', + 'pro_plan_feature5' => 'Přístup více uživatelů & sledování aktivit', + 'pro_plan_feature6' => 'Vytváření nabídek & proforem', + 'pro_plan_feature7' => 'Úprava popisu polí faktur & číslování', + 'pro_plan_feature8' => 'Možnost připojit PDF soubor do emailu klientům', + 'resume' => 'Pokračovat', + 'break_duration' => 'Přestávka', + 'edit_details' => 'Editovat detaily', + 'work' => 'Práce', + 'timezone_unset' => 'Prosím :link nastavte si vaší časovou zónu', + 'click_here' => 'klikněte zde', + 'email_receipt' => 'Odeslat potvrzení platby klientovi', + 'created_payment_emailed_client' => 'ˇUspěšně vytvořena platba a odesláno info klientovi', + 'add_company' => 'Přidat firmu', + 'untitled' => 'Neoznačené', + 'new_company' => 'Nová firma', + 'associated_accounts' => 'Účty úspěšně spojeny', + 'unlinked_account' => 'Účty úspěšně rozspojeny', + 'login' => 'Přihlášení', + 'or' => 'nebo', + 'email_error' => 'Nastal problém s odesláním emailu', + 'confirm_recurring_timing' => 'Poznámka: Emaily jsou odesílány na záčátku hodiny.', + 'payment_terms_help' => 'Nastavte výchozí datum splatnosti', + 'unlink_account' => 'Odpojit účet', + 'unlink' => 'Odpojit', + 'show_address' => 'Ukázat adresu', + 'show_address_help' => 'Klient musí poskytnout fakturační adresu', + 'update_address' => 'Změnit adresu', + 'update_address_help' => 'Změnit adresu klienta podle poskytnutých detailů', + 'times' => 'Časy', + 'set_now' => 'Nastavit nyní', + 'dark_mode' => 'Tmavý mód', + 'dark_mode_help' => 'Zobrazit bílý text na černém pozadí', + 'add_to_invoice' => 'Přidat k faktuře :invoice', + 'create_new_invoice' => 'Vytvořit novou fakturu', + 'task_errors' => 'Prosím opravte překrývající se časy', + 'from' => 'Od', + 'to' => 'Komu', + 'font_size' => 'Velikost fontu', + 'primary_color' => 'Základní barva', + 'secondary_color' => 'Druhá barva', + 'customize_design' => 'Upravit design', + 'content' => 'Obsah', + 'styles' => 'Styly', + 'defaults' => 'Výchozí', + 'margins' => 'Marže', + 'header' => 'Hlavička', + 'footer' => 'Patička', + 'custom' => 'Volitelné', + 'invoice_to' => 'Fakturovat komu', + 'invoice_no' => 'Faktura č.', + 'recent_payments' => 'Poslední platby', + 'outstanding' => 'Nevyrovnaný', + 'manage_companies' => 'Spravovat firmy', + 'total_revenue' => 'Celkové příjmy', + 'current_user' => 'Aktuální uživatel', + 'new_recurring_invoice' => 'Nová pravidelná faktura', + 'recurring_invoice' => 'Pravidelná faktura', + 'recurring_too_soon' => 'Brzy se vytvoří další pravidelná faktura, je nastavena na :date', + 'created_by_invoice' => 'Vytvořeno :invoice', + 'primary_user' => 'Primární uživatel', + 'help' => 'Pomoc', + 'customize_help' => '

    Používáme pdfmake pro definování vzhledu faktur. Pdfmake zkušebna poskytuje skvělou cestu jak knihovnu vidět v akci

    +

    Pro přístup k podřízené položce se používá tečkové konvence. Například zobrazit pro zobrazení jména klienta použijte $client.name.

    +

    Pokud potřebujete s něčím pomoci - pošlete dotaz na náš fórum podpory.

    ', + 'invoice_due_date' => 'Datum splatnosti', + 'quote_due_date' => 'Platí do', + 'valid_until' => 'Platí do', + 'reset_terms' => 'Resetovat podmínky', + 'reset_footer' => 'Resetovat patičku', + 'invoices_sent' => ':count faktur sent|:count faktur odesláno', + 'status_draft' => 'Návrh', + 'status_sent' => 'Odesláno', + 'status_viewed' => 'Zobrazené', + 'status_partial' => 'Částečné', + 'status_paid' => 'Placené', + 'show_line_item_tax' => 'Zobrazit daně v řádku v položkách', + 'iframe_url' => 'Web', + 'iframe_url_help1' => 'Zkopírujte následující kód na Váš web.', + 'iframe_url_help2' => 'Tuto vlastnost můžete vyzkoušet kliknutím na \'Vidět jako příjemce\' jako fakturu.', + 'auto_bill' => 'Automatické fakturování', + 'military_time' => '24 hodinový čas', + 'last_sent' => 'Poslední odeslány', + 'reminder_emails' => 'Připomínky emailem', + 'templates_and_reminders' => 'Šablony & Připomínky', + 'subject' => 'Předmět', + 'body' => 'Tělo', + 'first_reminder' => 'První připomínka', + 'second_reminder' => 'Druhá připomínka', + 'third_reminder' => 'Třetí připomínka', + 'num_days_reminder' => 'Dny po splatnosti', + 'reminder_subject' => 'Připomínka: Faktura :invoice od :account', + 'reset' => 'Resetovat', + 'invoice_not_found' => 'Požadovaná faktura není k dispozici', + 'referral_program' => 'Referral program', + 'referral_code' => 'Referral URL', + 'last_sent_on' => 'Poslední odeslání: :date', + 'page_expire' => 'Tato stránka brzy expiruje, :click_here pro pokračování zobrazení', + 'upcoming_quotes' => 'Nadcházející nabídky', + 'expired_quotes' => 'Prošlé nabídky', + 'sign_up_using' => 'Zaregistrujte se pro použití', + 'invalid_credentials' => 'Tyto údaje neodpovídají našim záznamům.', + 'show_all_options' => 'Zobrazit všechny možnosti', + 'user_details' => 'Uživatelské detaily', + 'oneclick_login' => 'Přihlášení na 1 klik', + 'disable' => 'Vypnout', + 'invoice_quote_number' => 'Čísla faktur a nabídek', + 'invoice_charges' => 'Faktura poplatky', + 'notification_invoice_bounced' => 'Nebyli jsme schopni doručit fakturu :invoice na :contact.', + 'notification_invoice_bounced_subject' => 'Nebylo možné doručit fakturu :invoice', + 'notification_quote_bounced' => 'Nebyli jsme schopni doručit nabídku :invoice na :contact.', + 'notification_quote_bounced_subject' => 'Nebylo možné doručit nabídku :invoice', + 'custom_invoice_link' => 'Odkaz na fakturu', + 'total_invoiced' => 'Celkem fakturováno', + 'open_balance' => 'Zůstatek', + 'verify_email' => 'Prosím klikněte na odkaz v potvrzovacím emailu pro ověření správné adresy.', + 'basic_settings' => 'Základní nastavení', + 'pro' => 'Profi', + 'gateways' => 'Platební brány', + 'next_send_on' => 'Další odeslání: :date', + 'no_longer_running' => 'Tato faktura není nastavena aby proběhla', + 'general_settings' => 'Obecné nastavení', + 'customize' => 'Přizpůsobení', + 'oneclick_login_help' => 'Připojte si účet pro přihlášení bez použití hesla', + 'referral_code_help' => 'Vydělejte si peníze díky sdílení odkazu na naší aplikaci', + 'enable_with_stripe' => 'Povolit | Vyžaduje Stripe', + 'tax_settings' => 'Nastavení daní', + 'create_tax_rate' => 'Přidat daňovou sazbu', + 'updated_tax_rate' => 'Daňová sazba úspěšně změněna', + 'created_tax_rate' => 'Daňová sazba úspěšně vytvořena', + 'edit_tax_rate' => 'Editovat daňovou sazbu', + 'archive_tax_rate' => 'Archivovat daňovou sazbu', + 'archived_tax_rate' => 'Daňová sazba úspěšně archivována', + 'default_tax_rate_id' => 'Výchozí daňová sazba', + 'tax_rate' => 'Daňová sazba', + 'recurring_hour' => 'Pravidelná hodina', + 'pattern' => 'Vzorec', + 'pattern_help_title' => 'Pomoc se vzorcem', + 'pattern_help_1' => 'Vytvořte si čísla faktur a nabídek pomocí nastavení vzorce', + 'pattern_help_2' => 'Dostupné proměnné:', + 'pattern_help_3' => 'Například, :example může být konvertováno na :value', + 'see_options' => 'Zobrazit možnosti', + 'invoice_counter' => 'Počítadlo faktur', + 'quote_counter' => 'Počítadlo nabídek', + 'type' => 'Typ', + 'activity_1' => ':user vytvořil klienta :client', + 'activity_2' => ':user archivoval klienta :client', + 'activity_3' => ':user smazal klienta :client', + 'activity_4' => ':user vytvořil fakturu :invoice', + 'activity_5' => ':user změnil fakturu :invoice', + 'activity_6' => ':user odeslal fakturu :invoice to :contact', + 'activity_7' => ':contact zobrazil fakturu :invoice', + 'activity_8' => ':user archivoval fakturu :invoice', + 'activity_9' => ':user smazal fakturu :invoice', + 'activity_10' => ':contact zadal platbu :payment na :invoice', + 'activity_11' => ':user změnil platbu :payment', + 'activity_12' => ':user archivoval platbu :payment', + 'activity_13' => ':user smazal platbu :payment', + 'activity_14' => ':user zadal :credit kredit', + 'activity_15' => ':user změnil :credit kredit', + 'activity_16' => ':user archivoval :credit kredit', + 'activity_17' => ':user smazal :credit kredit', + 'activity_18' => ':user vytvořil nabídku :quote', + 'activity_19' => ':user změnil nabídku :quote', + 'activity_20' => ':user odeslal nabídku :quote to :contact', + 'activity_21' => ':contact zobrazil nabídku :quote', + 'activity_22' => ':user archivoval nabídku :quote', + 'activity_23' => ':user smazal nabídku :quote', + 'activity_24' => ':user obnovil nabídku :quote', + 'activity_25' => ':user obnovil fakturu :invoice', + 'activity_26' => ':user obnovil klienta :client', + 'activity_27' => ':user obnovil platbu :payment', + 'activity_28' => ':user obnovil :credit kredit', + 'activity_29' => ':contact schválil nabídku :quote', + 'activity_30' => ':user vytvořil :vendor', + 'activity_31' => ':user vytvořil :vendor', + 'activity_32' => ':user vytvořil :vendor', + 'activity_33' => ':user vytvořil :vendor', + 'activity_34' => ':user vytvořil výdaj :expense', + 'activity_35' => ':user vytvořil :vendor', + 'activity_36' => ':user vytvořil :vendor', + 'activity_37' => ':user vytvořil :vendor', + 'payment' => 'Platba', + 'system' => 'Systém', + 'signature' => 'Emailový podpis', + 'default_messages' => 'Výchozí vzkazy', + 'quote_terms' => 'Podmínky nabídky', + 'default_quote_terms' => 'Výchozí podmínky nabídky', + 'default_invoice_terms' => 'Výchozí fakturační podmínky', + 'default_invoice_footer' => 'Výchozí patička faktury', + 'quote_footer' => 'Patička nabídky', + 'free' => 'Zdarma', + 'quote_is_approved' => 'Tato nabídka je schválena', + 'apply_credit' => 'Použít kredit', + 'system_settings' => 'Nastavení systému', + 'archive_token' => 'Archivovat token', + 'archived_token' => 'Token úspěšně archivován', + 'archive_user' => 'Archivovaný uživatel', + 'archived_user' => 'Užival úspěšně archivován', + 'archive_account_gateway' => 'Archivovat bránu', + 'archived_account_gateway' => 'Brána úspěšně archivována', + 'archive_recurring_invoice' => 'Archivovat pravidelnou fakturu', + 'archived_recurring_invoice' => 'Pravidelná faktura úspěšně archivována', + 'delete_recurring_invoice' => 'Smazat pravidelnou fakturu', + 'deleted_recurring_invoice' => 'Pravidelná faktura smazána', + 'restore_recurring_invoice' => 'Obnovit pravidelnou fakturu', + 'restored_recurring_invoice' => 'Pravidelná faktura obnovena', + 'archived' => 'Archivováno', + 'untitled_account' => 'Společnost bez názvu', + 'before' => 'Před', + 'after' => 'Po', + 'reset_terms_help' => 'Resetovat na výchozí podmínky účtu', + 'reset_footer_help' => 'Resetovat na výchozí hlavičku účtu', + 'export_data' => 'Exportovat data', + 'user' => 'Uživatel', + 'country' => 'Země', + 'include' => 'Zahrnout', + 'logo_too_large' => 'Vaše logo je :size, pro rychlejší zobrazení v PDF navrhujeme nahrát soubor menší než 200KB', + 'import_freshbooks' => 'Importovat z FreshBooks', + 'import_data' => 'Importovat data', + 'source' => 'Zdroj', + 'csv' => 'CSV', + 'client_file' => 'Soubor s klienty', + 'invoice_file' => 'Soubor s fakturami', + 'task_file' => 'Soubor s úlohami', + 'no_mapper' => 'Mapování pro soubor není k dispozici', + 'invalid_csv_header' => 'Naplatná hlavička v CSV souboru', + 'client_portal' => 'Klientský portál', + 'admin' => 'Administrátor', + 'disabled' => 'Nepovolen', + 'show_archived_users' => 'Zobrazit archivované uživatele', + 'notes' => 'Poznámky', + 'invoice_will_create' => 'klient bude vytvořen', + 'invoices_will_create' => 'faktury budou vytvořeny', + 'failed_to_import' => 'Následující záznamy selhaly u importu, buď již existují nebo nemají požadovaná pole.', + 'publishable_key' => 'Veřejný klíč', + 'secret_key' => 'Tajný klíč', + 'missing_publishable_key' => 'Nastavte veřejný klíč Stripe pro lepší proces platby', + 'email_design' => 'Vzhled emailu', + 'due_by' => 'Splatnost do :date', + 'enable_email_markup' => 'Umožnit mikroznačky', + 'enable_email_markup_help' => 'Přidejte si mikroznačky schema.org do emailu a usnadněte tak vašim klientům platby.', + 'template_help_title' => 'Nápověda k šablonám', + 'template_help_1' => 'Dostupné proměnné:', + 'email_design_id' => 'Styl emailu', + 'email_design_help' => 'Vytvořte si profesionální vzhled emailů pomocí HTML', + 'plain' => 'Prostý text', + 'light' => 'Světlý', + 'dark' => 'Tmavý', + 'industry_help' => 'Používá se pro porovnání proti průměru u firem podobné velikosti a podobného odvětví.', + 'subdomain_help' => 'Upravte si subdoménu odkazu na fakturu nebo zobrazte faktury na Vašem vlastním webu.', + 'invoice_number_help' => 'Určete prefix nebo použijte upravitelný vzorec pro nastavení číslování faktur.', + 'quote_number_help' => 'Určete prefix nebo použijte upravitelný vzorec pro nastavení číslování nabídek.', + 'custom_client_fields_helps' => 'Když vytváříte klienta - přidejte nové pole a jeho popis a hodnotu pro zobrazení v PDF.', + 'custom_account_fields_helps' => 'Přidejte si pole a hodnotu k detailům společnosti na PDF.', + 'custom_invoice_fields_helps' => 'Přidejte si nové pole když vytváříte fakturu a zobrazte si jeho popis a hodnotu v PDF.', + 'custom_invoice_charges_helps' => 'Přidejte si pole během vytváření faktury a zahrňte ho mezi poplatky do faktury.', + 'token_expired' => 'Validační token expiroval. Prosím vyzkoušejte znovu.', + 'invoice_link' => 'Odkaz na fakturu', + 'button_confirmation_message' => 'Klikněte pro potvrzení Vaší emailové adresy.', + 'confirm' => 'Potvrzuji', + 'email_preferences' => 'Email preference', + 'created_invoices' => 'Úspěšně vytvořeno :count faktur', + 'next_invoice_number' => 'Další číslo faktury je :number.', + 'next_quote_number' => 'Další číslo nabídky je :number.', + 'days_before' => 'dní před', + 'days_after' => 'dní po', + 'field_due_date' => 'datum splatnosti', + 'field_invoice_date' => 'datum vystavení', + 'schedule' => 'Rozvrh', + 'email_designs' => 'Vzhled emailů', + 'assigned_when_sent' => 'Přiřazeno při odeslání', + 'white_label_purchase_link' => 'Zakoupit white label licenci', + 'expense' => 'Náklad', + 'expenses' => 'Náklady', + 'new_expense' => 'Nový náklad', + 'enter_expense' => 'Zadat náklad', + 'vendors' => 'Dodavatelé', + 'new_vendor' => 'Nový dodavatel', + 'payment_terms_net' => 'Net', + 'vendor' => 'Dodavatel', + 'edit_vendor' => 'Editovat dodavatele', + 'archive_vendor' => 'Archivovat dodavatele', + 'delete_vendor' => 'Smazat dodavatele', + 'view_vendor' => 'Zobrazit dodavatele', + 'deleted_expense' => 'Náklad úspěšně smazán', + 'archived_expense' => 'Náklad úspěšně archivován', + 'deleted_expenses' => 'Náklad úspěšně smazán', + 'archived_expenses' => 'Náklady úspěšně archivovány', + 'expense_amount' => 'Částka nákladů', + 'expense_balance' => 'Zůstatek nákladů', + 'expense_date' => 'Datum nákladu', + 'expense_should_be_invoiced' => 'Má tento náklad být fakturován?', + 'public_notes' => 'Veřejné poznámky', + 'invoice_amount' => 'Částka faktury', + 'exchange_rate' => 'Měnový kurz', + 'yes' => 'Ano', + 'no' => 'Ne', + 'should_be_invoiced' => 'Má být fakturován', + 'view_expense' => 'Zobrazit náklad # :expense', + 'edit_expense' => 'Editovat náklad', + 'archive_expense' => 'Archivovat náklad', + 'delete_expense' => 'Smazat náklad', + 'view_expense_num' => 'Náklad # :expense', + 'updated_expense' => 'Náklad úspěšně změněn', + 'created_expense' => 'Náklad úspěšně vytvořen', + 'enter_expense' => 'Zadat náklady', + 'view' => 'Zobrazit', + 'restore_expense' => 'Obnovit náklady', + 'invoice_expense' => 'Fakturovat náklady', + 'expense_error_multiple_clients' => 'Náklady nemohou patřit různým klientům', + 'expense_error_invoiced' => 'Náklady byly již vyfakturovány', + 'convert_currency' => 'Zkonvertovat měnu', + 'num_days' => 'Počet dnů', + 'create_payment_term' => 'Vytvořit platební podmínky', + 'edit_payment_terms' => 'Editovat platební podmínky', + 'edit_payment_term' => 'Editovat platební podmínky', + 'archive_payment_term' => 'Archivovat platební podmínky', + 'recurring_due_dates' => 'Datumy splatnosti pravidelných faktur', + 'recurring_due_date_help' => '

    Automaticky nastavit datum splatnosti na fakturách

    +

    U faktury s měsíčním nebo ročním cyklem bude nastavena měsíční splatnost v dalším měsíci. Invoices on a monthly or yearly cycle set to be due on or before the day they are created will be due the next month. + Faktury se splatností k 29. nebo 30 v měsících, které tyto dny nemají se splatnost nastaví k poslednímu dni v měsíci.

    +

    Faktury s týdenním cyklem mají jako výchozí týdenní splatnost.

    +

    Například:

    +
      +
    • Dnes je 15tého, datum splatnosti je 1.den v měsíci. Datum splatnosti tedy bude prvního dne další měsíc.
    • +
    • Dnes je 15tého, datum splatnosti je poslední datum v měsíci. Datum splatnosti bude tedy poslední den v tomto měsíci. +
    • +
    • Dnes je 15tého , datum splatnosti je nastaven na 15. den v měsíci. Datum splatnosti tedy bude 15tého další měsíc. +
    • +
    • Dnes je pátek, datum splatnosti je nastaveno na první pátek za týden. Datum splatnosti bude tedy další pátek, nikoliv dnes. +
    • +
    ', + 'due' => 'Splatnost', + 'next_due_on' => 'Další splatnost: :date', + 'use_client_terms' => 'Použít podmínky klienta', + 'day_of_month' => ':ordinal den v měsíci', + 'last_day_of_month' => 'poslední den v měsíci', + 'day_of_week_after' => ':ordinal :týden poté', + 'sunday' => 'Neděle', + 'monday' => 'Pondělí', + 'tuesday' => 'Úterý', + 'wednesday' => 'Středa', + 'thursday' => 'Čtvrtek', + 'friday' => 'Pátek', + 'saturday' => 'Sobota', + 'header_font_id' => 'Hlavička font', + 'body_font_id' => 'Font těla', + 'color_font_help' => 'Poznámka: primární barva a fonty jsou rovněž použity v klientském portálu a upravených šablonách emailů.', + 'live_preview' => 'Náhled', + 'invalid_mail_config' => 'Nelze odeslat email, zkontrolujte prosím nastavení emailu.', + 'invoice_message_button' => 'Pro zobrazení faktury na :amount, klikněte na tlačítko níže.', + 'quote_message_button' => 'Pro zobrazení nabídky na :amount, klikněte na tlačítko níže.', + 'payment_message_button' => 'Děkujeme za Vaši platbu :amount.', + 'payment_type_direct_debit' => 'Platba převodem', + 'bank_accounts' => 'Platební karty & Banky', + 'add_bank_account' => 'Přidat bankovní účet', + 'setup_account' => 'Nastavení účtu', + 'import_expenses' => 'Importovat náklady', + 'bank_id' => 'Banka', + 'integration_type' => 'Typ integrace', + 'updated_bank_account' => 'Bankovní účet úspěšně změněn', + 'edit_bank_account' => 'Editovat bankovní účet', + 'archive_bank_account' => 'Archivovat bankovní účet', + 'archived_bank_account' => 'Bankovní účet úspěšně archivován', + 'created_bank_account' => 'Bankovní účet úspěšně vytvořen', + 'validate_bank_account' => 'Ověřit bankovní účet ', + 'bank_password_help' => 'Poznámka: Vaše heslo je bezpečně přeneseno a nikdy není uloženo na našich serverech.', + 'bank_password_warning' => 'Varování: vaše heslo může být přenášeno jako prostý text, zvažte prosím aktivaci HTTPS.', + 'username' => 'Uživatelské jméno', + 'account_number' => 'Číslo účtu', + 'account_name' => 'Název účtu', + 'bank_account_error' => 'Získání detailů účtu selhalo, prosím zkontrolujte si vaše přihlašovací údaje.', + 'status_approved' => 'Schváleno', + 'quote_settings' => 'Nastavení nabídek', + 'auto_convert_quote' => 'Automaticky zkonvertovat nabídku', + 'auto_convert_quote_help' => 'Automaticky zkonvertovat nabídku na fakturu po schválení klientem.', + 'validate' => 'Ověřit', + 'info' => 'Info', + 'imported_expenses' => 'Úspěšně vytvořeno :count_vendors dodavatelů a :count_expenses nákladů', + 'iframe_url_help3' => 'Poznámka: pokud chcete akceptovat kreditní karty nastavte si HTTPS na vašem webu.', + 'expense_error_multiple_currencies' => 'Náklady nemohou být v různých měnách.', + 'expense_error_mismatch_currencies' => 'Měna klienta neodpovídá měně u nákladu.', + 'trello_roadmap' => 'Trello roadmapa', + 'header_footer' => 'Hlavička/Patička', + 'first_page' => 'první stránka', + 'all_pages' => 'všechny stránky', + 'last_page' => 'poslední stránka', + 'all_pages_header' => 'Zobrazit hlavičku', + 'all_pages_footer' => 'Zobrazit patičku', + 'invoice_currency' => 'Měna faktury', + 'enable_https' => 'Pro akceptování platebních karet online používejte vždy HTTPS.', + 'quote_issued_to' => 'Náklad je vystaven', + 'show_currency_code' => 'Kód měny', + 'trial_message' => 'Váš účet získá zdarma 2 týdny zkušební verze Profi plánu.', + 'trial_footer' => 'Vaše zkušební verze trvá :count dnů, :link upradujte nyní.', + 'trial_footer_last_day' => 'Dnes je poslední den Vašeho zkušebního období , :link upradujte nyní.', + 'trial_call_to_action' => 'Vyzkoušet zdarma', + 'trial_success' => '14-ti denní zkušební lhůta úspěšně nastavena', + 'overdue' => 'Po termínu', + + + 'white_label_text' => 'Objednejte si white label licenci na JEDEN ROK $'.WHITE_LABEL_PRICE.' pro odstranění značky Invoice Ninja z klientského portálu a stránek podpory.', + 'user_email_footer' => 'Pro úpravu emailových notifikací prosím navštivte '.SITE_URL.'/settings/notifications', + 'reset_password_footer' => 'Pokud jste nepožádali o resetování hesla, prosím kontaktujte naši podporu na: '.CONTACT_EMAIL, + 'limit_users' => 'Omlouváme se, to už přesáhlo limit '.MAX_NUM_USERS.' uživatelů', + 'more_designs_self_host_header' => 'Získejte 6 dalších vzhledů faktur jen za $'.INVOICE_DESIGNS_PRICE, + 'old_browser' => 'Prosím použijte novější prohlížeč', + 'white_label_custom_css' => ':link za $'.WHITE_LABEL_PRICE.' získáte volitelné úpravy a pomůžete podpoře našeho projektu.', + 'bank_accounts_help' => 'Připojte si bankovní účet pro automatický import nákladů a tvorbu dodavatelů. K dispozici pro American Express přes 400 amerických bank.', + 'security' => [ + 'too_many_attempts' => 'Mnoho přístupů. Zkuste to prosím za několik minut.', + 'wrong_credentials' => 'Neplatný email nebo heslo.', + 'confirmation' => 'Váš účet byl potvrzen!', + 'wrong_confirmation' => 'Chybný potvrzovací kód.', + 'password_forgot' => 'Informace týkající se resetování hesla byla odeslána na Váš email.', + 'password_reset' => 'Heslo bylo změněno úspěšně.', + 'wrong_password_reset' => 'Neplatné heslo. Zkuste znovu', + ], + 'pro_plan' => [ + 'remove_logo' => ':link pro odstranění loga Invoice Ninja připojením se k profi plánu', + 'remove_logo_link' => 'Klikněte zde', + ], + 'invitation_status' => [ + 'sent' => 'Email odeslán', + 'opened' => 'Email otevřen', + 'viewed' => 'Faktura zobrazena', + ], + 'email_errors' => [ + 'inactive_client' => 'Emaily nemohou být odeslány neaktivním klientům', + 'inactive_contact' => 'Emaily nemohou být odeslány neaktivním kontaktům', + 'inactive_invoice' => 'Emaily nemohou být odeslány k neaktivním fakturám', + 'user_unregistered' => 'Pro odesílání emailů si prosím zaregistrujte účet', + 'user_unconfirmed' => 'Pro posílání emailů potvrďte prosím Váš účet.', + 'invalid_contact_email' => 'Neplatný kontaktní email', + ], + + 'navigation' => 'Navigace', + 'list_invoices' => 'Seznam faktur', + 'list_clients' => 'Seznam klientů', + 'list_quotes' => 'Seznam nabídek', + 'list_tasks' => 'Seznam úloh', + 'list_expenses' => 'Seznam nákladů', + 'list_recurring_invoices' => 'Seznam pravidelných faktur', + 'list_payments' => 'Seznam plateb', + 'list_credits' => 'Seznam kreditů', + 'tax_name' => 'Název daně', + 'report_settings' => 'Nastavení reportů', + 'search_hotkey' => 'Zkratka je /', + + 'new_user' => 'Nový uživatel', + 'new_product' => 'Nový produkt', + 'new_tax_rate' => 'Nová sazba daně', + 'invoiced_amount' => 'Fakturovaná částka', + 'invoice_item_fields' => 'Pole položky faktury', + 'custom_invoice_item_fields_help' => 'Během vytváření faktury si přidejte pole a zobrazte si jeho popis a hodnotu v PDF.', + 'recurring_invoice_number' => 'Číslo pravidelné faktury', + 'recurring_invoice_number_prefix_help' => 'Určete prefix, který se přidá k číslu pravidelných faktur. Výchozí hodnota je \'R\'.', + 'enable_client_portal' => 'Dashboard', + 'enable_client_portal_help' => 'Skrýt/zobrazit dashboard v klientském portálu.', + + // Client Passwords + 'enable_portal_password'=>'Faktury chráněné heslem', + 'enable_portal_password_help'=>'Umožní Vám nastavit heslo pro každý kontakt. Pokud heslo nastavíte, tak kontakt ho bude pro zobrazení faktury vždy použít.', + 'send_portal_password'=>'Generovat heslo automaticky', + 'send_portal_password_help'=>'Pokud heslo není nastaveno, bude vygenerováno a zasláno spolu s první fakturou.', + + 'expired' => 'Expirované', + 'invalid_card_number' => 'Číslo platební karty není platné.', + 'invalid_expiry' => 'Datum expirace není platné.', + 'invalid_cvv' => 'CVV není platné.', + 'cost' => 'Cena', + 'create_invoice_for_sample' => 'Poznámka: vytvořte si první fakturu a zde si ji prohlédněte.', + + // User Permissions + 'owner' => 'Vlastník', + 'administrator' => 'Administrátor', + 'administrator_help' => 'Povolit uživatelům spravovat další uživatele, měnit nastavení a všechny záznamy', + 'user_create_all' => 'Vytvářet klienty,faktury atd.', + 'user_view_all' => 'Vidět všechny klienty,faktury atd.', + 'user_edit_all' => 'Editovat všechny klienty,faktury atd.', + 'gateway_help_20' => ':link pro registraci na Sage Pay.', + 'gateway_help_21' => ':link pro registraci na Sage Pay.', + 'partial_due' => 'Částečně splaceno', + 'restore_vendor' => 'Obnovit dodavatele', + 'restored_vendor' => 'Dodavatel úspěšně obnoven', + 'restored_expense' => 'Náklady úspěšně obnoveny', + 'permissions' => 'Práva', + 'create_all_help' => 'Povolit uživatelům měnit záznamy', + 'view_all_help' => 'Povolit uživatelům zobrazit záznamy, které nevytvořili', + 'edit_all_help' => 'Povolit uživatelům měnit záznamy, které nevytvořili', + 'view_payment' => 'Zobrazit platbu', + + 'january' => 'Leden', + 'february' => 'Únor', + 'march' => 'Březen', + 'april' => 'Duben', + 'may' => 'Květen', + 'june' => 'Červen', + 'july' => 'Červenc', + 'august' => 'Srpen', + 'september' => 'Září', + 'october' => 'Říjen', + 'november' => 'Listopad', + 'december' => 'Prosinec', + + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + +); + +return $LANG; + +?>. diff --git a/resources/lang/cs/validation.php b/resources/lang/cs/validation.php new file mode 100644 index 000000000000..85ece14a7f00 --- /dev/null +++ b/resources/lang/cs/validation.php @@ -0,0 +1,115 @@ + ':attribute musí být akceptován.', + 'active_url' => ':attribute není platnou URL adresou.', + 'after' => ':attribute musí být datum po :date.', + 'alpha' => ':attribute může obsahovat pouze písmena.', + 'alpha_dash' => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.', + 'alpha_num' => ':attribute může obsahovat pouze písmena a číslice.', + 'array' => ':attribute musí být pole.', + 'before' => ':attribute musí být datum před :date.', + 'between' => [ + 'numeric' => ':attribute musí být hodnota mezi :min a :max.', + 'file' => ':attribute musí být větší než :min a menší než :max Kilobytů.', + 'string' => ':attribute musí být delší než :min a kratší než :max znaků.', + 'array' => ':attribute musí obsahovat nejméně :min a nesmí obsahovat více než :max prvků.', + ], + 'boolean' => ':attribute musí být true nebo false', + 'confirmed' => ':attribute nebylo odsouhlaseno.', + 'date' => ':attribute musí být platné datum.', + 'date_format' => ':attribute není platný formát data podle :format.', + 'different' => ':attribute a :other se musí lišit.', + 'digits' => ':attribute musí být :digits pozic dlouhé.', + 'digits_between' => ':attribute musí být dlouhé nejméně :min a nejvíce :max pozic.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => ':attribute není platný formát.', + 'exists' => 'Zvolená hodnota pro :attribute není platná.', + 'filled' => ':attribute musí být vyplněno.', + 'image' => ':attribute musí být obrázek.', + 'in' => 'Zvolená hodnota pro :attribute není platná.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => ':attribute musí být celé číslo.', + 'ip' => ':attribute musí být platnou IP adresou.', + 'json' => ':attribute musí být platný JSON řetězec.', + 'max' => [ + 'numeric' => ':attribute musí být nižší než :max.', + 'file' => ':attribute musí být menší než :max Kilobytů.', + 'string' => ':attribute musí být kratší než :max znaků.', + 'array' => ':attribute nesmí obsahovat více než :max prvků.', + ], + 'mimes' => ':attribute musí být jeden z následujících datových typů :values.', + 'min' => [ + 'numeric' => ':attribute musí být větší než :min.', + 'file' => ':attribute musí být větší než :min Kilobytů.', + 'string' => ':attribute musí být delší než :min znaků.', + 'array' => ':attribute musí obsahovat více než :min prvků.', + ], + 'not_in' => 'Zvolená hodnota pro :attribute je neplatná.', + 'numeric' => ':attribute musí být číslo.', + 'present' => 'The :attribute field must be present.', + 'regex' => ':attribute nemá správný formát.', + 'required' => ':attribute musí být vyplněno.', + 'required_if' => ':attribute musí být vyplněno pokud :other je :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => ':attribute musí být vyplněno pokud :values je vyplněno.', + 'required_with_all' => ':attribute musí být vyplněno pokud :values je zvoleno.', + 'required_without' => ':attribute musí být vyplněno pokud :values není vyplněno.', + 'required_without_all' => ':attribute musí být vyplněno pokud není žádné z :values zvoleno.', + 'same' => ':attribute a :other se musí shodovat.', + 'size' => [ + 'numeric' => ':attribute musí být přesně :size.', + 'file' => ':attribute musí mít přesně :size Kilobytů.', + 'string' => ':attribute musí být přesně :size znaků dlouhý.', + 'array' => ':attribute musí obsahovat právě :size prvků.', + ], + 'string' => ':attribute musí být řetězec znaků.', + 'timezone' => ':attribute musí být platná časová zóna.', + 'unique' => ':attribute musí být unikátní.', + 'url' => 'Formát :attribute je neplatný.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [ + // + ], + +]; diff --git a/resources/lang/da/texts.php b/resources/lang/da/texts.php index ffc74871cde0..af939b8f7a39 100644 --- a/resources/lang/da/texts.php +++ b/resources/lang/da/texts.php @@ -493,7 +493,7 @@ return array( 'invoice_history' => 'Faktura historik', 'quote_history' => 'Tilbuds historik', 'current_version' => 'Nuværende version', - 'select_versiony' => 'Vælg version', + 'select_version' => 'Vælg version', 'view_history' => 'Vis historik', 'edit_payment' => 'Redigér betaling', @@ -1199,5 +1199,93 @@ return array( 'october' => 'October', 'november' => 'November', 'december' => 'December', + + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); \ No newline at end of file diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php index 4bb50425823d..64c2222bab14 100644 --- a/resources/lang/de/texts.php +++ b/resources/lang/de/texts.php @@ -1201,4 +1201,92 @@ return array( 'november' => 'November', 'december' => 'Dezember', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 1df2bc2ca4d3..624a472f8ac9 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -81,7 +81,7 @@ $LANG = array( 'company_details' => 'Company Details', 'online_payments' => 'Online Payments', 'notifications' => 'Email Notifications', - 'import_export' => 'Import | Export | Cancel', + 'import_export' => 'Import | Export', 'done' => 'Done', 'save' => 'Save', 'create' => 'Create', @@ -268,7 +268,6 @@ $LANG = array( 'erase_data' => 'This will permanently erase your data.', 'password' => 'Password', 'pro_plan_product' => 'Pro Plan', - 'pro_plan_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_success' => 'Thanks for choosing Invoice Ninja\'s Pro plan!

     
    Next Steps

    A payable invoice has been sent to the email address associated with your account. To unlock all of the awesome @@ -368,7 +367,7 @@ $LANG = array( 'confirm_email_invoice' => 'Are you sure you want to email this invoice?', 'confirm_email_quote' => 'Are you sure you want to email this quote?', 'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?', - 'cancel_account' => 'Cancel Account', + 'cancel_account' => 'Delete Account', 'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.', 'go_back' => 'Go Back', 'data_visualizations' => 'Data Visualizations', @@ -424,7 +423,7 @@ $LANG = array( 'invoice_history' => 'Invoice History', 'quote_history' => 'Quote History', 'current_version' => 'Current version', - 'select_versiony' => 'Select version', + 'select_version' => 'Select version', 'view_history' => 'View History', 'edit_payment' => 'Edit Payment', 'updated_payment' => 'Successfully updated payment', @@ -976,9 +975,9 @@ $LANG = array( 'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.', 'trello_roadmap' => 'Trello Roadmap', 'header_footer' => 'Header/Footer', - 'first_page' => 'first page', - 'all_pages' => 'all pages', - 'last_page' => 'last page', + 'first_page' => 'First page', + 'all_pages' => 'All pages', + 'last_page' => 'Last page', 'all_pages_header' => 'Show header on', 'all_pages_footer' => 'Show footer on', 'invoice_currency' => 'Invoice Currency', @@ -1049,8 +1048,6 @@ $LANG = array( 'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.', 'recurring_invoice_number' => 'Recurring Invoice Number', 'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.', - 'enable_client_portal' => 'Dashboard', - 'enable_client_portal_help' => 'Show/hide the dashboard page in the client portal.', // Client Passwords 'enable_portal_password'=>'Password protect invoices', @@ -1097,6 +1094,94 @@ $LANG = array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); return $LANG; diff --git a/resources/lang/es/texts.php b/resources/lang/es/texts.php index 75bd1a75dae6..eccabaf4a620 100644 --- a/resources/lang/es/texts.php +++ b/resources/lang/es/texts.php @@ -466,7 +466,7 @@ return array( 'invoice_history' => 'Facturar Historial', 'quote_history' => 'Cotizar Historial', 'current_version' => 'Versión actual', - 'select_versiony' => 'Seleccionar versión', + 'select_version' => 'Seleccionar versión', 'view_history' => 'Ver Historial', 'edit_payment' => 'Editar Pago', @@ -1177,4 +1177,92 @@ return array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); diff --git a/resources/lang/es_ES/texts.php b/resources/lang/es_ES/texts.php index 25054072dac5..d6aa9f0aa6b6 100644 --- a/resources/lang/es_ES/texts.php +++ b/resources/lang/es_ES/texts.php @@ -486,7 +486,7 @@ return array( 'invoice_history' => 'Historial de Facturas', 'quote_history' => 'Historial de Presupuestos', 'current_version' => 'Versión Actual', - 'select_versiony' => 'Seleccione la Versión', + 'select_version' => 'Seleccione la Versión', 'view_history' => 'Ver Historial', 'edit_payment' => 'Editar Pago', @@ -1197,4 +1197,92 @@ return array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); diff --git a/resources/lang/fr/texts.php b/resources/lang/fr/texts.php index 89f323a6f6e7..10d994d8d894 100644 --- a/resources/lang/fr/texts.php +++ b/resources/lang/fr/texts.php @@ -1192,4 +1192,92 @@ return array( 'november' => 'Novembre', 'december' => 'Décembre', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); diff --git a/resources/lang/fr_CA/texts.php b/resources/lang/fr_CA/texts.php index c62227e45050..3661ad15b1c0 100644 --- a/resources/lang/fr_CA/texts.php +++ b/resources/lang/fr_CA/texts.php @@ -487,7 +487,7 @@ return array( 'invoice_history' => 'Historique des factures', 'quote_history' => 'Historique des soumissions', 'current_version' => 'Version courante', - 'select_versiony' => 'Choix de la verison', + 'select_version' => 'Choix de la verison', 'view_history' => 'Consulter l\'historique', 'edit_payment' => 'Éditer le paiement', @@ -1190,4 +1190,93 @@ return array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + + ); \ No newline at end of file diff --git a/resources/lang/it/texts.php b/resources/lang/it/texts.php index 0b2a843ccd2c..01edae395357 100644 --- a/resources/lang/it/texts.php +++ b/resources/lang/it/texts.php @@ -30,7 +30,7 @@ return array( 'invoice' => 'Fattura', 'client' => 'Cliente', 'invoice_date' => 'Data Fattura', - 'due_date' => 'Scadenza Fattura', + 'due_date' => 'Scadenza', 'invoice_number' => 'Numero Fattura', 'invoice_number_short' => 'Fattura #', /* Fattura N° */ 'po_number' => 'Numero d\'ordine d\'acquisto', @@ -45,8 +45,8 @@ return array( 'quantity' => 'Quantità', 'line_total' => 'Totale Riga', 'subtotal' => 'Subtotale', - 'paid_to_date' => 'Pagato in Data', - 'balance_due' => 'Saldo Dovuto', + 'paid_to_date' => 'Pagato a oggi', + 'balance_due' => 'Totale', 'invoice_design_id' => 'Stile', 'terms' => 'Condizioni', 'your_invoice' => 'Tua Fattura', @@ -67,7 +67,7 @@ return array( 'clone_invoice' => 'Duplica Fattura', 'archive_invoice' => 'Archivia Fattura', 'delete_invoice' => 'Elimina Fattura', - 'email_invoice' => 'Manda Fattura', /* Spedisci Fattura */ + 'email_invoice' => 'Invia Fattura', /* Spedisci Fattura */ 'enter_payment' => 'Inserisci Pagamento', 'tax_rates' => 'Aliquote Fiscali', /* ^^Unsure^^ */ 'rate' => 'Aliquota', /* ^^Unsure^^ */ @@ -104,12 +104,12 @@ return array( // recurring invoices 'recurring_invoices' => 'Fatture ricorrenti', 'recurring_help' => '

    Invia automaticamente al cliente le stesse fatture settimanalmente, bimestralmente, mensilmente, trimestralmente o annualmente.

    -

    Usa :MESE, :TRIMESRE o :ANNO per date dinamiche. Funziona anche con la matematica di base, ad esempio :MESE-1.

    +

    Usa :MONTH, :QUARTER o :YEAR per date dinamiche. Funziona anche con la matematica di base, ad esempio :MONTH-1.

    Esempi di variabili di fattura dinamiche:

      -
    • "Iscrizione palestra per il mese di :MESE" => "Iscrizione palestra per il mese di Luglio"
    • -
    • ":ANNO+1 iscrizione annuale" => "Anno d\'iscrizione 2015"
    • -
    • "Pagamento fermo a :TRIMESTRE+1" => "Pagamento fermo al 2° trimestre"
    • +
    • "Iscrizione palestra per il mese di :MONTH" => "Iscrizione palestra per il mese di Luglio"
    • +
    • ":YEAR+1 iscrizione annuale" => "Anno d\'iscrizione 2015"
    • +
    • "Pagamento fermo a :QUARTER+1" => "Pagamento fermo al 2° trimestre"
    ', /* ^^Variables translated in case you'll need it for front end^^ */ // dashboard @@ -118,7 +118,7 @@ return array( 'billed_clients' => 'Clienti fatturati', 'active_client' => 'cliente attivo', 'active_clients' => 'clienti attivi', - 'invoices_past_due' => 'Fatture Insolute', /* Insoluti */ + 'invoices_past_due' => 'Fatture Scadute', /* Insoluti */ 'upcoming_invoices' => 'Prossime fatture', 'average_invoice' => 'Fattura media', @@ -140,7 +140,7 @@ return array( 'contact' => 'Contatto', 'date_created' => 'Data di Creazione', 'last_login' => 'Ultimo Accesso', - 'balance' => 'Saldo', + 'balance' => 'Bilancio', 'action' => 'Azione', 'status' => 'Stato', 'invoice_total' => 'Totale Fattura', @@ -169,7 +169,7 @@ return array( 'activity' => 'Attività', 'date' => 'Data', 'message' => 'Messaggio', - 'adjustment' => 'Correzione', + 'adjustment' => 'Variazione', 'are_you_sure' => 'Sei sicuro?', // payment pages @@ -337,7 +337,7 @@ return array( 'archived_product' => 'Prodotto archiviato con successo', 'pro_plan_custom_fields' => ':link to enable custom fields by joining the Pro Plan', - 'advanced_settings' => 'Advanced Settings', + 'advanced_settings' => 'Impostazioni Avanzate', 'pro_plan_advanced_settings' => ':link to enable the advanced settings by joining the Pro Plan', 'invoice_design' => 'Invoice Design', 'specify_colors' => 'Specify colors', @@ -382,11 +382,11 @@ return array( 'converted_to_invoice' => 'Il preventivo è stato convertito a fattura con successo', 'quote_subject' => 'Nuovo preventivo da :account', - 'quote_message' => 'Per visualizzare il vostro preventivo per :amount, cliccare il collegamento sotto.', - 'quote_link_message' => 'Per visualizzare il preventivo del vostro cliante cliccate il collegamento sotto:', + 'quote_message' => 'Per visualizzare il vostro preventivo di :amount, cliccate il collegamento sotto.', + 'quote_link_message' => 'Per visualizzare il preventivo del vostro cliente cliccate il collegamento sotto:', 'notification_quote_sent_subject' => 'Il preventivo :invoice è stato inviato a :client', 'notification_quote_viewed_subject' => 'Il preventivo :invoice è stato visualizzato da :client', - 'notification_quote_sent' => 'Al seguente cliente :client è stata inviata la fattura :invoice per :amount.', + 'notification_quote_sent' => 'Al seguente cliente :client è stata inviato il preventivo :invoice per un importo di :amount.', 'notification_quote_viewed' => 'Il seguente cliente :client ha visualizzato il preventivo :invoice di :amount.', 'session_expired' => 'La vostra sessione è scaduta.', @@ -456,11 +456,11 @@ return array( 'sent' => 'sent', 'timesheets' => 'Timesheets', - 'payment_title' => 'Enter Your Billing Address and Credit Card information', + 'payment_title' => 'Inserisci il tuo indirizzo di fatturazione e i dati della tua carta di credito', 'payment_cvv' => '*This is the 3-4 digit number onthe back of your card', - 'payment_footer1' => '*Billing address must match address associated with credit card.', + 'payment_footer1' => '*L\'indirizzo di fatturazione deve corrispondere all\'indirizzo associato alla carta di credito.', 'payment_footer2' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', - 'vat_number' => 'Vat Number', + 'vat_number' => 'Partita IVA', 'id_number' => 'ID Number', 'white_label_link' => 'White label', @@ -489,7 +489,7 @@ return array( 'invoice_history' => 'Invoice History', 'quote_history' => 'Quote History', 'current_version' => 'Current version', - 'select_versiony' => 'Select version', + 'select_version' => 'Select version', 'view_history' => 'View History', 'edit_payment' => 'Edit Payment', @@ -503,30 +503,30 @@ return array( 'payment_email' => 'Payment Email', 'quote_email' => 'Quote Email', 'reset_all' => 'Reset All', - 'approve' => 'Approve', + 'approve' => 'Approva', 'token_billing_type_id' => 'Token Billing', 'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.', - 'token_billing_1' => 'Disabled', + 'token_billing_1' => 'Disabilitato', 'token_billing_2' => 'Opt-in - checkbox is shown but not selected', 'token_billing_3' => 'Opt-out - checkbox is shown and selected', - 'token_billing_4' => 'Always', - 'token_billing_checkbox' => 'Store credit card details', - 'view_in_stripe' => 'View in Stripe', - 'use_card_on_file' => 'Use card on file', - 'edit_payment_details' => 'Edit payment details', - 'token_billing' => 'Save card details', - 'token_billing_secure' => 'The data is stored securely by :stripe_link', + 'token_billing_4' => 'Sempre', + 'token_billing_checkbox' => 'Salva dettagli carta di credito', + 'view_in_stripe' => 'Vedi transazione in Stripe', + 'use_card_on_file' => 'Carta di credito salvata', + 'edit_payment_details' => 'Modifica dettagli pagamento', + 'token_billing' => 'Salva carta di credito', + 'token_billing_secure' => 'I dati sono memorizzati su piattaforma sicura mediante :stripe_link', 'support' => 'Support', - 'contact_information' => 'Contact information', + 'contact_information' => 'Informazioni di contatto', '256_encryption' => '256-Bit Encryption', - 'amount_due' => 'Amount due', - 'billing_address' => 'Billing address', - 'billing_method' => 'Billing method', - 'order_overview' => 'Order overview', - 'match_address' => '*Address must match address associated with credit card.', - 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'amount_due' => 'Saldo dovuto', + 'billing_address' => 'Indirizzo di fatturazione', + 'billing_method' => 'Metodo di pagamento', + 'order_overview' => 'Riepilogo ordine', + 'match_address' => '*L\'indirizzo deve corrispondere con quello associato alla carta di credito.', + 'click_once' => '*Per favore clicca "PAGA ADESSO" solo una volta - la transazione può impiegare sino a 1 minuto per essere completata.', 'default_invoice_footer' => 'Set default invoice footer', 'invoice_footer' => 'Invoice footer', @@ -550,7 +550,7 @@ return array( 'created_gateway' => 'Successfully created gateway', 'deleted_gateway' => 'Successfully deleted gateway', 'pay_with_paypal' => 'PayPal', - 'pay_with_card' => 'Credit card', + 'pay_with_card' => 'Carta di credito', 'change_password' => 'Change password', 'current_password' => 'Current password', @@ -579,12 +579,12 @@ return array( 'confirmation_resent' => 'The confirmation email was resent', 'gateway_help_42' => ':link to sign up for BitPay.
    Note: use a Legacy API Key, not an API token.', - 'payment_type_credit_card' => 'Credit card', + 'payment_type_credit_card' => 'Carta di credito', 'payment_type_paypal' => 'PayPal', 'payment_type_bitcoin' => 'Bitcoin', 'knowledge_base' => 'Knowledge Base', 'partial' => 'Partial', - 'partial_remaining' => ':partial of :balance', + 'partial_remaining' => ':partial di :balance', 'more_fields' => 'More Fields', 'less_fields' => 'Less Fields', @@ -614,40 +614,40 @@ return array( 'export' => 'Export', 'documentation' => 'Documentation', 'zapier' => 'Zapier', - 'recurring' => 'Recurring', - 'last_invoice_sent' => 'Last invoice sent :date', + 'recurring' => 'Ricorrenti', + 'last_invoice_sent' => 'Ultima fattura inviata :date', 'processed_updates' => 'Successfully completed update', - 'tasks' => 'Tasks', - 'new_task' => 'New Task', - 'start_time' => 'Start Time', + 'tasks' => 'Task', + 'new_task' => 'Nuovo Task', + 'start_time' => 'Tempo di inizio', 'created_task' => 'Successfully created task', 'updated_task' => 'Successfully updated task', - 'edit_task' => 'Edit Task', - 'archive_task' => 'Archive Task', - 'restore_task' => 'Restore Task', - 'delete_task' => 'Delete Task', - 'stop_task' => 'Stop Task', - 'time' => 'Time', - 'start' => 'Start', - 'stop' => 'Stop', - 'now' => 'Now', + 'edit_task' => 'Modifica il Task', + 'archive_task' => 'Archivia il Task', + 'restore_task' => 'Ripristina il Task', + 'delete_task' => 'Cancella il Task', + 'stop_task' => 'Ferma il Task', + 'time' => 'Tempo', + 'start' => 'Inizia', + 'stop' => 'Ferma', + 'now' => 'Adesso', 'timer' => 'Timer', - 'manual' => 'Manual', - 'date_and_time' => 'Date & Time', - 'second' => 'second', - 'seconds' => 'seconds', - 'minute' => 'minute', - 'minutes' => 'minutes', - 'hour' => 'hour', - 'hours' => 'hours', - 'task_details' => 'Task Details', - 'duration' => 'Duration', - 'end_time' => 'End Time', - 'end' => 'End', - 'invoiced' => 'Invoiced', - 'logged' => 'Logged', - 'running' => 'Running', + 'manual' => 'Manuale', + 'date_and_time' => 'Data e ora', + 'second' => 'secondo', + 'seconds' => 'secondi', + 'minute' => 'minuto', + 'minutes' => 'minuti', + 'hour' => 'ora', + 'hours' => 'ore', + 'task_details' => 'Dettagli Task', + 'duration' => 'Durata', + 'end_time' => 'Tempo di fine', + 'end' => 'Fine', + 'invoiced' => 'Fatturato', + 'logged' => 'Loggato', + 'running' => 'In corso', 'task_error_multiple_clients' => 'The tasks can\'t belong to different clients', 'task_error_running' => 'Please stop running tasks first', 'task_error_invoiced' => 'Tasks have already been invoiced', @@ -656,9 +656,9 @@ return array( 'archived_tasks' => 'Successfully archived :count tasks', 'deleted_task' => 'Successfully deleted task', 'deleted_tasks' => 'Successfully deleted :count tasks', - 'create_task' => 'Create Task', + 'create_task' => 'Crea Task', 'stopped_task' => 'Successfully stopped task', - 'invoice_task' => 'Invoice Task', + 'invoice_task' => 'Fattura il Task', 'invoice_labels' => 'Invoice Labels', 'prefix' => 'Prefix', 'counter' => 'Counter', @@ -666,7 +666,7 @@ return array( 'payment_type_dwolla' => 'Dwolla', 'gateway_help_43' => ':link to sign up for Dwolla.', 'partial_value' => 'Must be greater than zero and less than the total', - 'more_actions' => 'More Actions', + 'more_actions' => 'Altre Azioni', 'pro_plan_title' => 'NINJA PRO', @@ -680,12 +680,12 @@ return array( 'pro_plan_feature7' => 'Customize Invoice Field Titles & Numbering', 'pro_plan_feature8' => 'Option to Attach PDFs to Client Emails', - 'resume' => 'Resume', - 'break_duration' => 'Break', - 'edit_details' => 'Edit Details', + 'resume' => 'Riprendi', + 'break_duration' => 'Interrompi', + 'edit_details' => 'Modifica dettagli', 'work' => 'Work', 'timezone_unset' => 'Please :link to set your timezone', - 'click_here' => 'click here', + 'click_here' => 'clicca qui', 'email_receipt' => 'Email payment receipt to the client', 'created_payment_emailed_client' => 'Successfully created payment and emailed client', @@ -728,16 +728,16 @@ return array( 'header' => 'Header', 'footer' => 'Footer', 'custom' => 'Custom', - 'invoice_to' => 'Invoice to', - 'invoice_no' => 'Invoice No.', - 'recent_payments' => 'Recent Payments', - 'outstanding' => 'Outstanding', - 'manage_companies' => 'Manage Companies', - 'total_revenue' => 'Total Revenue', + 'invoice_to' => 'Fattura a', + 'invoice_no' => 'Fattura N.', + 'recent_payments' => 'Pagamenti recenti', + 'outstanding' => 'Inevaso', + 'manage_companies' => 'Gestisci aziende', + 'total_revenue' => 'Ricavo totale', 'current_user' => 'Current User', - 'new_recurring_invoice' => 'New Recurring Invoice', - 'recurring_invoice' => 'Recurring Invoice', + 'new_recurring_invoice' => 'Nuova Fattura Ricorrente', + 'recurring_invoice' => 'Fattura ricorrente', 'recurring_too_soon' => 'It\'s too soon to create the next recurring invoice, it\'s scheduled for :date', 'created_by_invoice' => 'Created by :invoice', 'primary_user' => 'Primary User', @@ -746,17 +746,17 @@ return array(

    To access a child property using dot notation. For example to show the client name you could use $client.name.

    If you need help figuring something out post a question to our support forum.

    ', - 'invoice_due_date' => 'Due Date', - 'quote_due_date' => 'Valid Until', - 'valid_until' => 'Valid Until', + 'invoice_due_date' => 'Scadenza fattura', + 'quote_due_date' => 'Validità preventivo', + 'valid_until' => 'Valido fino a', 'reset_terms' => 'Reset terms', 'reset_footer' => 'Reset footer', 'invoices_sent' => ':count invoice sent|:count invoices sent', - 'status_draft' => 'Draft', - 'status_sent' => 'Sent', - 'status_viewed' => 'Viewed', - 'status_partial' => 'Partial', - 'status_paid' => 'Paid', + 'status_draft' => 'Bozza', + 'status_sent' => 'Spedito', + 'status_viewed' => 'Visto', + 'status_partial' => 'Parziale', + 'status_paid' => 'Pagato', 'show_line_item_tax' => 'Display line item taxes inline', 'iframe_url' => 'Website', @@ -784,16 +784,16 @@ return array( 'last_sent_on' => 'Last sent on :date', 'page_expire' => 'This page will expire soon, :click_here to keep working', - 'upcoming_quotes' => 'Upcoming Quotes', - 'expired_quotes' => 'Expired Quotes', + 'upcoming_quotes' => 'Preventivi in scadenza', + 'expired_quotes' => 'Preventivi Scaduti', 'sign_up_using' => 'Sign up using', - 'invalid_credentials' => 'These credentials do not match our records', - 'show_all_options' => 'Show all options', - 'user_details' => 'User Details', + 'invalid_credentials' => 'Queste credenziali non corrispondono alle nostre registrazioni', + 'show_all_options' => 'Mostra tutte le opzioni', + 'user_details' => 'Dettagli Utente', 'oneclick_login' => 'One-Click Login', - 'disable' => 'Disable', - 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'disable' => 'Disabilita', + 'invoice_quote_number' => 'Numerazione Fatture e Preventivi', 'invoice_charges' => 'Invoice Charges', 'invitation_status' => [ @@ -807,10 +807,10 @@ return array( 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', 'custom_invoice_link' => 'Custom Invoice Link', - 'total_invoiced' => 'Total Invoiced', - 'open_balance' => 'Open Balance', + 'total_invoiced' => 'Fatturato totale', + 'open_balance' => 'Da saldare', 'verify_email' => 'Please visit the link in the account confirmation email to verify your email address.', - 'basic_settings' => 'Basic Settings', + 'basic_settings' => 'Impostazioni Base', 'pro' => 'Pro', 'gateways' => 'Payment Gateways', @@ -821,7 +821,7 @@ return array( 'oneclick_login_help' => 'Connect an account to login without a password', 'referral_code_help' => 'Earn money by sharing our app online', - 'enable_with_stripe' => 'Enable | Requires Stripe', + 'enable_with_stripe' => 'Abilita | Richiede Stripe', 'tax_settings' => 'Tax Settings', 'create_tax_rate' => 'Add Tax Rate', 'updated_tax_rate' => 'Successfully updated tax rate', @@ -842,19 +842,19 @@ return array( 'quote_counter' => 'Quote Counter', 'type' => 'Type', - 'activity_1' => ':user created client :client', - 'activity_2' => ':user archived client :client', + 'activity_1' => ':user ha creato il cliente :client', + 'activity_2' => ':user ha archiviato il cliente :client', 'activity_3' => ':user deleted client :client', - 'activity_4' => ':user created invoice :invoice', - 'activity_5' => ':user updated invoice :invoice', - 'activity_6' => ':user emailed invoice :invoice to :contact', - 'activity_7' => ':contact viewed invoice :invoice', - 'activity_8' => ':user archived invoice :invoice', - 'activity_9' => ':user deleted invoice :invoice', - 'activity_10' => ':contact entered payment :payment for :invoice', - 'activity_11' => ':user updated payment :payment', - 'activity_12' => ':user archived payment :payment', - 'activity_13' => ':user deleted payment :payment', + 'activity_4' => ':user ha creato la fattura :invoice', + 'activity_5' => ':user ha aggiornato la fattura :invoice', + 'activity_6' => ':user ha inviato per email la fattura :invoice a :contact', + 'activity_7' => ':contact ha visto la fattura :invoice', + 'activity_8' => ':user ha archiviato la fattura :invoice', + 'activity_9' => ':user ha cancellato la fattura :invoice', + 'activity_10' => ':contact ha inserito il pagamento :payment per :invoice', + 'activity_11' => ':user ha aggiornato il pagamento :payment', + 'activity_12' => ':user ha archiviato il pagamento :payment', + 'activity_13' => ':user ha cancellato il pagamento :payment', 'activity_14' => ':user entered :credit credit', 'activity_15' => ':user updated :credit credit', 'activity_16' => ':user archived :credit credit', @@ -862,7 +862,7 @@ return array( 'activity_18' => ':user created quote :quote', 'activity_19' => ':user updated quote :quote', 'activity_20' => ':user emailed quote :quote to :contact', - 'activity_21' => ':contact viewed quote :quote', + 'activity_21' => ':contact ha visto il preventivo :quote', 'activity_22' => ':user archived quote :quote', 'activity_23' => ':user deleted quote :quote', 'activity_24' => ':user restored quote :quote', @@ -870,7 +870,7 @@ return array( 'activity_26' => ':user restored client :client', 'activity_27' => ':user restored payment :payment', 'activity_28' => ':user restored :credit credit', - 'activity_29' => ':contact approved quote :quote', + 'activity_29' => ':contact ha approvato la fattura :quote', 'payment' => 'Payment', 'system' => 'System', @@ -883,7 +883,7 @@ return array( 'quote_footer' => 'Quote Footer', 'free' => 'Free', - 'quote_is_approved' => 'This quote is approved', + 'quote_is_approved' => 'Questo preventivo è stato approvato.', 'apply_credit' => 'Apply Credit', 'system_settings' => 'System Settings', 'archive_token' => 'Archive Token', @@ -944,7 +944,7 @@ return array( 'missing_publishable_key' => 'Set your Stripe publishable key for an improved checkout process', 'email_design' => 'Email Design', - 'due_by' => 'Due by :date', + 'due_by' => 'Scadenza :date', 'enable_email_markup' => 'Enable Markup', 'enable_email_markup_help' => 'Make it easier for your clients to pay you by adding schema.org markup to your emails.', 'template_help_title' => 'Templates Help', @@ -986,10 +986,10 @@ return array( 'white_label_purchase_link' => 'Purchase a white label license', // Expense / vendor - 'expense' => 'Expense', - 'expenses' => 'Expenses', - 'new_expense' => 'Enter Expense', - 'enter_expense' => 'Enter Expense', + 'expense' => 'Spesa', + 'expenses' => 'Spese', + 'new_expense' => 'Inserisci Spesa', + 'enter_expense' => 'Inserisci Spesa', 'vendors' => 'Vendors', 'new_vendor' => 'New Vendor', 'payment_terms_net' => 'Net', @@ -1004,33 +1004,33 @@ return array( 'archived_expenses' => 'Successfully archived expenses', // Expenses - 'expense_amount' => 'Expense Amount', - 'expense_balance' => 'Expense Balance', - 'expense_date' => 'Expense Date', - 'expense_should_be_invoiced' => 'Should this expense be invoiced?', - 'public_notes' => 'Public Notes', - 'invoice_amount' => 'Invoice Amount', - 'exchange_rate' => 'Exchange Rate', - 'yes' => 'Yes', + 'expense_amount' => 'Importo Spesa', + 'expense_balance' => 'Bilancio Spesa', + 'expense_date' => 'Data Spesa', + 'expense_should_be_invoiced' => 'Questa spesa deve essere fatturata?', + 'public_notes' => 'Note Pubbliche (Descrizione in fattura)', + 'invoice_amount' => 'Importo Fattura', + 'exchange_rate' => 'Tasso di Cambio', + 'yes' => 'Si', 'no' => 'No', - 'should_be_invoiced' => 'Should be invoiced', - 'view_expense' => 'View expense # :expense', - 'edit_expense' => 'Edit Expense', - 'archive_expense' => 'Archive Expense', - 'delete_expense' => 'Delete Expense', - 'view_expense_num' => 'Expense # :expense', - 'updated_expense' => 'Successfully updated expense', - 'created_expense' => 'Successfully created expense', - 'enter_expense' => 'Enter Expense', - 'view' => 'View', - 'restore_expense' => 'Restore Expense', - 'invoice_expense' => 'Invoice Expense', - 'expense_error_multiple_clients' =>'The expenses can\'t belong to different clients', - 'expense_error_invoiced' => 'Expense has already been invoiced', - 'convert_currency' => 'Convert currency', + 'should_be_invoiced' => 'Deve essere fatturata', + 'view_expense' => 'Vedi spesa # :expense', + 'edit_expense' => 'Modifica Spesa', + 'archive_expense' => 'Archivia Spesa', + 'delete_expense' => 'Cancella Spesa', + 'view_expense_num' => 'Spesa # :expense', + 'updated_expense' => 'Spesa aggiornata con successo', + 'created_expense' => 'Spesa creata con successo', + 'enter_expense' => 'Inserisci Spesa', + 'view' => 'Vedi', + 'restore_expense' => 'Ripristina Spesa', + 'invoice_expense' => 'Fattura Spesa', + 'expense_error_multiple_clients' =>'Le spese non possono appartenere a clienti differenti', + 'expense_error_invoiced' => 'La spesa è stata già fatturata', + 'convert_currency' => 'Converti valuta', // Payment terms - 'num_days' => 'Number of days', + 'num_days' => 'Numero di giorni', 'create_payment_term' => 'Create Payment Term', 'edit_payment_terms' => 'Edit Payment Term', 'edit_payment_term' => 'Edit Payment Term', @@ -1053,17 +1053,17 @@ return array( ', 'due' => 'Due', 'next_due_on' => 'Due Next: :date', - 'use_client_terms' => 'Use client terms', - 'day_of_month' => ':ordinal day of month', - 'last_day_of_month' => 'Last day of month', - 'day_of_week_after' => ':ordinal :day after', - 'sunday' => 'Sunday', - 'monday' => 'Monday', - 'tuesday' => 'Tuesday', - 'wednesday' => 'Wednesday', - 'thursday' => 'Thursday', - 'friday' => 'Friday', - 'saturday' => 'Saturday', + 'use_client_terms' => 'Usa i termini del cliente', + 'day_of_month' => ':ordinal giorno del mese', + 'last_day_of_month' => 'L\'ultimo giorno del mese', + 'day_of_week_after' => ':ordinal :day dopo', + 'sunday' => 'Domenica', + 'monday' => 'Lunedì', + 'tuesday' => 'Martedì', + 'wednesday' => 'Mercoledì', + 'thursday' => 'Giovedì', + 'friday' => 'Venerdì', + 'saturday' => 'Sabato', // Fonts 'header_font_id' => 'Header Font', @@ -1077,11 +1077,11 @@ return array( 'quote_message_button' => 'To view your quote for :amount, click the button below.', 'payment_message_button' => 'Thank you for your payment of :amount.', 'payment_type_direct_debit' => 'Direct Debit', - 'bank_accounts' => 'Bank Accounts', - 'add_bank_account' => 'Add Bank Account', + 'bank_accounts' => 'Conti corrente', + 'add_bank_account' => 'Nuovo conto corrente', 'setup_account' => 'Setup Account', - 'import_expenses' => 'Import Expenses', - 'bank_id' => 'bank', + 'import_expenses' => 'Importa Spese', + 'bank_id' => 'banca', 'integration_type' => 'Integration Type', 'updated_bank_account' => 'Successfully updated bank account', 'edit_bank_account' => 'Edit Bank Account', @@ -1172,7 +1172,7 @@ return array( 'user_edit_all' => 'Edit all clients, invoices, etc.', 'gateway_help_20' => ':link to sign up for Sage Pay.', 'gateway_help_21' => ':link to sign up for Sage Pay.', - 'partial_due' => 'Partial Due', + 'partial_due' => 'Da versare (parziale)', 'restore_vendor' => 'Restore Vendor', 'restored_vendor' => 'Successfully restored vendor', 'restored_expense' => 'Successfully restored expense', @@ -1182,17 +1182,105 @@ return array( 'edit_all_help' => 'Allow user to modify records they didn\'t create', 'view_payment' => 'View Payment', - 'january' => 'January', - 'february' => 'February', - 'march' => 'March', - 'april' => 'April', - 'may' => 'May', - 'june' => 'June', - 'july' => 'July', - 'august' => 'August', - 'september' => 'September', - 'october' => 'October', - 'november' => 'November', - 'december' => 'December', + 'january' => 'Gennaio', + 'february' => 'Febbraio', + 'march' => 'Marzo', + 'april' => 'Aprile', + 'may' => 'Maggio', + 'june' => 'Giugno', + 'july' => 'Luglio', + 'august' => 'Agosto', + 'september' => 'Settembre', + 'october' => 'Ottobre', + 'november' => 'Novembre', + 'december' => 'Dicembre', -); \ No newline at end of file + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + +); diff --git a/resources/lang/ja/texts.php b/resources/lang/ja/texts.php index 7e673ac11d33..3875de243544 100644 --- a/resources/lang/ja/texts.php +++ b/resources/lang/ja/texts.php @@ -424,7 +424,7 @@ $LANG = array( 'invoice_history' => '請求履歴', 'quote_history' => '見積履歴', 'current_version' => '現在のバージョン', - 'select_versiony' => 'バージョンを選択', + 'select_version' => 'バージョンを選択', 'view_history' => '履歴を閲覧', 'edit_payment' => '支払いを編集', 'updated_payment' => '支払いを更新しました', @@ -1096,6 +1096,95 @@ $LANG = array( 'october' => 'October', 'november' => 'November', 'december' => 'December', + + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); diff --git a/resources/lang/lt/texts.php b/resources/lang/lt/texts.php index 252a7f03bd9c..75cb494ca986 100644 --- a/resources/lang/lt/texts.php +++ b/resources/lang/lt/texts.php @@ -497,7 +497,7 @@ return array( 'invoice_history' => 'Invoice History', 'quote_history' => 'Quote History', 'current_version' => 'Current version', - 'select_versiony' => 'Select version', + 'select_version' => 'Select version', 'view_history' => 'View History', 'edit_payment' => 'Edit Payment', @@ -1202,4 +1202,92 @@ return array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); \ No newline at end of file diff --git a/resources/lang/nb_NO/texts.php b/resources/lang/nb_NO/texts.php index df06d7a32547..a06e7c60d679 100644 --- a/resources/lang/nb_NO/texts.php +++ b/resources/lang/nb_NO/texts.php @@ -493,7 +493,7 @@ return array( 'invoice_history' => 'Faktura Historikk', 'quote_history' => 'Tilbuds Historikk', 'current_version' => 'Nåværende versjon', - 'select_versiony' => 'Velg versjon', + 'select_version' => 'Velg versjon', 'view_history' => 'Vis Historikk', 'edit_payment' => 'Rediger Betaling', @@ -1200,4 +1200,92 @@ return array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); \ No newline at end of file diff --git a/resources/lang/nl/texts.php b/resources/lang/nl/texts.php index 83fef88816e1..6d19485ff8e8 100644 --- a/resources/lang/nl/texts.php +++ b/resources/lang/nl/texts.php @@ -10,15 +10,15 @@ return array( 'address' => 'Adres', 'address1' => 'Straat', 'address2' => 'Bus/Suite', - 'city' => 'Gemeente', + 'city' => 'Plaats', 'state' => 'Staat/Provincie', 'postal_code' => 'Postcode', 'country_id' => 'Land', - 'contacts' => 'Contacten', + 'contacts' => 'Contactpersonen', 'first_name' => 'Voornaam', 'last_name' => 'Achternaam', 'phone' => 'Telefoon', - 'email' => 'E-mail', + 'email' => 'E-mailadres', 'additional_info' => 'Extra informatie', 'payment_terms' => 'Betalingsvoorwaarden', 'currency_id' => 'Munteenheid', @@ -35,7 +35,7 @@ return array( 'invoice_number_short' => 'Factuur #', 'po_number' => 'Bestelnummer', 'po_number_short' => 'Bestel #', - 'frequency_id' => 'Hoe vaak', + 'frequency_id' => 'Frequentie', 'discount' => 'Korting', 'taxes' => 'Belastingen', 'tax' => 'Belasting', @@ -52,9 +52,9 @@ return array( 'your_invoice' => 'Jouw factuur', 'remove_contact' => 'Verwijder contact', - 'add_contact' => 'Voeg contact toe', + 'add_contact' => 'Contact toevoegen', 'create_new_client' => 'Maak nieuwe klant', - 'edit_client_details' => 'Pas klantdetails aan', + 'edit_client_details' => 'Klantdetails aanpassen', 'enable' => 'Activeer', 'learn_more' => 'Meer te weten komen', 'manage_rates' => 'Beheer prijzen', @@ -63,12 +63,12 @@ return array( 'save_as_default_terms' => 'Opslaan als standaard voorwaarden', 'download_pdf' => 'Download PDF', 'pay_now' => 'Betaal nu', - 'save_invoice' => 'Sla factuur op', + 'save_invoice' => 'Factuur opslaan', 'clone_invoice' => 'Kopieer factuur', 'archive_invoice' => 'Archiveer factuur', 'delete_invoice' => 'Verwijder factuur', 'email_invoice' => 'E-mail factuur', - 'enter_payment' => 'Betaling ingeven', + 'enter_payment' => 'Betaling invoeren', 'tax_rates' => 'BTW-tarief', 'rate' => 'Tarief', 'settings' => 'Instellingen', @@ -176,21 +176,21 @@ return array( 'amount' => 'Bedrag', // account/company pages - 'work_email' => 'E-mail', + 'work_email' => 'E-mailadres', 'language_id' => 'Taal', 'timezone_id' => 'Tijdszone', 'date_format_id' => 'Datum formaat', 'datetime_format_id' => 'Datum/Tijd formaat', 'users' => 'Gebruikers', 'localization' => 'Localisatie', - 'remove_logo' => 'Verwijder logo', + 'remove_logo' => 'Logo verwijderen', 'logo_help' => 'Ondersteund: JPEG, GIF en PNG', 'payment_gateway' => 'Betalingsmiddel', 'gateway_id' => 'Leverancier', 'email_notifications' => 'E-mailmeldingen', - 'email_sent' => 'E-mail me wanneer een factuur is verzonden', - 'email_viewed' => 'E-mail me wanneer een factuur is bekeken', - 'email_paid' => 'E-mail me wanneer een factuur is betaald', + 'email_sent' => 'E-mail mij wanneer een factuur is verzonden', + 'email_viewed' => 'E-mail mij wanneer een factuur is bekeken', + 'email_paid' => 'E-mail mij wanneer een factuur is betaald', 'site_updates' => 'Site Aanpassingen', 'custom_messages' => 'Aangepaste berichten', 'default_invoice_terms' => 'Stel standaard factuurvoorwaarden in', @@ -310,10 +310,10 @@ return array( 'close' => 'Sluiten', 'pro_plan_product' => 'Pro Plan', - 'pro_plan_description' => 'Één jaar abbonnement op het InvoiceNinja Pro Plan.', + 'pro_plan_description' => 'Eén jaar abbonnement op het InvoiceNinja Pro Plan.', 'pro_plan_success' => 'Bedankt voor het aanmelden! Zodra uw factuur betaald is zal uw Pro Plan lidmaatschap beginnen.', - 'unsaved_changes' => 'U hebt niet bewaarde wijzigingen', + 'unsaved_changes' => 'U hebt niet opgeslagen wijzigingen', 'custom_fields' => 'Aangepaste velden', 'company_fields' => 'Velden Bedrijf', 'client_fields' => 'Velden Klant', @@ -366,7 +366,7 @@ return array( 'archive_quote' => 'Archiveer offerte', 'delete_quote' => 'Verwijder offerte', 'save_quote' => 'Bewaar offerte', - 'email_quote' => 'Email offerte', + 'email_quote' => 'E-mail offerte', 'clone_quote' => 'Kloon offerte', 'convert_to_invoice' => 'Zet om naar factuur', 'view_invoice' => 'Bekijk factuur', @@ -404,10 +404,10 @@ return array( 'charge_taxes' => 'BTW berekenen', 'user_management' => 'Gebruikersbeheer', 'add_user' => 'Nieuwe gebruiker', - 'send_invite' => 'Verstuur uitnodiging', + 'send_invite' => 'Uitnodiging versturen', 'sent_invite' => 'Uitnodiging succesvol verzonden', 'updated_user' => 'Gebruiker succesvol aangepast', - 'invitation_message' => 'U bent uigenodigd door :invitor. ', + 'invitation_message' => 'U bent uitgenodigd door :invitor. ', 'register_to_add_user' => 'Meld u aan om een gebruiker toe te voegen', 'user_state' => 'Status', 'edit_user' => 'Bewerk gebruiker', @@ -417,11 +417,11 @@ return array( 'deleted_user' => 'Gebruiker succesvol verwijderd', 'limit_users' => 'Sorry, dit zou de limiet van '.MAX_NUM_USERS.' gebruikers overschrijden', - 'confirm_email_invoice' => 'Weet u zeker dat u deze factuur wilt mailen?', - 'confirm_email_quote' => 'Weet u zeker dat u deze offerte wilt mailen?', - 'confirm_recurring_email_invoice' => 'Terugkeren (herhalen) staat aan, weet u zeker dat u deze factuur wilt mailen?', + 'confirm_email_invoice' => 'Weet u zeker dat u deze factuur wilt e-mailen?', + 'confirm_email_quote' => 'Weet u zeker dat u deze offerte wilt e-mailen?', + 'confirm_recurring_email_invoice' => 'Terugkeren (herhalen) staat aan, weet u zeker dat u deze factuur wilt e-mailen?', - 'cancel_account' => 'Zeg Account Op', + 'cancel_account' => 'Account opzeggen', 'cancel_account_message' => 'Waarschuwing: Dit zal al uw data verwijderen. Er is geen manier om dit ongedaan te maken', 'go_back' => 'Ga Terug', @@ -457,10 +457,10 @@ return array( 'sent' => 'verzonden', 'timesheets' => 'Timesheets', - 'payment_title' => 'Geef uw betalingsadres en kredietkaartgegevens op', - 'payment_cvv' => '*Dit is de code van 3-4 tekens op de achterkant van uw kaart', + 'payment_title' => 'Geef uw betalingsadres en creditcardgegevens op', + 'payment_cvv' => '*Dit is de code van 3 of 4 tekens op de achterkant van uw kaart', 'payment_footer1' => '*Betalingsadres moet overeenkomen met het adres dat aan uw kaart gekoppeld is.', - 'payment_footer2' => '*Klik alstublieft slechts 1 keer op "PAY NOW" - verwerking kan tot 1 minuut duren.', + 'payment_footer2' => '*Klik alstublieft slechts één keer op "PAY NOW" - verwerking kan tot 1 minuut duren.', 'vat_number' => 'BTW-nummer', 'id_number' => 'Identificatienummer', @@ -489,7 +489,7 @@ return array( 'invoice_history' => 'Factuurgeschiedenis', 'quote_history' => 'Offertegeschiedenis', 'current_version' => 'Huidige versie', - 'select_versiony' => 'Selecteer versie', + 'select_version' => 'Selecteer versie', 'view_history' => 'Bekijk geschiedenis', 'edit_payment' => 'Bewerk betaling', @@ -498,20 +498,20 @@ return array( 'restore_user' => 'Herstel gebruiker', 'restored_user' => 'Gebruiker succesvol hersteld', 'show_deleted_users' => 'Toon verwijderde gebruikers', - 'email_templates' => 'Emailsjablonen', - 'invoice_email' => 'Factuuremail', - 'payment_email' => 'Betalingsemail', - 'quote_email' => 'Offerte-email', + 'email_templates' => 'E-mailsjablonen', + 'invoice_email' => 'Factuur-e-mail', + 'payment_email' => 'Betalings-e-mail', + 'quote_email' => 'Offerte-e-mail', 'reset_all' => 'Reset alles', 'approve' => 'Goedkeuren', 'token_billing_type_id' => 'Betalingstoken', - 'token_billing_help' => 'Stelt u in staat om kredietkaart gegevens bij uw gateway op te slaan en ze later te gebruiken.', + 'token_billing_help' => 'Stelt u in staat om creditcard gegevens bij uw gateway op te slaan en ze later te gebruiken.', 'token_billing_1' => 'Inactief', 'token_billing_2' => 'Opt-in - checkbox is getoond maar niet geselecteerd', 'token_billing_3' => 'Opt-out - checkbox is getoond en geselecteerd', 'token_billing_4' => 'Altijd', - 'token_billing_checkbox' => 'Sla kredietkaart gegevens op', + 'token_billing_checkbox' => 'Sla carditcard gegevens op', 'view_in_stripe' => 'In Stripe bekijken', 'use_card_on_file' => 'Gebruik opgeslagen kaart', 'edit_payment_details' => 'Betalingsdetails aanpassen', @@ -525,7 +525,7 @@ return array( 'billing_address' => 'Factuuradres', 'billing_method' => 'Betaalmethode', 'order_overview' => 'Orderoverzicht', - 'match_address' => '*Addres moet overeenkomen met adres van kredietkaart.', + 'match_address' => '*Adres moet overeenkomen met adres van creditcard.', 'click_once' => '*Klik alstublieft maar één keer; het kan een minuut duren om de betaling te verwerken.', 'default_invoice_footer' => 'Stel standaard factuurfooter in', @@ -550,7 +550,7 @@ return array( 'created_gateway' => 'Gateway succesvol aangemaakt', 'deleted_gateway' => 'Gateway succesvol verwijderd', 'pay_with_paypal' => 'PayPal', - 'pay_with_card' => 'Kredietkaart', + 'pay_with_card' => 'Creditcard', 'change_password' => 'Verander wachtwoord', 'current_password' => 'Huidig wachtwoord', @@ -572,17 +572,17 @@ return array( 'set_password' => 'Stel wachtwoord in', 'converted' => 'Omgezet', - 'email_approved' => 'Email me wanneer een offerte is goedgekeurd', + 'email_approved' => 'Email mij wanneer een offerte is goedgekeurd', 'notification_quote_approved_subject' => 'Offerte :invoice is goedgekeurd door :client', 'notification_quote_approved' => ':client heeft offerte :invoice goedgekeurd voor :amount.', 'resend_confirmation' => 'Verstuurd bevestingsmail opnieuw', 'confirmation_resent' => 'De bevestigingsmail is opnieuw verstuurd', 'gateway_help_42' => ':link om te registreren voor BitPay.
    Opmerking: gebruik een Legacy API Key, niet een API token.', - 'payment_type_credit_card' => 'Kredietkaart', + 'payment_type_credit_card' => 'Creditcard', 'payment_type_paypal' => 'PayPal', 'payment_type_bitcoin' => 'Bitcoin', - 'knowledge_base' => 'Kennis databank', + 'knowledge_base' => 'Kennisbank', 'partial' => 'Gedeeld', 'partial_remaining' => ':partial / :balance', @@ -591,11 +591,11 @@ return array( 'client_name' => 'Klantnaam', 'pdf_settings' => 'PDF-instellingen', 'product_settings' => 'Productinstellingen', - 'auto_wrap' => 'Automatisch lijn afbreken', + 'auto_wrap' => 'Automatisch regel afbreken', 'duplicate_post' => 'Opgelet: de volgende pagina is twee keer doorgestuurd. De tweede verzending is genegeerd.', 'view_documentation' => 'Bekijk documentatie', 'app_title' => 'Gratis Open-Source Online Facturatie', - 'app_description' => 'Invoice Ninja is een gratis, open-source oplossing voor het aanmkaen en versturen van facturen aan klanten. Met Invoice Ninja, kun je gemakkelijk mooie facturen aanmaken en verzenden van om het even welk toestel met internettoegang. Je klanten kunnen je facturen afdrukken, downloaden als pdf bestanden en je zelfs online betalen vanuit het systeem.', + 'app_description' => 'Invoice Ninja is een gratis, open-source oplossing voor het maken en versturen van facturen aan klanten. Met Invoice Ninja, kun je gemakkelijk mooie facturen maken en verzenden vanaf elk apparaat met internettoegang. Je klanten kunnen je facturen afdrukken, downloaden als pdf bestand en je zelfs online betalen vanuit het systeem.', 'rows' => 'rijen', 'www' => 'www', @@ -635,7 +635,7 @@ return array( 'timer' => 'Timer', 'manual' => 'Manueel', 'date_and_time' => 'Datum en tijd', - 'second' => 'second', + 'second' => 'seconde', 'seconds' => 'seconden', 'minute' => 'minuut', 'minutes' => 'minuten', @@ -670,9 +670,9 @@ return array( 'pro_plan_title' => 'NINJA PRO', 'pro_plan_call_to_action' => 'Nu upgraden!', - 'pro_plan_feature1' => 'Maak ongelimiteerd klanten aan', + 'pro_plan_feature1' => 'Ongelimiteerd klanten aanmaken', 'pro_plan_feature2' => 'Toegang tot 10 mooie factuur ontwerpen', - 'pro_plan_feature3' => 'Aangepaste URLs - "YourBrand.InvoiceNinja.com"', + 'pro_plan_feature3' => 'Aangepaste URLs - "UwMerk.InvoiceNinja.com"', 'pro_plan_feature4' => 'Verwijder "Aangemaakt door Invoice Ninja"', 'pro_plan_feature5' => 'Multi-user toegang & Activeit Tracking', 'pro_plan_feature6' => 'Maak offertes & Pro-forma facturen aan', @@ -696,14 +696,14 @@ return array( 'login' => 'Login', 'or' => 'of', - 'email_error' => 'Er was een probleem om de email te verzenden', - 'confirm_recurring_timing' => 'Opmerking: emails worden aan het begin van het uur verzonden.', - 'old_browser' => 'Gebruik a.u.b. een nieuwere browser', + 'email_error' => 'Er was een probleem met versturen van de e-mail', + 'confirm_recurring_timing' => 'Opmerking: e-mails worden aan het begin van het uur verzonden.', + 'old_browser' => 'Gebruik a.u.b. een moderne browser', 'payment_terms_help' => 'Stel de standaard factuurvervaldatum in', 'unlink_account' => 'Koppel account los', 'unlink' => 'Koppel los', 'show_address' => 'Toon Adres', - 'show_address_help' => 'Verplicht de klant om zijn factuur adres op te geven', + 'show_address_help' => 'Verplicht de klant om zijn factuuradres op te geven', 'update_address' => 'Adres aanpassen', 'update_address_help' => 'Pas het adres van de klant aan met de ingevulde gegevens', 'times' => 'Tijden', @@ -718,7 +718,7 @@ return array( 'font_size' => 'Tekstgrootte', 'primary_color' => 'Primaire kleur', 'secondary_color' => 'Secundaire kleur', - 'customize_design' => 'Pas design aan', + 'customize_design' => 'Pas ontwerp aan', 'content' => 'Inhoud', 'styles' => 'Stijlen', @@ -740,9 +740,9 @@ return array( 'created_by_invoice' => 'Aangemaakt door :invoice', 'primary_user' => 'Primaire gebruiker', 'help' => 'Help', - 'customize_help' => '

    We gebruiken pdfmake om de factuur ontwerpen declaratief te definieren. De pdfmake playground is een interessante manier om de library in actie te zien.

    -

    Gebruik dot notatie om een "kind eigenschap" te gebruiken. Bijvoorbeeld voor de klant naam te tonen gebruik je $client.name.

    -

    Als je ergens hulp bij nodig hebt, post dan een vraag op ons support forum.

    ', + 'customize_help' => '

    We gebruiken pdfmake om de factuurontwerpen declaratief te definieren. De pdfmake playground is een interessante manier om de library in actie te zien.

    +

    Gebruik puntnotatie om een "dochter eigenschap" te gebruiken. Bijvoorbeeld: om de naam van een klant te tonen gebruik je $client.name.

    +

    Als je ergens hulp bij nodig hebt, stel dan een vraag op ons support forum.

    ', 'invoice_due_date' => 'Vervaldatum', 'quote_due_date' => 'Geldig tot', @@ -763,11 +763,11 @@ return array( 'iframe_url_help2' => 'U kunt de functionaliteit testen door te klikken op \'Bekijk als ontvanger\' bij een factuur.', 'auto_bill' => 'Automatische incasso', - 'military_time' => '24 uurs tijd', + 'military_time' => '24-uurs klok', 'last_sent' => 'Laatst verstuurd', - 'reminder_emails' => 'Herinneringse-mails', - 'templates_and_reminders' => 'Templates en herinneringen', + 'reminder_emails' => 'Herinnerings-e-mails', + 'templates_and_reminders' => 'Sjablonen en herinneringen', 'subject' => 'Onderwerp', 'body' => 'Tekst', 'first_reminder' => 'Eerste herinnering', @@ -787,17 +787,17 @@ return array( 'expired_quotes' => 'Verlopen offertes', 'sign_up_using' => 'Meld u aan met', - 'invalid_credentials' => 'Deze credentials zijn niet bij ons bekend', + 'invalid_credentials' => 'Deze inloggegevens zijn niet bij ons bekend', 'show_all_options' => 'Alle opties tonen', 'user_details' => 'Gebruiker gegevens', 'oneclick_login' => 'One-Click Login', 'disable' => 'Uitzetten', - 'invoice_quote_number' => 'Factuur en offerte nummers', - 'invoice_charges' => 'Facturatie kosten', + 'invoice_quote_number' => 'Factuur- en offertenummers', + 'invoice_charges' => 'Facturatiekosten', 'invitation_status' => [ - 'sent' => 'Email verstuurd', - 'opened' => 'Email geopend', + 'sent' => 'E-mail verstuurd', + 'opened' => 'E-mail geopend', 'viewed' => 'Factuur bekeken', ], 'notification_invoice_bounced' => 'We konden factuur :invoice niet afleveren bij :contact.', @@ -808,7 +808,7 @@ return array( 'custom_invoice_link' => 'Eigen factuurlink', 'total_invoiced' => 'Totaal gefactureerd', 'open_balance' => 'Openstaand bedrag', - 'verify_email' => 'Klik alstublieft op de link in de accountbevestigingse-mail om uw e-mailadres te bevestigen.', + 'verify_email' => 'Klik alstublieft op de link in de accountbevestigings-e-mail om uw e-mailadres te bevestigen.', 'basic_settings' => 'Basisinstellingen', 'pro' => 'Pro', 'gateways' => 'Betalingsverwerkers', @@ -817,7 +817,7 @@ return array( 'next_send_on' => 'Verstuur volgende: :date', 'no_longer_running' => 'Deze factuur is niet ingepland', 'general_settings' => 'Algemene instellingen', - 'customize' => 'Pas aan', + 'customize' => 'Aanpassen', 'oneclick_login_help' => 'Verbind een account om zonder wachtwoord in te kunnen loggen', 'referral_code_help' => 'Verdien geld door onze applicatie online te delen', @@ -842,39 +842,39 @@ return array( 'quote_counter' => 'Offerteteller', 'type' => 'Type', - 'activity_1' => ':user created client :client', - 'activity_2' => ':user archived client :client', - 'activity_3' => ':user deleted client :client', - 'activity_4' => ':user created invoice :invoice', - 'activity_5' => ':user updated invoice :invoice', - 'activity_6' => ':user emailed invoice :invoice to :contact', - 'activity_7' => ':contact viewed invoice :invoice', - 'activity_8' => ':user archived invoice :invoice', - 'activity_9' => ':user deleted invoice :invoice', - 'activity_10' => ':contact entered payment :payment for :invoice', - 'activity_11' => ':user updated payment :payment', - 'activity_12' => ':user archived payment :payment', - 'activity_13' => ':user deleted payment :payment', - 'activity_14' => ':user entered :credit credit', - 'activity_15' => ':user updated :credit credit', - 'activity_16' => ':user archived :credit credit', - 'activity_17' => ':user deleted :credit credit', - 'activity_18' => ':user created quote :quote', - 'activity_19' => ':user updated quote :quote', - 'activity_20' => ':user emailed quote :quote to :contact', - 'activity_21' => ':contact viewed quote :quote', - 'activity_22' => ':user archived quote :quote', - 'activity_23' => ':user deleted quote :quote', - 'activity_24' => ':user restored quote :quote', - 'activity_25' => ':user restored invoice :invoice', - 'activity_26' => ':user restored client :client', - 'activity_27' => ':user restored payment :payment', - 'activity_28' => ':user restored :credit credit', - 'activity_29' => ':contact approved quote :quote', + 'activity_1' => ':user heeft klant :client aangemaakt', + 'activity_2' => ':user heeft klant :client gearchiveerd', + 'activity_3' => ':user heeft klant :client verwijderd', + 'activity_4' => ':user heeft factuur :invoice aangemaakt', + 'activity_5' => ':user heeft factuur :invoice bijgewerkt', + 'activity_6' => ':user heeft factuur :invoice verstuurd naar :contact', + 'activity_7' => ':contact heeft factuur :invoice bekeken', + 'activity_8' => ':user heeft factuur :invoice gearchiveerd', + 'activity_9' => ':user heeft factuur :invoice verwijderd', + 'activity_10' => ':contact heeft betaling :payment ingevoerd voor factuur :invoice', + 'activity_11' => ':user heeft betaling :payment bijgewerkt', + 'activity_12' => ':user heeft betaling :payment gearchiveerd', + 'activity_13' => ':user heeft betaling :payment verwijderd', + 'activity_14' => ':user heeft :credit krediet ingevoerd', + 'activity_15' => ':user heeft :credit krediet bijgewerkt', + 'activity_16' => ':user heeft :credit krediet gearchiveerd', + 'activity_17' => ':user heeft :credit krediet verwijderd', + 'activity_18' => ':user heeft offerte :quote aangemaakt', + 'activity_19' => ':user heeft offerte :quote bijgewerkt', + 'activity_20' => ':user heeft offerte :quote verstuurd naar :contact', + 'activity_21' => ':contact heeft offerte :quote bekeken', + 'activity_22' => ':user heeft offerte :quote gearchiveerd', + 'activity_23' => ':user heeft offerte :quote verwijderd', + 'activity_24' => ':user heeft offerte :quote hersteld', + 'activity_25' => ':user heeft factuur :invoice hersteld', + 'activity_26' => ':user heeft klant :client hersteld', + 'activity_27' => ':user heeft betaling :payment hersteld', + 'activity_28' => ':user heeft :credit krediet hersteld', + 'activity_29' => ':contact heeft offerte :quote goedgekeurd', 'payment' => 'Betaling', 'system' => 'Systeem', - 'signature' => 'Emailondertekening', + 'signature' => 'E-mailhandtekening', 'default_messages' => 'Standaardberichten', 'quote_terms' => 'Offertevoorwaarden', 'default_quote_terms' => 'Standaard offertevoorwaarden', @@ -884,7 +884,7 @@ return array( 'free' => 'Gratis', 'quote_is_approved' => 'Deze offerte is geaccordeerd', - 'apply_credit' => 'Apply Credit', + 'apply_credit' => 'Krediet gebruiken', 'system_settings' => 'Systeeminstellingen', 'archive_token' => 'Archiveer token', 'archived_token' => 'Token succesvol gearchiveerd', @@ -910,289 +910,374 @@ return array( 'country' => 'Land', 'include' => 'Voeg in', - 'logo_too_large' => 'Your logo is :size, for better PDF performance we suggest uploading an image file less than 200KB', - 'import_freshbooks' => 'Import From FreshBooks', + 'logo_too_large' => 'Je logo is :size groot, voor betere PDF prestaties raden we je aan om een afbeelding kleiner dan 200KB te uploaden.', + 'import_freshbooks' => 'Importeren van FreshBooks', 'import_data' => 'Importeer data', 'source' => 'Bron', 'csv' => 'CSV', - 'client_file' => 'Klantbestand', + 'client_file' => 'Klantenbestand', 'invoice_file' => 'Factuurbestand', 'task_file' => 'Urenbestand', - 'no_mapper' => 'No valid mapping for file', - 'invalid_csv_header' => 'Invalid CSV Header', + 'no_mapper' => 'Geen geldige mapping voor bestand', + 'invalid_csv_header' => 'Ongeldige CSV kop', 'email_errors' => [ - 'inactive_client' => 'Emails can not be sent to inactive clients', - 'inactive_contact' => 'Emails can not be sent to inactive contacts', - 'inactive_invoice' => 'Emails can not be sent to inactive invoices', - 'user_unregistered' => 'Please register your account to send emails', - 'user_unconfirmed' => 'Please confirm your account to send emails', - 'invalid_contact_email' => 'Invalid contact email', + 'inactive_client' => 'E-mails kunnen niet worden verstuurd naar inactieve klanten', + 'inactive_contact' => 'E-mails kunnen niet worden verstuurd naar inactieve contactpersonen', + 'inactive_invoice' => 'E-mails kunnen niet worden verstuurd naar inactieve facturen', + 'user_unregistered' => 'Registreer een account om e-mails te kunnen versturen', + 'user_unconfirmed' => 'Bevestig uw account om e-mails te kunnen versturen', + 'invalid_contact_email' => 'Ongeldig e-mailadres van contactpersoon', ], - 'client_portal' => 'Klantportaal', + 'client_portal' => 'Klantenportaal', 'admin' => 'Admin', 'disabled' => 'Uitgeschakeld', 'show_archived_users' => 'Toon gearchiveerde gebruikers', 'notes' => 'Notities', 'invoice_will_create' => 'klant zal worden aangemaakt', 'invoices_will_create' => 'factuur zal worden aangemaakt', - 'failed_to_import' => 'The following records failed to import, they either already exist or are missing required fields.', + 'failed_to_import' => 'De volgende regels konden niet worden geïmporteerd, ze bestaan al of missen verplichte velden.', 'publishable_key' => 'Publishable Key', 'secret_key' => 'Secret Key', 'missing_publishable_key' => 'Set your Stripe publishable key for an improved checkout process', - 'email_design' => 'Email Design', - 'due_by' => 'Due by :date', - 'enable_email_markup' => 'Enable Markup', - 'enable_email_markup_help' => 'Make it easier for your clients to pay you by adding schema.org markup to your emails.', - 'template_help_title' => 'Templates Help', - 'template_help_1' => 'Available variables:', - 'email_design_id' => 'Email Style', - 'email_design_help' => 'Make your emails look more professional with HTML layouts', - 'plain' => 'Plain', - 'light' => 'Light', - 'dark' => 'Dark', + 'email_design' => 'E-mail Ontwerp', + 'due_by' => 'Vervaldatum :date', + 'enable_email_markup' => 'Opmaak inschakelen', + 'enable_email_markup_help' => 'Maak het gemakkelijker voor uw klanten om te betalen door scherma.org opmaak toe te voegen aan uw e-mails.', + 'template_help_title' => 'Hulp bij sjablonen', + 'template_help_1' => 'Beschikbare variabelen:', + 'email_design_id' => 'E-mail stijl', + 'email_design_help' => 'Geef uw e-mails een professionele uitstraling met HTML ontwerpen', + 'plain' => 'Platte tekst', + 'light' => 'Licht', + 'dark' => 'Donker', - 'industry_help' => 'Used to provide comparisons against the averages of companies of similar size and industry.', - 'subdomain_help' => 'Customize the invoice link subdomain or display the invoice on your own website.', - 'invoice_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the invoice number.', - 'quote_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the quote number.', - 'custom_client_fields_helps' => 'Add a text input to the client create/edit page and display the label and value on the PDF.', - 'custom_account_fields_helps' => 'Add a label and value to the company details section of the PDF.', - 'custom_invoice_fields_helps' => 'Add a text input to the invoice create/edit page and display the label and value on the PDF.', - 'custom_invoice_charges_helps' => 'Add a text input to the invoice create/edit page and include the charge in the invoice subtotals.', - 'color_help' => 'Note: the primary color is also used in the client portal and custom email designs.', + 'industry_help' => 'Wordt gebruikt om een vergelijking te kunnen maken met de gemiddelden van andere bedrijven uit dezelfde sector en van dezelfde grootte.', + 'subdomain_help' => 'Pas het factuur link subdomein aan of toon de factuur op uw eigen website.', + 'invoice_number_help' => 'Kies een voorvoegsel of gebruik een patroon om het factuurnummer dynamisch te genereren.', + 'quote_number_help' => 'Kies een voorvoegsel of gebruik een patroon om het offertenummer dynamisch te genereren.', + 'custom_client_fields_helps' => 'Plaatst een tekstveld op de klanten aanmaak-/bewerkpagina en toont het gekozen label op de PDF.', + 'custom_account_fields_helps' => 'Plaatst een tekstveld op de bedrijven aanmaak-/bewerkpagina en toont het gekozen label op de PDF.', + 'custom_invoice_fields_helps' => 'Plaatst een tekstveld op de factuur aanmaak-/bewerkpagina en toont het gekozen label op de PDF.', + 'custom_invoice_charges_helps' => 'Plaatst een tekstveld op de factuur aanmaak-/bewerkpagina en verwerkt de facturatiekosten in het subtotaal.', + 'color_help' => 'Opmerking: de primaire kleur wordt ook gebruikt in het klantenportaal en in aangepaste e-mailontwerpen.', - 'token_expired' => 'Validation token was expired. Please try again.', - 'invoice_link' => 'Invoice Link', - 'button_confirmation_message' => 'Click to confirm your email address.', - 'confirm' => 'Confirm', - 'email_preferences' => 'Email Preferences', - 'created_invoices' => 'Successfully created :count invoice(s)', - 'next_invoice_number' => 'The next invoice number is :number.', - 'next_quote_number' => 'The next quote number is :number.', + 'token_expired' => 'De validatie token is verlopen. Probeer het opnieuw.', + 'invoice_link' => 'Factuur Link', + 'button_confirmation_message' => 'Klik om uw e-mailadres te bevestigen.', + 'confirm' => 'Bevestigen', + 'email_preferences' => 'E-mailvoorkeuren', + 'created_invoices' => ':count facturen succesvol aangemaakt', //TODO: Implement pluralization? + 'next_invoice_number' => 'Het volgende factuurnummer is :number.', + 'next_quote_number' => 'Het volgende offertenummer is :number.', - 'days_before' => 'days before', - 'days_after' => 'days after', - 'field_due_date' => 'due date', - 'field_invoice_date' => 'invoice date', - 'schedule' => 'Schedule', - 'email_designs' => 'Email Designs', - 'assigned_when_sent' => 'Assigned when sent', + 'days_before' => 'dagen voor', + 'days_after' => 'dagen na', + 'field_due_date' => 'vervaldatum', + 'field_invoice_date' => 'factuurdatum', + 'schedule' => 'Schema', + 'email_designs' => 'E-mail Ontwerpen', + 'assigned_when_sent' => 'Toegwezen zodra verzonden', - 'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.', - 'white_label_purchase_link' => 'Purchase a white label license', + 'white_label_custom_css' => ':link voor $'.WHITE_LABEL_PRICE.' om eigen opmaak te gebruiken en ons project te ondersteunen.', + 'white_label_purchase_link' => 'Koop een whitelabel licentie', // Expense / vendor - 'expense' => 'Expense', - 'expenses' => 'Expenses', - 'new_expense' => 'Enter Expense', - 'enter_expense' => 'Enter Expense', - 'vendors' => 'Vendors', - 'new_vendor' => 'New Vendor', - 'payment_terms_net' => 'Net', - 'vendor' => 'Vendor', - 'edit_vendor' => 'Edit Vendor', - 'archive_vendor' => 'Archive Vendor', - 'delete_vendor' => 'Delete Vendor', - 'view_vendor' => 'View Vendor', - 'deleted_expense' => 'Successfully deleted expense', - 'archived_expense' => 'Successfully archived expense', - 'deleted_expenses' => 'Successfully deleted expenses', - 'archived_expenses' => 'Successfully archived expenses', + 'expense' => 'Uitgave', + 'expenses' => 'Uitgaven', + 'new_expense' => 'Nieuwe uitgave', + 'enter_expense' => 'Uitgave invoeren', + 'vendors' => 'Verkopers', + 'new_vendor' => 'Nieuwe verkoper', + 'payment_terms_net' => 'Betaaltermijn', + 'vendor' => 'Verkoper', + 'edit_vendor' => 'Bewerk verkoper', + 'archive_vendor' => 'Archiveer verkoper', + 'delete_vendor' => 'Verwijder verkoper', + 'view_vendor' => 'Bekijk verkoper', + 'deleted_expense' => 'Uitgave succesvol verwijderd', + 'archived_expense' => 'Uitgave succesvol gearchiveerd', + 'deleted_expenses' => 'Uitgaven succesvol verwijderd', + 'archived_expenses' => 'Uitgaven succesvol gearchiveerd', // Expenses - 'expense_amount' => 'Expense Amount', - 'expense_balance' => 'Expense Balance', - 'expense_date' => 'Expense Date', - 'expense_should_be_invoiced' => 'Should this expense be invoiced?', - 'public_notes' => 'Public Notes', - 'invoice_amount' => 'Invoice Amount', - 'exchange_rate' => 'Exchange Rate', - 'yes' => 'Yes', - 'no' => 'No', - 'should_be_invoiced' => 'Should be invoiced', - 'view_expense' => 'View expense # :expense', - 'edit_expense' => 'Edit Expense', - 'archive_expense' => 'Archive Expense', - 'delete_expense' => 'Delete Expense', - 'view_expense_num' => 'Expense # :expense', - 'updated_expense' => 'Successfully updated expense', - 'created_expense' => 'Successfully created expense', - 'enter_expense' => 'Enter Expense', - 'view' => 'View', - 'restore_expense' => 'Restore Expense', - 'invoice_expense' => 'Invoice Expense', - 'expense_error_multiple_clients' => 'The expenses can\'t belong to different clients', - 'expense_error_invoiced' => 'Expense has already been invoiced', - 'convert_currency' => 'Convert currency', + 'expense_amount' => 'Uitgave bedrag', + 'expense_balance' => 'Uitgave saldo', + 'expense_date' => 'Uitgave datum', + 'expense_should_be_invoiced' => 'Moet deze uitgave worden gefactureerd?', + 'public_notes' => 'Publieke opmerkingen', + 'invoice_amount' => 'Factuurbedrag', + 'exchange_rate' => 'Wisselkoers', + 'yes' => 'Ja', + 'no' => 'Nee', + 'should_be_invoiced' => 'Moet worden gefactureerd', + 'view_expense' => 'Bekijk uitgave #:expense', + 'edit_expense' => 'Bewerk uitgave', + 'archive_expense' => 'Archiveer uitgave', + 'delete_expense' => 'Verwijder uitgave', + 'view_expense_num' => 'Uitgave #:expense', + 'updated_expense' => 'Uitgave succesvol bijgewerkt', + 'created_expense' => 'Uitgave succesvol aangemaakt', + 'enter_expense' => 'Uitgave invoeren', + 'view' => 'Bekijken', + 'restore_expense' => 'Herstel uitgave', + 'invoice_expense' => 'Factuur uitgave', + 'expense_error_multiple_clients' => 'De uitgaven kunnen niet bij verschillende klanten horen', + 'expense_error_invoiced' => 'Uitgave is al gefactureerd', + 'convert_currency' => 'Valuta omrekenen', // Payment terms - 'num_days' => 'Number of days', - 'create_payment_term' => 'Create Payment Term', - 'edit_payment_terms' => 'Edit Payment Term', - 'edit_payment_term' => 'Edit Payment Term', - 'archive_payment_term' => 'Archive Payment Term', + 'num_days' => 'Aantal dagen', + 'create_payment_term' => 'Betalingstermijn aanmaken', + 'edit_payment_terms' => 'Bewerk betalingstermijnen', + 'edit_payment_term' => 'Bewerk betalingstermijn', + 'archive_payment_term' => 'Archiveer betalingstermijn', // recurring due dates - 'recurring_due_dates' => 'Recurring Invoice Due Dates', - 'recurring_due_date_help' => '

    Automatically sets a due date for the invoice.

    -

    Invoices on a monthly or yearly cycle set to be due on or before the day they are created will be due the next month. Invoices set to be due on the 29th or 30th in months that don\'t have that day will be due the last day of the month.

    -

    Invoices on a weekly cycle set to be due on the day of the week they are created will be due the next week.

    -

    For example:

    + 'recurring_due_dates' => 'Vervaldatums van terugkerende facturen', + 'recurring_due_date_help' => '

    Stelt automatisch een vervaldatum in voor de factuur.

    +

    Facturen die maandelijks of jaarlijks terugkeren en ingesteld zijn om te vervallen op of voor de datum waarop ze gemaakt zijn zullen de volgende maand vervallen. Facturen die ingesteld zijn te vervallen op de 29e of 30e van een maand die deze dag niet heeft zullen vervallen op de laatste dag van die maand.

    +

    Facturen die wekelijks terugkeren en ingesteld zijn om te vervallen op de dag van de week dat ze gemaakt zijn zullen de volgende week vervallen.

    +

    Bijvoorbeeld:

      -
    • Today is the 15th, due date is 1st of the month. The due date should likely be the 1st of the next month.
    • -
    • Today is the 15th, due date is the last day of the month. The due date will be the last day of the this month. -
    • -
    • Today is the 15th, due date is the 15th day of the month. The due date will be the 15th day of next month. -
    • -
    • Today is the Friday, due date is the 1st Friday after. The due date will be next Friday, not today. -
    • +
    • Vandaag is het de 15e, de vervaldatum is ingesteld op de eerste dag van de maand. De vervaldatum zal de eerste dag van de volgende maand zijn.
    • +
    • Vandaag is het de 15e, de vervaldatum is ingesteld op de laatste dag van de maand. De vervaldatum zal de laatste dag van deze maand zijn.
    • +
    • Vandaag is het de 15e, de vervaldatum is ingesteld op de 15e dag van de maand. De vervaldatum zal de 15e dag van de volgende maand zijn.
    • +
    • Vandaag is het vrijdag, de vervaldatum is ingesteld op de 1e vrijdag erna. De vervaldatum zal volgende week vrijdag zijn, niet vandaag.
    ', - 'due' => 'Due', - 'next_due_on' => 'Due Next: :date', - 'use_client_terms' => 'Use client terms', - 'day_of_month' => ':ordinal day of month', - 'last_day_of_month' => 'Last day of month', - 'day_of_week_after' => ':ordinal :day after', - 'sunday' => 'Sunday', - 'monday' => 'Monday', - 'tuesday' => 'Tuesday', - 'wednesday' => 'Wednesday', - 'thursday' => 'Thursday', - 'friday' => 'Friday', - 'saturday' => 'Saturday', + 'due' => 'Vervaldatum', + 'next_due_on' => 'Vervaldatum volgende: :date', + 'use_client_terms' => 'Gebruik betalingsvoorwaarden klant', + 'day_of_month' => ':ordinal dag van de maand', + 'last_day_of_month' => 'Laatste dag van de maand', + 'day_of_week_after' => ':ordinal :day erna', + 'sunday' => 'Zondag', + 'monday' => 'Maandag', + 'tuesday' => 'Dinsdag', + 'wednesday' => 'Woensdag', + 'thursday' => 'Donderdag', + 'friday' => 'Vrijdag', + 'saturday' => 'Zaterdag', // Fonts - 'header_font_id' => 'Header Font', - 'body_font_id' => 'Body Font', - 'color_font_help' => 'Note: the primary color and fonts are also used in the client portal and custom email designs.', + 'header_font_id' => 'Header lettertype', + 'body_font_id' => 'Body lettertype', + 'color_font_help' => 'Opmerking: de primaire kleuren en lettertypen wordt ook gebruikt in het klantenportaal en in aangepaste e-mailontwerpen.', - 'live_preview' => 'Live Preview', - 'invalid_mail_config' => 'Unable to send email, please check that the mail settings are correct.', + 'live_preview' => 'Live Voorbeeld', + 'invalid_mail_config' => 'Kon de e-mail niet verzenden, controleer of de e-mailinstellingen kloppen.', - 'invoice_message_button' => 'To view your invoice for :amount, click the button below.', - 'quote_message_button' => 'To view your quote for :amount, click the button below.', - 'payment_message_button' => 'Thank you for your payment of :amount.', - 'payment_type_direct_debit' => 'Direct Debit', - 'bank_accounts' => 'Bank Accounts', - 'add_bank_account' => 'Add Bank Account', - 'setup_account' => 'Setup Account', - 'import_expenses' => 'Import Expenses', - 'bank_id' => 'bank', - 'integration_type' => 'Integration Type', - 'updated_bank_account' => 'Successfully updated bank account', - 'edit_bank_account' => 'Edit Bank Account', - 'archive_bank_account' => 'Archive Bank Account', - 'archived_bank_account' => 'Successfully archived bank account', - 'created_bank_account' => 'Successfully created bank account', - 'validate_bank_account' => 'Validate Bank Account', - 'bank_accounts_help' => 'Connect a bank account to automatically import expenses and create vendors. Supports American Express and 400+ US banks.', - 'bank_password_help' => 'Note: your password is transmitted securely and never stored on our servers.', - 'bank_password_warning' => 'Warning: your password may be transmitted in plain text, consider enabling HTTPS.', - 'username' => 'Username', - 'account_number' => 'Account Number', - 'account_name' => 'Account Name', - 'bank_account_error' => 'Failed to retreive account details, please check your credentials.', - 'status_approved' => 'Approved', - 'quote_settings' => 'Quote Settings', - 'auto_convert_quote' => 'Auto convert quote', - 'auto_convert_quote_help' => 'Automatically convert a quote to an invoice when approved by a client.', - 'validate' => 'Validate', - 'info' => 'Info', - 'imported_expenses' => 'Successfully created :count_vendors vendor(s) and :count_expenses expense(s)', + 'invoice_message_button' => 'Klik op de onderstaande link om uw factuur van :amount te bekijken.', + 'quote_message_button' => 'Klik op de onderstaande link om uw offerte van :amount te bekijken.', + 'payment_message_button' => 'Bedankt voor uw betaling van :amount.', + 'payment_type_direct_debit' => 'Automatisch incasso', + 'bank_accounts' => 'Bankrekeningen', + 'add_bank_account' => 'Bankrekening toevoegen', + 'setup_account' => 'Rekening instellen', + 'import_expenses' => 'Uitgaven importeren', + 'bank_id' => 'Bank', + 'integration_type' => 'Integratie Type', + 'updated_bank_account' => 'Bankrekening succesvol bijgewerkt', + 'edit_bank_account' => 'Bewerk bankrekening', + 'archive_bank_account' => 'Archiveer bankrekening', + 'archived_bank_account' => 'Bankrekening succesvol gearchiveerd', + 'created_bank_account' => 'Bankrekening succesvol toegevoegd', + 'validate_bank_account' => 'Bankrekening valideren', + 'bank_accounts_help' => 'Koppel een bankrekening om automatisch uitgaven en leveranciers te importeren. Ondersteund American Express en 400+ banken uit de VS.', + 'bank_password_help' => 'Opmerking: uw wachtwoord wordt beveiligd verstuurd en wordt nooit op onze servers opgeslagen.', + 'bank_password_warning' => 'Waarschuwing: uw wachtwoord wordt mogelijk als leesbare tekst verzonden, overweeg HTTPS in te schakelen.', + 'username' => 'Gebruikersnaam', + 'account_number' => 'Rekeningnummer', + 'account_name' => 'Rekeninghouder', + 'bank_account_error' => 'Het ophalen van rekeninggegevens is mislukt, controleer uw inloggegevens.', + 'status_approved' => 'Goedgekeurd', + 'quote_settings' => 'Offerte instellingen', + 'auto_convert_quote' => 'Offerte automatisch omzetten', + 'auto_convert_quote_help' => 'Zet een offerte automatisch om in een factuur zodra deze door een klant wordt goedgekeurd.', + 'validate' => 'Valideren', + 'info' => 'Informatie', + 'imported_expenses' => 'Er zijn succesvol :count_vendors leverancier(s) en :count_expenses uitgaven aangemaakt.', - 'iframe_url_help3' => 'Note: if you plan on accepting credit cards details we strongly recommend enabling HTTPS on your site.', - 'expense_error_multiple_currencies' => 'The expenses can\'t have different currencies.', - 'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.', + 'iframe_url_help3' => 'Opmerking: als u van plan bent om creditcard betalingen te accepteren raden wij u dringend aan om HTTPS in te schakelen op uw website.', + 'expense_error_multiple_currencies' => 'De uitgaven kunnen geen verschillende munteenheden hebben.', + 'expense_error_mismatch_currencies' => 'De munteenheid van de klant komt niet overeen met de munteenheid van de uitgave.', 'trello_roadmap' => 'Trello Roadmap', 'header_footer' => 'Header/Footer', - 'first_page' => 'first page', - 'all_pages' => 'all pages', - 'last_page' => 'last page', - 'all_pages_header' => 'Show header on', - 'all_pages_footer' => 'Show footer on', - 'invoice_currency' => 'Invoice Currency', - 'enable_https' => 'We strongly recommend using HTTPS to accept credit card details online.', - 'quote_issued_to' => 'Quote issued to', - 'show_currency_code' => 'Currency Code', - 'trial_message' => 'Your account will receive a free two week trial of our pro plan.', - 'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.', - 'trial_footer_last_day' => 'This is the last day of your free trial, :link to upgrade now.', - 'trial_call_to_action' => 'Start Free Trial', - 'trial_success' => 'Successfully enabled two week free pro plan trial', - 'overdue' => 'Overdue', - 'white_label_text' => 'Purchase a ONE YEAR white label license for $'.WHITE_LABEL_PRICE.' to remove the Invoice Ninja branding from the client portal and help support our project.', + 'first_page' => 'eerste pagina', + 'all_pages' => 'alle pagina\'s', + 'last_page' => 'laatste pagina', + 'all_pages_header' => 'Toon header op', + 'all_pages_footer' => 'Toon footer op', + 'invoice_currency' => 'Factuur valuta', + 'enable_https' => 'We raden u dringend aan om HTTPS te gebruiken om creditcard informatie digitaal te accepteren.', + 'quote_issued_to' => 'Offerte uitgeschreven voor', + 'show_currency_code' => 'Valutacode', + 'trial_message' => 'Uw account zal een gratis twee weken durende probeerversie van ons pro plan krijgen.', + 'trial_footer' => 'Uw gratis probeerversie duurt nog :count dagen, :link om direct te upgraden.', + 'trial_footer_last_day' => 'Dit is de laatste dag van uw gratis probeerversie, :link om direct te upgraden.', + 'trial_call_to_action' => 'Start gratis probeerversie', + 'trial_success' => 'De gratis twee weken durende probeerversie van het pro plan is succesvol geactiveerd.', + 'overdue' => 'Verlopen', + 'white_label_text' => 'Koop een één jaar geldige white label licentie van $'.WHITE_LABEL_PRICE.' om de Invoice Ninja logo\'s in het klantenportaal te verbergen en ons project te ondersteunen.', - 'navigation' => 'Navigation', - 'list_invoices' => 'List Invoices', - 'list_clients' => 'List Clients', - 'list_quotes' => 'List Quotes', - 'list_tasks' => 'List Tasks', - 'list_expenses' => 'List Expenses', - 'list_recurring_invoices' => 'List Recurring Invoices', - 'list_payments' => 'List Payments', - 'list_credits' => 'List Credits', - 'tax_name' => 'Tax Name', - 'report_settings' => 'Report Settings', - 'search_hotkey' => 'shortcut is /', + 'navigation' => 'Navigatie', + 'list_invoices' => 'Toon Facturen', + 'list_clients' => 'Toon Klanten', + 'list_quotes' => 'Toon Offertes', + 'list_tasks' => 'Toon Taken', + 'list_expenses' => 'Toon Uitgaven', + 'list_recurring_invoices' => 'Toon Terugkerende Facturen', + 'list_payments' => 'Toon Betalingen', + 'list_credits' => 'Toon Kredieten', + 'tax_name' => 'Belasting naam', + 'report_settings' => 'Rapport instellingen', + 'search_hotkey' => 'Snelkoppeling is /', - 'new_user' => 'New User', - 'new_product' => 'New Product', - 'new_tax_rate' => 'New Tax Rate', - 'invoiced_amount' => 'Invoiced Amount', - 'invoice_item_fields' => 'Invoice Item Fields', - 'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.', - 'recurring_invoice_number' => 'Recurring Invoice Number', - 'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.', + 'new_user' => 'Nieuwe Gebruiker', + 'new_product' => 'Nieuw Product', + 'new_tax_rate' => 'Nieuw BTW-tarief', + 'invoiced_amount' => 'Gefactureerd bedrag', + 'invoice_item_fields' => 'Factuurregels', + 'custom_invoice_item_fields_help' => 'Voeg een veld toe bij het aanmaken van een factuurregel en toon het label met de waarde op de PDF.', + 'recurring_invoice_number' => 'Nummer terugkerende factuur', + 'recurring_invoice_number_prefix_help' => 'Kies een voorvoegsel voor het factuurnummer van terugkerende facturen. De standaard is: \'R\'.', 'enable_client_portal' => 'Dashboard', - 'enable_client_portal_help' => 'Show/hide the dashboard page in the client portal.', + 'enable_client_portal_help' => 'Toon/verberg de dashboard pagina in het klantenportaal.', // Client Passwords - 'enable_portal_password'=>'Password protect invoices', - 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', - 'send_portal_password'=>'Generate password automatically', - 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', + 'enable_portal_password'=>'Facturen beveiligen met een wachtwoord', + 'enable_portal_password_help'=>'Geeft u de mogelijkheid om een wachtwoord in te stellen voor elke contactpersoon. Als er een wachtwoord is ingesteld moet de contactpersoon het wachtwoord invoeren voordat deze facturen kan bekijken.', + 'send_portal_password'=>'Wachtwoord automatisch genereren', + 'send_portal_password_help'=>'Als er geen wachtwoord is ingesteld zal deze automatisch worden gegenereerd en verzonden bij de eerste factuur.', - 'expired' => 'Expired', - 'invalid_card_number' => 'The credit card number is not valid.', - 'invalid_expiry' => 'The expiration date is not valid.', - 'invalid_cvv' => 'The CVV is not valid.', - 'cost' => 'Cost', - 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', + 'expired' => 'Verlopen', + 'invalid_card_number' => 'Het creditcardnummer is niet geldig.', + 'invalid_expiry' => 'De verloopdatum is niet geldig.', + 'invalid_cvv' => 'Het CVV-nummer is niet geldig.', + 'cost' => 'Kosten', + 'create_invoice_for_sample' => 'Opmerking: maak uw eerste factuur om hier een voorbeeld te zien.', // User Permissions - 'owner' => 'Owner', - 'administrator' => 'Administrator', - 'administrator_help' => 'Allow user to manage users, change settings and modify all records', - 'user_create_all' => 'Create clients, invoices, etc.', - 'user_view_all' => 'View all clients, invoices, etc.', - 'user_edit_all' => 'Edit all clients, invoices, etc.', - 'gateway_help_20' => ':link to sign up for Sage Pay.', - 'gateway_help_21' => ':link to sign up for Sage Pay.', - 'partial_due' => 'Partial Due', - 'restore_vendor' => 'Restore Vendor', - 'restored_vendor' => 'Successfully restored vendor', - 'restored_expense' => 'Successfully restored expense', - 'permissions' => 'Permissions', - 'create_all_help' => 'Allow user to create and modify records', - 'view_all_help' => 'Allow user to view records they didn\'t create', - 'edit_all_help' => 'Allow user to modify records they didn\'t create', - 'view_payment' => 'View Payment', + 'owner' => 'Eigenaar', + 'administrator' => 'Beheerder', + 'administrator_help' => 'Geef gebruiker de toestemming om andere gebruikers te beheren, instellingen te wijzigen en alle regels te bewerken.', + 'user_create_all' => 'Aanmaken van klanten, facturen, enz.', + 'user_view_all' => 'Bekijken van klanten, facturen, enz.', + 'user_edit_all' => 'Bewerken van alle klanten, facturen, enz.', + 'gateway_help_20' => ':link om aan te melden voor Sage Pay.', + 'gateway_help_21' => ':link om aan te melden voor Sage Pay.', + 'partial_due' => 'Gedeeltelijke vervaldatum', + 'restore_vendor' => 'Leverancier herstellen', + 'restored_vendor' => 'Leverancier succesvol hersteld', + 'restored_expense' => 'Uitgave succesvol hersteld', + 'permissions' => 'Rechten', + 'create_all_help' => 'Gebruiker toestemming geven om nieuwe regels aan te maken en te bewerken', + 'view_all_help' => 'Gebruiker toestemming geven om regels te bekijken die hij niet heeft gemaakt', + 'edit_all_help' => 'Gebruiker toestemming geven om regels te bewerken die hij niet heeft gemaakt', + 'view_payment' => 'Betaling bekijken', - 'january' => 'January', - 'february' => 'February', - 'march' => 'March', - 'april' => 'April', - 'may' => 'May', - 'june' => 'June', - 'july' => 'July', - 'august' => 'August', - 'september' => 'September', - 'october' => 'October', - 'november' => 'November', - 'december' => 'December', + 'january' => 'januari', + 'february' => 'februari', + 'march' => 'maart', + 'april' => 'april', + 'may' => 'mei', + 'june' => 'juni', + 'july' => 'juli', + 'august' => 'augustus', + 'september' => 'september', + 'october' => 'oktober', + 'november' => 'november', + 'december' => 'december', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); \ No newline at end of file diff --git a/resources/lang/nl/validation.php b/resources/lang/nl/validation.php index 09fadc64c161..4a4aebfa41d6 100644 --- a/resources/lang/nl/validation.php +++ b/resources/lang/nl/validation.php @@ -17,14 +17,14 @@ return array( "active_url" => ":attribute is geen geldige URL.", "after" => ":attribute moet een datum na :date zijn.", "alpha" => ":attribute mag alleen letters bevatten.", - "alpha_dash" => ":attribute mag alleen letters, nummers, onderstreep(_) en strepen(-) bevatten.", + "alpha_dash" => ":attribute mag alleen letters, nummers, lage streep (_) en liggende streep (-) bevatten.", "alpha_num" => ":attribute mag alleen letters en nummers bevatten.", "array" => ":attribute moet geselecteerde elementen bevatten.", "before" => ":attribute moet een datum voor :date zijn.", "between" => array( "numeric" => ":attribute moet tussen :min en :max zijn.", "file" => ":attribute moet tussen :min en :max kilobytes zijn.", - "string" => ":attribute moet tussen :min en :max karakters zijn.", + "string" => ":attribute moet tussen :min en :max tekens zijn.", "array" => ":attribute moet tussen :min en :max items bevatten.", ), "confirmed" => ":attribute bevestiging komt niet overeen.", @@ -47,14 +47,14 @@ return array( "max" => array( "numeric" => ":attribute moet minder dan :max zijn.", "file" => ":attribute moet minder dan :max kilobytes zijn.", - "string" => ":attribute moet minder dan :max karakters zijn.", + "string" => ":attribute moet minder dan :max tekens zijn.", "array" => ":attribute mag maximaal :max items bevatten.", ), "mimes" => ":attribute moet een bestand zijn van het bestandstype :values.", "min" => array( "numeric" => ":attribute moet minimaal :min zijn.", "file" => ":attribute moet minimaal :min kilobytes zijn.", - "string" => ":attribute moet minimaal :min karakters zijn.", + "string" => ":attribute moet minimaal :min tekens zijn.", "array" => ":attribute moet minimaal :min items bevatten.", ), "not_in" => "Het geselecteerde :attribute is ongeldig.", @@ -70,7 +70,7 @@ return array( "size" => array( "numeric" => ":attribute moet :size zijn.", "file" => ":attribute moet :size kilobyte zijn.", - "string" => ":attribute moet :size karakters lang zijn.", + "string" => ":attribute moet :size tekens lang zijn.", "array" => ":attribute moet :size items bevatten.", ), "unique" => ":attribute is al in gebruik.", @@ -81,7 +81,7 @@ return array( "notmasked" => "De waarden zijn verborgen", "less_than" => 'Het :attribute moet minder zijn dan :value', "has_counter" => 'De waarde moet {$counter} bevatten', - "valid_contacts" => "Alle contacten moeten een e-mailadres of een naam hebben", + "valid_contacts" => "Alle contactpersonen moeten een e-mailadres of een naam hebben", "valid_invoice_items" => "De factuur overschrijd het maximale aantal", /* diff --git a/resources/lang/pl/pagination.php b/resources/lang/pl/pagination.php new file mode 100644 index 000000000000..950c5e96cc41 --- /dev/null +++ b/resources/lang/pl/pagination.php @@ -0,0 +1,20 @@ + '« Poprzedni', + + 'next' => 'Następny »', + +); diff --git a/resources/lang/pl/passwords.php b/resources/lang/pl/passwords.php new file mode 100644 index 000000000000..28ccb957b349 --- /dev/null +++ b/resources/lang/pl/passwords.php @@ -0,0 +1,22 @@ + "Hasło musi mieć conajmniej sześć znaków i być takie samo jak potwierdzające.", + "user" => "Użytkownik o podanym adresie e-mail nie istnieje.", + "token" => "Wprowadzony token jest nieprawidłowy.", + "sent" => "Link do resetowania hasła został wysłany.", + "reset" => "Twoje hasło zostało zresetowane!", + +]; \ No newline at end of file diff --git a/resources/lang/pl/reminders.php b/resources/lang/pl/reminders.php new file mode 100644 index 000000000000..25824a7f285a --- /dev/null +++ b/resources/lang/pl/reminders.php @@ -0,0 +1,24 @@ + "Hasło musi mieć conajmniej sześć znaków i być takie samo jak potwierdzające.", + + "user" => "Użytkownik o podanym adresie e-mail nie istnieje.", + + "token" => "Wprowadzony token jest nieprawidłowy.", + + "sent" => "Przypomnienie hasła zostało wysłane!", + +); \ No newline at end of file diff --git a/resources/lang/pl/texts.php b/resources/lang/pl/texts.php new file mode 100644 index 000000000000..9b91d00cfdd6 --- /dev/null +++ b/resources/lang/pl/texts.php @@ -0,0 +1,1189 @@ + 'Organizacja', + 'name' => 'Nazwa', + 'website' => 'Strona internetowa', + 'work_phone' => 'Telefon służbowy', + 'address' => 'Adres', + 'address1' => 'Ulica', + 'address2' => 'Nr', + 'city' => 'Miasto', + 'state' => 'Województwo', + 'postal_code' => 'Kod pocztowy', + 'country_id' => 'Kraj', + 'contacts' => 'Kontakty', + 'first_name' => 'Imię', + 'last_name' => 'Nazwisko', + 'phone' => 'Telefon', + 'email' => 'Email', + 'additional_info' => 'Dodatkowe informacje', + 'payment_terms' => 'Warunki płatnicze', + 'currency_id' => 'Waluta', + 'size_id' => 'Wielkość firmy', + 'industry_id' => 'Branża', + 'private_notes' => 'Prywatne notatki', + 'invoice' => 'Faktura', + 'client' => 'Klient', + 'invoice_date' => 'Data Faktury', + 'due_date' => 'Termin', + 'invoice_number' => 'Numer Faktury', + 'invoice_number_short' => 'Faktura #', + 'po_number' => 'PO Number', + 'po_number_short' => 'PO #', + 'frequency_id' => 'Jak często', + 'discount' => 'Przecena', + 'taxes' => 'Podatki', + 'tax' => 'Podatek', + 'item' => 'Pozycja', + 'description' => 'Opis', + 'unit_cost' => 'Cena jednostkowa', + 'quantity' => 'Ilość', + 'line_total' => 'Wartość całkowita', + 'subtotal' => 'Suma częściowa', + 'paid_to_date' => 'Wypłacono do tej pory', + 'balance_due' => 'Balance Due', + 'invoice_design_id' => 'Szablon', + 'terms' => 'Warunki', + 'your_invoice' => 'Twoja faktura', + 'remove_contact' => 'Usuń kontakt', + 'add_contact' => 'Dodaj kontakt', + 'create_new_client' => 'Dodaj nowego klienta', + 'edit_client_details' => 'Edytuj dane klienta', + 'enable' => 'Aktywuj', + 'learn_more' => 'Więcej informacji', + 'manage_rates' => 'Zarządzaj stawkami', + 'note_to_client' => 'Informacja dla klienta', + 'invoice_terms' => 'Warunki do faktury', + 'save_as_default_terms' => 'Zapisz jako domyślne warunki', + 'download_pdf' => 'Pobierz PDF', + 'pay_now' => 'Zapłać teraz', + 'save_invoice' => 'Zapisz Fakturę', + 'clone_invoice' => 'Skopiuj Fakturę', + 'archive_invoice' => 'Zarchiwizuj Fakturę', + 'delete_invoice' => 'Usuń Fakturę', + 'email_invoice' => 'Wyślij Fakturę', + 'enter_payment' => 'Wprowadź Płatność', + 'tax_rates' => 'Stawki podatkowe', + 'rate' => 'Stawka', + 'settings' => 'Ustawienia', + 'enable_invoice_tax' => 'Aktywuj możliwość ustawienia podatku do faktury', + 'enable_line_item_tax' => 'Aktywuj możliwość ustawienia podatku do pozycji na fakturze', + 'dashboard' => 'Pulpit', + 'clients' => 'Klienci', + 'invoices' => 'Faktury', + 'payments' => 'Płatności', + 'credits' => 'Kredyty', + 'history' => 'Historia', + 'search' => 'Szukaj', + 'sign_up' => 'Zapisz się', + 'guest' => 'Gość', + 'company_details' => 'Dane Firmy', + 'online_payments' => 'Płatności Online', + 'notifications' => 'Powiadomienia E-mail', + 'import_export' => 'Import | Export', + 'done' => 'Gotowe', + 'save' => 'Zapisz', + 'create' => 'Dodaj', + 'upload' => 'Prześlij', + 'import' => 'Import', + 'download' => 'Pobierz', + 'cancel' => 'Anuluj', + 'close' => 'Zamknij', + 'provide_email' => 'Wprowadź poprawny e-mail.', + 'powered_by' => 'Oparte na', + 'no_items' => 'Brak pozycji', + 'recurring_invoices' => 'Faktury okresowe', + 'recurring_help' => '

    Wysyła automatycznie te same faktury klientom tygodniowo, miesięcznie, kwartałowo lub rocznie.

    +

    Użyj :MONTH, :QUARTER lub :YEAR dla dynamicznych dat. Podstawowa arytmetyka działa również, np: :MONTH-1.

    +

    Przykłady dynamicznych zmiennych na fakturze:

    +
      +
    • "Gym membership for the month of :MONTH" => "Gym membership for the month of July"
    • +
    • ":YEAR+1 yearly subscription" => "2015 Yearly Subscription"
    • +
    • "Retainer payment for :QUARTER+1" => "Retainer payment for Q2"
    • +
    ', + 'in_total_revenue' => 'całkowity przychód', + 'billed_client' => 'Obciążony klient', + 'billed_clients' => 'Obciążeni klienci', + 'active_client' => 'Aktywny klient', + 'active_clients' => 'Aktywni klienci', + 'invoices_past_due' => 'Opóźnione faktury', + 'upcoming_invoices' => 'Nadchodzące Faktury', + 'average_invoice' => 'Średnia wartość Faktury', + 'archive' => 'Archiwum', + 'delete' => 'Usuń', + 'archive_client' => 'Zarchiwizuj klienta', + 'delete_client' => 'Usuń klienta', + 'archive_payment' => 'Zarchiwizuj płatność', + 'delete_payment' => 'Usuń płatność', + 'archive_credit' => 'Zarchiwizuj kredyt', + 'delete_credit' => 'Usuń kredyt', + 'show_archived_deleted' => 'Pokaż zarchiwizowane/usunięte', + 'filter' => 'Filtruj', + 'new_client' => 'Nowy klient', + 'new_invoice' => 'Nowa Faktura', + 'new_payment' => 'Nowa Płatność', + 'new_credit' => 'Nowy Kredyt', + 'contact' => 'Kontakt', + 'date_created' => 'Data utworzenia', + 'last_login' => 'Ostatnie logowanie', + 'balance' => 'Saldo', + 'action' => 'Akcja', + 'status' => 'Status', + 'invoice_total' => 'Faktura ogółem', + 'frequency' => 'Częstotliwość', + 'start_date' => 'Początkowa data', + 'end_date' => 'Końcowa data', + 'transaction_reference' => 'Referencja transakcji', + 'method' => 'Metoda', + 'payment_amount' => 'Kwota płatności', + 'payment_date' => 'Data płatności', + 'credit_amount' => 'Kwota kredytu', + 'credit_balance' => 'Saldo kredytowe', + 'credit_date' => 'Data kredytu', + 'empty_table' => 'Brak danych w tabeli', + 'select' => 'Wybierz', + 'edit_client' => 'Edytuj klienta', + 'edit_invoice' => 'Edytuj fakturę', + 'create_invoice' => 'Utwórz Fakturę', + 'enter_credit' => 'Wprowadź kredyt', + 'last_logged_in' => 'Ostatnie logowanie w', + 'details' => 'Szczegóły', + 'standing' => 'Standing', + 'credit' => 'Kredyt', + 'activity' => 'Czynność', + 'date' => 'Data', + 'message' => 'Wiadomość', + 'adjustment' => 'Dostosowanie', + 'are_you_sure' => 'Jesteś pewny?', + 'payment_type_id' => 'Typ płatności', + 'amount' => 'Kwota', + 'work_email' => 'Email', + 'language_id' => 'Język', + 'timezone_id' => 'Strefa czasowa', + 'date_format_id' => 'Format daty', + 'datetime_format_id' => 'Format Data/Godzina', + 'users' => 'Użytkownicy', + 'localization' => 'Lokalizacja', + 'remove_logo' => 'Usuń logo', + 'logo_help' => 'Obsługiwane: JPEG, GIF i PNG', + 'payment_gateway' => 'Bramka płatnicza', + 'gateway_id' => 'Bramka płatnicza', + 'email_notifications' => 'Email powiadomienia', + 'email_sent' => 'Powiadom mnie kiedy faktura jest wysłana', + 'email_viewed' => 'Powiadom mnie kiedy faktura jest otworzona', + 'email_paid' => 'Powiadom mnie kiedy faktura jest zapłacona', + 'site_updates' => 'Aktualizacje strony', + 'custom_messages' => 'Niestandardowe komunikaty', + 'default_email_footer' => 'Ustaw domyślny podpis email', + 'select_file' => 'Wybierz plik', + 'first_row_headers' => 'Użyj pierwszego wiersza jako nagłówek', + 'column' => 'Kolumna', + 'sample' => 'Przykład', + 'import_to' => 'Zaimportuj do', + 'client_will_create' => 'klient będzie utworzony', + 'clients_will_create' => 'klienci będą utworzeni', + 'email_settings' => 'Ustawienia e-mail', + 'client_view_styling' => 'Client View Styling', + 'pdf_email_attachment' => 'Attach PDFs', + 'custom_css' => 'Custom CSS', + 'import_clients' => 'Import Client Data', + 'csv_file' => 'CSV file', + 'export_clients' => 'Export Client Data', + 'created_client' => 'Successfully created client', + 'created_clients' => 'Successfully created :count client(s)', + 'updated_settings' => 'Successfully updated settings', + 'removed_logo' => 'Successfully removed logo', + 'sent_message' => 'Successfully sent message', + 'invoice_error' => 'Please make sure to select a client and correct any errors', + 'limit_clients' => 'Sorry, this will exceed the limit of :count clients', + 'payment_error' => 'There was an error processing your payment. Please try again later.', + 'registration_required' => 'Please sign up to email an invoice', + 'confirmation_required' => 'Please confirm your email address', + 'updated_client' => 'Successfully updated client', + 'created_client' => 'Successfully created client', + 'archived_client' => 'Successfully archived client', + 'archived_clients' => 'Successfully archived :count clients', + 'deleted_client' => 'Successfully deleted client', + 'deleted_clients' => 'Successfully deleted :count clients', + 'updated_invoice' => 'Successfully updated invoice', + 'created_invoice' => 'Successfully created invoice', + 'cloned_invoice' => 'Successfully cloned invoice', + 'emailed_invoice' => 'Successfully emailed invoice', + 'and_created_client' => 'and created client', + 'archived_invoice' => 'Successfully archived invoice', + 'archived_invoices' => 'Successfully archived :count invoices', + 'deleted_invoice' => 'Successfully deleted invoice', + 'deleted_invoices' => 'Successfully deleted :count invoices', + 'created_payment' => 'Successfully created payment', + 'created_payments' => 'Successfully created :count payment(s)', + 'archived_payment' => 'Successfully archived payment', + 'archived_payments' => 'Successfully archived :count payments', + 'deleted_payment' => 'Successfully deleted payment', + 'deleted_payments' => 'Successfully deleted :count payments', + 'applied_payment' => 'Successfully applied payment', + 'created_credit' => 'Successfully created credit', + 'archived_credit' => 'Successfully archived credit', + 'archived_credits' => 'Successfully archived :count credits', + 'deleted_credit' => 'Successfully deleted credit', + 'deleted_credits' => 'Successfully deleted :count credits', + 'imported_file' => 'Successfully imported file', + 'updated_vendor' => 'Successfully updated vendor', + 'created_vendor' => 'Successfully created vendor', + 'archived_vendor' => 'Successfully archived vendor', + 'archived_vendors' => 'Successfully archived :count vendors', + 'deleted_vendor' => 'Successfully deleted vendor', + 'deleted_vendors' => 'Successfully deleted :count vendors', + 'confirmation_subject' => 'Invoice Ninja Account Confirmation', + 'confirmation_header' => 'Account Confirmation', + 'confirmation_message' => 'Please access the link below to confirm your account.', + 'invoice_subject' => 'New invoice :invoice from :account', + 'invoice_message' => 'To view your invoice for :amount, click the link below.', + 'payment_subject' => 'Payment Received', + 'payment_message' => 'Thank you for your payment of :amount.', + 'email_salutation' => 'Drogi :name,', + 'email_signature' => 'Pozdrowienia,', + 'email_from' => 'Zespół The Invoice Ninja', + 'invoice_link_message' => 'To view the invoice click the link below:', + 'notification_invoice_paid_subject' => 'Invoice :invoice was paid by :client', + 'notification_invoice_sent_subject' => 'Invoice :invoice was sent to :client', + 'notification_invoice_viewed_subject' => 'Invoice :invoice was viewed by :client', + 'notification_invoice_paid' => 'A payment of :amount was made by client :client towards Invoice :invoice.', + 'notification_invoice_sent' => 'The following client :client was emailed Invoice :invoice for :amount.', + 'notification_invoice_viewed' => 'The following client :client viewed Invoice :invoice for :amount.', + 'reset_password' => 'You can reset your account password by clicking the following button:', + 'secure_payment' => 'Bezpieczna płatność', + 'card_number' => 'Numer karty', + 'expiration_month' => 'Expiration Month', + 'expiration_year' => 'Expiration Year', + 'cvv' => 'CVV', + 'logout' => 'Wyloguj się', + 'sign_up_to_save' => 'Sign up to save your work', + 'agree_to_terms' => 'I agree to the Invoice Ninja :terms', + 'terms_of_service' => 'Terms of Service', + 'email_taken' => 'The email address is already registered', + 'working' => 'Working', + 'success' => 'Success', + 'success_message' => 'You have successfully registered! Please visit the link in the account confirmation email to verify your email address.', + 'erase_data' => 'This will permanently erase your data.', + 'password' => 'Hasło', + 'pro_plan_product' => 'Pro Plan', + 'pro_plan_success' => 'Thanks for choosing Invoice Ninja\'s Pro plan!

     
    + Next Steps

    A payable invoice has been sent to the email + address associated with your account. To unlock all of the awesome + Pro features, please follow the instructions on the invoice to pay + for a year of Pro-level invoicing.

    + Can\'t find the invoice? Need further assistance? We\'re happy to help + -- email us at contact@invoiceninja.com', + 'unsaved_changes' => 'You have unsaved changes', + 'custom_fields' => 'Custom Fields', + 'company_fields' => 'Company Fields', + 'client_fields' => 'Client Fields', + 'field_label' => 'Field Label', + 'field_value' => 'Field Value', + 'edit' => 'Edytuj', + 'set_name' => 'Set your company name', + 'view_as_recipient' => 'View as recipient', + 'product_library' => 'Product Library', + 'product' => 'Produkt', + 'products' => 'Produkty', + 'fill_products' => 'Auto-fill products', + 'fill_products_help' => 'Selecting a product will automatically fill in the description and cost', + 'update_products' => 'Auto-update products', + 'update_products_help' => 'Updating an invoice will automatically update the product library', + 'create_product' => 'Add Product', + 'edit_product' => 'Edit Product', + 'archive_product' => 'Archive Product', + 'updated_product' => 'Successfully updated product', + 'created_product' => 'Successfully created product', + 'archived_product' => 'Successfully archived product', + 'pro_plan_custom_fields' => ':link to enable custom fields by joining the Pro Plan', + 'advanced_settings' => 'Advanced Settings', + 'pro_plan_advanced_settings' => ':link to enable the advanced settings by joining the Pro Plan', + 'invoice_design' => 'Invoice Design', + 'specify_colors' => 'Specify colors', + 'specify_colors_label' => 'Select the colors used in the invoice', + 'chart_builder' => 'Chart Builder', + 'ninja_email_footer' => 'Use :site to invoice your clients and get paid online for free!', + 'go_pro' => 'Go Pro', + 'quote' => 'Oferta', + 'quotes' => 'Oferty', + 'quote_number' => 'Numer oferty', + 'quote_number_short' => 'Oferta #', + 'quote_date' => 'Data oferty', + 'quote_total' => 'Suma oferty', + 'your_quote' => 'Twoja oferta', + 'total' => 'Suma', + 'clone' => 'Clone', + 'new_quote' => 'Nowa oferta', + 'create_quote' => 'Stwórz ofertę', + 'edit_quote' => 'Edytuj ofertę', + 'archive_quote' => 'Archiwizuj ofertę', + 'delete_quote' => 'Usuń ofertę', + 'save_quote' => 'Zapisz ofertę', + 'email_quote' => 'Wyślij ofertę', + 'clone_quote' => 'Clone Quote', + 'convert_to_invoice' => 'Konwertuj do faktury', + 'view_invoice' => 'Zobacz fakturę', + 'view_client' => 'Zobacz klienta', + 'view_quote' => 'Zobacz ofertę', + 'updated_quote' => 'Successfully updated quote', + 'created_quote' => 'Successfully created quote', + 'cloned_quote' => 'Successfully cloned quote', + 'emailed_quote' => 'Successfully emailed quote', + 'archived_quote' => 'Successfully archived quote', + 'archived_quotes' => 'Successfully archived :count quotes', + 'deleted_quote' => 'Successfully deleted quote', + 'deleted_quotes' => 'Successfully deleted :count quotes', + 'converted_to_invoice' => 'Successfully converted quote to invoice', + 'quote_subject' => 'New quote $quote from :account', + 'quote_message' => 'To view your quote for :amount, click the link below.', + 'quote_link_message' => 'To view your client quote click the link below:', + 'notification_quote_sent_subject' => 'Quote :invoice was sent to :client', + 'notification_quote_viewed_subject' => 'Quote :invoice was viewed by :client', + 'notification_quote_sent' => 'The following client :client was emailed Quote :invoice for :amount.', + 'notification_quote_viewed' => 'The following client :client viewed Quote :invoice for :amount.', + 'session_expired' => 'Your session has expired.', + 'invoice_fields' => 'Invoice Fields', + 'invoice_options' => 'Invoice Options', + 'hide_quantity' => 'Hide Quantity', + 'hide_quantity_help' => 'If your line items quantities are always 1, then you can declutter invoices by no longer displaying this field.', + 'hide_paid_to_date' => 'Hide Paid to Date', + 'hide_paid_to_date_help' => 'Only display the "Paid to Date" area on your invoices once a payment has been received.', + 'charge_taxes' => 'Charge taxes', + 'user_management' => 'User Management', + 'add_user' => 'Add User', + 'send_invite' => 'Send invitation', + 'sent_invite' => 'Successfully sent invitation', + 'updated_user' => 'Successfully updated user', + 'invitation_message' => 'You\'ve been invited by :invitor. ', + 'register_to_add_user' => 'Please sign up to add a user', + 'user_state' => 'State', + 'edit_user' => 'Edit User', + 'delete_user' => 'Delete User', + 'active' => 'Active', + 'pending' => 'Pending', + 'deleted_user' => 'Successfully deleted user', + 'confirm_email_invoice' => 'Are you sure you want to email this invoice?', + 'confirm_email_quote' => 'Are you sure you want to email this quote?', + 'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?', + 'cancel_account' => 'Usuń konto', + 'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.', + 'go_back' => 'Go Back', + 'data_visualizations' => 'Data Visualizations', + 'sample_data' => 'Sample data shown', + 'hide' => 'Hide', + 'new_version_available' => 'A new version of :releases_link is available. You\'re running v:user_version, the latest is v:latest_version', + 'invoice_settings' => 'Invoice Settings', + 'invoice_number_prefix' => 'Invoice Number Prefix', + 'invoice_number_counter' => 'Invoice Number Counter', + 'quote_number_prefix' => 'Quote Number Prefix', + 'quote_number_counter' => 'Quote Number Counter', + 'share_invoice_counter' => 'Share invoice counter', + 'invoice_issued_to' => 'Invoice issued to', + 'invalid_counter' => 'To prevent a possible conflict please set either an invoice or quote number prefix', + 'mark_sent' => 'Mark Sent', + 'gateway_help_1' => ':link to sign up for Authorize.net.', + 'gateway_help_2' => ':link to sign up for Authorize.net.', + 'gateway_help_17' => ':link to get your PayPal API signature.', + 'gateway_help_27' => ':link to sign up for TwoCheckout.', + 'more_designs' => 'More designs', + 'more_designs_title' => 'Additional Invoice Designs', + 'more_designs_cloud_header' => 'Go Pro for more invoice designs', + 'more_designs_cloud_text' => '', + 'more_designs_self_host_text' => '', + 'buy' => 'Buy', + 'bought_designs' => 'Successfully added additional invoice designs', + 'sent' => 'sent', + 'vat_number' => 'VAT Number', + 'timesheets' => 'Timesheets', + 'payment_title' => 'Enter Your Billing Address and Credit Card information', + 'payment_cvv' => '*This is the 3-4 digit number onthe back of your card', + 'payment_footer1' => '*Billing address must match address associated with credit card.', + 'payment_footer2' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'id_number' => 'ID Number', + 'white_label_link' => 'White label', + 'white_label_header' => 'White Label', + 'bought_white_label' => 'Successfully enabled white label license', + 'white_labeled' => 'White labeled', + 'restore' => 'Przywróć', + 'restore_invoice' => 'Przywróć fakturę', + 'restore_quote' => 'Przywróć ofertę', + 'restore_client' => 'Przywróć klienta', + 'restore_credit' => 'Przywróć kredyt', + 'restore_payment' => 'Przywróć płatność', + 'restored_invoice' => 'Faktura została przywrócona', + 'restored_quote' => 'Oferta została przywrócona', + 'restored_client' => 'Klient został przywrócony', + 'restored_payment' => 'Płatność została przywrócona', + 'restored_credit' => 'Kredyt został przywrócony', + 'reason_for_canceling' => 'Help us improve our site by telling us why you\'re leaving.', + 'discount_percent' => 'Procent', + 'discount_amount' => 'Kwota', + 'invoice_history' => 'Historia faktury', + 'quote_history' => 'Historia oferty', + 'current_version' => 'Aktualna wersja', + 'select_version' => 'Wybierz wersję', + 'view_history' => 'Zobacz historię', + 'edit_payment' => 'Edytuj płatność', + 'updated_payment' => 'Successfully updated payment', + 'deleted' => 'Usunięte', + 'restore_user' => 'Przywróć użytkownika', + 'restored_user' => 'Użytkownik został przywrócony', + 'show_deleted_users' => 'Pokaż usuniętych użytkowników', + 'email_templates' => 'Szablony e-mail', + 'invoice_email' => 'Email faktury', + 'payment_email' => 'Email płatności', + 'quote_email' => 'Email oferty', + 'reset_all' => 'Resetuj wszystko', + 'approve' => 'Zatwierdź', + 'token_billing_type_id' => 'Token Billing', + 'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.', + 'token_billing_1' => 'Wyłączone', + 'token_billing_2' => 'Opt-in - checkbox is shown but not selected', + 'token_billing_3' => 'Opt-out - checkbox is shown and selected', + 'token_billing_4' => 'Zawsze', + 'token_billing_checkbox' => 'Store credit card details', + 'view_in_stripe' => 'View in Stripe', + 'use_card_on_file' => 'Use card on file', + 'edit_payment_details' => 'Edit payment details', + 'token_billing' => 'Save card details', + 'token_billing_secure' => 'The data is stored securely by :stripe_link', + 'support' => 'Support', + 'contact_information' => 'Contact Information', + '256_encryption' => '256-Bit Encryption', + 'amount_due' => 'Amount due', + 'billing_address' => 'Adres billingowy (rozliczeniowy)', + 'billing_method' => 'Billing Method', + 'order_overview' => 'Order overview', + 'match_address' => '*Address must match address associated with credit card.', + 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'invoice_footer' => 'Stopka faktury', + 'save_as_default_footer' => 'Save as default footer', + 'token_management' => 'Token Management', + 'tokens' => 'Tokens', + 'add_token' => 'Add Token', + 'show_deleted_tokens' => 'Show deleted tokens', + 'deleted_token' => 'Successfully deleted token', + 'created_token' => 'Successfully created token', + 'updated_token' => 'Successfully updated token', + 'edit_token' => 'Edit Token', + 'delete_token' => 'Delete Token', + 'token' => 'Token', + 'add_gateway' => 'Add Gateway', + 'delete_gateway' => 'Delete Gateway', + 'edit_gateway' => 'Edit Gateway', + 'updated_gateway' => 'Successfully updated gateway', + 'created_gateway' => 'Successfully created gateway', + 'deleted_gateway' => 'Successfully deleted gateway', + 'pay_with_paypal' => 'PayPal', + 'pay_with_card' => 'Credit Card', + 'change_password' => 'Zmień hasło', + 'current_password' => 'Aktualne hasło', + 'new_password' => 'Nowe hasło', + 'confirm_password' => 'Potwierdź hasło', + 'password_error_incorrect' => 'The current password is incorrect.', + 'password_error_invalid' => 'The new password is invalid.', + 'updated_password' => 'Successfully updated password', + 'api_tokens' => 'API Tokens', + 'users_and_tokens' => 'Users & Tokens', + 'account_login' => 'Account Login', + 'recover_password' => 'Recover your password', + 'forgot_password' => 'Forgot your password?', + 'email_address' => 'Email address', + 'lets_go' => 'Let\'s go', + 'password_recovery' => 'Password Recovery', + 'send_email' => 'Wyślij email', + 'set_password' => 'Ustaw hasło', + 'converted' => 'Converted', + 'email_approved' => 'Email me when a quote is approved', + 'notification_quote_approved_subject' => 'Quote :invoice was approved by :client', + 'notification_quote_approved' => 'The following client :client approved Quote :invoice for :amount.', + 'resend_confirmation' => 'Resend confirmation email', + 'confirmation_resent' => 'The confirmation email was resent', + 'gateway_help_42' => ':link to sign up for BitPay.
    Note: use a Legacy API Key, not an API token.', + 'payment_type_credit_card' => 'Karta kredytowa', + 'payment_type_paypal' => 'PayPal', + 'payment_type_bitcoin' => 'Bitcoin', + 'knowledge_base' => 'Baza wiedzy', + 'partial' => 'Partial', + 'partial_remaining' => ':partial of :balance', + 'more_fields' => 'More Fields', + 'less_fields' => 'Less Fields', + 'client_name' => 'Nazwa klienta', + 'pdf_settings' => 'Ustawienia PDF', + 'product_settings' => 'Ustawienia produktu', + 'auto_wrap' => 'Auto Line Wrap', + 'duplicate_post' => 'Warning: the previous page was submitted twice. The second submission had been ignored.', + 'view_documentation' => 'View Documentation', + 'app_title' => 'Free Open-Source Online Invoicing', + 'app_description' => 'Invoice Ninja is a free, open-source solution for invoicing and billing customers. With Invoice Ninja, you can easily build and send beautiful invoices from any device that has access to the web. Your clients can print your invoices, download them as pdf files, and even pay you online from within the system.', + 'rows' => 'wierszy', + 'www' => 'www', + 'logo' => 'Logo', + 'subdomain' => 'Subdomain', + 'provide_name_or_email' => 'Proszę podać imię i nazwisko lub adres e-mail', + 'charts_and_reports' => 'Raporty i wykresy', + 'chart' => 'Wykres', + 'report' => 'Raport', + 'group_by' => 'Grupuj według', + 'paid' => 'Zapłacone', + 'enable_report' => 'Raport', + 'enable_chart' => 'Wykres', + 'totals' => 'Suma', + 'run' => 'Run', + 'export' => 'Export', + 'documentation' => 'Documentation', + 'zapier' => 'Zapier', + 'recurring' => 'Okresowe', + 'last_invoice_sent' => 'Last invoice sent :date', + 'processed_updates' => 'Pomyślnie zakończona aktualizacja', + 'tasks' => 'Zadania', + 'new_task' => 'Nowe zadanie', + 'start_time' => 'Czas rozpoczęcia', + 'created_task' => 'Pomyślnie utworzono zadanie', + 'updated_task' => 'Pomyślnie zaktualizowano zadanie', + 'edit_task' => 'Edytuj zadanie', + 'archive_task' => 'Archiwizuj zadanie', + 'restore_task' => 'Przywróć zadanie', + 'delete_task' => 'Usuń zadanie', + 'stop_task' => 'Zatrzymaj zadanie', + 'time' => 'Czas', + 'start' => 'Rozpocznij', + 'stop' => 'Zatrzymaj', + 'now' => 'Teraz', + 'timer' => 'Czasomierz', + 'manual' => 'Manualnie', + 'date_and_time' => 'Data i czas', + 'second' => 'sekunda', + 'seconds' => 'sekund', + 'minute' => 'minuta', + 'minutes' => 'minut', + 'hour' => 'godzina', + 'hours' => 'godzin', + 'task_details' => 'Szczegóły zadania', + 'duration' => 'Czas trwania', + 'end_time' => 'Czas zakończenia', + 'end' => 'Koniec', + 'invoiced' => 'Invoiced', + 'logged' => 'Zalogowany', + 'running' => 'Running', + 'task_error_multiple_clients' => 'The tasks can\'t belong to different clients', + 'task_error_running' => 'Please stop running tasks first', + 'task_error_invoiced' => 'Tasks have already been invoiced', + 'restored_task' => 'Successfully restored task', + 'archived_task' => 'Successfully archived task', + 'archived_tasks' => 'Successfully archived :count tasks', + 'deleted_task' => 'Successfully deleted task', + 'deleted_tasks' => 'Successfully deleted :count tasks', + 'create_task' => 'Create Task', + 'stopped_task' => 'Successfully stopped task', + 'invoice_task' => 'Invoice Task', + 'invoice_labels' => 'Invoice Labels', + 'prefix' => 'Prefix', + 'counter' => 'Counter', + 'payment_type_dwolla' => 'Dwolla', + 'gateway_help_43' => ':link to sign up for Dwolla', + 'partial_value' => 'Must be greater than zero and less than the total', + 'more_actions' => 'More Actions', + 'pro_plan_title' => 'NINJA PRO', + 'pro_plan_call_to_action' => 'Upgrade Now!', + 'pro_plan_feature1' => 'Create Unlimited Clients', + 'pro_plan_feature2' => 'Access to 10 Beautiful Invoice Designs', + 'pro_plan_feature3' => 'Custom URLs - "YourBrand.InvoiceNinja.com"', + 'pro_plan_feature4' => 'Remove "Created by Invoice Ninja"', + 'pro_plan_feature5' => 'Multi-user Access & Activity Tracking', + 'pro_plan_feature6' => 'Create Quotes & Pro-forma Invoices', + 'pro_plan_feature7' => 'Customize Invoice Field Titles & Numbering', + 'pro_plan_feature8' => 'Option to Attach PDFs to Client Emails', + 'resume' => 'Resume', + 'break_duration' => 'Break', + 'edit_details' => 'Edit Details', + 'work' => 'Work', + 'timezone_unset' => 'Please :link to set your timezone', + 'click_here' => 'click here', + 'email_receipt' => 'Wyślij potwierdzenie zapłaty do klienta', + 'created_payment_emailed_client' => 'Successfully created payment and emailed client', + 'add_company' => 'Add Company', + 'untitled' => 'Untitled', + 'new_company' => 'New Company', + 'associated_accounts' => 'Successfully linked accounts', + 'unlinked_account' => 'Successfully unlinked accounts', + 'login' => 'Login', + 'or' => 'or', + 'email_error' => 'There was a problem sending the email', + 'confirm_recurring_timing' => 'Note: emails are sent at the start of the hour.', + 'payment_terms_help' => 'Sets the default invoice due date', + 'unlink_account' => 'Unlink Account', + 'unlink' => 'Unlink', + 'show_address' => 'Show Address', + 'show_address_help' => 'Require client to provide their billing address', + 'update_address' => 'Update Address', + 'update_address_help' => 'Update client\'s address with provided details', + 'times' => 'krotnie', + 'set_now' => 'Ustaw na teraz', + 'dark_mode' => 'Tryb ciemny', + 'dark_mode_help' => 'Show white text on black background', + 'add_to_invoice' => 'Add to invoice :invoice', + 'create_new_invoice' => 'Create new invoice', + 'task_errors' => 'Please correct any overlapping times', + 'from' => 'From', + 'to' => 'To', + 'font_size' => 'Font Size', + 'primary_color' => 'Primary Color', + 'secondary_color' => 'Secondary Color', + 'customize_design' => 'Customize Design', + 'content' => 'Content', + 'styles' => 'Styles', + 'defaults' => 'Defaults', + 'margins' => 'Margins', + 'header' => 'Header', + 'footer' => 'Footer', + 'custom' => 'Custom', + 'invoice_to' => 'Invoice to', + 'invoice_no' => 'Invoice No.', + 'recent_payments' => 'Ostatnie płatności', + 'outstanding' => 'Zaległe', + 'manage_companies' => 'Manage Companies', + 'total_revenue' => 'Całkowity dochód', + 'current_user' => 'Aktualny użytkownik', + 'new_recurring_invoice' => 'Nowa faktura okresowa', + 'recurring_invoice' => 'Okresowa faktura', + 'recurring_too_soon' => 'It\'s too soon to create the next recurring invoice, it\'s scheduled for :date', + 'created_by_invoice' => 'Utworzona przez :invoice', + 'primary_user' => 'Główny Użytkownik', + 'help' => 'Pomoc', + 'customize_help' => '

    We use pdfmake to define the invoice designs declaratively. The pdfmake playground provide\'s a great way to see the library in action.

    +

    To access a child property using dot notation. For example to show the client name you could use $client.name.

    +

    If you need help figuring something out post a question to our support forum.

    ', + 'invoice_due_date' => 'Termin', + 'quote_due_date' => 'Valid Until', + 'valid_until' => 'Valid Until', + 'reset_terms' => 'Resetuj warunki', + 'reset_footer' => 'Resetuj stópkę', + 'invoices_sent' => ':count faktura wysłana|:count faktury wysłane', + 'status_draft' => 'Wersja robocza', + 'status_sent' => 'Wysłane', + 'status_viewed' => 'Przeglądnięte', + 'status_partial' => 'Częściowo', + 'status_paid' => 'Zapłacone', + 'show_line_item_tax' => 'Wyświetl podatki pozycji w tej samej linii', + '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' => 'Automatyczny Rachunek', + 'military_time' => '24 godzinny czas', + 'last_sent' => 'Ostatnio wysłany', + 'reminder_emails' => 'Reminder Emails', + 'templates_and_reminders' => 'Szablony i przypomnienia', + 'subject' => 'Temat', + 'body' => 'Treść', + 'first_reminder' => 'Pierwsze przypomnienie', + 'second_reminder' => 'Drugie przypomnienie', + 'third_reminder' => 'Trzecie przypomnienie', + 'num_days_reminder' => 'Dni po terminie', + 'reminder_subject' => 'Reminder: Invoice :invoice from :account', + 'reset' => 'Reset', + 'invoice_not_found' => 'The requested invoice is not available', + 'referral_program' => 'Program referencyjny', + 'referral_code' => 'Referencyjny URL', + 'last_sent_on' => 'Ostatnio wysłany: :date', + 'page_expire' => 'This page will expire soon, :click_here to keep working', + 'upcoming_quotes' => 'Nadchodzące oferty', + 'expired_quotes' => 'Wygaśnięte oferty', + 'sign_up_using' => 'Zarejestruj się przy użyciu', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Pokaż wszystkie opcje', + 'user_details' => 'Dane użytkownika', + 'oneclick_login' => 'One-Click Logowanie', + 'disable' => 'Wyłącz', + 'invoice_quote_number' => 'Numery faktur i ofert', + 'invoice_charges' => 'Opłaty faktury', + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + 'custom_invoice_link' => 'Custom Invoice Link', + 'total_invoiced' => 'Total Invoiced', + 'open_balance' => 'Open Balance', + 'verify_email' => 'Please visit the link in the account confirmation email to verify your email address.', + 'basic_settings' => 'Ustawienia podstawowe', + 'pro' => 'Pro', + 'gateways' => 'Payment Gateways', + 'next_send_on' => 'Send Next: :date', + 'no_longer_running' => 'This invoice is not scheduled to run', + 'general_settings' => 'General Settings', + 'customize' => 'Customize', + 'oneclick_login_help' => 'Connect an account to login without a password', + 'referral_code_help' => 'Earn money by sharing our app online', + 'enable_with_stripe' => 'Aktywuj | Wymaga Stripe', + 'tax_settings' => 'Tax Settings', + 'create_tax_rate' => 'Dodaj stawkę podatkową', + 'updated_tax_rate' => 'Successfully updated tax rate', + 'created_tax_rate' => 'Successfully created tax rate', + 'edit_tax_rate' => 'Edit tax rate', + 'archive_tax_rate' => 'Archive Tax Rate', + 'archived_tax_rate' => 'Successfully archived the tax rate', + 'default_tax_rate_id' => 'Domyśłna stawka podatkowa', + 'tax_rate' => 'Stawka podatkowa', + 'recurring_hour' => 'Okresowa godzina', + 'pattern' => 'Pattern', + 'pattern_help_title' => 'Pattern Help', + 'pattern_help_1' => 'Create custom invoice and quote numbers by specifying a pattern', + 'pattern_help_2' => 'Available variables:', + 'pattern_help_3' => 'For example, :example would be converted to :value', + 'see_options' => 'See options', + 'invoice_counter' => 'Invoice Counter', + 'quote_counter' => 'Quote Counter', + 'type' => 'Type', + 'activity_1' => ':user created client :client', + 'activity_2' => ':user archived client :client', + 'activity_3' => ':user deleted client :client', + 'activity_4' => ':user created invoice :invoice', + 'activity_5' => ':user updated invoice :invoice', + 'activity_6' => ':user emailed invoice :invoice to :contact', + 'activity_7' => ':contact viewed invoice :invoice', + 'activity_8' => ':user archived invoice :invoice', + 'activity_9' => ':user deleted invoice :invoice', + 'activity_10' => ':contact entered payment :payment for :invoice', + 'activity_11' => ':user updated payment :payment', + 'activity_12' => ':user archived payment :payment', + 'activity_13' => ':user deleted payment :payment', + 'activity_14' => ':user entered :credit credit', + 'activity_15' => ':user updated :credit credit', + 'activity_16' => ':user archived :credit credit', + 'activity_17' => ':user deleted :credit credit', + 'activity_18' => ':user created quote :quote', + 'activity_19' => ':user updated quote :quote', + 'activity_20' => ':user emailed quote :quote to :contact', + 'activity_21' => ':contact viewed quote :quote', + 'activity_22' => ':user archived quote :quote', + 'activity_23' => ':user deleted quote :quote', + 'activity_24' => ':user restored quote :quote', + 'activity_25' => ':user restored invoice :invoice', + 'activity_26' => ':user restored client :client', + 'activity_27' => ':user restored payment :payment', + 'activity_28' => ':user restored :credit credit', + 'activity_29' => ':contact approved quote :quote', + 'activity_30' => ':user created :vendor', + 'activity_31' => ':user created :vendor', + 'activity_32' => ':user created :vendor', + 'activity_33' => ':user created :vendor', + 'activity_34' => ':user created expense :expense', + 'activity_35' => ':user created :vendor', + 'activity_36' => ':user created :vendor', + 'activity_37' => ':user created :vendor', + 'payment' => 'Payment', + 'system' => 'System', + 'signature' => 'Podpis e-mail', + 'default_messages' => 'Default Messages', + 'quote_terms' => 'Warunki oferty', + 'default_quote_terms' => 'Domyślne warunki oferty', + 'default_invoice_terms' => 'Domyślne warunki faktury', + 'default_invoice_footer' => 'Domyślna stopka faktury', + 'quote_footer' => 'Quote Footer', + 'free' => 'Free', + 'quote_is_approved' => 'This quote is approved', + 'apply_credit' => 'Apply Credit', + 'system_settings' => 'System Settings', + 'archive_token' => 'Archive Token', + 'archived_token' => 'Successfully archived token', + 'archive_user' => 'Archive User', + 'archived_user' => 'Successfully archived user', + 'archive_account_gateway' => 'Archive Gateway', + 'archived_account_gateway' => 'Successfully archived gateway', + 'archive_recurring_invoice' => 'Archiwizuj okresową fakturę', + 'archived_recurring_invoice' => 'Okresowa faktura została zarchiwizowana', + 'delete_recurring_invoice' => 'Usuń okresową fakturę', + 'deleted_recurring_invoice' => 'Okresowa faktura została usunięta.', + 'restore_recurring_invoice' => 'Przywróć okresową fakturę', + 'restored_recurring_invoice' => 'Okresowa faktura została przywrócona', + 'archived' => 'Zarchiwizowany', + 'untitled_account' => 'Firma bez nazwy', + 'before' => 'Przed', + 'after' => 'Po', + 'reset_terms_help' => 'Reset to the default account terms', + 'reset_footer_help' => 'Reset to the default account footer', + 'export_data' => 'Exportuj dane', + 'user' => 'Użytkownik', + 'country' => 'Kraj', + 'include' => 'Include', + 'logo_too_large' => 'Your logo is :size, for better PDF performance we suggest uploading an image file less than 200KB', + 'import_freshbooks' => 'Import From FreshBooks', + 'import_data' => 'Import Data', + 'source' => 'Źródło', + 'csv' => 'CSV', + 'client_file' => 'Plik klienta', + 'invoice_file' => 'Plik faktury', + 'task_file' => 'Plik zadania', + 'no_mapper' => 'No valid mapping for file', + 'invalid_csv_header' => 'Invalid CSV Header', + 'client_portal' => 'Portal klienta', + 'admin' => 'Administrator', + 'disabled' => 'Wyłączony', + 'show_archived_users' => 'Pokaż zarchiwizowanych użytkowników', + 'notes' => 'Notatki', + 'invoice_will_create' => 'client will be created', + 'invoices_will_create' => 'invoices will be created', + 'failed_to_import' => 'The following records failed to import, they either already exist or are missing required fields.', + 'publishable_key' => 'Publishable Key', + 'secret_key' => 'Sekretny klucz', + 'missing_publishable_key' => 'Set your Stripe publishable key for an improved checkout process', + 'email_design' => 'Email Design', + 'due_by' => 'Płatny do :date', + 'enable_email_markup' => 'Enable Markup', + 'enable_email_markup_help' => 'Make it easier for your clients to pay you by adding schema.org markup to your emails.', + 'template_help_title' => 'Templates Help', + 'template_help_1' => 'Available variables:', + 'email_design_id' => 'Email Style', + 'email_design_help' => 'Make your emails look more professional with HTML layouts', + 'plain' => 'Plain', + 'light' => 'Light', + 'dark' => 'Dark', + 'industry_help' => 'Used to provide comparisons against the averages of companies of similar size and industry.', + 'subdomain_help' => 'Customize the invoice link subdomain or display the invoice on your own website.', + 'invoice_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the invoice number.', + 'quote_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the quote number.', + 'custom_client_fields_helps' => 'Add a field when creating a client and display the label and value on the PDF.', + 'custom_account_fields_helps' => 'Add a label and value to the company details section of the PDF.', + 'custom_invoice_fields_helps' => 'Add a field when creating an invoice and display the label and value on the PDF.', + 'custom_invoice_charges_helps' => 'Add a field when creating an invoice and include the charge in the invoice subtotals.', + 'token_expired' => 'Validation token was expired. Please try again.', + 'invoice_link' => 'Invoice Link', + 'button_confirmation_message' => 'Click to confirm your email address.', + 'confirm' => 'Confirm', + 'email_preferences' => 'Email Preferences', + 'created_invoices' => 'Successfully created :count invoice(s)', + 'next_invoice_number' => 'The next invoice number is :number.', + 'next_quote_number' => 'The next quote number is :number.', + 'days_before' => 'days before', + 'days_after' => 'days after', + 'field_due_date' => 'termin', + 'field_invoice_date' => 'invoice date', + 'schedule' => 'Schedule', + 'email_designs' => 'Email Designs', + 'assigned_when_sent' => 'Assigned when sent', + 'white_label_purchase_link' => 'Purchase a white label license', + 'expense' => 'Wydatek', + 'expenses' => 'Wydatki', + 'new_expense' => 'Nowy wydatek', + 'enter_expense' => 'Dodaj wydatek', + 'vendors' => 'Vendors', + 'new_vendor' => 'New Vendor', + 'payment_terms_net' => 'Net', + 'vendor' => 'Vendor', + 'edit_vendor' => 'Edit Vendor', + 'archive_vendor' => 'Archive Vendor', + 'delete_vendor' => 'Delete Vendor', + 'view_vendor' => 'View Vendor', + 'deleted_expense' => 'Successfully deleted expense', + 'archived_expense' => 'Successfully archived expense', + 'deleted_expenses' => 'Successfully deleted expenses', + 'archived_expenses' => 'Successfully archived expenses', + 'expense_amount' => 'Expense Amount', + 'expense_balance' => 'Expense Balance', + 'expense_date' => 'Expense Date', + 'expense_should_be_invoiced' => 'Should this expense be invoiced?', + 'public_notes' => 'Public Notes', + 'invoice_amount' => 'Invoice Amount', + 'exchange_rate' => 'Exchange Rate', + 'yes' => 'Yes', + 'no' => 'No', + 'should_be_invoiced' => 'Should be invoiced', + 'view_expense' => 'View expense # :expense', + 'edit_expense' => 'Edit Expense', + 'archive_expense' => 'Archive Expense', + 'delete_expense' => 'Delete Expense', + 'view_expense_num' => 'Expense # :expense', + 'updated_expense' => 'Successfully updated expense', + 'created_expense' => 'Successfully created expense', + 'enter_expense' => 'Dodaj wydatek', + 'view' => 'View', + 'restore_expense' => 'Przywróć wydatek', + 'invoice_expense' => 'Faktura na wydatek', + 'expense_error_multiple_clients' => 'The expenses can\'t belong to different clients', + 'expense_error_invoiced' => 'Expense has already been invoiced', + 'convert_currency' => 'Konwersja waluty', + 'num_days' => 'Liczba dni', + 'create_payment_term' => 'Utwórz warunki płatności', + 'edit_payment_terms' => 'Edytuj warunki płatności', + 'edit_payment_term' => 'Edytuj warunki płatności', + 'archive_payment_term' => 'Zarchiwizuj warunki płatności', + 'recurring_due_dates' => 'Terminy faktur okresowych', + 'recurring_due_date_help' => '

    Automatycznie ustawia termin faktury.

    +

    Invoices on a monthly or yearly cycle set to be due on or before the day they are created will be due the next month. Invoices set to be due on the 29th or 30th in months that don\'t have that day will be due the last day of the month.

    +

    Invoices on a weekly cycle set to be due on the day of the week they are created will be due the next week.

    +

    For example:

    +
      +
    • Today is the 15th, due date is 1st of the month. The due date should likely be the 1st of the next month.
    • +
    • Today is the 15th, due date is the last day of the month. The due date will be the last day of the this month. +
    • +
    • Today is the 15th, due date is the 15th day of the month. The due date will be the 15th day of next month. +
    • +
    • Today is the Friday, due date is the 1st Friday after. The due date will be next Friday, not today. +
    • +
    ', + 'due' => 'Opłata', + 'next_due_on' => 'Następna opłata: :date', + 'use_client_terms' => 'Use client terms', + 'day_of_month' => ':ordinal day of month', + 'last_day_of_month' => 'Last day of month', + 'day_of_week_after' => ':ordinal :day after', + 'sunday' => 'Niedziela', + 'monday' => 'Poniedziałek', + 'tuesday' => 'Wtorek', + 'wednesday' => 'Środa', + 'thursday' => 'Czwartek', + 'friday' => 'Piątek', + 'saturday' => 'Sobota', + 'header_font_id' => 'Czcionka nagłówka', + 'body_font_id' => 'Czcionka', + 'color_font_help' => 'Notatka: Podstawowe kolory i czcionki są wykorzystywane na portalu klienta i w niestandardowych szablonach email-owych.', + 'live_preview' => 'Podgląd', + 'invalid_mail_config' => 'E-mail nie został wysłany. Sprawdź czy ustawienia mailowe są poprawne.', + 'invoice_message_button' => 'Aby wyświetlić fakturę za :amount, kliknij przycisk poniżej.', + 'quote_message_button' => 'Aby wyświetlić swoją ofertę na :amount, kliknij przycisk poniżej.', + 'payment_message_button' => 'Dziekuję za wpłatę :amount.', + 'payment_type_direct_debit' => 'Polecenie zapłaty', + 'bank_accounts' => 'Karty kredytowe i banki', + 'add_bank_account' => 'Dodaj konto bankowe', + 'setup_account' => 'Ustawienia konta', + 'import_expenses' => 'Koszty importu', + 'bank_id' => 'Bank', + 'integration_type' => 'Rodzaj integracji', + 'updated_bank_account' => 'Konto bankowe zostało zaktualizowane', + 'edit_bank_account' => 'Edytuj konto bankowe', + 'archive_bank_account' => 'Archiwizuj konto bankowe', + 'archived_bank_account' => 'Konto bankowe zostało zarchiwizowane.', + 'created_bank_account' => 'Konto bankowe zostało utworzone', + 'validate_bank_account' => 'Zatwierdź konto bankowe', + 'bank_password_help' => 'Notatka: Twoje hasło zostało wysłane bezpiecznie i nie jest przechowywane na naszych serwerach.', + 'bank_password_warning' => 'Ostrzeżenie: Twoje hasło może być wysłane w postaci zwykłego tekstu, rozwaź aktywację protokołu HTTPS.', + 'username' => 'Użytkownik', + 'account_number' => 'Numer konta', + 'account_name' => 'Nazwa konta', + 'bank_account_error' => 'Nie można pobrać danych konta, sprawdź uprawnienia.', + 'status_approved' => 'Zatwierdzono', + 'quote_settings' => 'Ustawienia oferty', + 'auto_convert_quote' => 'Automatycznie konwertuj ofertę', + 'auto_convert_quote_help' => 'Utwórz automatycznie fakturę z oferty zaakceptowanej przez klienta.', + 'validate' => 'Zatwierdź', + 'info' => 'Informacja', + 'imported_expenses' => 'Successfully created :count_vendors vendor(s) and :count_expenses expense(s)', + 'iframe_url_help3' => 'Note: if you plan on accepting credit cards details we strongly recommend enabling HTTPS on your site.', + 'expense_error_multiple_currencies' => 'The expenses can\'t have different currencies.', + 'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.', + 'trello_roadmap' => 'Trello Roadmap', + 'header_footer' => 'Header/Footer', + 'first_page' => 'Pierwsza strona', + 'all_pages' => 'Wszystkie strony', + 'last_page' => 'Ostatnia strona', + 'all_pages_header' => 'Pokaż nagłówek na', + 'all_pages_footer' => 'Pokaż stopkę na', + 'invoice_currency' => 'Waluta faktury', + 'enable_https' => 'Zalecamy korzystanie z protokołu HTTPS, aby zaakceptować dane karty kredytowej online.', + 'quote_issued_to' => 'Oferta wydana do', + 'show_currency_code' => 'Kod waluty', + 'trial_message' => 'Twoje konto otrzyma bezpłatny dwutygodniowy okres próbny naszego pro planu.', + 'trial_footer' => 'Bezpłatny okres próbny ważny tylko :count dni, aby aktualizować kliknij: :link.', + 'trial_footer_last_day' => 'To ostatni dzień twojego bezpłatnego okresu próbnego, aby zaktualizować kliknij: :link.', + 'trial_call_to_action' => 'Rozpocznij darmowy okres próbny', + 'trial_success' => 'Darmowy okres próbny został włączony', + 'overdue' => 'Zaległy', + + + 'white_label_text' => 'Kup white label licencję na JEDEN ROK za $'.WHITE_LABEL_PRICE.' aby usunąć z portalu klienta logo Invoice Ninja i wesprzeć nasz projekt.', + 'user_email_footer' => 'Aby dostosować ustawienia powiadomień email, zobacz '.SITE_URL.'/settings/notifications', + 'reset_password_footer' => 'If you did not request this password reset please email our support: '.CONTACT_EMAIL, + 'limit_users' => 'Sorry, this will exceed the limit of '.MAX_NUM_USERS.' users', + 'more_designs_self_host_header' => 'Kup 6 szablonów faktur za jedyne $'.INVOICE_DESIGNS_PRICE, + 'old_browser' => 'Proszę użyć nowszej przeglądarki', + 'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.', + 'bank_accounts_help' => 'Connect a bank account to automatically import expenses and create vendors. Supports American Express and 400+ US banks.', + 'security' => [ + 'too_many_attempts' => 'Zbyt wiele prób. Spróbuj ponownie za kilka minut.', + 'wrong_credentials' => 'Nieprawidłowy e-mail lub hasło.', + 'confirmation' => 'Twoje konto zostało potwierdzone!', + 'wrong_confirmation' => 'Błędny kod potwierdzający.', + 'password_forgot' => 'Informacje dotyczące resetowania hasła zostały wysłane na Twój adres e-mail.', + 'password_reset' => 'Twoje hasło zostało zmienione.', + 'wrong_password_reset' => 'Nieprawidłowe hasło. Spróbuj ponownie', + ], + 'pro_plan' => [ + 'remove_logo' => ':link to remove the Invoice Ninja logo by joining the Pro Plan', + 'remove_logo_link' => 'Kliknij tutaj', + ], + 'invitation_status' => [ + 'sent' => 'E-mail wysłany', + 'opened' => 'Email otwarty', + 'viewed' => 'Przeglądana faktura', + ], + 'email_errors' => [ + 'inactive_client' => 'E-maile nie mogą być wysyłane do klientów nieaktywnych', + 'inactive_contact' => 'E-mail nie może zostać wysłany do nieaktywnych kontaktów', + 'inactive_invoice' => 'E-mail nie może zostać wysłany do nieaktywnych faktur', + 'user_unregistered' => 'Proszę zarejestrować swoje konto, aby wysyłać e-maile', + 'user_unconfirmed' => 'Proszę potwierdzić swoje konto do wysyłania e-maili', + 'invalid_contact_email' => 'Nieprawidłowy e-mail kontaktowy', + ], + + 'navigation' => 'Nawigacja', + 'list_invoices' => 'Lista faktur', + 'list_clients' => 'Lista klientów', + 'list_quotes' => 'Lista ofert', + 'list_tasks' => 'Lista zadań', + 'list_expenses' => 'Lista wydatków', + 'list_recurring_invoices' => 'Lista faktur okresowych', + 'list_payments' => 'Lista wpłat', + 'list_credits' => 'Lista kredytów', + 'tax_name' => 'Nazwa podatku', + 'report_settings' => 'Ustawienia raportu', + 'search_hotkey' => 'skrót to /', + + 'new_user' => 'Nowy użytkownik', + 'new_product' => 'Nowy produkt', + 'new_tax_rate' => 'Nowa stawka podatkowa', + 'invoiced_amount' => 'Fakturowana kwota', + 'invoice_item_fields' => 'Invoice Item Fields', + 'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.', + 'recurring_invoice_number' => 'Numer faktury okresowej', + 'recurring_invoice_number_prefix_help' => 'Dodaj własny prefix do numeru faktury okresowej. Wartość domyślna to \'R\'.', + + // Client Passwords + 'enable_portal_password'=>'Hasło ochrony faktur', + 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', + 'send_portal_password'=>'Generate password automatically', + 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', + + 'expired' => 'Wygasło', + 'invalid_card_number' => 'Numer karty kredytowej jest nieprawidłowy.', + 'invalid_expiry' => 'Data ważności jest nieprawidłowa.', + 'invalid_cvv' => 'Kod CVV jest nieprawidłowy.', + 'cost' => 'Koszt', + 'create_invoice_for_sample' => 'Notatka: aby zobaczyć podgląd, utwórz fakturę.', + + // User Permissions + 'owner' => 'Właściciel', + 'administrator' => 'Administrator', + 'administrator_help' => 'Allow user to manage users, change settings and modify all records', + 'user_create_all' => 'Create clients, invoices, etc.', + 'user_view_all' => 'View all clients, invoices, etc.', + 'user_edit_all' => 'Edit all clients, invoices, etc.', + 'gateway_help_20' => ':link to sign up for Sage Pay.', + 'gateway_help_21' => ':link to sign up for Sage Pay.', + 'partial_due' => 'Partial Due', + 'restore_vendor' => 'Restore Vendor', + 'restored_vendor' => 'Successfully restored vendor', + 'restored_expense' => 'Successfully restored expense', + 'permissions' => 'Permissions', + 'create_all_help' => 'Allow user to create and modify records', + 'view_all_help' => 'Allow user to view records they didn\'t create', + 'edit_all_help' => 'Allow user to modify records they didn\'t create', + 'view_payment' => 'Zobacz wpłatę', + + 'january' => 'Styczeń', + 'february' => 'Luty', + 'march' => 'Marzec', + 'april' => 'Kwiecień', + 'may' => 'Maj', + 'june' => 'Czerwiec', + 'july' => 'Lipiec', + 'august' => 'Sierpień', + 'september' => 'Wrzesień', + 'october' => 'Październik', + 'november' => 'Listopad', + 'december' => 'Grudzień', + + // Documents + 'documents_header' => 'Dokumenty:', + 'email_documents_header' => 'Dokumenty:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Dokumenty', + 'expense_documents' => 'Załączone dokumenty', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Załącz dokumenty', + 'download_documents' => 'Ściągnij dokumenty (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Upuść pliki lub kliknij, aby przesłać', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'Plik jest zbyt duży ({{filesize}}MiB). Max rozmiar pliku: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'Nie możesz przesłać plików tego typu.', + 'ResponseError' => 'Serwer zwraca {{statusCode}} kod.', + 'CancelUpload' => 'Anuluj przesyłanie', + 'CancelUploadConfirmation' => 'Czy na pewno chcesz anulować przesyłanie pliku?', + 'RemoveFile' => 'Usuń plik', + ), + 'documents' => 'Dokumenty', + 'document_date' => 'Data dokumentu', + 'document_size' => 'Rozmiar', + + 'enable_client_portal' => 'Portal Klienta', + 'enable_client_portal_help' => 'Pokaż/ukryj portal klienta.', + 'enable_client_portal_dashboard' => 'Pulpit', + 'enable_client_portal_dashboard_help' => 'Pokaż/ukryj pulpit w panelu klienta.', + + // Plans + 'account_management' => 'Zarządzanie kontem', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Aktualizuj', + 'plan_change' => 'Zmień plan', + 'pending_change_to' => 'Zmienia się na', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Anuluj zmianę', + 'plan' => 'Plan', + 'expires' => 'Wygasa', + 'renews' => 'Odnawia', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Nigdy', + 'plan_free' => 'Darmowy', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Miesięcznie', + 'plan_term_yearly' => 'Rocznie', + 'plan_term_month' => 'Miesiąc', + 'plan_term_year' => 'Rok', + 'plan_price_monthly' => '$:price/miesiąc', + 'plan_price_yearly' => '$:price/rok', + 'updated_plan' => 'Ustawienia planu zaktualizowane', + 'plan_paid' => 'Termin rozpoczął', + 'plan_started' => 'Plan rozpoczął', + 'plan_expires' => 'Plan Wygasa', + + 'white_label_button' => 'Biała etykieta', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Plan Enterprise', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Kredyt', + 'plan_credit_description' => 'Kredyt za niewykorzystany czas', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'Zwrot został wystawiony.', + + 'live_preview' => 'Podgląd', + 'page_size' => 'Rozmiar strony', + 'live_preview_disabled' => 'Podgląd obrazu na żywo został wyłączony w celu wsparcia wybranej czcionki', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + +); + +return $LANG; + +?>. \ No newline at end of file diff --git a/resources/lang/pl/validation.php b/resources/lang/pl/validation.php new file mode 100644 index 000000000000..d3ffdb6a8155 --- /dev/null +++ b/resources/lang/pl/validation.php @@ -0,0 +1,106 @@ + ":attribute musi być zaakceptowany.", + "active_url" => ":attribute nie jest poprawnym URL-em.", + "after" => ":attribute musi być datą za :date.", + "alpha" => ":attribute może zawierać tylko litery.", + "alpha_dash" => ":attribute może zawierać tylko litery, liczby i myślniki.", + "alpha_num" => ":attribute może zawierać tylko litery i liczby.", + "array" => ":attribute musi być tablicą.", + "before" => ":attribute musi być datą przed :date.", + "between" => array( + "numeric" => ":attribute musi być pomiędzy :min - :max.", + "file" => ":attribute musi mieć rozmiar pomiędzy :min - :max kilobajtów.", + "string" => ":attribute musi mieć pomiędzy :min - :max znaków.", + "array" => ":attribute musi zawierać :min - :max pozycji.", + ), + "confirmed" => ":attribute potwierdzenie nie jest zgodne.", + "date" => ":attribute nie jest prawidłową datą.", + "date_format" => ":attribute nie jest zgodne z formatem :format.", + "different" => ":attribute i :other muszą być różne.", + "digits" => ":attribute musi mieć :digits cyfr.", + "digits_between" => ":attribute musi być w przedziale od :min do :max cyfr.", + "email" => ":attribute format jest nieprawidłowy.", + "exists" => "Zaznaczony :attribute jest niepoprawny.", + "image" => ":attribute musi być zdjęciem.", + "in" => "Zaznaczony :attribute jest niepoprawny.", + "integer" => ":attribute musi być liczbą całkowitą.", + "ip" => ":attribute musi być poprawnym adresem IP.", + "max" => array( + "numeric" => ":attribute nie może być większy niż :max.", + "file" => ":attribute nie może być większy niż :max kilobajtów.", + "string" => ":attribute nie może być dłuższy niż :max znaków.", + "array" => ":attribute nie może zawierać więcej niż :max pozycji.", + ), + "mimes" => ":attribute musi być plikiem o typie: :values.", + "min" => array( + "numeric" => ":attribute musi być przynajmniej :min.", + "file" => ":attribute musi mieć przynajmniej :min kilobajtów.", + "string" => ":attribute musi mieć przynajmniej :min znaków.", + "array" => ":attribute musi zawierać przynajmniej :min pozycji.", + ), + "not_in" => "Zaznaczony :attribute jest niepoprawny.", + "numeric" => ":attribute musi być cyfrą.", + "regex" => ":attribute format jest niepoprawny.", + "required" => ":attribute pole jest wymagane.", + "required_if" => ":attribute pole jest wymagane jeśli :other ma :value.", + "required_with" => ":attribute pole jest wymagane kiedy :values jest obecne.", + "required_without" => ":attribute pole jest wymagane kiedy :values nie występuje.", + "same" => ":attribute i :other muszą być takie same.", + "size" => array( + "numeric" => ":attribute musi mieć :size.", + "file" => ":attribute musi mieć :size kilobajtów.", + "string" => ":attribute musi mieć :size znaków.", + "array" => ":attribute musi zawierać :size pozycji.", + ), + "unique" => ":attribute już istnieje.", + "url" => ":attribute format jest nieprawidłowy.", + + "positive" => ":attribute musi być większe niż zero.", + "has_credit" => "Klient ma niewystarczająco kredytu.", + "notmasked" => "Wartości są maskowane", + "less_than" => ":attribute musi być mniejsze od :value", + "has_counter" => "Wartość musi zawierać {\$counter}", + "valid_contacts" => "Kontakt musi posiadać e-mail lub nazwę", + "valid_invoice_items" => "Faktura przekracza maksymalną kwotę", + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => array(), + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => array(), + +); \ No newline at end of file diff --git a/resources/lang/pt_BR/texts.php b/resources/lang/pt_BR/texts.php index 30fcfbf05c27..3791e5b0e68e 100644 --- a/resources/lang/pt_BR/texts.php +++ b/resources/lang/pt_BR/texts.php @@ -487,7 +487,7 @@ return array( 'invoice_history' => 'Histórico de Faturas', 'quote_history' => 'Histórico de Orçamentos', 'current_version' => 'Versão Atual', - 'select_versiony' => 'Selecionar versão', + 'select_version' => 'Selecionar versão', 'view_history' => 'Visualizar Histórico', 'edit_payment' => 'Editar Pagamento', @@ -929,267 +929,352 @@ return array( 'client_portal' => 'Portal do Cliente', 'admin' => 'Admin', - 'disabled' => 'Disabilitado', + 'disabled' => 'Desabilitado', 'show_archived_users' => 'Mostrar usuários arquivados', 'notes' => 'Observações', 'invoice_will_create' => 'cliente será criado', 'invoices_will_create' => 'faturas serão criadas', 'failed_to_import' => 'A importação dos seguintes registros falhou', - 'publishable_key' => 'Publishable Key', - 'secret_key' => 'Secret Key', - 'missing_publishable_key' => 'Set your Stripe publishable key for an improved checkout process', + 'publishable_key' => 'Chave Publicável', + 'secret_key' => 'Chave Secreta', + 'missing_publishable_key' => 'Defina o sua chave publicável do Stripe para um processo de pagamento melhorado', - 'email_design' => 'Email Design', - 'due_by' => 'Due by :date', - 'enable_email_markup' => 'Enable Markup', - 'enable_email_markup_help' => 'Make it easier for your clients to pay you by adding schema.org markup to your emails.', - 'template_help_title' => 'Templates Help', - 'template_help_1' => 'Available variables:', - 'email_design_id' => 'Email Style', - 'email_design_help' => 'Make your emails look more professional with HTML layouts', - 'plain' => 'Plain', - 'light' => 'Light', - 'dark' => 'Dark', + 'email_design' => 'Template de E-mail', + 'due_by' => 'Vencido em :date', + 'enable_email_markup' => 'Habilitar Marcação', + 'enable_email_markup_help' => 'Tornar mais fácil para os seus clientes efetuarem seus pagamentos, acrescentando marcação schema.org a seus e-mails.', + 'template_help_title' => 'Ajuda de Templates', + 'template_help_1' => 'Variáveis disponíveis:', + 'email_design_id' => 'Estilo de e-mails', + 'email_design_help' => 'Deixe seus e-mails mais profissionais com layouts HTML', + 'plain' => 'Plano', + 'light' => 'Claro', + 'dark' => 'Escuro', - 'industry_help' => 'Used to provide comparisons against the averages of companies of similar size and industry.', - 'subdomain_help' => 'Customize the invoice link subdomain or display the invoice on your own website.', - 'invoice_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the invoice number.', - 'quote_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the quote number.', - 'custom_client_fields_helps' => 'Add a text input to the client create/edit page and display the label and value on the PDF.', - 'custom_account_fields_helps' => 'Add a label and value to the company details section of the PDF.', - 'custom_invoice_fields_helps' => 'Add a text input to the invoice create/edit page and display the label and value on the PDF.', - 'custom_invoice_charges_helps' => 'Add a text input to the invoice create/edit page and include the charge in the invoice subtotals.', - 'color_help' => 'Note: the primary color is also used in the client portal and custom email designs.', + 'industry_help' => 'Usado para fornecer comparações contra as médias das empresas de tamanho e indústria similar.', + 'subdomain_help' => 'Personalizar o link da fatura ou exibir a fatura em seu próprio site.', + 'invoice_number_help' => 'Especifique um prefixo ou usar um padrão personalizado para definir dinamicamente o número da fatura.', + 'quote_number_help' => 'Especifique um prefixo ou usar um padrão personalizado para definir dinamicamente o número do orçamento.', + 'custom_client_fields_helps' => 'Adicionar uma entrada de texto na página Criar/Editar Cliente e exibir no PDF.', + 'custom_account_fields_helps' => 'Adicionar um rótulo e um valor para a seção detalhes da empresa do PDF.', + 'custom_invoice_fields_helps' => 'Adicionar uma entrada de texto na página Criar/Editar Fatura e exibir no PDF.', + 'custom_invoice_charges_helps' => 'Adicionar uma entrada de texto na página Criar/Editar Fatura e incluir nos subtotais da fatura.', + 'color_help' => 'Nota: A cor primária também é utilizada nos projetos do portal do cliente e-mail personalizado.', - 'token_expired' => 'Validation token was expired. Please try again.', - 'invoice_link' => 'Invoice Link', - 'button_confirmation_message' => 'Click to confirm your email address.', - 'confirm' => 'Confirm', - 'email_preferences' => 'Email Preferences', - 'created_invoices' => 'Successfully created :count invoice(s)', - 'next_invoice_number' => 'The next invoice number is :number.', - 'next_quote_number' => 'The next quote number is :number.', + 'token_expired' => 'Token de acesso expirado. Tente novamente!', + 'invoice_link' => 'Link da Fatura', + 'button_confirmation_message' => 'Clique para confirmar seu endereço de e-mail.', + 'confirm' => 'Confirmar', + 'email_preferences' => 'Preferências de E-mails', + 'created_invoices' => ':count fatura(s) criadas com sucesso', + 'next_invoice_number' => 'O número da próxima fatura será :number.', + 'next_quote_number' => 'O número do próximo orçamento será :number.', - 'days_before' => 'days before', - 'days_after' => 'days after', - 'field_due_date' => 'due date', - 'field_invoice_date' => 'invoice date', - 'schedule' => 'Schedule', - 'email_designs' => 'Email Designs', - 'assigned_when_sent' => 'Assigned when sent', + 'days_before' => 'dias antes', + 'days_after' => 'dias depois', + 'field_due_date' => 'data de vencimento', + 'field_invoice_date' => 'data da fatura', + 'schedule' => 'Agendamento', + 'email_designs' => 'Design de E-mails', + 'assigned_when_sent' => 'Assinar quando enviar', - 'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.', - 'white_label_purchase_link' => 'Purchase a white label license', + 'white_label_custom_css' => ':link apenas $'.WHITE_LABEL_PRICE.' para permitir um estilo personalizado e apoiar o nosso projecto.', + 'white_label_purchase_link' => 'Adquira uma licença white label', // Expense / vendor - 'expense' => 'Expense', - 'expenses' => 'Expenses', - 'new_expense' => 'Enter Expense', - 'enter_expense' => 'Enter Expense', - 'vendors' => 'Vendors', - 'new_vendor' => 'New Vendor', - 'payment_terms_net' => 'Net', - 'vendor' => 'Vendor', - 'edit_vendor' => 'Edit Vendor', - 'archive_vendor' => 'Archive Vendor', - 'delete_vendor' => 'Delete Vendor', - 'view_vendor' => 'View Vendor', - 'deleted_expense' => 'Successfully deleted expense', - 'archived_expense' => 'Successfully archived expense', - 'deleted_expenses' => 'Successfully deleted expenses', - 'archived_expenses' => 'Successfully archived expenses', + 'expense' => 'Despesa', + 'expenses' => 'Despesas', + 'new_expense' => 'Adicionar Despesa', + 'enter_expense' => 'Incluir Despesa', + 'vendors' => 'Fornecedor', + 'new_vendor' => 'Novo Fornecedor', + 'payment_terms_net' => 'Rede', + 'vendor' => 'Fornecedor', + 'edit_vendor' => 'Editar Fornecedor', + 'archive_vendor' => 'Arquivar Fornecedor', + 'delete_vendor' => 'Deletar Fornecedor', + 'view_vendor' => 'Visualizar Fornecedor', + 'deleted_expense' => 'Despesa excluída com sucesso', + 'archived_expense' => 'Despesa arquivada com sucesso', + 'deleted_expenses' => 'Despesas excluídas com sucesso', + 'archived_expenses' => 'Despesas arquivada com sucesso', // Expenses - 'expense_amount' => 'Expense Amount', - 'expense_balance' => 'Expense Balance', - 'expense_date' => 'Expense Date', - 'expense_should_be_invoiced' => 'Should this expense be invoiced?', - 'public_notes' => 'Public Notes', - 'invoice_amount' => 'Invoice Amount', - 'exchange_rate' => 'Exchange Rate', - 'yes' => 'Yes', - 'no' => 'No', - 'should_be_invoiced' => 'Should be invoiced', - 'view_expense' => 'View expense # :expense', - 'edit_expense' => 'Edit Expense', - 'archive_expense' => 'Archive Expense', - 'delete_expense' => 'Delete Expense', - 'view_expense_num' => 'Expense # :expense', - 'updated_expense' => 'Successfully updated expense', - 'created_expense' => 'Successfully created expense', - 'enter_expense' => 'Enter Expense', - 'view' => 'View', - 'restore_expense' => 'Restore Expense', - 'invoice_expense' => 'Invoice Expense', - 'expense_error_multiple_clients' => 'The expenses can\'t belong to different clients', - 'expense_error_invoiced' => 'Expense has already been invoiced', - 'convert_currency' => 'Convert currency', + 'expense_amount' => 'Total de Despesas', + 'expense_balance' => 'Saldo de Despesas', + 'expense_date' => 'Data da Despesa', + 'expense_should_be_invoiced' => 'Esta despesa deve ser faturada?', + 'public_notes' => 'Notas Públicas', + 'invoice_amount' => 'Total da Fatura', + 'exchange_rate' => 'Taxa de Câmbio', + 'yes' => 'Sim', + 'no' => 'Não', + 'should_be_invoiced' => 'Deve ser Faturada', + 'view_expense' => 'Visualizar despesa # :expense', + 'edit_expense' => 'Editar Despesa', + 'archive_expense' => 'Arquivar Despesa', + 'delete_expense' => 'Deletar Despesa', + 'view_expense_num' => 'Despesa # :expense', + 'updated_expense' => 'Despesa atualizada com sucesso', + 'created_expense' => 'Despesa criada com sucesso', + 'enter_expense' => 'Incluir Despesa', + 'view' => 'Visualizar', + 'restore_expense' => 'Restaurar Despesa', + 'invoice_expense' => 'Faturar Despesa', + 'expense_error_multiple_clients' => 'Despesas não podem pertencer a clientes diferentes', + 'expense_error_invoiced' => 'Despeja já faturada', + 'convert_currency' => 'Converter moeda', // Payment terms - 'num_days' => 'Number of days', - 'create_payment_term' => 'Create Payment Term', - 'edit_payment_terms' => 'Edit Payment Term', - 'edit_payment_term' => 'Edit Payment Term', - 'archive_payment_term' => 'Archive Payment Term', + 'num_days' => 'Número de dias', + 'create_payment_term' => 'Criar Termo de Pagamento', + 'edit_payment_terms' => 'Editar Termos de Pagamento', + 'edit_payment_term' => 'Editar Termo de Pagamento', + 'archive_payment_term' => 'Arquivar Termo de Pagamento', // recurring due dates - 'recurring_due_dates' => 'Recurring Invoice Due Dates', - 'recurring_due_date_help' => '

    Automatically sets a due date for the invoice.

    -

    Invoices on a monthly or yearly cycle set to be due on or before the day they are created will be due the next month. Invoices set to be due on the 29th or 30th in months that don\'t have that day will be due the last day of the month.

    -

    Invoices on a weekly cycle set to be due on the day of the week they are created will be due the next week.

    -

    For example:

    + 'recurring_due_dates' => 'Data de Vencimento das Faturas Recorrentes', + 'recurring_due_date_help' => '

    Definir automaticamente a data de vencimento da fatura.

    +

    Faturas em um ciclo mensal ou anual com vencimento anterior ou na data em que são criadas serão faturadas para o próximo mês. Faturas com vencimento no dia 29 ou 30 nos meses que não tem esse dia será faturada no último dia do mês..

    +

    Faturas em um clclo mensal com vencimento no dia da semana em que foi criada serão faturadas para a próxima semana.

    +

    Exemplo:

      -
    • Today is the 15th, due date is 1st of the month. The due date should likely be the 1st of the next month.
    • -
    • Today is the 15th, due date is the last day of the month. The due date will be the last day of the this month. -
    • -
    • Today is the 15th, due date is the 15th day of the month. The due date will be the 15th day of next month. -
    • -
    • Today is the Friday, due date is the 1st Friday after. The due date will be next Friday, not today. -
    • +
    • Hoje é dia 15, vencimento no primeiro dia do mês. O Vencimento será no primeiro dia do próximo mês.
    • +
    • Hoje é dia 15, vencimento no último dia do mês. O Vencimento será no último dia do mês corrente
    • +
    • Hoje é dia 15, vencimento no dia 15. O venciemnto será no dia 15 do próximo mês.
    • +
    • Hoje é Sexta-Feira, vencimento na primeira sexta-feira. O venciemnto será na próxima sexta-feira, não hoje.
    ', - 'due' => 'Due', - 'next_due_on' => 'Due Next: :date', - 'use_client_terms' => 'Use client terms', - 'day_of_month' => ':ordinal day of month', - 'last_day_of_month' => 'Last day of month', - 'day_of_week_after' => ':ordinal :day after', - 'sunday' => 'Sunday', - 'monday' => 'Monday', - 'tuesday' => 'Tuesday', - 'wednesday' => 'Wednesday', - 'thursday' => 'Thursday', - 'friday' => 'Friday', - 'saturday' => 'Saturday', + 'due' => 'Vencimento', + 'next_due_on' => 'Próximo Vencimento: :date', + 'use_client_terms' => 'Usar condições do cliente', + 'day_of_month' => ':ordinal dia do mês ', + 'last_day_of_month' => 'Último dia do mês', + 'day_of_week_after' => ':ordinal :day depois', + 'sunday' => 'Domingo', + 'monday' => 'Segunda-Feira', + 'tuesday' => 'Terça-Feira', + 'wednesday' => 'Quarta-Feira', + 'thursday' => 'Quinta-Feira', + 'friday' => 'Sexta-Feira', + 'saturday' => 'Sábado', // Fonts - 'header_font_id' => 'Header Font', - 'body_font_id' => 'Body Font', - 'color_font_help' => 'Note: the primary color and fonts are also used in the client portal and custom email designs.', + 'header_font_id' => 'Fonte do Cabeçalho', + 'body_font_id' => 'Fonte dos Textos', + 'color_font_help' => 'Nota: A cor primária também é utilizada nos projetos do portal do cliente e-mail personalizado.', - 'live_preview' => 'Live Preview', - 'invalid_mail_config' => 'Unable to send email, please check that the mail settings are correct.', + 'live_preview' => 'Preview', + 'invalid_mail_config' => 'Falha ao enviar e-mail, verifique as configurações.', - 'invoice_message_button' => 'To view your invoice for :amount, click the button below.', - 'quote_message_button' => 'To view your quote for :amount, click the button below.', - 'payment_message_button' => 'Thank you for your payment of :amount.', - 'payment_type_direct_debit' => 'Direct Debit', - 'bank_accounts' => 'Bank Accounts', - 'add_bank_account' => 'Add Bank Account', - 'setup_account' => 'Setup Account', - 'import_expenses' => 'Import Expenses', - 'bank_id' => 'bank', - 'integration_type' => 'Integration Type', - 'updated_bank_account' => 'Successfully updated bank account', - 'edit_bank_account' => 'Edit Bank Account', - 'archive_bank_account' => 'Archive Bank Account', - 'archived_bank_account' => 'Successfully archived bank account', - 'created_bank_account' => 'Successfully created bank account', - 'validate_bank_account' => 'Validate Bank Account', - 'bank_accounts_help' => 'Connect a bank account to automatically import expenses and create vendors. Supports American Express and 400+ US banks.', - 'bank_password_help' => 'Note: your password is transmitted securely and never stored on our servers.', - 'bank_password_warning' => 'Warning: your password may be transmitted in plain text, consider enabling HTTPS.', - 'username' => 'Username', - 'account_number' => 'Account Number', - 'account_name' => 'Account Name', - 'bank_account_error' => 'Failed to retreive account details, please check your credentials.', - 'status_approved' => 'Approved', - 'quote_settings' => 'Quote Settings', - 'auto_convert_quote' => 'Auto convert quote', - 'auto_convert_quote_help' => 'Automatically convert a quote to an invoice when approved by a client.', - 'validate' => 'Validate', + 'invoice_message_button' => 'Para visualizar sua fatura de :amount, clique no botão abaixo.', + 'quote_message_button' => 'Para visualizar seu orçamento de :amount, clique no botão abaixo.', + 'payment_message_button' => 'Obrigado pelo seu pagamento de :amount.', + 'payment_type_direct_debit' => 'Débito', + 'bank_accounts' => 'Contas Bancárias', + 'add_bank_account' => 'Adicionar Conta Bancária', + 'setup_account' => 'Configurar Conta', + 'import_expenses' => 'Importar Despesas', + 'bank_id' => 'banco', + 'integration_type' => 'Tipo de Integração', + 'updated_bank_account' => 'Conta bancária atualizada com sucesso', + 'edit_bank_account' => 'Editar Conta Bancária', + 'archive_bank_account' => 'Arquivar Conta Bancária', + 'archived_bank_account' => 'Conta bancária arquivada com sucesso', + 'created_bank_account' => 'Conta bancária criada com sucesso', + 'validate_bank_account' => 'Validar Conta Bancária', + 'bank_accounts_help' => 'Conecte sua conta bancária para importar suas despesas e criar fornecedores. Suporte ao American Express e 400+ bancos americanos.', + 'bank_password_help' => 'Nota: sua senha é transferida de forma segura e não será armazenada em nossos servidores.', + 'bank_password_warning' => 'Atenção: sua senha será transferida de forma não segura, considere habilitar HTTPS.', + 'username' => 'Usuário', + 'account_number' => 'Conta', + 'account_name' => 'Nome da Conta', + 'bank_account_error' => 'Falha ao receber os detalhes da sua conta, verifique seus dados de acesso.', + 'status_approved' => 'Aprovado', + 'quote_settings' => 'Configuração de Orçamentos', + 'auto_convert_quote' => 'Auto converter orçamento', + 'auto_convert_quote_help' => 'Converter automaticamente um orçamento quando for aprovado pelo cliente.', + 'validate' => 'Validado', 'info' => 'Info', - 'imported_expenses' => 'Successfully created :count_vendors vendor(s) and :count_expenses expense(s)', + 'imported_expenses' => ':count_vendors fornecedor(s) e :count_expenses despesa(s) importadas com sucesso', - 'iframe_url_help3' => 'Note: if you plan on accepting credit cards details we strongly recommend enabling HTTPS on your site.', - 'expense_error_multiple_currencies' => 'The expenses can\'t have different currencies.', - 'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.', + 'iframe_url_help3' => 'Nota: se o seu plano aceita detalhes do cartão de crédito recomendamos que seja habilitado o HTTPS em seu site.', + 'expense_error_multiple_currencies' => 'As despesas não podem ter diferentes moedas.', + 'expense_error_mismatch_currencies' => 'As configurações de moeda do cliente não coincide com a moeda nesta despesa.', 'trello_roadmap' => 'Trello Roadmap', - 'header_footer' => 'Header/Footer', - 'first_page' => 'first page', - 'all_pages' => 'all pages', - 'last_page' => 'last page', - 'all_pages_header' => 'Show header on', - 'all_pages_footer' => 'Show footer on', - 'invoice_currency' => 'Invoice Currency', - 'enable_https' => 'We strongly recommend using HTTPS to accept credit card details online.', - 'quote_issued_to' => 'Quote issued to', - 'show_currency_code' => 'Currency Code', - 'trial_message' => 'Your account will receive a free two week trial of our pro plan.', - 'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.', - 'trial_footer_last_day' => 'This is the last day of your free trial, :link to upgrade now.', - 'trial_call_to_action' => 'Start Free Trial', - 'trial_success' => 'Successfully enabled two week free pro plan trial', - 'overdue' => 'Overdue', - 'white_label_text' => 'Purchase a ONE YEAR white label license for $'.WHITE_LABEL_PRICE.' to remove the Invoice Ninja branding from the client portal and help support our project.', + 'header_footer' => 'Cabeçalho/Rodapé', + 'first_page' => 'primeira página', + 'all_pages' => 'todas as páginas', + 'last_page' => 'última página', + 'all_pages_header' => 'Mostrar cabeçalho on', + 'all_pages_footer' => 'Mostrar rodapé on', + 'invoice_currency' => 'Moeda da Fatura', + 'enable_https' => 'Recomendamos a utilização de HTTPS para receber os detalhes do cartão de crédito online.', + 'quote_issued_to' => 'Orçamento emitido para', + 'show_currency_code' => 'Código da Moeda', + 'trial_message' => 'Sua conta receberá duas semanas receberá duas semanas gratuitamente para testar nosso plano pro.', + 'trial_footer' => 'Seu período de teste expira em :count dias, :link para adquirir o plano pro.', + 'trial_footer_last_day' => 'Seu período de testes encerra hoje, :link para adiquirir o plano pro.', + 'trial_call_to_action' => 'Iniciar período de testes', + 'trial_success' => 'Duas semanas de testes foi habilitado com sucesso', + 'overdue' => 'Vencido', + 'white_label_text' => 'Adquira UM ano de licença white label por $'.WHITE_LABEL_PRICE.' para remover a marca Invoice Ninja do portal do cliente e ajudar nosso projeto.', - 'navigation' => 'Navigation', - 'list_invoices' => 'List Invoices', - 'list_clients' => 'List Clients', - 'list_quotes' => 'List Quotes', - 'list_tasks' => 'List Tasks', - 'list_expenses' => 'List Expenses', - 'list_recurring_invoices' => 'List Recurring Invoices', - 'list_payments' => 'List Payments', - 'list_credits' => 'List Credits', - 'tax_name' => 'Tax Name', - 'report_settings' => 'Report Settings', - 'search_hotkey' => 'shortcut is /', + 'navigation' => 'Navegação', + 'list_invoices' => 'Listar Faturas', + 'list_clients' => 'Listar Clientes', + 'list_quotes' => 'Listar Orçamentos', + 'list_tasks' => 'Listar Tarefas', + 'list_expenses' => 'Listar Despesas', + 'list_recurring_invoices' => 'Listar Faturas Recorrentes', + 'list_payments' => 'Listar Pagamentos', + 'list_credits' => 'Listar Créditos', + 'tax_name' => 'Nome da Taxa', + 'report_settings' => 'Configuração de Relatórios', + 'search_hotkey' => 'atalho /', - 'new_user' => 'New User', - 'new_product' => 'New Product', - 'new_tax_rate' => 'New Tax Rate', - 'invoiced_amount' => 'Invoiced Amount', - 'invoice_item_fields' => 'Invoice Item Fields', - 'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.', - 'recurring_invoice_number' => 'Recurring Invoice Number', - 'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.', - 'enable_client_portal' => 'Dashboard', - 'enable_client_portal_help' => 'Show/hide the dashboard page in the client portal.', + 'new_user' => 'Novo Usuário', + 'new_product' => 'Novo Produto', + 'new_tax_rate' => 'Nova Taxa de Juro', + 'invoiced_amount' => 'Total Faturado', + 'invoice_item_fields' => 'Campos de Ítens da Fatura', + 'custom_invoice_item_fields_help' => 'Adicionar um campo ao adicionar um ítem na fatura e exibir no PDF.', + 'recurring_invoice_number' => 'Número da Fatura Recorrente', + 'recurring_invoice_number_prefix_help' => 'Informe um prefixo para a numeração das faturas recorrentes. O valor padrão é \'R\'.', + 'enable_client_portal' => 'Painel', + 'enable_client_portal_help' => 'Mostrar/Ocultar o painel no portal do cliente.', // Client Passwords - 'enable_portal_password'=>'Password protect invoices', - 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', - 'send_portal_password'=>'Generate password automatically', - 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', + 'enable_portal_password'=>'Faturas protegidas por senha', + 'enable_portal_password_help'=>'Permite definir uma senha para cada contato. Se uma senha for definida, o contato deverá informar sua senha antes de visualizar a fatura.', + 'send_portal_password'=>'Gerar senha automaticamente', + 'send_portal_password_help'=>'Se uma senha não for definida, uma senha será gerada e enviada juntamente com a primeira fatura.', - 'expired' => 'Expired', - 'invalid_card_number' => 'The credit card number is not valid.', - 'invalid_expiry' => 'The expiration date is not valid.', - 'invalid_cvv' => 'The CVV is not valid.', - 'cost' => 'Cost', - 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', + 'expired' => 'Expireda', + 'invalid_card_number' => 'Cartão de Crédito inválido.', + 'invalid_expiry' => 'Data para expirar não é valida.', + 'invalid_cvv' => 'O código CVV não é válido.', + 'cost' => 'Custo', + 'create_invoice_for_sample' => 'Nota: cria sua primeira fatura para visualizar aqui.', // User Permissions - 'owner' => 'Owner', - 'administrator' => 'Administrator', - 'administrator_help' => 'Allow user to manage users, change settings and modify all records', - 'user_create_all' => 'Create clients, invoices, etc.', - 'user_view_all' => 'View all clients, invoices, etc.', - 'user_edit_all' => 'Edit all clients, invoices, etc.', - 'gateway_help_20' => ':link to sign up for Sage Pay.', - 'gateway_help_21' => ':link to sign up for Sage Pay.', - 'partial_due' => 'Partial Due', - 'restore_vendor' => 'Restore Vendor', - 'restored_vendor' => 'Successfully restored vendor', - 'restored_expense' => 'Successfully restored expense', - 'permissions' => 'Permissions', - 'create_all_help' => 'Allow user to create and modify records', - 'view_all_help' => 'Allow user to view records they didn\'t create', - 'edit_all_help' => 'Allow user to modify records they didn\'t create', - 'view_payment' => 'View Payment', + 'owner' => 'Proprietário', + 'administrator' => 'Administrador', + 'administrator_help' => 'Permite usuário gerenciar usuários, configurações e alterar todos os cadastros', + 'user_create_all' => 'Criar clientes, faturas, etc.', + 'user_view_all' => 'Visualizar todos os clientes, faturas, etc.', + 'user_edit_all' => 'Editar todos os clientes, faturas, etc.', + 'gateway_help_20' => ':link para habilitar Sage Pay.', + 'gateway_help_21' => ':link para habilitar Sage Pay.', + 'partial_due' => 'Vencimento Parcial', + 'restore_vendor' => 'Restaurar Fornecedor', + 'restored_vendor' => 'Fornecedor restarurado com sucesso', + 'restored_expense' => 'Despesa restaurada com sucesso', + 'permissions' => 'Permissões', + 'create_all_help' => 'Permite o usuário criar e alterar todos os regitros', + 'view_all_help' => 'Permite usuario visualizar regitros que ele não criou', + 'edit_all_help' => 'Permite usuario editar regitros que ele não criou', + 'view_payment' => 'Visualizar ', - 'january' => 'January', - 'february' => 'February', - 'march' => 'March', - 'april' => 'April', - 'may' => 'May', - 'june' => 'June', - 'july' => 'July', - 'august' => 'August', - 'september' => 'September', - 'october' => 'October', - 'november' => 'November', - 'december' => 'December', + 'january' => 'Janeiro', + 'february' => 'Fevereiro', + 'march' => 'Março', + 'april' => 'Abril', + 'may' => 'Maio', + 'june' => 'Junho', + 'july' => 'Julho', + 'august' => 'Agosto', + 'september' => 'Setembro', + 'october' => 'Outubro', + 'november' => 'Novembro', + 'december' => 'Dezembro', -); + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + +); \ No newline at end of file diff --git a/resources/lang/sv/texts.php b/resources/lang/sv/texts.php index 050076bd6e3e..634a5bbe15a4 100644 --- a/resources/lang/sv/texts.php +++ b/resources/lang/sv/texts.php @@ -492,7 +492,7 @@ return array( 'invoice_history' => 'Fakturahistorik', 'quote_history' => 'Offerthistorik', 'current_version' => 'Nuvarande version', - 'select_versiony' => 'Välj version', + 'select_version' => 'Välj version', 'view_history' => 'Visa historik', 'edit_payment' => 'Ändra betalning', @@ -1197,4 +1197,92 @@ return array( 'november' => 'November', 'december' => 'December', + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Live Preview', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + ); diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index c19268c258fd..db7200907263 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -6,7 +6,7 @@ @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) {!! Former::open($url)->method($method)->rule()->addClass('warn-on-exit') !!} - {!! Former::populate($account) !!} + {!! Former::populateField('token_billing_type_id', $account->token_billing_type_id) !!}
    @@ -63,7 +63,7 @@ @foreach ($gateway->fields as $field => $details) - @if ($details && !$accountGateway) + @if ($details && !$accountGateway && !is_array($details)) {!! Former::populateField($gateway->id.'_'.$field, $details) !!} @endif diff --git a/resources/views/accounts/api_tokens.blade.php b/resources/views/accounts/api_tokens.blade.php index 0b338c80268a..49029bf8d2b3 100644 --- a/resources/views/accounts/api_tokens.blade.php +++ b/resources/views/accounts/api_tokens.blade.php @@ -6,10 +6,10 @@
    {!! Button::normal(trans('texts.documentation'))->asLinkTo(NINJA_WEB_URL.'/api-documentation/')->withAttributes(['target' => '_blank'])->appendIcon(Icon::create('info-sign')) !!} - @if (Utils::isNinja()) + @if (Utils::isNinja() && !Utils::isReseller()) {!! Button::normal(trans('texts.zapier'))->asLinkTo(ZAPIER_URL)->withAttributes(['target' => '_blank']) !!} @endif - @if (Utils::isPro()) + @if (Utils::hasFeature(FEATURE_API)) {!! Button::primary(trans('texts.add_token'))->asLinkTo(URL::to('/tokens/create'))->appendIcon(Icon::create('plus-sign')) !!} @endif
    diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php index 8737c015d06e..710858ae9817 100644 --- a/resources/views/accounts/client_portal.blade.php +++ b/resources/views/accounts/client_portal.blade.php @@ -13,11 +13,12 @@ ->addClass('warn-on-exit') !!} {!! Former::populateField('enable_client_portal', intval($account->enable_client_portal)) !!} +{!! Former::populateField('enable_client_portal_dashboard', intval($account->enable_client_portal_dashboard)) !!} {!! Former::populateField('client_view_css', $client_view_css) !!} {!! Former::populateField('enable_portal_password', intval($enable_portal_password)) !!} {!! Former::populateField('send_portal_password', intval($send_portal_password)) !!} -@if (!Utils::isNinja() && !Auth::user()->account->isWhiteLabel()) +@if (!Utils::isNinja() && !Auth::user()->account->hasFeature(FEATURE_WHITE_LABEL))
    {!! trans('texts.white_label_custom_css', ['link'=>''.trans('texts.white_label_purchase_link').'']) !!} @@ -39,6 +40,11 @@ ->text(trans('texts.enable')) ->help(trans('texts.enable_client_portal_help')) !!}
    +
    + {!! Former::checkbox('enable_client_portal_dashboard') + ->text(trans('texts.enable')) + ->help(trans('texts.enable_client_portal_dashboard_help')) !!} +
    {!! Former::checkbox('enable_portal_password') ->text(trans('texts.enable_portal_password')) @@ -53,6 +59,7 @@
    + @if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_CSS))

    {!! trans('texts.custom_css') !!}

    @@ -68,6 +75,7 @@ ->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!}
    + @endif diff --git a/resources/views/accounts/customize_design.blade.php b/resources/views/accounts/customize_design.blade.php index 439ef1d4acef..9c9fff51121c 100644 --- a/resources/views/accounts/customize_design.blade.php +++ b/resources/views/accounts/customize_design.blade.php @@ -45,7 +45,11 @@ var customDesign = origCustomDesign = {!! $customDesign ?: 'JSON.parse(invoiceDesigns[0].javascript);' !!}; function getPDFString(cb, force) { - invoice.is_pro = {!! Auth::user()->isPro() ? 'true' : 'false' !!}; + invoice.features = { + customize_invoice_design:{{ Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) ? 'true' : 'false' }}, + remove_created_by:{{ Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY) ? 'true' : 'false' }}, + invoice_settings:{{ Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS) ? 'true' : 'false' }} + }; invoice.account.hide_quantity = {!! Auth::user()->account->hide_quantity ? 'true' : 'false' !!}; invoice.account.hide_paid_to_date = {!! Auth::user()->account->hide_paid_to_date ? 'true' : 'false' !!}; invoice.invoice_design_id = {!! Auth::user()->account->invoice_design_id !!}; @@ -194,7 +198,7 @@ +@stop \ No newline at end of file diff --git a/resources/views/accounts/template.blade.php b/resources/views/accounts/template.blade.php index 0d4294af5b6f..68279299f607 100644 --- a/resources/views/accounts/template.blade.php +++ b/resources/views/accounts/template.blade.php @@ -62,11 +62,14 @@
    +

     

    -
    -

     

    +

    @include('partials/quill_toolbar', ['name' => $field])
    +
    + {!! Button::primary(trans('texts.preview'))->withAttributes(['onclick' => 'serverPreview("'.$field.'")'])->small() !!} +
    diff --git a/resources/views/accounts/templates_and_reminders.blade.php b/resources/views/accounts/templates_and_reminders.blade.php index 22a3a8a4a08c..87453e80dae6 100644 --- a/resources/views/accounts/templates_and_reminders.blade.php +++ b/resources/views/accounts/templates_and_reminders.blade.php @@ -25,11 +25,16 @@ {!! Former::vertical_open()->addClass('warn-on-exit') !!} - {!! Former::populate($account) !!} @foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) @foreach (['subject', 'template'] as $field) - {!! Former::populateField("email_{$field}_{$type}", $templates[$type][$field]) !!} + {{ Former::populateField("email_{$field}_{$type}", $templates[$type][$field]) }} + @endforeach + @endforeach + + @foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) + @foreach (['enable', 'num_days', 'direction', 'field'] as $field) + {{ Former::populateField("{$field}_{$type}", $account->{"{$field}_{$type}"}) }} @endforeach @endforeach @@ -80,6 +85,26 @@ + + + - @if (Auth::user()->isPro()) + @if (Auth::user()->hasFeature(FEATURE_API)) {!! Former::actions( Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/api_tokens'))->appendIcon(Icon::create('remove-circle'))->large(), Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) diff --git a/resources/views/accounts/user_management.blade.php b/resources/views/accounts/user_management.blade.php index a7f70f925b7a..5d8f8cf07e5e 100644 --- a/resources/views/accounts/user_management.blade.php +++ b/resources/views/accounts/user_management.blade.php @@ -4,13 +4,13 @@ @parent @include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT, 'advanced' => true]) - -
    - @if (Utils::isPro() && ! Utils::isTrial()) - {!! Button::primary(trans('texts.add_user'))->asLinkTo(URL::to('/users/create'))->appendIcon(Icon::create('plus-sign')) !!} + @if (Utils::hasFeature(FEATURE_USERS)) +
    + {!! Button::primary(trans('texts.add_user'))->asLinkTo(URL::to('/users/create'))->appendIcon(Icon::create('plus-sign')) !!} +
    + @elseif (Utils::isTrial()) +
    {!! trans('texts.add_users_not_supported') !!}
    @endif -
    -