diff --git a/.env.example b/.env.example
index 66d2d5251b27..41ce2039f7e5 100644
--- a/.env.example
+++ b/.env.example
@@ -41,4 +41,30 @@ API_SECRET=password
#GOOGLE_CLIENT_ID=
#GOOGLE_CLIENT_SECRET=
-#GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google
\ No newline at end of file
+#GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google
+
+#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/.travis.yml b/.travis.yml
index f76fe32447b1..9e8b983e795d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -87,6 +87,8 @@ 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
+ - mysql -u root -e 'select * from payments;' ninja
+ - mysql -u root -e 'select * from credits;' ninja
- cat storage/logs/laravel.log
notifications:
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/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/Kernel.php b/app/Console/Kernel.php
index d04eab9fad91..af5bbe7bc9d2 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
*/
protected $commands = [
'App\Console\Commands\SendRecurringInvoices',
+ 'App\Console\Commands\RemoveOrphanedDocuments',
'App\Console\Commands\ResetData',
'App\Console\Commands\CheckData',
'App\Console\Commands\SendRenewalInvoices',
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 9d17fb099057..76b2087ad89e 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -4,7 +4,11 @@ use Redirect;
use Utils;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
+use Illuminate\Http\Exception\HttpResponseException;
+use Illuminate\Auth\Access\AuthorizationException;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Illuminate\Foundation\Validation\ValidationException;
class Handler extends ExceptionHandler {
@@ -14,7 +18,10 @@ class Handler extends ExceptionHandler {
* @var array
*/
protected $dontReport = [
- 'Symfony\Component\HttpKernel\Exception\HttpException'
+ AuthorizationException::class,
+ HttpException::class,
+ ModelNotFoundException::class,
+ ValidationException::class,
];
/**
@@ -27,6 +34,11 @@ class Handler extends ExceptionHandler {
*/
public function report(Exception $e)
{
+ // don't show these errors in the logs
+ if ($e instanceof HttpResponseException) {
+ return false;
+ }
+
if (Utils::isNinja()) {
Utils::logError(Utils::getErrorString($e));
return false;
@@ -59,6 +71,9 @@ class Handler extends ExceptionHandler {
}
}
+ return parent::render($request, $e);
+
+ /*
// In production, except for maintenance mode, we'll show a custom error screen
if (Utils::isNinjaProd() && !Utils::isDownForMaintenance()) {
$data = [
@@ -70,5 +85,6 @@ class Handler extends ExceptionHandler {
} else {
return parent::render($request, $e);
}
+ */
}
}
diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php
index 912ade56f962..6fe75a63363c 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;
@@ -234,7 +236,7 @@ class AccountController extends BaseController
{
$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 +349,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 +375,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->isPro() ? [$document] : [];
+ $invoice->documents = [];
$data['account'] = $account;
$data['invoice'] = $invoice;
@@ -546,6 +553,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');
@@ -647,6 +655,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()) {
@@ -749,6 +758,7 @@ class AccountController extends BaseController
$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');
@@ -793,39 +803,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 +939,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);
}
diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php
index 394323d7420a..1fc3f7588873 100644
--- a/app/Http/Controllers/AppController.php
+++ b/app/Http/Controllers/AppController.php
@@ -83,23 +83,35 @@ class AppController extends BaseController
return Redirect::to('/');
}
- $config = "APP_ENV=production\n".
- "APP_DEBUG={$app['debug']}\n".
- "APP_URL={$app['url']}\n".
- "APP_KEY={$app['key']}\n\n".
- "DB_TYPE={$dbType}\n".
- "DB_HOST={$database['type']['host']}\n".
- "DB_DATABASE={$database['type']['database']}\n".
- "DB_USERNAME={$database['type']['username']}\n".
- "DB_PASSWORD={$database['type']['password']}\n\n".
- "MAIL_DRIVER={$mail['driver']}\n".
- "MAIL_PORT={$mail['port']}\n".
- "MAIL_ENCRYPTION={$mail['encryption']}\n".
- "MAIL_HOST={$mail['host']}\n".
- "MAIL_USERNAME={$mail['username']}\n".
- "MAIL_FROM_NAME={$mail['from']['name']}\n".
- "MAIL_PASSWORD={$mail['password']}\n\n".
- "PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'";
+ $_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) {
+ if (is_array($val)) {
+ continue;
+ }
+ if (preg_match('/\s/', $val)) {
+ $val = "'{$val}'";
+ }
+ $config .= "{$key}={$val}\n";
+ }
+
// Write Config Settings
$fp = fopen(base_path()."/.env", 'w');
@@ -166,6 +178,9 @@ class AppController extends BaseController
$config = '';
foreach ($_ENV as $key => $val) {
+ if (preg_match('/\s/',$val)) {
+ $val = "'{$val}'";
+ }
$config .= "{$key}={$val}\n";
}
@@ -311,4 +326,4 @@ class AppController extends BaseController
return json_encode($data);
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php
index 7e1c01de9e6e..7f2fac3eed46 100644
--- a/app/Http/Controllers/ClientController.php
+++ b/app/Http/Controllers/ClientController.php
@@ -112,10 +112,10 @@ class ClientController extends BaseController
$actionLinks = [];
if(Task::canCreate()){
- $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => '/tasks/create/'.$client->public_id];
+ $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];
+ $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)];
}
if(!empty($actionLinks)){
@@ -123,15 +123,15 @@ class ClientController extends BaseController
}
if(Payment::canCreate()){
- $actionLinks[] = ['label' => trans('texts.enter_payment'), 'url' => '/payments/create/'.$client->public_id];
+ $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];
+ $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];
+ $actionLinks[] = ['label' => trans('texts.enter_expense'), 'url' => URL::to('/expenses/create/0/'.$client->public_id)];
}
$data = array(
diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php
index 5439efc2871e..06393e3ddc3c 100644
--- a/app/Http/Controllers/DashboardApiController.php
+++ b/app/Http/Controllers/DashboardApiController.php
@@ -11,7 +11,7 @@ class DashboardApiController extends BaseAPIController
{
$view_all = !Auth::user()->hasPermission('view_all');
$user_id = Auth::user()->id;
-
+
// total_income, billed_clients, invoice_sent and active_clients
$select = DB::raw('COUNT(DISTINCT CASE WHEN invoices.id IS NOT NULL THEN clients.id ELSE null END) billed_clients,
SUM(CASE WHEN invoices.invoice_status_id >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent,
@@ -25,17 +25,17 @@ class DashboardApiController extends BaseAPIController
->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false)
->where('invoices.is_quote', '=', false);
-
+
if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){
$query->where('invoices.user_id', '=', $user_id);
$query->orwhere(function($query) use($user_id){
- $query->where('invoices.user_id', '=', null);
+ $query->where('invoices.user_id', '=', null);
$query->where('clients.user_id', '=', $user_id);
});
});
}
-
+
$metrics = $metrics->groupBy('accounts.id')
->first();
@@ -45,11 +45,11 @@ class DashboardApiController extends BaseAPIController
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false);
-
+
if(!$view_all){
$paidToDate = $paidToDate->where('clients.user_id', '=', $user_id);
}
-
+
$paidToDate = $paidToDate->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END'))
->get();
@@ -64,11 +64,11 @@ class DashboardApiController extends BaseAPIController
->where('invoices.is_deleted', '=', false)
->where('invoices.is_quote', '=', false)
->where('invoices.is_recurring', '=', false);
-
+
if(!$view_all){
$averageInvoice = $averageInvoice->where('invoices.user_id', '=', $user_id);
}
-
+
$averageInvoice = $averageInvoice->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END'))
->get();
@@ -96,11 +96,11 @@ class DashboardApiController extends BaseAPIController
->where('invoices.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '<', date('Y-m-d'));
-
+
if(!$view_all){
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
}
-
+
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote'])
->orderBy('invoices.due_date', 'asc')
->take(50)
@@ -120,11 +120,11 @@ class DashboardApiController extends BaseAPIController
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '>=', date('Y-m-d'))
->orderBy('invoices.due_date', 'asc');
-
+
if(!$view_all){
$upcoming = $upcoming->where('invoices.user_id', '=', $user_id);
}
-
+
$upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote'])
->get();
@@ -139,11 +139,11 @@ class DashboardApiController extends BaseAPIController
->where('clients.is_deleted', '=', false)
->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true);
-
+
if(!$view_all){
$payments = $payments->where('payments.user_id', '=', $user_id);
}
-
+
$payments = $payments->select(['payments.payment_date', 'payments.amount', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id'])
->orderBy('payments.payment_date', 'desc')
->take(50)
@@ -158,20 +158,22 @@ class DashboardApiController extends BaseAPIController
}
}
- $data = [
- 'paidToDate' => $paidToDate,
- 'balances' => $balances,
- 'averageInvoice' => $averageInvoice,
- 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0,
- 'activeClients' => $metrics ? $metrics->active_clients : 0,
- 'pastDue' => $pastDue,
- 'upcoming' => $upcoming,
- 'payments' => $payments,
- 'title' => trans('texts.dashboard'),
- 'hasQuotes' => $hasQuotes,
- ];
- return $this->response($data);
+ $data = [
+ 'id' => 1,
+ 'paidToDate' => $paidToDate[0]->value,
+ 'paidToDateCurrency' => $paidToDate[0]->currency_id,
+ 'balances' => $balances[0]->value,
+ 'balancesCurrency' => $balances[0]->currency_id,
+ 'averageInvoice' => $averageInvoice[0]->invoice_avg,
+ 'averageInvoiceCurrency' => $averageInvoice[0]->currency_id,
+ 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0,
+ 'activeClients' => $metrics ? $metrics->active_clients : 0,
+ ];
+
+
+
+ return $this->response($data);
}
}
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..8af0d560d679
--- /dev/null
+++ b/app/Http/Controllers/DocumentController.php
@@ -0,0 +1,140 @@
+documentRepo = $documentRepo;
+ }
+
+ public function get($publicId)
+ {
+ $document = Document::scope($publicId)
+ ->firstOrFail();
+
+ if(!$this->checkViewPermission($document, $response)){
+ return $response;
+ }
+
+ return static::getDownloadResponse($document);
+ }
+
+ 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($publicId)
+ {
+ $document = Document::scope($publicId)
+ ->firstOrFail();
+
+ if(!$this->checkViewPermission($document, $response)){
+ return $response;
+ }
+
+ 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($publicId, $name){
+ $document = Document::scope($publicId)
+ ->firstOrFail();
+
+ if(substr($name, -3)=='.js'){
+ $name = substr($name, 0, -3);
+ }
+
+ if(!$this->checkViewPermission($document, $response)){
+ return $response;
+ }
+
+ 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()
+ {
+ if (!Utils::isPro()) {
+ return;
+ }
+
+ if(!$this->checkCreatePermission($response)){
+ return $response;
+ }
+
+ $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/ExpenseController.php b/app/Http/Controllers/ExpenseController.php
index b1fd28e41941..b8e82cddda48 100644
--- a/app/Http/Controllers/ExpenseController.php
+++ b/app/Http/Controllers/ExpenseController.php
@@ -99,7 +99,7 @@ class ExpenseController extends BaseController
public function edit($publicId)
{
- $expense = Expense::scope($publicId)->firstOrFail();
+ $expense = Expense::scope($publicId)->with('documents')->firstOrFail();
if(!$this->checkEditPermission($expense, $response)){
return $response;
@@ -146,12 +146,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 +157,14 @@ class ExpenseController extends BaseController
*/
public function update(UpdateExpenseRequest $request)
{
- $expense = $this->expenseService->save($request->input());
+ $data = $request->input();
+ $data['documents'] = $request->file('documents');
+
+ if(!$this->checkUpdatePermission($data, $response)){
+ return $response;
+ }
+
+ $expense = $this->expenseService->save($data, true);
Session::flash('message', trans('texts.updated_expense'));
@@ -177,7 +178,14 @@ class ExpenseController extends BaseController
public function store(CreateExpenseRequest $request)
{
- $expense = $this->expenseService->save($request->input());
+ $data = $request->input();
+ $data['documents'] = $request->file('documents');
+
+ if(!$this->checkUpdatePermission($data, $response)){
+ return $response;
+ }
+
+ $expense = $this->expenseService->save($data);
Session::flash('message', trans('texts.created_expense'));
@@ -195,8 +203,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 +227,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/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php
index 46f40b0bf251..53d2a2548e39 100644
--- a/app/Http/Controllers/InvoiceApiController.php
+++ b/app/Http/Controllers/InvoiceApiController.php
@@ -166,6 +166,7 @@ class InvoiceApiController extends BaseAPIController
'state',
'postal_code',
'private_notes',
+ 'currency_code',
] as $field) {
if (isset($data[$field])) {
$clientData[$field] = $data[$field];
@@ -258,10 +259,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);
diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php
index dc9cc501db74..794f09404a2d 100644
--- a/app/Http/Controllers/InvoiceController.php
+++ b/app/Http/Controllers/InvoiceController.php
@@ -17,12 +17,14 @@ 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;
@@ -32,11 +34,12 @@ class InvoiceController extends BaseController
protected $mailer;
protected $invoiceRepo;
protected $clientRepo;
+ protected $documentRepo;
protected $invoiceService;
protected $recurringInvoiceService;
protected $model = 'App\Models\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();
@@ -89,7 +92,7 @@ class InvoiceController extends BaseController
{
$account = Auth::user()->account;
$invoice = Invoice::scope($publicId)
- ->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items')
+ ->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'payments')
->withTrashed()
->firstOrFail();
@@ -155,6 +158,14 @@ class InvoiceController extends BaseController
if (!$invoice->is_recurring && $invoice->balance > 0) {
$actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')];
}
+
+ foreach ($invoice->payments as $payment) {
+ $label = trans("texts.view_payment");
+ if (count($invoice->payments) > 1) {
+ $label .= ' - ' . $account->formatMoney($payment->amount, $invoice->client);
+ }
+ $actions[] = ['url' => $payment->present()->url, 'label' => $label];
+ }
}
if (count($actions) > 3) {
@@ -183,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;
@@ -203,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();
}
}
@@ -221,7 +233,7 @@ class InvoiceController extends BaseController
return $response;
}
- $account = Auth::user()->account;
+ $account = Auth::user()->account;
$entityType = $isRecurring ? ENTITY_RECURRING_INVOICE : ENTITY_INVOICE;
$clientId = null;
@@ -231,6 +243,11 @@ class InvoiceController extends BaseController
$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')){
@@ -245,7 +262,7 @@ 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);
}
@@ -255,7 +272,7 @@ class InvoiceController extends BaseController
return self::create($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) {
@@ -316,11 +333,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'),
@@ -342,7 +385,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,
];
@@ -356,6 +398,7 @@ class InvoiceController extends BaseController
public function store(SaveInvoiceWithClientRequest $request)
{
$data = $request->input();
+ $data['documents'] = $request->file('documents');
if(!$this->checkUpdatePermission($data, $response)){
return $response;
@@ -396,6 +439,7 @@ class InvoiceController extends BaseController
public function update(SaveInvoiceWithClientRequest $request)
{
$data = $request->input();
+ $data['documents'] = $request->file('documents');
if(!$this->checkUpdatePermission($data, $response)){
return $response;
@@ -526,7 +570,7 @@ class InvoiceController extends BaseController
public function invoiceHistory($publicId)
{
$invoice = Invoice::withTrashed()->scope($publicId)->firstOrFail();
- $invoice->load('user', 'invoice_items', 'account.country', 'client.contacts', 'client.country');
+ $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();
diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php
index 1ba681ba57dc..2f2cdd92cc7e 100644
--- a/app/Http/Controllers/PaymentController.php
+++ b/app/Http/Controllers/PaymentController.php
@@ -460,6 +460,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();
}
@@ -551,7 +553,15 @@ 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 {
+ Session::flash('error', Input::get('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 +582,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());
}
@@ -642,6 +650,6 @@ class PaymentController extends BaseController
$message .= $error ?: trans('texts.payment_error');
Session::flash('error', $message);
- Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message));
+ Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true);
}
}
diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php
index d067ff38a740..b7649471f051 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;
}
@@ -117,8 +123,9 @@ class PublicClientController extends BaseController
'showApprove' => $showApprove,
'showBreadcrumbs' => false,
'hideLogo' => $account->isWhiteLabel(),
- 'hideHeader' => $account->isNinjaAccount(),
- 'hideDashboard' => !$account->enable_client_portal,
+ 'hideHeader' => $account->isNinjaAccount() || !$account->enable_client_portal,
+ 'hideDashboard' => !$account->enable_client_portal_dashboard,
+ 'showDocuments' => $account->isPro(),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'invoice' => $invoice->hidePrivateFields(),
@@ -132,6 +139,15 @@ class PublicClientController extends BaseController
'checkoutComDebug' => $checkoutComDebug,
'phantomjs' => Input::has('phantomjs'),
);
+
+ if($account->isPro() && $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 +212,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();
}
@@ -205,6 +221,7 @@ class PublicClientController extends BaseController
'account' => $account,
'client' => $client,
'hideLogo' => $account->isWhiteLabel(),
+ 'showDocuments' => $account->isPro(),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
];
@@ -245,13 +262,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,
+ 'hideDashboard' => !$account->enable_client_portal_dashboard,
+ 'showDocuments' => $account->isPro(),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.invoices'),
@@ -278,12 +302,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,
+ 'hideDashboard' => !$account->enable_client_portal_dashboard,
+ 'showDocuments' => $account->isPro(),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'entityType' => ENTITY_PAYMENT,
@@ -302,7 +331,7 @@ class PublicClientController extends BaseController
$payments = $this->paymentRepo->findForContact($invitation->contact->id, Input::get('sSearch'));
return Datatable::query($payments)
- ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number) : $model->invoice_number; })->toHtml()
+ ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number)->toHtml() : $model->invoice_number; })
->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry '; })
->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment ' : ''); })
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
@@ -315,13 +344,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,
+ 'hideDashboard' => !$account->enable_client_portal_dashboard,
+ 'showDocuments' => $account->isPro(),
'clientViewCSS' => $account->clientViewCSS(),
'clientFontUrl' => $account->getFontsUrl(),
'title' => trans('texts.quotes'),
@@ -342,6 +377,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->isWhiteLabel(),
+ 'hideDashboard' => !$account->enable_client_portal_dashboard,
+ 'showDocuments' => $account->isPro(),
+ '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 +445,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
index 0868fbe97188..8111872b10d9 100644
--- a/app/Http/Controllers/QuoteApiController.php
+++ b/app/Http/Controllers/QuoteApiController.php
@@ -7,7 +7,7 @@ use Response;
use App\Models\Invoice;
use App\Ninja\Repositories\InvoiceRepository;
use App\Http\Controllers\BaseAPIController;
-use App\Ninja\Transformers\QuoteTransformer;
+use App\Ninja\Transformers\InvoiceTransformer;
class QuoteApiController extends BaseAPIController
{
@@ -53,7 +53,7 @@ class QuoteApiController extends BaseAPIController
$invoices = $invoices->orderBy('created_at', 'desc')->paginate();
- $transformer = new QuoteTransformer(\Auth::user()->account, Input::get('serializer'));
+ $transformer = new InvoiceTransformer(\Auth::user()->account, Input::get('serializer'));
$paginator = $paginator->paginate();
$data = $this->createCollection($invoices, $transformer, 'quotes', $paginator);
diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php
index 0c2dc3a8a5ba..20fdd484ba80 100644
--- a/app/Http/Controllers/QuoteController.php
+++ b/app/Http/Controllers/QuoteController.php
@@ -111,10 +111,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 1c4c033b9ed3..2d21e0b593c6 100644
--- a/app/Http/Controllers/ReportController.php
+++ b/app/Http/Controllers/ReportController.php
@@ -105,7 +105,7 @@ class ReportController extends BaseController
$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) {
@@ -158,8 +158,10 @@ class ReportController extends BaseController
}
$records = DB::table($entityType.'s')
- ->select(DB::raw('sum(amount) as total, '.$timeframe.' as '.$groupBy))
- ->where('account_id', '=', Auth::user()->account_id)
+ ->select(DB::raw('sum('.$entityType.'s.amount) as total, '.$timeframe.' as '.$groupBy))
+ ->join('clients', 'clients.id', '=', $entityType.'s.client_id')
+ ->where('clients.is_deleted', '=', false)
+ ->where($entityType.'s.account_id', '=', Auth::user()->account_id)
->where($entityType.'s.is_deleted', '=', false)
->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d'))
->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d'))
@@ -168,6 +170,9 @@ class ReportController extends BaseController
if ($entityType == ENTITY_INVOICE) {
$records->where('is_quote', '=', false)
->where('is_recurring', '=', false);
+ } elseif ($entityType == ENTITY_PAYMENT) {
+ $records->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
+ ->where('invoices.is_deleted', '=', false);
}
$totals = $records->lists('total');
@@ -354,8 +359,8 @@ class ReportController extends BaseController
private function generateInvoiceReport($startDate, $endDate, $isExport)
{
- $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance'];
-
+ $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'payment_date', 'paid', 'method'];
+
$account = Auth::user()->account;
$displayData = [];
$reportTotals = [];
@@ -379,19 +384,25 @@ class ReportController extends BaseController
}]);
foreach ($clients->get() as $client) {
- $currencyId = $client->currency_id ?: Auth::user()->account->getCurrencyId();
+ foreach ($client->invoices as $invoice) {
+
+ $payments = count($invoice->payments) ? $invoice->payments : [false];
+ foreach ($payments as $payment) {
+ $displayData[] = [
+ $isExport ? $client->getDisplayName() : $client->present()->link,
+ $isExport ? $invoice->invoice_number : $invoice->present()->link,
+ $invoice->present()->invoice_date,
+ $account->formatMoney($invoice->amount, $client),
+ $payment ? $payment->present()->payment_date : '',
+ $payment ? $account->formatMoney($payment->amount, $client) : '',
+ $payment ? $payment->present()->method : '',
+ ];
+ if ($payment) {
+ $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment->amount);
+ }
+ }
- foreach ($client->invoices as $invoice) {
- $displayData[] = [
- $isExport ? $client->getDisplayName() : $client->present()->link,
- $isExport ? $invoice->invoice_number : $invoice->present()->link,
- $invoice->present()->invoice_date,
- $account->formatMoney($invoice->amount, $client),
- $account->formatMoney($invoice->getAmountPaid(), $client),
- $account->formatMoney($invoice->balance, $client),
- ];
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount);
- $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $invoice->getAmountPaid());
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'balance', $invoice->balance);
}
}
@@ -503,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/UserController.php b/app/Http/Controllers/UserController.php
index 0e5a8dd70dd6..2e3f675aa53e 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -77,7 +77,6 @@ class UserController extends BaseController
'user' => $user,
'method' => 'PUT',
'url' => 'users/'.$publicId,
- 'title' => trans('texts.edit_user'),
];
return View::make('users.edit', $data);
@@ -120,7 +119,6 @@ class UserController extends BaseController
'user' => null,
'method' => 'POST',
'url' => 'users',
- 'title' => trans('texts.add_user'),
];
return View::make('users.edit', $data);
diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php
index 989246f824ca..032f8d423d34 100644
--- a/app/Http/Controllers/VendorController.php
+++ b/app/Http/Controllers/VendorController.php
@@ -107,7 +107,7 @@ class VendorController extends BaseController
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(
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
index a6d1363e4cdd..3a49c762a94f 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->isPro())){
$authenticated = true;
}
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/CreateClientRequest.php b/app/Http/Requests/CreateClientRequest.php
index 6fb7060e422f..26ce5c4b5371 100644
--- a/app/Http/Requests/CreateClientRequest.php
+++ b/app/Http/Requests/CreateClientRequest.php
@@ -26,21 +26,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/CreateVendorRequest.php b/app/Http/Requests/CreateVendorRequest.php
index d901f9e481c8..9b2accb36524 100644
--- a/app/Http/Requests/CreateVendorRequest.php
+++ b/app/Http/Requests/CreateVendorRequest.php
@@ -26,21 +26,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/routes.php b/app/Http/routes.php
index dec31aa26cb8..8066690990cb 100644
--- a/app/Http/routes.php
+++ b/app/Http/routes.php
@@ -42,17 +42,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/document/js/{public_id}/{filename}', 'PublicClientController@getDocumentVFSJS');
+ Route::get('client/document/{invitation_key}/{public_id}/{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');
@@ -73,8 +79,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');
@@ -83,8 +89,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'));
@@ -101,7 +107,6 @@ if (Utils::isReseller()) {
Route::group(['middleware' => 'auth:user'], function() {
Route::get('dashboard', 'DashboardController@index');
- Route::get('dashboard2', 'DashboardApiController@index');
Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible');
Route::get('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
@@ -133,6 +138,11 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::post('invoices/bulk', 'InvoiceController@bulk');
Route::post('recurring_invoices/bulk', 'InvoiceController@bulk');
+ Route::get('document/{public_id}/{filename?}', 'DocumentController@get');
+ Route::get('document/js/{public_id}/{filename}', 'DocumentController@getVFSJS');
+ Route::get('document/preview/{public_id}/{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');
@@ -258,6 +268,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function()
Route::resource('expenses','ExpenseApiController');
Route::post('add_token', 'AccountApiController@addDeviceToken');
Route::post('update_notifications', 'AccountApiController@updatePushNotifications');
+ Route::get('dashboard', 'DashboardApiController@index');
// Vendor
Route::resource('vendors', 'VendorApiController');
@@ -267,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);
});
@@ -295,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'));
@@ -310,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');
@@ -425,6 +436,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
@@ -520,6 +535,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);
@@ -535,7 +551,7 @@ if (!defined('CONTACT_EMAIL')) {
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.0.4');
+ define('NINJA_VERSION', '2.5.1.3');
define('NINJA_DATE', '2000-01-01');
define('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja');
@@ -708,4 +724,4 @@ if (Utils::isNinjaDev())
//ini_set('memory_limit','1024M');
//Auth::loginUsingId(1);
}
-*/
+*/
\ No newline at end of file
diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php
index cfcc97c00114..439247f337c0 100644
--- a/app/Libraries/Utils.php
+++ b/app/Libraries/Utils.php
@@ -247,7 +247,7 @@ class Utils
return "***{$class}*** [{$code}] : {$exception->getFile()} [Line {$exception->getLine()}] => {$exception->getMessage()}";
}
- public static function logError($error, $context = 'PHP')
+ public static function logError($error, $context = 'PHP', $info = false)
{
if ($error instanceof Exception) {
$error = self::getErrorString($error);
@@ -271,7 +271,11 @@ class Utils
'count' => Session::get('error_count', 0),
];
- Log::error($error."\n", $data);
+ if ($info) {
+ Log::info($error."\n", $data);
+ } else {
+ Log::error($error."\n", $data);
+ }
/*
Mail::queue('emails.error', ['message'=>$error.' '.json_encode($data)], function($message)
@@ -620,8 +624,8 @@ class Utils
private static function getMonth($offset)
{
- $months = [ "January", "February", "March", "April", "May", "June",
- "July", "August", "September", "October", "November", "December", ];
+ $months = [ "january", "february", "march", "april", "may", "june",
+ "july", "august", "september", "october", "november", "december", ];
$month = intval(date('n')) - 1;
@@ -632,7 +636,7 @@ class Utils
$month += 12;
}
- return $months[$month];
+ return trans('texts.' . $months[$month]);
}
private static function getQuarter($offset)
diff --git a/app/Models/Account.php b/app/Models/Account.php
index f6a7194bdf6f..f50ed2cec1fa 100644
--- a/app/Models/Account.php
+++ b/app/Models/Account.php
@@ -8,7 +8,9 @@ use Event;
use Cache;
use App;
use File;
+use App\Models\Document;
use App\Events\UserSettingsChanged;
+use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
@@ -384,26 +386,69 @@ 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()
- {
- $fileName = 'logo/' . $this->account_key;
-
- return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg';
+ public function getLogoRaw(){
+ if(!$this->hasLogo()){
+ return null;
+ }
+
+ $disk = $this->getLogoDisk();
+ return $disk->get($this->logo);
}
-
- public function getLogoFullPath()
+
+ public function getLogoURL($cachebuster = false)
{
- $fileName = public_path() . '/logo/' . $this->account_key;
-
- return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg';
- }
-
- public function getLogoURL()
- {
- return SITE_URL . '/' . $this->getLogoPath();
+ 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 getToken($name)
@@ -419,24 +464,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)
@@ -815,12 +856,11 @@ class Account extends Eloquent
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()
@@ -948,7 +988,7 @@ class Account extends Eloquent
// 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 p>";
+ return "
" . trans('texts.email_signature') . "\n \$account
";
}
}
diff --git a/app/Models/Client.php b/app/Models/Client.php
index 92232647117c..71970b7c9d48 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();
diff --git a/app/Models/Document.php b/app/Models/Document.php
new file mode 100644
index 000000000000..96051d9db1b2
--- /dev/null
+++ b/app/Models/Document.php
@@ -0,0 +1,263 @@
+ '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('document/'.$this->public_id.'/'.$this->name);
+ }
+
+ public function getClientUrl($invitation){
+ return url('client/document/'.$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('document/js/'.$this->public_id.'/'.$this->name.'.js');
+ }
+
+ public function getClientVFSJSUrl(){
+ if(!$this->isPDFEmbeddable())return null;
+ return url('client/document/js/'.$this->public_id.'/'.$this->name.'.js');
+ }
+
+ public function getPreviewUrl(){
+ return $this->preview?url('document/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;
+ }
+
+ public static function canCreate(){
+ return true;
+ }
+
+ public static function canViewItem($document){
+ if(Auth::user()->hasPermission('view_all'))return true;
+ if($document->expense){
+ if($document->expense->invoice)return $document->expense->invoice->canView();
+ return $document->expense->canView();
+ }
+ if($document->invoice)return $document->invoice->canView();
+ return Auth::user()->id == $item->user_id;
+ }
+}
+
+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..53bb1d0d1a48 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) {
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 2cc71029578d..687e9873457d 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');
@@ -39,8 +41,8 @@ class Invitation extends EntityModel
$iframe_url = $this->account->iframe_url;
if ($this->account->isPro()) {
- if ($iframe_url) {
- return "{$iframe_url}/?{$this->invitation_key}";
+ 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..4d9b82c79416 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');
@@ -385,9 +398,13 @@ 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',
@@ -457,6 +474,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'custom_invoice_text_label2',
'custom_invoice_item_label1',
'custom_invoice_item_label2',
+ 'invoice_embed_documents'
]);
foreach ($this->invoice_items as $invoiceItem) {
@@ -467,8 +485,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 +501,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,7 +789,7 @@ 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();
@@ -814,53 +854,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/InvoiceItem.php b/app/Models/InvoiceItem.php
index b7b3c8ffc8e9..d3981c931c62 100644
--- a/app/Models/InvoiceItem.php
+++ b/app/Models/InvoiceItem.php
@@ -7,6 +7,13 @@ class InvoiceItem extends EntityModel
use SoftDeletes;
protected $dates = ['deleted_at'];
+ protected $fillable = [
+ 'tax_name1',
+ 'tax_rate1',
+ 'tax_name2',
+ 'tax_rate2',
+ ];
+
public function invoice()
{
return $this->belongsTo('App\Models\Invoice');
diff --git a/app/Models/TaxRate.php b/app/Models/TaxRate.php
index 15c39a7572a0..cf0a576a8f0d 100644
--- a/app/Models/TaxRate.php
+++ b/app/Models/TaxRate.php
@@ -1,5 +1,6 @@
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 +105,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;
@@ -127,6 +152,7 @@ class ContactMailer extends Mailer
'account' => $account,
'client' => $client,
'invoice' => $invoice,
+ 'documents' => $documentStrings,
];
if ($account->attatchPDF()) {
@@ -263,7 +289,21 @@ class ContactMailer extends Mailer
$invitation = $data['invitation'];
$invoice = $invitation->invoice;
$passwordHTML = isset($data['password'])?''.trans('texts.password').': '.$data['password'].'
':false;
+ $documentsHTML = '';
+ if($account->isPro() && $invoice->hasDocuments()){
+ $documentsHTML .= trans('texts.email_documents_header').'
';
+ }
+
$variables = [
'$footer' => $account->getEmailFooter(),
'$client' => $client->getDisplayName(),
@@ -285,6 +325,7 @@ class ContactMailer extends Mailer
'$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
diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php
index c30c9a10d74c..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);
@@ -81,7 +88,7 @@ class Mailer
$emailError = $exception->getMessage();
}
- Utils::logError("Email Error: $emailError");
+ //Utils::logError("Email Error: $emailError");
if (isset($data['invitation'])) {
$invitation = $data['invitation'];
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/PaymentPresenter.php b/app/Ninja/Presenters/PaymentPresenter.php
index a0a58663e5a7..a1c3692991fe 100644
--- a/app/Ninja/Presenters/PaymentPresenter.php
+++ b/app/Ninja/Presenters/PaymentPresenter.php
@@ -1,5 +1,6 @@
entity->public_id . '/edit');
+ }
+
+ public function link()
+ {
+ return link_to('/payments/' . $this->entity->public_id . '/edit', $this->entity->getDisplayName());
+ }
+
}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php
index 0e08fbba5d06..cd07a37b5db2 100644
--- a/app/Ninja/Repositories/AccountRepository.php
+++ b/app/Ninja/Repositories/AccountRepository.php
@@ -475,7 +475,7 @@ class AccountRepository
$item->account_id = $user->account->id;
$item->account_name = $user->account->getDisplayName();
$item->pro_plan_paid = $user->account->pro_plan_paid;
- $item->logo_path = $user->account->hasLogo() ? $user->account->getLogoPath() : null;
+ $item->logo_url = $user->account->hasLogo() ? $user->account->getLogoUrl() : null;
$data[] = $item;
}
diff --git a/app/Ninja/Repositories/BaseRepository.php b/app/Ninja/Repositories/BaseRepository.php
index bec95fb96921..f674c406549e 100644
--- a/app/Ninja/Repositories/BaseRepository.php
+++ b/app/Ninja/Repositories/BaseRepository.php
@@ -20,6 +20,10 @@ class BaseRepository
public function archive($entity)
{
+ if ($entity->trashed()) {
+ return;
+ }
+
$entity->delete();
$className = $this->getEventClass($entity, 'Archived');
@@ -31,6 +35,10 @@ class BaseRepository
public function restore($entity)
{
+ if ( ! $entity->trashed()) {
+ return;
+ }
+
$fromDeleted = false;
$entity->restore();
@@ -49,6 +57,10 @@ class BaseRepository
public function delete($entity)
{
+ if ($entity->is_deleted) {
+ return;
+ }
+
$entity->is_deleted = true;
$entity->save();
diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php
index 9df81663a5e5..d3f943a07001 100644
--- a/app/Ninja/Repositories/ClientRepository.php
+++ b/app/Ninja/Repositories/ClientRepository.php
@@ -100,6 +100,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'] : 0) - (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/DocumentRepository.php b/app/Ninja/Repositories/DocumentRepository.php
new file mode 100644
index 000000000000..4da51c3797d4
--- /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/document/'.$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..aae7729c474d 100644
--- a/app/Ninja/Repositories/ExpenseRepository.php
+++ b/app/Ninja/Repositories/ExpenseRepository.php
@@ -4,17 +4,25 @@ use DB;
use Utils;
use App\Models\Expense;
use App\Models\Vendor;
+use App\Models\Document;
use App\Ninja\Repositories\BaseRepository;
use Session;
class ExpenseRepository extends BaseRepository
{
+ protected $documentRepo;
+
// Expenses
public function getClassName()
{
return 'App\Models\Expense';
}
+ public function __construct(DocumentRepository $documentRepo)
+ {
+ $this->documentRepo = $documentRepo;
+ }
+
public function all()
{
return Expense::scope()
@@ -113,7 +121,7 @@ class ExpenseRepository extends BaseRepository
return $query;
}
- public function save($input)
+ public function save($input, $checkSubPermissions=false)
{
$publicId = isset($input['public_id']) ? $input['public_id'] : false;
@@ -144,9 +152,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 && !$checkSubPermissions || $document->canEdit()){
+ $document->invoice_id = null;
+ $document->expense_id = $expense->id;
+ $document->save();
+ }
+ }
+
+ if(!empty($input['documents']) && Document::canCreate()){
+ // 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..845bb0082d79 100644
--- a/app/Ninja/Repositories/InvoiceRepository.php
+++ b/app/Ninja/Repositories/InvoiceRepository.php
@@ -2,24 +2,29 @@
use DB;
use Utils;
+use Session;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Invitation;
use App\Models\Product;
use App\Models\Task;
+use App\Models\Document;
use App\Models\Expense;
use App\Services\PaymentService;
use App\Ninja\Repositories\BaseRepository;
class InvoiceRepository extends BaseRepository
{
+ protected $documentRepo;
+
public function getClassName()
{
return 'App\Models\Invoice';
}
- public function __construct(PaymentService $paymentService)
+ public function __construct(PaymentService $paymentService, DocumentRepository $documentRepo)
{
+ $this->documentRepo = $documentRepo;
$this->paymentService = $paymentService;
}
@@ -216,6 +221,8 @@ class InvoiceRepository extends BaseRepository
$invoice = Invoice::scope($publicId)->firstOrFail();
}
+ $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']) {
@@ -292,12 +299,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 +323,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 +382,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 +407,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 && !$checkSubPermissions || $document->canEdit()){
+
+ 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']) && Document::canCreate()){
+ // 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;
@@ -450,7 +507,7 @@ 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;
+ //$invoiceItem->tax_rate = 0;
if (isset($item['custom_value1'])) {
$invoiceItem->custom_value1 = $item['custom_value1'];
@@ -459,11 +516,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 +554,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 +569,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 +606,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 +650,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,8 +701,10 @@ 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;
@@ -652,11 +723,18 @@ 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;
$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/Transformers/InvoiceItemTransformer.php b/app/Ninja/Transformers/InvoiceItemTransformer.php
index 66d9fe137dd8..895d8e8d2f4a 100644
--- a/app/Ninja/Transformers/InvoiceItemTransformer.php
+++ b/app/Ninja/Transformers/InvoiceItemTransformer.php
@@ -19,8 +19,10 @@ class InvoiceItemTransformer extends EntityTransformer
'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..413f43ce1a10 100644
--- a/app/Ninja/Transformers/InvoiceTransformer.php
+++ b/app/Ninja/Transformers/InvoiceTransformer.php
@@ -87,8 +87,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,
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 3cd691098747..d5f26393681f 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -22,8 +22,15 @@ class AppServiceProvider extends ServiceProvider {
*/
public function boot()
{
- Form::macro('image_data', function($imagePath) {
- return 'data:image/jpeg;base64,' . base64_encode(file_get_contents($imagePath));
+ Form::macro('image_data', function($image, $contents = false) {
+ if(!$contents){
+ $contents = file_get_contents($image);
+ }
+ else{
+ $contents = $image;
+ }
+
+ return 'data:image/jpeg;base64,' . base64_encode($contents);
});
Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') {
@@ -152,6 +159,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/Services/ExpenseService.php b/app/Services/ExpenseService.php
index 2fc2afbc84b1..b574aa9983b8 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, $checkSubPermissions=false)
{
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, $checkSubPermissions);
}
public function getDatatable($search)
diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php
index 3bf26d8ba206..1669d94d88d4 100644
--- a/app/Services/PaymentService.php
+++ b/app/Services/PaymentService.php
@@ -74,6 +74,7 @@ class PaymentService extends BaseService
if ($input) {
$data = self::convertInputForOmnipay($input);
+ $data['email'] = $invitation->contact->email;
Session::put($key, $data);
} elseif (Session::get($key)) {
$data = Session::get($key);
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 354e5dd90538..600e0fb5e9c8 100755
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -58,4 +58,11 @@ if (strstr($_SERVER['HTTP_USER_AGENT'], 'PhantomJS') && Utils::isNinjaDev()) {
}
*/
+// Write info messages to a separate file
+$app->configureMonologUsing(function($monolog) {
+ $monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-info.log', Monolog\Logger::INFO, false));
+ $monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-warning.log', Monolog\Logger::WARNING, false));
+ $monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-error.log', Monolog\Logger::ERROR, false));
+});
+
return $app;
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 bc315ba6c285..a1ee15eff291 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.22",
+ "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": "2.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.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",
+ "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 bd2369f87954..0c76e7216c49 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": "a33dce96f4ded3fb269a6d9dcbf24b27",
- "content-hash": "f73a83c64422ef3560da4adb988850ae",
+ "hash": "2ab7ab9013e31d8a2f0dcf43b31beefa",
+ "content-hash": "188fba7fcc31b702098d5417bc0e63e2",
"packages": [
{
"name": "agmscode/omnipay-agms",
@@ -123,12 +123,12 @@
"source": {
"type": "git",
"url": "https://github.com/formers/former.git",
- "reference": "e196c4336db77be97131f6a3b3c3b69b3a22b683"
+ "reference": "d97f907741323b390f43954a90a227921ecc6b96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/formers/former/zipball/e196c4336db77be97131f6a3b3c3b69b3a22b683",
- "reference": "e196c4336db77be97131f6a3b3c3b69b3a22b683",
+ "url": "https://api.github.com/repos/formers/former/zipball/d97f907741323b390f43954a90a227921ecc6b96",
+ "reference": "d97f907741323b390f43954a90a227921ecc6b96",
"shasum": ""
},
"require": {
@@ -174,7 +174,7 @@
"foundation",
"laravel"
],
- "time": "2016-03-02 17:21:21"
+ "time": "2016-03-16 01:43:45"
},
{
"name": "anahkiasen/html-object",
@@ -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",
@@ -880,12 +1000,12 @@
"source": {
"type": "git",
"url": "https://github.com/delatbabel/omnipay-fatzebra.git",
- "reference": "7b3cb869abe8327d4cf6ccc6591a89a95c02bfbc"
+ "reference": "d0a56a8704357d91457672741a48a4cb6c7ecd53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/delatbabel/omnipay-fatzebra/zipball/7b3cb869abe8327d4cf6ccc6591a89a95c02bfbc",
- "reference": "7b3cb869abe8327d4cf6ccc6591a89a95c02bfbc",
+ "url": "https://api.github.com/repos/delatbabel/omnipay-fatzebra/zipball/d0a56a8704357d91457672741a48a4cb6c7ecd53",
+ "reference": "d0a56a8704357d91457672741a48a4cb6c7ecd53",
"shasum": ""
},
"require": {
@@ -929,7 +1049,7 @@
"payment",
"paystream"
],
- "time": "2015-02-15 11:27:23"
+ "time": "2016-03-21 09:21:14"
},
{
"name": "dercoder/omnipay-ecopayz",
@@ -1039,12 +1159,12 @@
"source": {
"type": "git",
"url": "https://github.com/descubraomundo/omnipay-pagarme.git",
- "reference": "528953568929b57189de16fa7431eaab75d61840"
+ "reference": "8571396139eb1fb1a7011450714a5e8d8d604d8c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/descubraomundo/omnipay-pagarme/zipball/528953568929b57189de16fa7431eaab75d61840",
- "reference": "528953568929b57189de16fa7431eaab75d61840",
+ "url": "https://api.github.com/repos/descubraomundo/omnipay-pagarme/zipball/8571396139eb1fb1a7011450714a5e8d8d604d8c",
+ "reference": "8571396139eb1fb1a7011450714a5e8d8d604d8c",
"shasum": ""
},
"require": {
@@ -1081,7 +1201,7 @@
"pay",
"payment"
],
- "time": "2015-10-27 19:17:20"
+ "time": "2016-03-18 19:37:37"
},
{
"name": "dioscouri/omnipay-cybersource",
@@ -1938,16 +2058,16 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "6.1.1",
+ "version": "6.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "c6851d6e48f63b69357cbfa55bca116448140e0c"
+ "reference": "d094e337976dff9d8e2424e8485872194e768662"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c6851d6e48f63b69357cbfa55bca116448140e0c",
- "reference": "c6851d6e48f63b69357cbfa55bca116448140e0c",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d094e337976dff9d8e2424e8485872194e768662",
+ "reference": "d094e337976dff9d8e2424e8485872194e768662",
"shasum": ""
},
"require": {
@@ -1963,7 +2083,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.1-dev"
+ "dev-master": "6.2-dev"
}
},
"autoload": {
@@ -1996,7 +2116,7 @@
"rest",
"web service"
],
- "time": "2015-11-23 00:47:50"
+ "time": "2016-03-21 20:02:09"
},
{
"name": "guzzlehttp/promises",
@@ -2603,12 +2723,12 @@
"source": {
"type": "git",
"url": "https://github.com/labs7in0/omnipay-wechat.git",
- "reference": "4e279ff4535dfa0636a3d6af5c92b8e9dcc4311a"
+ "reference": "40c9f86df6573ad98ae1dd0d29712ccbc789a74e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/labs7in0/omnipay-wechat/zipball/4e279ff4535dfa0636a3d6af5c92b8e9dcc4311a",
- "reference": "4e279ff4535dfa0636a3d6af5c92b8e9dcc4311a",
+ "url": "https://api.github.com/repos/labs7in0/omnipay-wechat/zipball/40c9f86df6573ad98ae1dd0d29712ccbc789a74e",
+ "reference": "40c9f86df6573ad98ae1dd0d29712ccbc789a74e",
"shasum": ""
},
"require": {
@@ -2644,7 +2764,7 @@
"purchase",
"wechat"
],
- "time": "2015-11-16 11:04:21"
+ "time": "2016-03-18 09:59:11"
},
{
"name": "laracasts/presenter",
@@ -2694,16 +2814,16 @@
},
{
"name": "laravel/framework",
- "version": "v5.2.22",
+ "version": "v5.2.24",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "aec1b7cb9ec0bac0107361a3730cac9b6f945ef4"
+ "reference": "396297a5fd3c70c2fc1af68f09ee574a2380175c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/aec1b7cb9ec0bac0107361a3730cac9b6f945ef4",
- "reference": "aec1b7cb9ec0bac0107361a3730cac9b6f945ef4",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/396297a5fd3c70c2fc1af68f09ee574a2380175c",
+ "reference": "396297a5fd3c70c2fc1af68f09ee574a2380175c",
"shasum": ""
},
"require": {
@@ -2716,7 +2836,7 @@
"monolog/monolog": "~1.11",
"mtdowling/cron-expression": "~1.0",
"nesbot/carbon": "~1.20",
- "paragonie/random_compat": "~1.1",
+ "paragonie/random_compat": "~1.4",
"php": ">=5.5.9",
"psy/psysh": "0.7.*",
"swiftmailer/swiftmailer": "~5.1",
@@ -2818,7 +2938,7 @@
"framework",
"laravel"
],
- "time": "2016-02-27 22:09:19"
+ "time": "2016-03-22 13:45:19"
},
{
"name": "laravel/socialite",
@@ -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",
@@ -3815,7 +4111,7 @@
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/omnipay-2checkout/zipball/31394ce58d5999b6f49b321cb3547747837c1297",
+ "url": "https://api.github.com/repos/thephpleague/omnipay-2checkout/zipball/b27d2823d052f5c227eeb29324bb564cfdb8f9af",
"reference": "e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"shasum": ""
},
@@ -4328,16 +4624,16 @@
},
{
"name": "omnipay/eway",
- "version": "v2.2.0",
+ "version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/omnipay-eway.git",
- "reference": "0dcf28596f0382fbfc3ee229e98e60798675ed16"
+ "reference": "1c953630f7097bfdeed200b17a847015a4df5607"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/omnipay-eway/zipball/0dcf28596f0382fbfc3ee229e98e60798675ed16",
- "reference": "0dcf28596f0382fbfc3ee229e98e60798675ed16",
+ "url": "https://api.github.com/repos/thephpleague/omnipay-eway/zipball/1c953630f7097bfdeed200b17a847015a4df5607",
+ "reference": "1c953630f7097bfdeed200b17a847015a4df5607",
"shasum": ""
},
"require": {
@@ -4381,7 +4677,7 @@
"pay",
"payment"
],
- "time": "2015-03-30 00:28:33"
+ "time": "2016-03-22 01:11:02"
},
{
"name": "omnipay/firstdata",
@@ -5536,16 +5832,16 @@
},
{
"name": "paragonie/random_compat",
- "version": "v1.2.2",
+ "version": "v1.4.1",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
- "reference": "b3313b618f4edd76523572531d5d7e22fe747430"
+ "reference": "c7e26a21ba357863de030f0b9e701c7d04593774"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/random_compat/zipball/b3313b618f4edd76523572531d5d7e22fe747430",
- "reference": "b3313b618f4edd76523572531d5d7e22fe747430",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/c7e26a21ba357863de030f0b9e701c7d04593774",
+ "reference": "c7e26a21ba357863de030f0b9e701c7d04593774",
"shasum": ""
},
"require": {
@@ -5580,7 +5876,7 @@
"pseudorandom",
"random"
],
- "time": "2016-03-11 19:54:08"
+ "time": "2016-03-18 20:34:03"
},
{
"name": "patricktalmadge/bootstrapper",
@@ -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",
@@ -7585,20 +7938,20 @@
},
{
"name": "wildbit/laravel-postmark-provider",
- "version": "2.0.0",
+ "version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/wildbit/laravel-postmark-provider.git",
- "reference": "79a7e8bde66b2bd6f314829b00ee08616847ebc5"
+ "reference": "b80815602f618abe24030ea6d3f117da49a72885"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/wildbit/laravel-postmark-provider/zipball/79a7e8bde66b2bd6f314829b00ee08616847ebc5",
- "reference": "79a7e8bde66b2bd6f314829b00ee08616847ebc5",
+ "url": "https://api.github.com/repos/wildbit/laravel-postmark-provider/zipball/b80815602f618abe24030ea6d3f117da49a72885",
+ "reference": "b80815602f618abe24030ea6d3f117da49a72885",
"shasum": ""
},
"require": {
- "illuminate/mail": "~5.0",
+ "illuminate/mail": "~5.2",
"wildbit/swiftmailer-postmark": "~2.0"
},
"type": "library",
@@ -7612,7 +7965,7 @@
"MIT"
],
"description": "An officially supported mail provider to send mail from Laravel through Postmark, see instructions for integrating it here: https://github.com/wildbit/laravel-postmark-provider/blob/master/README.md",
- "time": "2015-11-10 14:43:06"
+ "time": "2016-02-10 14:15:58"
},
{
"name": "wildbit/swiftmailer-postmark",
@@ -8475,16 +8828,16 @@
},
{
"name": "phpspec/phpspec",
- "version": "2.4.1",
+ "version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/phpspec.git",
- "reference": "5528ce1e93a1efa090c9404aba3395c329b4e6ed"
+ "reference": "385ecb015e97c13818074f1517928b24d4a26067"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/phpspec/zipball/5528ce1e93a1efa090c9404aba3395c329b4e6ed",
- "reference": "5528ce1e93a1efa090c9404aba3395c329b4e6ed",
+ "url": "https://api.github.com/repos/phpspec/phpspec/zipball/385ecb015e97c13818074f1517928b24d4a26067",
+ "reference": "385ecb015e97c13818074f1517928b24d4a26067",
"shasum": ""
},
"require": {
@@ -8549,7 +8902,7 @@
"testing",
"tests"
],
- "time": "2016-01-01 10:17:54"
+ "time": "2016-03-20 20:34:32"
},
{
"name": "phpspec/prophecy",
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/migrations/2016_03_22_168362_add_documents.php b/database/migrations/2016_03_22_168362_add_documents.php
new file mode 100644
index 000000000000..b57e72da2079
--- /dev/null
+++ b/database/migrations/2016_03_22_168362_add_documents.php
@@ -0,0 +1,70 @@
+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');
+ });
+
+ 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/seeds/CurrenciesSeeder.php b/database/seeds/CurrenciesSeeder.php
index 9a8304b181f6..b3da84dad01d 100644
--- a/database/seeds/CurrenciesSeeder.php
+++ b/database/seeds/CurrenciesSeeder.php
@@ -55,6 +55,7 @@ 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' => '.'],
];
foreach ($currencies as $currency) {
diff --git a/database/seeds/UpdateSeeder.php b/database/seeds/UpdateSeeder.php
new file mode 100644
index 000000000000..e4d43f78e512
--- /dev/null
+++ b/database/seeds/UpdateSeeder.php
@@ -0,0 +1,23 @@
+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');
+ }
+}
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..327b7e5fb2ae 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 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='',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 a {
background-color: #09334f !important;
background-image: none;
@@ -3178,4 +3195,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 1ba4afb31115..2a02c2db94b6 100644
--- a/public/css/built.public.css
+++ b/public/css/built.public.css
@@ -790,14 +790,24 @@ html {
overflow-y: scroll;
}
-@media screen and (min-width: 700px) {
- .navbar-header {
- padding-top: 16px;
- padding-bottom: 16px;
- }
- .navbar li a {
- padding: 31px 20px 31px 20px;
- }
+
+.navbar-header {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+.navbar li a {
+ padding-top: 18px;
+ font-weight: 500;
+ font-size: 15px;
+ font-weight: bold;
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+.navbar {
+ 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 da5a469decb7..b1f91c25739f 100644
--- a/public/css/public.style.css
+++ b/public/css/public.style.css
@@ -7,14 +7,24 @@ html {
overflow-y: scroll;
}
-@media screen and (min-width: 700px) {
- .navbar-header {
- padding-top: 16px;
- padding-bottom: 16px;
- }
- .navbar li a {
- padding: 31px 20px 31px 20px;
- }
+
+.navbar-header {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+.navbar li a {
+ padding-top: 18px;
+ font-weight: 500;
+ font-size: 15px;
+ font-weight: bold;
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+.navbar {
+ 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 7480cbe0f2d1..0ac30b8a4694 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -402,6 +402,21 @@ font-weight: bold;
filter: none;
}
+.navbar,
+ul.dropdown-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);
+}
+
+.panel-default,
+canvas {
+ border: 1px solid;
+ border-color: #e5e6e9 #dfe0e4 #d0d1d5;
+ border-radius: 3px;
+}
+
.navbar .active > a {
background-color: #09334f !important;
background-image: none;
@@ -1051,4 +1066,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..afabd47f8875 100644
--- a/public/js/pdf.pdfmake.js
+++ b/public/js/pdf.pdfmake.js
@@ -109,11 +109,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 +145,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),
@@ -348,13 +350,15 @@ 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 = '';
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);
}
}
@@ -391,7 +395,17 @@ NINJA.invoiceLines = function(invoice) {
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 +415,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 +471,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') {
diff --git a/public/js/script.js b/public/js/script.js
index c447905f81b9..b806bd538ac5 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 fe4cb90bdc3b..8a5483584427 100644
--- a/readme.md
+++ b/readme.md
@@ -8,10 +8,6 @@
[](https://travis-ci.org/invoiceninja/invoiceninja)
[](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
@@ -52,7 +48,7 @@ Note: we've recently updated this branch to Laravel 5.2. If you're upgrading her
* [Debian and Nginx](https://www.rosehosting.com/blog/install-invoice-ninja-on-a-debian-7-vps/)
* [User Guide](https://www.invoiceninja.com/app-user-guide/)
* [Developer Guide](https://www.invoiceninja.com/knowledgebase/developer-guide/)
-* [API Documentation](https://www.invoiceninja.com/knowledgebase/api-documentation/)
+* [API Documentation](https://www.invoiceninja.com/api-documentation/)
* [Support Forum](https://www.invoiceninja.com/forums/forum/support/)
* [Feature Roadmap](https://trello.com/b/63BbiVVe/)
diff --git a/resources/lang/da/texts.php b/resources/lang/da/texts.php
index e3fe7a081917..ffc74871cde0 100644
--- a/resources/lang/da/texts.php
+++ b/resources/lang/da/texts.php
@@ -1131,4 +1131,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
\ No newline at end of file
diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php
index a3ea1ac658f9..08ffaf7b8a0b 100644
--- a/resources/lang/de/texts.php
+++ b/resources/lang/de/texts.php
@@ -1132,4 +1132,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php
index 6d0f963d2893..799cf6cbaab2 100644
--- a/resources/lang/en/texts.php
+++ b/resources/lang/en/texts.php
@@ -1049,8 +1049,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',
@@ -1068,7 +1066,7 @@ $LANG = array(
// User Permissions
'owner' => 'Owner',
'administrator' => 'Administrator',
- 'administrator_help' => 'Allow user to manage users, change settings, and view and modify all data',
+ '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.',
@@ -1078,7 +1076,57 @@ $LANG = array(
'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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ '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.',
+
);
return $LANG;
diff --git a/resources/lang/es/texts.php b/resources/lang/es/texts.php
index 56fb42534f5c..75bd1a75dae6 100644
--- a/resources/lang/es/texts.php
+++ b/resources/lang/es/texts.php
@@ -1108,4 +1108,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
diff --git a/resources/lang/es_ES/texts.php b/resources/lang/es_ES/texts.php
index 4e29075d559a..25054072dac5 100644
--- a/resources/lang/es_ES/texts.php
+++ b/resources/lang/es_ES/texts.php
@@ -1128,4 +1128,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
diff --git a/resources/lang/fr/texts.php b/resources/lang/fr/texts.php
index 2bc358e6a822..6cd75a9f0efa 100644
--- a/resources/lang/fr/texts.php
+++ b/resources/lang/fr/texts.php
@@ -1123,4 +1123,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
diff --git a/resources/lang/fr_CA/texts.php b/resources/lang/fr_CA/texts.php
index 946fac0696ed..c62227e45050 100644
--- a/resources/lang/fr_CA/texts.php
+++ b/resources/lang/fr_CA/texts.php
@@ -1121,4 +1121,73 @@ return array(
'overdue' => 'En souffrance',
'white_label_text' => 'Achetez une licence sans pub d\'un an à $'.WHITE_LABEL_PRICE.' pour retirer le logo de Invoice Ninja du portail client et supporter notre projet.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
\ No newline at end of file
diff --git a/resources/lang/it/texts.php b/resources/lang/it/texts.php
index acc4cdeebab3..154b9f3e5d21 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',
@@ -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',
@@ -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',
@@ -736,8 +736,8 @@ return array(
'total_revenue' => 'Total Revenue',
'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',
@@ -785,15 +785,15 @@ return array(
'page_expire' => 'This page will expire soon, :click_here to keep working',
'upcoming_quotes' => 'Upcoming Quotes',
- 'expired_quotes' => 'Expired Quotes',
+ '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',
@@ -845,8 +845,8 @@ return array(
'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_4' => ':user ha creato la fattura :invoice',
+ 'activity_5' => ':user ha aggiornato la fattura :invoice',
'activity_6' => ':user emailed invoice :invoice to :contact',
'activity_7' => ':contact viewed invoice :invoice',
'activity_8' => ':user archived invoice :invoice',
@@ -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',
@@ -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',
@@ -1126,4 +1126,73 @@ return array(
'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.',
-);
\ No newline at end of file
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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' => 'Da versare (parziale)',
+ '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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
+);
diff --git a/resources/lang/ja/texts.php b/resources/lang/ja/texts.php
index a6b046e42f46..7e673ac11d33 100644
--- a/resources/lang/ja/texts.php
+++ b/resources/lang/ja/texts.php
@@ -1051,6 +1051,51 @@ $LANG = array(
'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' => 'ダッシュボード',
'enable_client_portal_help' => 'Show/hide the dashboard page in the client portal.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
);
diff --git a/resources/lang/lt/texts.php b/resources/lang/lt/texts.php
index 584f7776b435..252a7f03bd9c 100644
--- a/resources/lang/lt/texts.php
+++ b/resources/lang/lt/texts.php
@@ -1133,4 +1133,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
\ No newline at end of file
diff --git a/resources/lang/nb_NO/texts.php b/resources/lang/nb_NO/texts.php
index afc916a5def3..df06d7a32547 100644
--- a/resources/lang/nb_NO/texts.php
+++ b/resources/lang/nb_NO/texts.php
@@ -1131,4 +1131,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
\ No newline at end of file
diff --git a/resources/lang/nl/texts.php b/resources/lang/nl/texts.php
index db06a981f4f1..26c639245bb9 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',
@@ -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,220 +910,286 @@ 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' => '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' => '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' => 'Toon/verberg de dashboard pagina in het klantenportaal.',
+
+ // Client Passwords
+ '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' => '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' => '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' => 'januari',
+ 'february' => 'februari',
+ 'march' => 'maart',
+ 'april' => 'april',
+ 'may' => 'mei',
+ 'june' => 'juni',
+ 'july' => 'juli',
+ 'august' => 'augustus',
+ 'september' => 'september',
+ 'october' => 'oktober',
+ 'november' => 'november',
+ 'december' => 'december',
);
\ 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/pt_BR/texts.php b/resources/lang/pt_BR/texts.php
index 731be8bbbdc6..30fcfbf05c27 100644
--- a/resources/lang/pt_BR/texts.php
+++ b/resources/lang/pt_BR/texts.php
@@ -1123,4 +1123,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
diff --git a/resources/lang/sv/texts.php b/resources/lang/sv/texts.php
index 5d526092c4a8..050076bd6e3e 100644
--- a/resources/lang/sv/texts.php
+++ b/resources/lang/sv/texts.php
@@ -1128,4 +1128,73 @@ return array(
'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.',
+ '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 /',
+
+ '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.',
+
+ // 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.',
+
+ '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.',
+
+ // 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',
+
+ 'january' => 'January',
+ 'february' => 'February',
+ 'march' => 'March',
+ 'april' => 'April',
+ 'may' => 'May',
+ 'june' => 'June',
+ 'july' => 'July',
+ 'august' => 'August',
+ 'september' => 'September',
+ 'october' => 'October',
+ 'november' => 'November',
+ 'december' => 'December',
+
);
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 4612b1cbfedd..0b338c80268a 100644
--- a/resources/views/accounts/api_tokens.blade.php
+++ b/resources/views/accounts/api_tokens.blade.php
@@ -5,7 +5,7 @@
@include('accounts.nav', ['selected' => ACCOUNT_API_TOKENS, 'advanced' => true])
- {!! Button::normal(trans('texts.documentation'))->asLinkTo(NINJA_WEB_URL.'/knowledgebase/api-documentation/')->withAttributes(['target' => '_blank'])->appendIcon(Icon::create('info-sign')) !!}
+ {!! Button::normal(trans('texts.documentation'))->asLinkTo(NINJA_WEB_URL.'/api-documentation/')->withAttributes(['target' => '_blank'])->appendIcon(Icon::create('info-sign')) !!}
@if (Utils::isNinja())
{!! Button::normal(trans('texts.zapier'))->asLinkTo(ZAPIER_URL)->withAttributes(['target' => '_blank']) !!}
@endif
@@ -51,4 +51,9 @@
+ @if (Utils::isNinja() && !Utils::isReseller())
+
+
+ @endif
+
@stop
diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php
index 745574ad7254..8c4171b0f7a2 100644
--- a/resources/views/accounts/client_portal.blade.php
+++ b/resources/views/accounts/client_portal.blade.php
@@ -13,6 +13,7 @@
->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)) !!}
@@ -28,45 +29,52 @@
@include('accounts.nav', ['selected' => ACCOUNT_CLIENT_PORTAL])
-
-
-
{!! trans('texts.client_portal') !!}
-
-
-
- {!! Former::checkbox('enable_client_portal')
- ->text(trans('texts.enable'))
- ->help(trans('texts.enable_client_portal_help')) !!}
-
-
- {!! Former::checkbox('enable_portal_password')
- ->text(trans('texts.enable_portal_password'))
- ->help(trans('texts.enable_portal_password_help'))
- ->label(' ') !!}
-
-
- {!! Former::checkbox('send_portal_password')
- ->text(trans('texts.send_portal_password'))
- ->help(trans('texts.send_portal_password_help'))
- ->label(' ') !!}
-
-
-
-
-
-
{!! trans('texts.custom_css') !!}
-
-
-
- {!! Former::textarea('client_view_css')
- ->label(trans('texts.custom_css'))
- ->rows(10)
- ->raw()
- ->autofocus()
- ->maxlength(60000)
- ->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!}
-
-
+
+
+
+
{!! trans('texts.client_portal') !!}
+
+
+
+ {!! Former::checkbox('enable_client_portal')
+ ->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'))
+ ->help(trans('texts.enable_portal_password_help'))
+ ->label(' ') !!}
+
+
+ {!! Former::checkbox('send_portal_password')
+ ->text(trans('texts.send_portal_password'))
+ ->help(trans('texts.send_portal_password_help'))
+ ->label(' ') !!}
+
+
+
+
+
+
{!! trans('texts.custom_css') !!}
+
+
+
+ {!! Former::textarea('client_view_css')
+ ->label(trans('texts.custom_css'))
+ ->rows(10)
+ ->raw()
+ ->autofocus()
+ ->maxlength(60000)
+ ->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!}
+
+
+
diff --git a/resources/views/accounts/details.blade.php b/resources/views/accounts/details.blade.php
index 56dc8f33e73c..7601ff4eb34b 100644
--- a/resources/views/accounts/details.blade.php
+++ b/resources/views/accounts/details.blade.php
@@ -51,8 +51,8 @@
diff --git a/resources/views/accounts/localization.blade.php b/resources/views/accounts/localization.blade.php
index f2c28e3684a8..b921c8710567 100644
--- a/resources/views/accounts/localization.blade.php
+++ b/resources/views/accounts/localization.blade.php
@@ -11,26 +11,29 @@
@include('accounts.nav', ['selected' => ACCOUNT_LOCALIZATION])
+
+
-
-
-
{!! trans('texts.localization') !!}
-
-
+
+
+
{!! trans('texts.localization') !!}
+
+
- {!! Former::select('currency_id')->addOption('','')
- ->fromQuery($currencies, 'name', 'id') !!}
- {!! Former::select('language_id')->addOption('','')
- ->fromQuery($languages, 'name', 'id') !!}
- {!! Former::select('timezone_id')->addOption('','')
- ->fromQuery($timezones, 'location', 'id') !!}
- {!! Former::select('date_format_id')->addOption('','')
- ->fromQuery($dateFormats, 'label', 'id') !!}
- {!! Former::select('datetime_format_id')->addOption('','')
- ->fromQuery($datetimeFormats, 'label', 'id') !!}
- {!! Former::checkbox('military_time')->text(trans('texts.enable')) !!}
- {{-- Former::checkbox('show_currency_code')->text(trans('texts.enable')) --}}
+ {!! Former::select('currency_id')->addOption('','')
+ ->fromQuery($currencies, 'name', 'id') !!}
+ {!! Former::select('language_id')->addOption('','')
+ ->fromQuery($languages, 'name', 'id') !!}
+ {!! Former::select('timezone_id')->addOption('','')
+ ->fromQuery($timezones, 'location', 'id') !!}
+ {!! Former::select('date_format_id')->addOption('','')
+ ->fromQuery($dateFormats, 'label', 'id') !!}
+ {!! Former::select('datetime_format_id')->addOption('','')
+ ->fromQuery($datetimeFormats, 'label', 'id') !!}
+ {!! Former::checkbox('military_time')->text(trans('texts.enable')) !!}
+ {{-- Former::checkbox('show_currency_code')->text(trans('texts.enable')) --}}
+
diff --git a/resources/views/accounts/system_settings.blade.php b/resources/views/accounts/system_settings.blade.php
index 31be6a9d0962..ce889364f943 100644
--- a/resources/views/accounts/system_settings.blade.php
+++ b/resources/views/accounts/system_settings.blade.php
@@ -6,22 +6,23 @@
@include('accounts.nav', ['selected' => ACCOUNT_SYSTEM_SETTINGS])
-
- {!! Former::open('/update_setup')
- ->addClass('warn-on-exit')
- ->autocomplete('off')
- ->rules([
- 'app[url]' => 'required',
- //'database[default]' => 'required',
- 'database[type][host]' => 'required',
- 'database[type][database]' => 'required',
- 'database[type][username]' => 'required',
- 'database[type][password]' => 'required',
- ]) !!}
+
+ {!! Former::open('/update_setup')
+ ->addClass('warn-on-exit')
+ ->autocomplete('off')
+ ->rules([
+ 'app[url]' => 'required',
+ //'database[default]' => 'required',
+ 'database[type][host]' => 'required',
+ 'database[type][database]' => 'required',
+ 'database[type][username]' => 'required',
+ 'database[type][password]' => 'required',
+ ]) !!}
- @include('partials.system_settings')
+ @include('partials.system_settings')
+
diff --git a/resources/views/accounts/templates_and_reminders.blade.php b/resources/views/accounts/templates_and_reminders.blade.php
index 22a3a8a4a08c..7bf6638167d6 100644
--- a/resources/views/accounts/templates_and_reminders.blade.php
+++ b/resources/views/accounts/templates_and_reminders.blade.php
@@ -201,6 +201,13 @@
var keys = {!! json_encode(\App\Ninja\Mailers\ContactMailer::$variableFields) !!};
var passwordHtml = "{!! $account->isPro() && $account->enable_portal_password && $account->send_portal_password?''.trans('texts.password').': 6h2NWNdw6
':'' !!}";
+
+ @if ($account->isPro())
+ var documentsHtml = "{!! trans('texts.email_documents_header').'
' !!}";
+ @else
+ var documentsHtml = "";
+ @endif
+
var vals = [
{!! json_encode($emailFooter) !!},
"{{ $account->getDisplayName() }}",
@@ -213,6 +220,7 @@
"0001",
"0001",
passwordHtml,
+ documentsHtml,
"{{ URL::to('/view/...') }}$password",
'{!! Form::flatButton('view_invoice', '#0b4d78') !!}$password',
"{{ URL::to('/payment/...') }}$password",
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index fe275fe1d54f..b51734c80a9a 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -105,7 +105,7 @@
@endif
- {!! link_to('/forgot', trans('texts.forgot_password')) !!}
+ {!! link_to('/recover_password', trans('texts.recover_password')) !!}
{!! link_to(NINJA_WEB_URL.'/knowledgebase/', trans('texts.knowledge_base'), ['target' => '_blank', 'class' => 'pull-right']) !!}
diff --git a/resources/views/auth/password.blade.php b/resources/views/auth/password.blade.php
index 0cb2596e78a4..50436beab357 100644
--- a/resources/views/auth/password.blade.php
+++ b/resources/views/auth/password.blade.php
@@ -53,7 +53,7 @@
@section('body')
-{!! Former::open('forgot')->rules(['email' => 'required|email'])->addClass('form-signin') !!}
+{!! Former::open('recover_password')->rules(['email' => 'required|email'])->addClass('form-signin') !!}
diff --git a/resources/views/clientauth/login.blade.php b/resources/views/clientauth/login.blade.php
index 0469e882b092..6ad28a593831 100644
--- a/resources/views/clientauth/login.blade.php
+++ b/resources/views/clientauth/login.blade.php
@@ -89,7 +89,7 @@
->large()->submit()->block() !!}
- {!! link_to('/client/forgot', trans('texts.forgot_password')) !!}
+ {!! link_to('/client/recover_password', trans('texts.recover_password')) !!}
diff --git a/resources/views/clientauth/password.blade.php b/resources/views/clientauth/password.blade.php
index 7216765b4d04..79fc36a9a775 100644
--- a/resources/views/clientauth/password.blade.php
+++ b/resources/views/clientauth/password.blade.php
@@ -52,7 +52,7 @@
@section('body')
-{!! Former::open('client/forgot')->addClass('form-signin') !!}
+{!! Former::open('client/recover_password')->addClass('form-signin') !!}
+ @if ($account->isPro())
+
+
{{trans('texts.expense_documents')}}
+
+
+ @endif
@@ -122,6 +143,7 @@
{!! Former::close() !!}
@stop
\ No newline at end of file
diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php
index 3ce364da6441..40b499fe8848 100644
--- a/resources/views/header.blade.php
+++ b/resources/views/header.blade.php
@@ -275,6 +275,7 @@
@if (Auth::check() && Auth::user()->account->custom_client_label1)
,{
name: 'data',
+ limit: 3,
display: 'value',
source: searchData(data['{{ Auth::user()->account->custom_client_label1 }}'], 'tokens'),
templates: {
@@ -285,6 +286,7 @@
@if (Auth::check() && Auth::user()->account->custom_client_label2)
,{
name: 'data',
+ limit: 3,
display: 'value',
source: searchData(data['{{ Auth::user()->account->custom_client_label2 }}'], 'tokens'),
templates: {
@@ -295,6 +297,7 @@
@foreach (['clients', 'contacts', 'invoices', 'quotes', 'navigation'] as $type)
,{
name: 'data',
+ limit: 3,
display: 'value',
source: searchData(data['{{ $type }}'], 'tokens', true),
templates: {
@@ -448,7 +451,7 @@
'user_id' => $item->user_id,
'account_name' => $item->account_name,
'user_name' => $item->user_name,
- 'logo_path' => isset($item->logo_path) ? $item->logo_path : "",
+ 'logo_url' => isset($item->logo_url) ? $item->logo_url : "",
'selected' => true,
])
@endif
@@ -460,7 +463,7 @@
'user_id' => $item->user_id,
'account_name' => $item->account_name,
'user_name' => $item->user_name,
- 'logo_path' => isset($item->logo_path) ? $item->logo_path : "",
+ 'logo_url' => isset($item->logo_url) ? $item->logo_url : "",
'selected' => false,
])
@endif
@@ -469,7 +472,7 @@
@include('user_account', [
'account_name' => Auth::user()->account->name ?: trans('texts.untitled'),
'user_name' => Auth::user()->getDisplayName(),
- 'logo_path' => Auth::user()->account->getLogoPath(),
+ 'logo_url' => Auth::user()->account->getLogoURL(),
'selected' => true,
])
@endif
diff --git a/resources/views/invited/dashboard.blade.php b/resources/views/invited/dashboard.blade.php
index a5b698c46e2e..4af2cc53c9d3 100644
--- a/resources/views/invited/dashboard.blade.php
+++ b/resources/views/invited/dashboard.blade.php
@@ -95,7 +95,7 @@
@if ($account->hasLogo())
- {!! HTML::image($account->getLogoPath()) !!}
+ {!! HTML::image($account->getLogoURL()) !!}
@else
{{ $account->name}}
@endif
diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php
index a12ab9e1f996..5c70a5be1d17 100644
--- a/resources/views/invoices/edit.blade.php
+++ b/resources/views/invoices/edit.blade.php
@@ -17,6 +17,17 @@
label.control-label[for=invoice_number] {
font-weight: normal !important;
}
+
+ select.tax-select {
+ width: 50%;
+ float: left;
+ }
+
+ #scrollable-dropdown-menu .tt-menu {
+ max-height: 150px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
@stop
@@ -38,7 +49,7 @@
{!! Former::open($url)
->method($method)
- ->addClass('warn-on-exit')
+ ->addClass('warn-on-exit main-form')
->autocomplete('off')
->onsubmit('return onFormSubmit(event)')
->rules(array(
@@ -53,7 +64,7 @@
-
+
@@ -105,7 +116,7 @@
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}">
+ style: {color: $data.info_color}">
@endif
@@ -204,7 +215,7 @@
@endif
{{ $invoiceLabels['unit_cost'] }}
{{ $invoiceLabels['quantity'] }}
-
{{ trans('texts.tax') }}
+
{{ trans('texts.tax') }}
{{ trans('texts.line_total') }}
@@ -216,7 +227,9 @@
$parent.invoice_items().length > 1" class="fa fa-sort">
-
+
-
-
-
+ {!! Former::select('')
+ ->addOption('', '')
+ ->options($taxRateOptions)
+ ->data_bind('value: tax1')
+ ->addClass('tax-select')
+ ->raw() !!}
+
+
+ {!! Former::select('')
+ ->addOption('', '')
+ ->options($taxRateOptions)
+ ->data_bind('value: tax2')
+ ->addClass('tax-select')
+ ->raw() !!}
+
+
@@ -268,18 +294,21 @@
{!! Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'")
- ->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!}
+ ->label(null)->style('resize: none; width: 500px;')->rows(4) !!}
{!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'")
- ->label(false)->style('resize: none; min-width: 450px')->rows(3)
+ ->label(false)->style('resize: none; width: 500px')->rows(4)
->help('
'.trans('texts.save_as_default_terms').'
@@ -291,7 +320,7 @@
') !!}
+ @if ($account->isPro())
+
+
+
+ @if ($invoice->hasExpenseDocuments())
+
{{trans('texts.documents_from_expenses')}}
+ @foreach($invoice->expenses as $expense)
+ @foreach($expense->documents as $document)
+
{{$document->name}}
+ @endforeach
+ @endforeach
+ @endif
+
+
+ @endif
@@ -351,9 +406,23 @@
{{ trans('texts.tax') }}
@endif
-
-
-
+ {!! Former::select('')
+ ->id('taxRateSelect1')
+ ->addOption('', '')
+ ->options($taxRateOptions)
+ ->addClass('tax-select')
+ ->data_bind('value: tax1')
+ ->raw() !!}
+
+
+ {!! Former::select('')
+ ->addOption('', '')
+ ->options($taxRateOptions)
+ ->addClass('tax-select')
+ ->data_bind('value: tax2')
+ ->raw() !!}
+
+
@@ -675,17 +744,18 @@
@include('invoices.knockout')
+ @if ($account->isPro() && $account->invoice_embed_documents)
+ @foreach ($invoice->documents as $document)
+ @if($document->isPDFEmbeddable())
+
+ @endif
+ @endforeach
+ @foreach ($invoice->expenses as $expense)
+ @foreach ($expense->documents as $document)
+ @if($document->isPDFEmbeddable())
+
+ @endif
+ @endforeach
+ @endforeach
+ @endif
@stop
diff --git a/resources/views/invoices/history.blade.php b/resources/views/invoices/history.blade.php
index 3a9fd36e333d..baa5c925aadb 100644
--- a/resources/views/invoices/history.blade.php
+++ b/resources/views/invoices/history.blade.php
@@ -56,5 +56,19 @@
@include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800])
-
+
+ @if (Utils::isPro() && $invoice->account->invoice_embed_documents)
+ @foreach ($invoice->documents as $document)
+ @if($document->isPDFEmbeddable())
+
+ @endif
+ @endforeach
+ @foreach ($invoice->expenses as $expense)
+ @foreach ($expense->documents as $document)
+ @if($document->isPDFEmbeddable())
+
+ @endif
+ @endforeach
+ @endforeach
+ @endif
@stop
\ No newline at end of file
diff --git a/resources/views/invoices/knockout.blade.php b/resources/views/invoices/knockout.blade.php
index 029a81bc0b67..8fe4bcbcce80 100644
--- a/resources/views/invoices/knockout.blade.php
+++ b/resources/views/invoices/knockout.blade.php
@@ -7,8 +7,6 @@ function ViewModel(data) {
//self.invoice = data ? false : new InvoiceModel();
self.invoice = ko.observable(data ? false : new InvoiceModel());
self.expense_currency_id = ko.observable();
- self.tax_rates = ko.observableArray();
- self.tax_rates.push(new TaxRateModel()); // add blank row
self.products = {!! $products !!};
self.loadClient = function(client) {
@@ -47,11 +45,6 @@ function ViewModel(data) {
return new InvoiceModel(options.data);
}
},
- 'tax_rates': {
- create: function(options) {
- return new TaxRateModel(options.data);
- }
- },
}
if (data) {
@@ -59,13 +52,11 @@ function ViewModel(data) {
}
self.invoice_taxes.show = ko.computed(function() {
- if (self.invoice_taxes() && self.tax_rates().length > 1) {
+ if (self.invoice().tax_name1() || self.invoice().tax_name2()) {
return true;
}
- if (self.invoice().tax_rate() > 0) {
- return true;
- }
- return false;
+
+ return self.invoice_taxes() && {{ count($taxRateOptions) ? 'true' : 'false' }};
});
self.invoice_item_taxes.show = ko.computed(function() {
@@ -74,47 +65,13 @@ function ViewModel(data) {
}
for (var i=0; i
0) {
+ if (item.tax_name1() || item.tax_name2()) {
return true;
}
}
return false;
});
- self.addTaxRate = function(data) {
- var itemModel = new TaxRateModel(data);
- self.tax_rates.push(itemModel);
- applyComboboxListeners();
- }
-
- self.getTaxRateById = function(id) {
- for (var i=0; i 0) {
- // var tax = roundToTwo(total * (taxRate/100));
- // return self.formatMoney(tax);
- //} else {
- // return self.formatMoney(0);
- //}
- var tax = roundToTwo(total * (taxRate/100));
- return self.formatMoney(tax);
+ var taxRate1 = parseFloat(self.tax_rate1());
+ var tax1 = roundToTwo(total * (taxRate1/100));
+
+ var taxRate2 = parseFloat(self.tax_rate2());
+ var tax2 = roundToTwo(total * (taxRate2/100));
+
+ return self.formatMoney(tax1 + tax2);
});
self.totals.itemTaxes = ko.computed(function() {
@@ -413,13 +402,24 @@ function InvoiceModel(data) {
lineTotal -= roundToTwo(lineTotal * (self.discount()/100));
}
}
- var taxAmount = roundToTwo(lineTotal * item.tax_rate() / 100);
+
+ var taxAmount = roundToTwo(lineTotal * item.tax_rate1() / 100);
if (taxAmount) {
- var key = item.tax_name() + item.tax_rate();
+ var key = item.tax_name1() + item.tax_rate1();
if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount;
} else {
- taxes[key] = {name:item.tax_name(), rate:item.tax_rate(), amount:taxAmount};
+ taxes[key] = {name:item.tax_name1(), rate:item.tax_rate1(), amount:taxAmount};
+ }
+ }
+
+ var taxAmount = roundToTwo(lineTotal * item.tax_rate2() / 100);
+ if (taxAmount) {
+ var key = item.tax_name2() + item.tax_rate2();
+ if (taxes.hasOwnProperty(key)) {
+ taxes[key].amount += taxAmount;
+ } else {
+ taxes[key] = {name:item.tax_name2(), rate:item.tax_rate2(), amount:taxAmount};
}
}
}
@@ -485,8 +485,9 @@ function InvoiceModel(data) {
total = NINJA.parseFloat(total) + customValue2;
}
- var taxRate = parseFloat(self.tax_rate());
- total = NINJA.parseFloat(total) + roundToTwo(total * (taxRate/100));
+ var taxAmount1 = roundToTwo(total * (parseFloat(self.tax_rate1())/100));
+ var taxAmount2 = roundToTwo(total * (parseFloat(self.tax_rate2())/100));
+ total = NINJA.parseFloat(total) + taxAmount1 + taxAmount2;
total = roundToTwo(total);
var taxes = self.totals.itemTaxes();
@@ -620,6 +621,7 @@ function ContactModel(data) {
self.send_invoice = ko.observable(false);
self.invitation_link = ko.observable('');
self.invitation_status = ko.observable('');
+ self.invitation_openend = ko.observable(false);
self.invitation_viewed = ko.observable(false);
self.email_error = ko.observable('');
@@ -661,55 +663,16 @@ function ContactModel(data) {
return str;
});
-}
-
-function TaxRateModel(data) {
- var self = this;
- self.public_id = ko.observable('');
- self.rate = ko.observable(0);
- self.name = ko.observable('');
- self.is_deleted = ko.observable(false);
- self.is_blank = ko.observable(false);
- self.actionsVisible = ko.observable(false);
-
- if (data) {
- ko.mapping.fromJS(data, {}, this);
- }
-
- this.prettyRate = ko.computed({
- read: function () {
- return this.rate() ? roundToTwo(this.rate()) : '';
- },
- write: function (value) {
- this.rate(value);
- },
- owner: this
+
+ self.info_color = ko.computed(function() {
+ if (self.invitation_viewed()) {
+ return '#57D172';
+ } else if (self.invitation_openend()) {
+ return '#FFCC00';
+ } else {
+ return '#B1B5BA';
+ }
});
-
-
- self.displayName = ko.computed({
- read: function () {
- var name = self.name() ? self.name() : '';
- var rate = self.rate() ? parseFloat(self.rate()) + '%' : '';
- return name + ' ' + rate;
- },
- write: function (value) {
- // do nothing
- },
- owner: this
- });
-
- self.hideActions = function() {
- self.actionsVisible(false);
- }
-
- self.showActions = function() {
- self.actionsVisible(true);
- }
-
- self.isEmpty = function() {
- return !self.rate() && !self.name();
- }
}
function ItemModel(data) {
@@ -720,21 +683,35 @@ function ItemModel(data) {
self.qty = ko.observable(0);
self.custom_value1 = ko.observable('');
self.custom_value2 = ko.observable('');
- self.tax_name = ko.observable('');
- self.tax_rate = ko.observable(0);
+ self.tax_name1 = ko.observable('');
+ self.tax_rate1 = ko.observable(0);
+ self.tax_name2 = ko.observable('');
+ self.tax_rate2 = ko.observable(0);
self.task_public_id = ko.observable('');
self.expense_public_id = ko.observable('');
self.actionsVisible = ko.observable(false);
- self._tax = ko.observable();
- this.tax = ko.computed({
+ this.tax1 = ko.computed({
read: function () {
- return self._tax();
+ return self.tax_rate1() + ' ' + self.tax_name1();
},
write: function(value) {
- self._tax(value);
- self.tax_name(value.name());
- self.tax_rate(value.rate());
+ var rate = value.substr(0, value.indexOf(' '));
+ var name = value.substr(value.indexOf(' ') + 1);
+ self.tax_name1(name);
+ self.tax_rate1(rate);
+ }
+ })
+
+ this.tax2 = ko.computed({
+ read: function () {
+ return self.tax_rate2() + ' ' + self.tax_name2();
+ },
+ write: function(value) {
+ var rate = value.substr(0, value.indexOf(' '));
+ var name = value.substr(value.indexOf(' ') + 1);
+ self.tax_name2(name);
+ self.tax_rate2(rate);
}
})
@@ -758,16 +735,8 @@ function ItemModel(data) {
owner: this
});
- self.mapping = {
- 'tax': {
- create: function(options) {
- return new TaxRateModel(options.data);
- }
- }
- }
-
if (data) {
- ko.mapping.fromJS(data, self.mapping, this);
+ ko.mapping.fromJS(data, {}, this);
}
self.wrapped_notes = ko.computed({
@@ -810,6 +779,45 @@ function ItemModel(data) {
this.onSelect = function() {}
}
+
+function DocumentModel(data) {
+ var self = this;
+ self.public_id = ko.observable(0);
+ self.size = ko.observable(0);
+ self.name = ko.observable('');
+ self.type = ko.observable('');
+ self.url = ko.observable('');
+
+ self.update = function(data){
+ ko.mapping.fromJS(data, {}, this);
+ }
+
+ if (data) {
+ self.update(data);
+ }
+}
+
+var ExpenseModel = function(data) {
+ var self = this;
+
+ self.mapping = {
+ 'documents': {
+ create: function(options) {
+ return new DocumentModel(options.data);
+ }
+ }
+ }
+
+ self.description = ko.observable('');
+ self.qty = ko.observable(0);
+ self.public_id = ko.observable(0);
+ self.amount = ko.observable();
+ self.converted_amount = ko.observable();
+
+ if (data) {
+ ko.mapping.fromJS(data, self.mapping, this);
+ }
+};
/* Custom binding for product key typeahead */
ko.bindingHandlers.typeahead = {
@@ -824,6 +832,7 @@ ko.bindingHandlers.typeahead = {
{
name: 'data',
display: allBindings.key,
+ limit: 50,
source: searchData(allBindings.items, allBindings.key)
}).on('typeahead:select', function(element, datum, name) {
@if (Auth::user()->account->fill_products)
@@ -842,7 +851,9 @@ ko.bindingHandlers.typeahead = {
}
@if ($account->invoice_item_taxes)
if (datum.default_tax_rate) {
- model.tax(self.model.getTaxRateById(datum.default_tax_rate.public_id));
+ model.tax_rate1(datum.default_tax_rate.rate);
+ model.tax_name1(datum.default_tax_rate.name);
+ model.tax1(datum.default_tax_rate.rate + ' ' + datum.default_tax_rate.name);
}
@endif
@endif
diff --git a/resources/views/invoices/pdf.blade.php b/resources/views/invoices/pdf.blade.php
index d2e8648b53f7..1c8389affeba 100644
--- a/resources/views/invoices/pdf.blade.php
+++ b/resources/views/invoices/pdf.blade.php
@@ -75,7 +75,7 @@
logoImages.imageLogoHeight3 = 81/2;
@if ($account->hasLogo())
- window.accountLogo = "{{ Form::image_data($account->getLogoPath()) }}";
+ window.accountLogo = "{{ Form::image_data($account->getLogoRaw(), true) }}";
if (window.invoice) {
invoice.image = window.accountLogo;
invoice.imageWidth = {{ $account->getLogoWidth() }};
@@ -123,7 +123,7 @@
$('#theFrame').attr('src', string).show();
} else {
if (isRefreshing) {
- //needsRefresh = true;
+ needsRefresh = true;
return;
}
isRefreshing = true;
diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php
index c1ba3822899a..08f5cd9827cd 100644
--- a/resources/views/invoices/view.blade.php
+++ b/resources/views/invoices/view.blade.php
@@ -24,7 +24,6 @@
@if ($checkoutComToken)
@include('partials.checkout_com_payment')
@else
-
@if ($invoice->is_quote)
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
@@ -39,13 +38,47 @@
{{ trans('texts.pay_now') }}
@endif
@else
- {!! Button::normal('Download PDF')->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
+ {!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
@endif
+
+ @if(!empty($documentsZipURL))
+ {!! Button::normal(trans('texts.download_documents', array('size'=>Form::human_filesize($documentsZipSize))))->asLinkTo($documentsZipURL)->large() !!}
+ @endif
+
@endif
-
+ @if ($account->isPro() && $invoice->hasDocuments())
+
+
{{ trans('texts.documents_header') }}
+
+
+ @endif
+
+ @if ($account->isPro() && $account->invoice_embed_documents)
+ @foreach ($invoice->documents as $document)
+ @if($document->isPDFEmbeddable())
+
+ @endif
+ @endforeach
+ @foreach ($invoice->expenses as $expense)
+ @foreach ($expense->documents as $document)
+ @if($document->isPDFEmbeddable())
+
+ @endif
+ @endforeach
+ @endforeach
+ @endif
+
@endif
@stop
@@ -174,22 +174,8 @@
window.location = '{{ URL::to('expenses/create/' . $vendor->public_id ) }}';
});
- // load datatable data when tab is shown and remember last tab selected
- $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
- var target = $(e.target).attr("href") // activated tab
- target = target.substring(1);
- localStorage.setItem('vendor_tab', target);
- if (!loadedTabs.hasOwnProperty(target)) {
- loadedTabs[target] = true;
- window['load_' + target]();
- }
- });
- var tab = localStorage.getItem('vendor_tab');
- if (tab && tab != 'activity') {
- $('.nav-tabs a[href="#' + tab.replace('#', '') + '"]').tab('show');
- } else {
- //window['load_activity']();
- }
+ $('.nav-tabs a[href="#expenses"]').tab('show');
+ load_expenses();
});
function onArchiveClick() {
diff --git a/storage/templates/bold.js b/storage/templates/bold.js
old mode 100644
new mode 100755
index b877f5f4b86c..69e9d55ddfab
--- a/storage/templates/bold.js
+++ b/storage/templates/bold.js
@@ -50,7 +50,7 @@
"paddingTop": "$amount:14",
"paddingBottom": "$amount:14"
}
- },
+ },
{
"columns": [
{
@@ -72,7 +72,13 @@
"paddingBottom": "$amount:4"
}
}]
- }
+ },
+ {
+ "stack": [
+ "$invoiceDocuments"
+ ],
+ "style": "invoiceDocuments"
+ }
],
"footer":
[
@@ -254,6 +260,12 @@
"help": {
"fontSize": "$fontSizeSmaller",
"color": "#737373"
+ },
+ "invoiceDocuments": {
+ "margin": [47, 0, 47, 0]
+ },
+ "invoiceDocument": {
+ "margin": [0, 10, 0, 10]
}
},
"pageMargins": [0, 80, 0, 40]
diff --git a/storage/templates/clean.js b/storage/templates/clean.js
old mode 100644
new mode 100755
index e367a5893217..a154bb0f95dc
--- a/storage/templates/clean.js
+++ b/storage/templates/clean.js
@@ -87,6 +87,12 @@
}
}
]
+ },
+ {
+ "stack": [
+ "$invoiceDocuments"
+ ],
+ "style": "invoiceDocuments"
}
],
"defaultStyle": {
@@ -200,6 +206,12 @@
"help": {
"fontSize": "$fontSizeSmaller",
"color": "#737373"
+ },
+ "invoiceDocuments": {
+ "margin": [7, 0, 7, 0]
+ },
+ "invoiceDocument": {
+ "margin": [0, 10, 0, 10]
}
},
"pageMargins": [40, 40, 40, 60]
diff --git a/storage/templates/modern.js b/storage/templates/modern.js
old mode 100644
new mode 100755
index 5c1767bb4bab..a4043413a2eb
--- a/storage/templates/modern.js
+++ b/storage/templates/modern.js
@@ -85,166 +85,179 @@
"margin": [0, 16, 8, 0]
}
]
- } ],
- "footer": [
- {
- "canvas": [
- {
- "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 100,"lineColor":"$primaryColor:#f26621"
- }]
- ,"width":10
- },
- {
- "columns": [
- {
- "width": 350,
- "stack": [
- {
- "text": "$invoiceFooter",
- "margin": [40, -40, 40, 0],
- "alignment": "left",
- "color": "#FFFFFF"
-
- }
- ]
- },
- {
- "stack": "$accountDetails",
- "margin": [0, -40, 0, 0],
- "width": "*"
- },
- {
- "stack": "$accountAddress",
- "margin": [0, -40, 0, 0],
- "width": "*"
- }
- ]
- }
- ],
- "header": [
- {
- "canvas": [{ "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 200,"lineColor":"$primaryColor:#f26621"}],"width":10
},
{
- "columns": [
+ "stack": [
+ "$invoiceDocuments"
+ ],
+ "style": "invoiceDocuments"
+ }
+ ],
+ "footer": [
+ {
+ "canvas": [
+ {
+ "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 100,"lineColor":"$primaryColor:#f26621"
+ }]
+ ,"width":10
+ },
+ {
+ "columns": [
+ {
+ "width": 350,
+ "stack": [
{
- "text": "$accountName", "bold": true,"font":"$headerFont","fontSize":30,"color":"#ffffff","margin":[40,20,0,0],"width":350
+ "text": "$invoiceFooter",
+ "margin": [40, -40, 40, 0],
+ "alignment": "left",
+ "color": "#FFFFFF"
+
}
]
},
{
- "width": 300,
- "table": {
- "body": "$invoiceDetails"
- },
- "layout": "noBorders",
- "margin": [400, -40, 0, 0]
+ "stack": "$accountDetails",
+ "margin": [0, -40, 0, 0],
+ "width": "*"
+ },
+ {
+ "stack": "$accountAddress",
+ "margin": [0, -40, 0, 0],
+ "width": "*"
}
- ],
- "defaultStyle": {
- "font": "$bodyFont",
- "fontSize": "$fontSize",
- "margin": [8, 4, 8, 4]
+ ]
+ }
+ ],
+ "header": [
+ {
+ "canvas": [{ "type": "line", "x1": 0, "y1": 0, "x2": 600, "y2": 0,"lineWidth": 200,"lineColor":"$primaryColor:#f26621"}],"width":10
+ },
+ {
+ "columns": [
+ {
+ "text": "$accountName", "bold": true,"font":"$headerFont","fontSize":30,"color":"#ffffff","margin":[40,20,0,0],"width":350
+ }
+ ]
+ },
+ {
+ "width": 300,
+ "table": {
+ "body": "$invoiceDetails"
+ },
+ "layout": "noBorders",
+ "margin": [400, -40, 0, 0]
+ }
+ ],
+ "defaultStyle": {
+ "font": "$bodyFont",
+ "fontSize": "$fontSize",
+ "margin": [8, 4, 8, 4]
+ },
+ "styles": {
+ "primaryColor":{
+ "color": "$primaryColor:#299CC2"
},
- "styles": {
- "primaryColor":{
- "color": "$primaryColor:#299CC2"
- },
- "accountName": {
- "margin": [4, 2, 4, 2],
- "color": "$primaryColor:#299CC2"
- },
- "accountDetails": {
- "margin": [4, 2, 4, 2],
- "color": "#FFFFFF"
- },
- "accountAddress": {
- "margin": [4, 2, 4, 2],
- "color": "#FFFFFF"
- },
- "clientDetails": {
- "margin": [0, 2, 4, 2]
- },
- "invoiceDetails": {
- "color": "#FFFFFF"
- },
- "invoiceLineItemsTable": {
- "margin": [0, 0, 0, 16]
- },
- "productKey": {
- "bold": true
- },
- "clientName": {
- "bold": true
- },
- "tableHeader": {
- "bold": true,
- "color": "#FFFFFF",
- "fontSize": "$fontSizeLargest",
- "fillColor": "$secondaryColor:#403d3d"
- },
- "costTableHeader": {
- "alignment": "right"
- },
- "qtyTableHeader": {
- "alignment": "right"
- },
- "taxTableHeader": {
- "alignment": "right"
- },
- "lineTotalTableHeader": {
- "alignment": "right"
- },
- "balanceDueLabel": {
- "fontSize": "$fontSizeLargest",
- "color":"#FFFFFF",
- "alignment":"right",
- "bold": true
- },
- "balanceDue": {
- "fontSize": "$fontSizeLargest",
- "color":"#FFFFFF",
- "bold": true,
- "alignment":"right"
- },
- "cost": {
- "alignment": "right"
- },
- "quantity": {
- "alignment": "right"
- },
- "tax": {
- "alignment": "right"
- },
- "lineTotal": {
- "alignment": "right"
- },
- "subtotals": {
- "alignment": "right"
- },
- "termsLabel": {
- "bold": true,
- "margin": [0, 0, 0, 4]
- },
- "invoiceNumberLabel": {
- "bold": true
- },
- "invoiceNumber": {
- "bold": true
- },
- "header": {
- "font": "$headerFont",
- "fontSize": "$fontSizeLargest",
- "bold": true
- },
- "subheader": {
- "font": "$headerFont",
- "fontSize": "$fontSizeLarger"
- },
- "help": {
- "fontSize": "$fontSizeSmaller",
- "color": "#737373"
- }
+ "accountName": {
+ "margin": [4, 2, 4, 2],
+ "color": "$primaryColor:#299CC2"
},
- "pageMargins": [40, 120, 40, 50]
- }
\ No newline at end of file
+ "accountDetails": {
+ "margin": [4, 2, 4, 2],
+ "color": "#FFFFFF"
+ },
+ "accountAddress": {
+ "margin": [4, 2, 4, 2],
+ "color": "#FFFFFF"
+ },
+ "clientDetails": {
+ "margin": [0, 2, 4, 2]
+ },
+ "invoiceDetails": {
+ "color": "#FFFFFF"
+ },
+ "invoiceLineItemsTable": {
+ "margin": [0, 0, 0, 16]
+ },
+ "productKey": {
+ "bold": true
+ },
+ "clientName": {
+ "bold": true
+ },
+ "tableHeader": {
+ "bold": true,
+ "color": "#FFFFFF",
+ "fontSize": "$fontSizeLargest",
+ "fillColor": "$secondaryColor:#403d3d"
+ },
+ "costTableHeader": {
+ "alignment": "right"
+ },
+ "qtyTableHeader": {
+ "alignment": "right"
+ },
+ "taxTableHeader": {
+ "alignment": "right"
+ },
+ "lineTotalTableHeader": {
+ "alignment": "right"
+ },
+ "balanceDueLabel": {
+ "fontSize": "$fontSizeLargest",
+ "color":"#FFFFFF",
+ "alignment":"right",
+ "bold": true
+ },
+ "balanceDue": {
+ "fontSize": "$fontSizeLargest",
+ "color":"#FFFFFF",
+ "bold": true,
+ "alignment":"right"
+ },
+ "cost": {
+ "alignment": "right"
+ },
+ "quantity": {
+ "alignment": "right"
+ },
+ "tax": {
+ "alignment": "right"
+ },
+ "lineTotal": {
+ "alignment": "right"
+ },
+ "subtotals": {
+ "alignment": "right"
+ },
+ "termsLabel": {
+ "bold": true,
+ "margin": [0, 0, 0, 4]
+ },
+ "invoiceNumberLabel": {
+ "bold": true
+ },
+ "invoiceNumber": {
+ "bold": true
+ },
+ "header": {
+ "font": "$headerFont",
+ "fontSize": "$fontSizeLargest",
+ "bold": true
+ },
+ "subheader": {
+ "font": "$headerFont",
+ "fontSize": "$fontSizeLarger"
+ },
+ "help": {
+ "fontSize": "$fontSizeSmaller",
+ "color": "#737373"
+ },
+ "invoiceDocuments": {
+ "margin": [7, 0, 7, 0]
+ },
+ "invoiceDocument": {
+ "margin": [0, 10, 0, 10]
+ }
+ },
+ "pageMargins": [40, 120, 40, 50]
+}
\ No newline at end of file
diff --git a/storage/templates/plain.js b/storage/templates/plain.js
old mode 100644
new mode 100755
index b260a6727e75..916ace88189f
--- a/storage/templates/plain.js
+++ b/storage/templates/plain.js
@@ -56,7 +56,7 @@
"paddingTop": "$amount:8",
"paddingBottom": "$amount:8"
}
- },
+ },
{
"columns": [
"$notesAndTerms",
@@ -77,6 +77,12 @@
}
}
]
+ },
+ {
+ "stack": [
+ "$invoiceDocuments"
+ ],
+ "style": "invoiceDocuments"
}
],
"footer": {
@@ -166,7 +172,13 @@
"help": {
"fontSize": "$fontSizeSmaller",
"color": "#737373"
- }
+ },
+ "invoiceDocuments": {
+ "margin": [7, 0, 7, 0]
+ },
+ "invoiceDocument": {
+ "margin": [0, 10, 0, 10]
+ }
},
"pageMargins": [40, 40, 40, 60]
}
\ No newline at end of file
diff --git a/tests/_support/_generated/AcceptanceTesterActions.php b/tests/_support/_generated/AcceptanceTesterActions.php
index cebe7fff8f20..82427d1e2a5e 100644
--- a/tests/_support/_generated/AcceptanceTesterActions.php
+++ b/tests/_support/_generated/AcceptanceTesterActions.php
@@ -1,4 +1,4 @@
-scrollTo(['css' => '.checkout'], 20, 50);
+ * ?>
+ * ```
+ *
+ * @param $selector
+ * @param int $offsetX
+ * @param int $offsetY
+ * @see \Codeception\Module\WebDriver::scrollTo()
+ */
+ public function scrollTo($selector, $offsetX = null, $offsetY = null) {
+ return $this->getScenario()->runStep(new \Codeception\Step\Action('scrollTo', func_get_args()));
+ }
+
+
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
diff --git a/tests/acceptance/TaxRatesCest.php b/tests/acceptance/TaxRatesCest.php
index 6320482c9e62..75f7de7a7508 100644
--- a/tests/acceptance/TaxRatesCest.php
+++ b/tests/acceptance/TaxRatesCest.php
@@ -72,13 +72,13 @@ class TaxRatesCest
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
$I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable');
- $I->selectOption('#taxRateSelect', $invoiceTaxName . ' ' . floatval($invoiceTaxRate) . '%');
- $I->wait(2);
+ $I->selectOption('#taxRateSelect1', $invoiceTaxName . ' ' . floatval($invoiceTaxRate) . '%');
+ $I->wait(3);
// check total is right before saving
$I->see("\${$total}");
$I->click('Save');
- $I->wait(1);
+ $I->wait(2);
$I->see($clientEmail);
// check total is right after saving