diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php
index b0c81ad8f265..ee5dfed09405 100644
--- a/app/Console/Commands/CheckData.php
+++ b/app/Console/Commands/CheckData.php
@@ -64,7 +64,7 @@ class CheckData extends Command
public function fire()
{
- $this->logMessage(date('Y-m-d') . ' Running CheckData...');
+ $this->logMessage(date('Y-m-d h:i:s') . ' Running CheckData...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
@@ -84,9 +84,9 @@ class CheckData extends Command
if (! $this->option('client_id')) {
$this->checkOAuth();
$this->checkInvitations();
- $this->checkFailedJobs();
$this->checkAccountData();
$this->checkLookupData();
+ $this->checkFailedJobs();
}
$this->logMessage('Done: ' . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
@@ -143,6 +143,11 @@ class CheckData extends Command
return;
}
+ if ($this->option('fix') == 'true') {
+ return;
+ }
+
+ $isValid = true;
$date = new Carbon();
$date = $date->subDays(1)->format('Y-m-d');
@@ -159,12 +164,12 @@ class CheckData extends Command
//$this->logMessage('Result: ' . $result);
if ($result && $result != $invoice->balance) {
- $this->logMessage("Amounts do not match {$link} - PHP: {$invoice->balance}, JS: {$result}");
- $this->isValid = false;
+ $this->logMessage("PHP/JS amounts do not match {$link} - PHP: {$invoice->balance}, JS: {$result}");
+ $this->isValid = $isValid = false;
}
}
- if ($this->isValid) {
+ if ($isValid) {
$this->logMessage('0 invoices with mismatched PHP/JS balances');
}
}
@@ -371,6 +376,13 @@ class CheckData extends Command
private function checkFailedJobs()
{
+ if (Utils::isTravis()) {
+ return;
+ }
+
+ $current = config('database.default');
+ config(['database.default' => env('QUEUE_DATABASE')]);
+
$count = DB::table('failed_jobs')->count();
if ($count > 0) {
@@ -378,6 +390,8 @@ class CheckData extends Command
}
$this->logMessage($count . ' failed jobs');
+
+ config(['database.default' => $current]);
}
private function checkBlankInvoiceHistory()
diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php
index cb76a83ef9f0..b95ff4d90df8 100644
--- a/app/Console/Commands/SendReminders.php
+++ b/app/Console/Commands/SendReminders.php
@@ -63,8 +63,30 @@ class SendReminders extends Command
config(['database.default' => $database]);
}
+ $accounts = $this->accountRepo->findWithFees();
+ $this->info(count($accounts) . ' accounts found with fees');
+
+ foreach ($accounts as $account) {
+ if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
+ continue;
+ }
+
+ $invoices = $this->invoiceRepo->findNeedingReminding($account, false);
+ $this->info($account->name . ': ' . count($invoices) . ' invoices found');
+
+ foreach ($invoices as $invoice) {
+ if ($reminder = $account->getInvoiceReminder($invoice, false)) {
+ $this->info('Charge fee: ' . $invoice->id);
+ $number = preg_replace('/[^0-9]/', '', $reminder);
+ $amount = $account->account_email_settings->{"late_fee{$number}_amount"};
+ $percent = $account->account_email_settings->{"late_fee{$number}_percent"};
+ $this->invoiceRepo->setLateFee($invoice, $amount, $percent);
+ }
+ }
+ }
+
$accounts = $this->accountRepo->findWithReminders();
- $this->info(count($accounts) . ' accounts found');
+ $this->info(count($accounts) . ' accounts found with reminders');
/** @var \App\Models\Account $account */
foreach ($accounts as $account) {
@@ -78,7 +100,7 @@ class SendReminders extends Command
/** @var Invoice $invoice */
foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice)) {
- $this->info('Send to ' . $invoice->id);
+ $this->info('Send email: ' . $invoice->id);
$this->mailer->sendInvoice($invoice, $reminder);
}
}
diff --git a/app/Console/Commands/UpdateKey.php b/app/Console/Commands/UpdateKey.php
new file mode 100644
index 000000000000..d2c1b2f2098c
--- /dev/null
+++ b/app/Console/Commands/UpdateKey.php
@@ -0,0 +1,80 @@
+info(date('Y-m-d h:i:s') . ' Running UpdateKey...');
+
+ // load the current values
+ $gatewayConfigs = [];
+ $bankUsernames = [];
+
+ foreach (AccountGateway::all() as $gateway) {
+ $gatewayConfigs[$gateway->id] = $gateway->getConfig();
+ }
+
+ foreach (BankAccount::all() as $bank) {
+ $bankUsernames[$bank->id] = $bank->getUsername();
+ }
+
+ // set the new key and create a new encrypter
+ Artisan::call('key:generate');
+ $key = base64_decode(str_replace('base64:', '', config('app.key')));
+ $crypt = new Encrypter($key, config('app.cipher'));
+
+ // update values using the new key/encrypter
+ foreach (AccountGateway::all() as $gateway) {
+ $config = $gatewayConfigs[$gateway->id];
+ $gateway->config = $crypt->encrypt(json_encode($config));
+ $gateway->save();
+ }
+
+ foreach (BankAccount::all() as $bank) {
+ $username = $bankUsernames[$bank->id];
+ $bank->username = $crypt->encrypt($username);
+ $bank->save();
+ }
+
+ $this->info(date('Y-m-d h:i:s') . ' Successfully updated the application key');
+ }
+
+ /**
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return [];
+ }
+
+ /**
+ * @return array
+ */
+ protected function getOptions()
+ {
+ return [];
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 46ab045be1c1..f43e8fafaa3e 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -29,6 +29,7 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\MakeClass',
'App\Console\Commands\InitLookup',
'App\Console\Commands\CalculatePayouts',
+ 'App\Console\Commands\UpdateKey',
];
/**
diff --git a/app/Constants.php b/app/Constants.php
index 177f28d9f566..650aee0b1373 100644
--- a/app/Constants.php
+++ b/app/Constants.php
@@ -46,6 +46,7 @@ if (! defined('APP_NAME')) {
define('INVOICE_ITEM_TYPE_TASK', 2);
define('INVOICE_ITEM_TYPE_PENDING_GATEWAY_FEE', 3);
define('INVOICE_ITEM_TYPE_PAID_GATEWAY_FEE', 4);
+ define('INVOICE_ITEM_TYPE_LATE_FEE', 5);
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
@@ -307,7 +308,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01');
- define('NINJA_VERSION', '3.5.1' . env('NINJA_VERSION_SUFFIX'));
+ define('NINJA_VERSION', '3.6.0' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@@ -569,6 +570,9 @@ if (! defined('APP_NAME')) {
];
define('CACHED_TABLES', serialize($cachedTables));
+ // Fix for mPDF: https://github.com/kartik-v/yii2-mpdf/issues/9
+ define('_MPDF_TTFONTDATAPATH', storage_path('framework/cache/'));
+
// TODO remove these translation functions
function uctrans($text)
{
diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php
index dce0cbec061c..b76051abe1c8 100644
--- a/app/Http/Controllers/AccountController.php
+++ b/app/Http/Controllers/AccountController.php
@@ -423,7 +423,6 @@ class AccountController extends BaseController
'timezones' => Cache::get('timezones'),
'dateFormats' => Cache::get('dateFormats'),
'datetimeFormats' => Cache::get('datetimeFormats'),
- 'currencies' => Cache::get('currencies'),
'title' => trans('texts.localization'),
'weekdays' => Utils::getTranslatedWeekdayNames(),
'months' => Utils::getMonthOptions(),
@@ -823,6 +822,10 @@ class AccountController extends BaseController
$account->{"num_days_{$type}"} = Input::get("num_days_{$type}");
$account->{"field_{$type}"} = Input::get("field_{$type}");
$account->{"direction_{$type}"} = Input::get("field_{$type}") == REMINDER_FIELD_INVOICE_DATE ? REMINDER_DIRECTION_AFTER : Input::get("direction_{$type}");
+
+ $number = preg_replace('/[^0-9]/', '', $type);
+ $account->account_email_settings->{"late_fee{$number}_amount"} = Input::get("late_fee{$number}_amount");
+ $account->account_email_settings->{"late_fee{$number}_percent"} = Input::get("late_fee{$number}_percent");
}
$account->save();
diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php
index a9e4d9ae02b9..e590bae9eba7 100644
--- a/app/Http/Controllers/ClientController.php
+++ b/app/Http/Controllers/ClientController.php
@@ -191,7 +191,6 @@ class ClientController extends BaseController
'data' => Input::old('data'),
'account' => Auth::user()->account,
'sizes' => Cache::get('sizes'),
- 'currencies' => Cache::get('currencies'),
'customLabel1' => Auth::user()->account->custom_client_label1,
'customLabel2' => Auth::user()->account->custom_client_label2,
];
@@ -225,25 +224,51 @@ class ClientController extends BaseController
return $this->returnBulk(ENTITY_CLIENT, $action, $ids);
}
- public function statement()
+ public function statement($clientPublicId, $statusId = false, $startDate = false, $endDate = false)
{
$account = Auth::user()->account;
+ $statusId = intval($statusId);
$client = Client::scope(request()->client_id)->with('contacts')->firstOrFail();
+
+ if (! $startDate) {
+ $startDate = Utils::today(false)->modify('-6 month')->format('Y-m-d');
+ $endDate = Utils::today(false)->format('Y-m-d');
+ }
+
$invoice = $account->createInvoice(ENTITY_INVOICE);
$invoice->client = $client;
$invoice->date_format = $account->date_format ? $account->date_format->format_moment : 'MMM D, YYYY';
- $invoice->invoice_items = Invoice::scope()
+
+ $invoices = Invoice::scope()
->with(['client'])
- ->whereClientId($client->id)
->invoices()
+ ->whereClientId($client->id)
->whereIsPublic(true)
- ->where('balance', '>', 0)
- ->get();
+ ->orderBy('invoice_date', 'asc');
+
+ if ($statusId == INVOICE_STATUS_PAID) {
+ $invoices->where('invoice_status_id', '=', INVOICE_STATUS_PAID);
+ } elseif ($statusId == INVOICE_STATUS_UNPAID) {
+ $invoices->where('invoice_status_id', '!=', INVOICE_STATUS_PAID);
+ }
+
+ if ($statusId == INVOICE_STATUS_PAID || ! $statusId) {
+ $invoices->where('invoice_date', '>=', $startDate)
+ ->where('invoice_date', '<=', $endDate);
+ }
+
+ $invoice->invoice_items = $invoices->get();
+
+ if (request()->json) {
+ return json_encode($invoice);
+ }
$data = [
'showBreadcrumbs' => false,
'client' => $client,
- 'invoice' => $invoice,
+ 'account' => $account,
+ 'startDate' => $startDate,
+ 'endDate' => $endDate,
];
return view('clients.statement', $data);
diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php
index bde187be3386..7548ed46e273 100644
--- a/app/Http/Controllers/ClientPortalController.php
+++ b/app/Http/Controllers/ClientPortalController.php
@@ -130,7 +130,7 @@ class ClientPortalController extends BaseController
$showApprove = $invoice->quote_invoice_id ? false : true;
if ($invoice->due_date) {
- $showApprove = time() < strtotime($invoice->due_date);
+ $showApprove = time() < strtotime($invoice->getOriginal('due_date'));
}
if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) {
$showApprove = false;
@@ -369,7 +369,7 @@ class ClientPortalController extends BaseController
'client' => $contact->client,
'title' => trans('texts.invoices'),
'entityType' => ENTITY_INVOICE,
- 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']),
+ 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date', 'status']),
];
return response()->view('public_list', $data);
@@ -431,7 +431,7 @@ class ClientPortalController extends BaseController
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 : ''.trans('texts.manual_entry').'';
+ return $model->transaction_reference ? e($model->transaction_reference) : ''.trans('texts.manual_entry').'';
})
->addColumn('payment_type', function ($model) {
return ($model->payment_type && ! $model->last4) ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : '');
@@ -497,7 +497,7 @@ class ClientPortalController extends BaseController
'account' => $account,
'title' => trans('texts.quotes'),
'entityType' => ENTITY_QUOTE,
- 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']),
+ 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date', 'status']),
];
return response()->view('public_list', $data);
@@ -584,6 +584,10 @@ class ClientPortalController extends BaseController
private function returnError($error = false)
{
+ if (request()->phantomjs) {
+ abort(404);
+ }
+
return response()->view('error', [
'error' => $error ?: trans('texts.invoice_not_found'),
'hideHeader' => true,
@@ -745,7 +749,9 @@ class ClientPortalController extends BaseController
$document = Document::scope($publicId, $invitation->account_id)->firstOrFail();
$authorized = false;
- if ($document->expense && $document->expense->invoice_documents && $document->expense->client_id == $invitation->invoice->client_id) {
+ if ($document->is_default) {
+ $authorized = true;
+ } elseif ($document->expense && $document->expense->invoice_documents && $document->expense->client_id == $invitation->invoice->client_id) {
$authorized = true;
} elseif ($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id) {
$authorized = true;
diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php
index d45a11366c2f..1940a26f3d97 100644
--- a/app/Http/Controllers/ExpenseController.php
+++ b/app/Http/Controllers/ExpenseController.php
@@ -94,11 +94,20 @@ class ExpenseController extends BaseController
return View::make('expenses.edit', $data);
}
- public function edit(ExpenseRequest $request)
+ public function clone(ExpenseRequest $request, $publicId)
+ {
+ return self::edit($request, $publicId, true);
+ }
+
+ public function edit(ExpenseRequest $request, $publicId = false, $clone = false)
{
$expense = $request->entity();
$actions = [];
+
+ if (! $clone) {
+ $actions[] = ['url' => 'javascript:submitAction("clone")', 'label' => trans("texts.clone_expense")];
+ }
if ($expense->invoice) {
$actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')];
} else {
@@ -124,12 +133,28 @@ class ExpenseController extends BaseController
$actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_expense')];
}
+ if ($clone) {
+ $expense->id = null;
+ $expense->public_id = null;
+ $expense->expense_date = date_create()->format('Y-m-d');
+ $expense->deleted_at = null;
+ $expense->invoice_id = null;
+ $expense->payment_date = null;
+ $expense->payment_type_id = null;
+ $expense->transaction_reference = null;
+ $method = 'POST';
+ $url = 'expenses';
+ } else {
+ $method = 'PUT';
+ $url = 'expenses/' . $expense->public_id;
+ }
+
$data = [
'vendor' => null,
'expense' => $expense,
'entity' => $expense,
- 'method' => 'PUT',
- 'url' => 'expenses/'.$expense->public_id,
+ 'method' => $method,
+ 'url' => $url,
'title' => 'Edit Expense',
'actions' => $actions,
'vendors' => Vendor::scope()->with('vendor_contacts')->orderBy('name')->get(),
@@ -165,7 +190,11 @@ class ExpenseController extends BaseController
return self::bulk();
}
- return redirect()->to("expenses/{$expense->public_id}/edit");
+ if ($action == 'clone') {
+ return redirect()->to(sprintf('expenses/%s/clone', $expense->public_id));
+ } else {
+ return redirect()->to("expenses/{$expense->public_id}/edit");
+ }
}
public function store(CreateExpenseRequest $request)
@@ -260,14 +289,6 @@ class ExpenseController extends BaseController
return [
'data' => Input::old('data'),
'account' => Auth::user()->account,
- 'sizes' => Cache::get('sizes'),
- 'paymentTerms' => Cache::get('paymentTerms'),
- 'industries' => Cache::get('industries'),
- 'currencies' => Cache::get('currencies'),
- 'languages' => Cache::get('languages'),
- 'countries' => Cache::get('countries'),
- 'customLabel1' => Auth::user()->account->custom_vendor_label1,
- 'customLabel2' => Auth::user()->account->custom_vendor_label2,
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(),
'isRecurring' => false,
diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php
index bef47f2386a6..046682cc7ee2 100644
--- a/app/Http/Controllers/ExportController.php
+++ b/app/Http/Controllers/ExportController.php
@@ -170,7 +170,7 @@ class ExportController extends BaseController
if ($request->input('include') === 'all' || $request->input('clients')) {
$data['clients'] = Client::scope()
- ->with('user', 'contacts', 'country')
+ ->with('user', 'contacts', 'country', 'currency')
->withArchived()
->get();
}
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 6672944bc0ec..f073b99cfae0 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -132,9 +132,11 @@ class HomeController extends BaseController
public function contactUs()
{
Mail::raw(request()->contact_us_message, function ($message) {
- $subject = 'Customer Message';
- if (! Utils::isNinja()) {
- $subject .= ': v' . NINJA_VERSION;
+ $subject = 'Customer Message: ';
+ if (Utils::isNinja()) {
+ $subject .= config('database.default');
+ } else {
+ $subject .= 'v' . NINJA_VERSION;
}
$message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com'))
->from(CONTACT_EMAIL, Auth::user()->present()->fullName)
diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php
index 7eeb44e1ebd7..a6f700ef0f84 100644
--- a/app/Http/Controllers/InvoiceApiController.php
+++ b/app/Http/Controllers/InvoiceApiController.php
@@ -423,7 +423,12 @@ class InvoiceApiController extends BaseAPIController
public function download(InvoiceRequest $request)
{
$invoice = $request->entity();
+ $pdfString = $invoice->getPDFString();
- return $this->fileReponse($invoice->getFileName(), $invoice->getPDFString());
+ if ($pdfString) {
+ return $this->fileReponse($invoice->getFileName(), $pdfString);
+ } else {
+ abort(404);
+ }
}
}
diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php
index 902ffcf61302..d8232b426439 100644
--- a/app/Http/Controllers/InvoiceController.php
+++ b/app/Http/Controllers/InvoiceController.php
@@ -305,7 +305,6 @@ class InvoiceController extends BaseController
'account' => Auth::user()->account->load('country'),
'products' => Product::scope()->orderBy('product_key')->get(),
'taxRateOptions' => $taxRateOptions,
- 'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'),
'invoiceDesigns' => InvoiceDesign::getDesigns(),
'invoiceFonts' => Cache::get('fonts'),
@@ -480,6 +479,8 @@ class InvoiceController extends BaseController
$key = 'emailed_' . $entityType;
} elseif ($action == 'markPaid') {
$key = 'created_payment';
+ } elseif ($action == 'download') {
+ $key = 'downloaded_invoice';
} else {
$key = "{$action}d_{$entityType}";
}
diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php
index 61014bbcc8d3..696a4c6736dd 100644
--- a/app/Http/Controllers/OnlinePaymentController.php
+++ b/app/Http/Controllers/OnlinePaymentController.php
@@ -339,6 +339,9 @@ class OnlinePaymentController extends BaseController
if (request()->currency_code) {
$data['currency_code'] = request()->currency_code;
}
+ if (request()->country_code) {
+ $data['country_code'] = request()->country_code;
+ }
$client = $clientRepo->save($data, $client);
}
diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php
index 69383e337194..e0e92015920e 100644
--- a/app/Http/Controllers/PaymentController.php
+++ b/app/Http/Controllers/PaymentController.php
@@ -236,7 +236,6 @@ class PaymentController extends BaseController
public function bulk()
{
$action = Input::get('action');
- $amount = Input::get('refund_amount');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
if ($action === 'email') {
@@ -244,7 +243,10 @@ class PaymentController extends BaseController
$this->contactMailer->sendPaymentConfirmation($payment);
Session::flash('message', trans('texts.emailed_payment'));
} else {
- $count = $this->paymentService->bulk($ids, $action, ['refund_amount' => $amount]);
+ $count = $this->paymentService->bulk($ids, $action, [
+ 'refund_amount' => Input::get('refund_amount'),
+ 'refund_email' => Input::get('refund_email'),
+ ]);
if ($count > 0) {
$message = Utils::pluralize($action == 'refund' ? 'refunded_payment' : $action.'d_payment', $count);
Session::flash('message', $message);
diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php
index 264b2f7acb27..630c4b8f18a0 100644
--- a/app/Http/Controllers/QuoteController.php
+++ b/app/Http/Controllers/QuoteController.php
@@ -100,18 +100,15 @@ class QuoteController extends BaseController
'account' => $account,
'products' => Product::scope()->orderBy('product_key')->get(),
'taxRateOptions' => $account->present()->taxRateOptions,
- 'countries' => Cache::get('countries'),
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->orderBy('name')->get(),
- 'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'),
'paymentTerms' => Cache::get('paymentTerms'),
- 'languages' => Cache::get('languages'),
- 'industries' => Cache::get('industries'),
'invoiceDesigns' => InvoiceDesign::getDesigns(),
'invoiceFonts' => Cache::get('fonts'),
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
'isRecurring' => false,
+ 'expenses' => [],
];
}
@@ -133,7 +130,13 @@ class QuoteController extends BaseController
$count = $this->invoiceService->bulk($ids, $action);
if ($count > 0) {
- $key = $action == 'markSent' ? 'updated_quote' : "{$action}d_quote";
+ if ($action == 'markSent') {
+ $key = 'updated_quote';
+ } elseif ($action == 'download') {
+ $key = 'downloaded_quote';
+ } else {
+ $key = "{$action}d_quote";
+ }
$message = Utils::pluralize($key, $count);
Session::flash('message', $message);
}
diff --git a/app/Http/Controllers/RecurringExpenseController.php b/app/Http/Controllers/RecurringExpenseController.php
index 2f72077466b9..3e512b751e7c 100644
--- a/app/Http/Controllers/RecurringExpenseController.php
+++ b/app/Http/Controllers/RecurringExpenseController.php
@@ -115,14 +115,6 @@ class RecurringExpenseController extends BaseController
return [
'data' => Input::old('data'),
'account' => Auth::user()->account,
- 'sizes' => Cache::get('sizes'),
- 'paymentTerms' => Cache::get('paymentTerms'),
- 'industries' => Cache::get('industries'),
- 'currencies' => Cache::get('currencies'),
- 'languages' => Cache::get('languages'),
- 'countries' => Cache::get('countries'),
- 'customLabel1' => Auth::user()->account->custom_vendor_label1,
- 'customLabel2' => Auth::user()->account->custom_vendor_label2,
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(),
'isRecurring' => true,
diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php
index 354b6b00f286..f0ab3fa7ef30 100644
--- a/app/Http/Controllers/ReportController.php
+++ b/app/Http/Controllers/ReportController.php
@@ -8,6 +8,7 @@ use Input;
use Str;
use Utils;
use View;
+use Excel;
/**
* Class ReportController.
@@ -53,6 +54,7 @@ class ReportController extends BaseController
}
$action = Input::get('action');
+ $format = Input::get('format');
if (Input::get('report_type')) {
$reportType = Input::get('report_type');
@@ -104,7 +106,7 @@ class ReportController extends BaseController
$params['report'] = $report;
$params = array_merge($params, $report->results());
if ($isExport) {
- self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']);
+ return self::export($format, $reportType, $params);
}
} else {
$params['columns'] = [];
@@ -117,49 +119,80 @@ class ReportController extends BaseController
}
/**
+ * @param $format
* @param $reportType
- * @param $data
- * @param $columns
- * @param $totals
+ * @param $params
+ * @todo: Add summary to export
*/
- private function export($reportType, $data, $columns, $totals)
+ private function export($format, $reportType, $params)
{
if (! Auth::user()->hasPermission('view_all')) {
exit;
}
- $output = fopen('php://output', 'w') or Utils::fatalError();
- $date = date('Y-m-d');
+ $format = strtolower($format);
+ $data = $params['displayData'];
+ $columns = $params['columns'];
+ $totals = $params['reportTotals'];
+ $report = $params['report'];
- $columns = array_map(function($key, $val) {
- return is_array($val) ? $key : $val;
- }, array_keys($columns), $columns);
+ $filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report";
- header('Content-Type:application/csv');
- header("Content-Disposition:attachment;filename={$date}-invoiceninja-{$reportType}-report.csv");
-
- Utils::exportData($output, $data, Utils::trans($columns));
-
- /*
- fwrite($output, trans('texts.totals'));
- foreach ($totals as $currencyId => $fields) {
- foreach ($fields as $key => $value) {
- fwrite($output, ',' . trans("texts.{$key}"));
- }
- fwrite($output, "\n");
- break;
+ $formats = ['csv', 'pdf', 'xlsx'];
+ if(!in_array($format, $formats)) {
+ throw new \Exception("Invalid format request to export report");
}
- foreach ($totals as $currencyId => $fields) {
- $csv = Utils::getFromCache($currencyId, 'currencies')->name . ',';
- foreach ($fields as $key => $value) {
- $csv .= '"' . Utils::formatMoney($value, $currencyId).'",';
- }
- fwrite($output, $csv."\n");
- }
- */
+ //Get labeled header
+ $columns_labeled = $report->tableHeaderArray();
- fclose($output);
- exit;
+ /*$summary = [];
+ if(count(array_values($totals))) {
+ $summary[] = array_merge([
+ trans("texts.totals")
+ ], array_map(function ($key) {return trans("texts.{$key}");}, array_keys(array_values(array_values($totals)[0])[0])));
+ }
+
+ foreach ($totals as $currencyId => $each) {
+ foreach ($each as $dimension => $val) {
+ $tmp = [];
+ $tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
+
+ foreach ($val as $id => $field) $tmp[] = Utils::formatMoney($field, $currencyId);
+
+ $summary[] = $tmp;
+ }
+ }
+
+ dd($summary);*/
+
+ return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $columns_labeled) {
+ $excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $columns_labeled) {
+
+ $sheet->setOrientation('landscape');
+ $sheet->freezeFirstRow();
+
+ //Add border on PDF
+ if($format == 'pdf')
+ $sheet->setAllBorders('thin');
+
+ $sheet->rows(array_merge(
+ [array_map(function($col) {return $col['label'];}, $columns_labeled)],
+ $data
+ ));
+
+ //Styling header
+ $sheet->cells('A1:'.Utils::num2alpha(count($columns_labeled)-1).'1', function($cells) {
+ $cells->setBackground('#777777');
+ $cells->setFontColor('#FFFFFF');
+ $cells->setFontSize(13);
+ $cells->setFontFamily('Calibri');
+ $cells->setFontWeight('bold');
+ });
+
+
+ $sheet->setAutoSize(true);
+ });
+ })->export($format);
}
}
diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php
index 03eec1a7510d..62404903b91f 100644
--- a/app/Http/Controllers/TaskController.php
+++ b/app/Http/Controllers/TaskController.php
@@ -161,7 +161,7 @@ class TaskController extends BaseController
$invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : [];
foreach ($invoices as $invoice) {
- $actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans('texts.add_to_invoice', ['invoice' => $invoice->invoice_number])];
+ $actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans('texts.add_to_invoice', ['invoice' => e($invoice->invoice_number)])];
}
}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index dd7f96f23cf8..7f364b7716a9 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -156,79 +156,81 @@ class UserController extends BaseController
*/
public function save($userPublicId = false)
{
- if (Auth::user()->hasFeature(FEATURE_USERS)) {
- $rules = [
- 'first_name' => 'required',
- 'last_name' => 'required',
- ];
-
- if ($userPublicId) {
- $user = User::where('account_id', '=', Auth::user()->account_id)
- ->where('public_id', '=', $userPublicId)
- ->withTrashed()
- ->firstOrFail();
-
- $rules['email'] = 'required|email|unique:users,email,'.$user->id.',id';
- } else {
- $user = false;
- $rules['email'] = 'required|email|unique:users';
- }
-
- $validator = Validator::make(Input::all(), $rules);
-
- if ($validator->fails()) {
- return Redirect::to($userPublicId ? 'users/edit' : 'users/create')
- ->withErrors($validator)
- ->withInput();
- }
-
- if (! \App\Models\LookupUser::validateField('email', Input::get('email'), $user)) {
- return Redirect::to($userPublicId ? 'users/edit' : 'users/create')
- ->withError(trans('texts.email_taken'))
- ->withInput();
- }
-
- if ($userPublicId) {
- $user->first_name = trim(Input::get('first_name'));
- $user->last_name = trim(Input::get('last_name'));
- $user->username = trim(Input::get('email'));
- $user->email = trim(Input::get('email'));
- if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
- $user->is_admin = boolval(Input::get('is_admin'));
- $user->permissions = Input::get('permissions');
- }
- } else {
- $lastUser = User::withTrashed()->where('account_id', '=', Auth::user()->account_id)
- ->orderBy('public_id', 'DESC')->first();
-
- $user = new User();
- $user->account_id = Auth::user()->account_id;
- $user->first_name = trim(Input::get('first_name'));
- $user->last_name = trim(Input::get('last_name'));
- $user->username = trim(Input::get('email'));
- $user->email = trim(Input::get('email'));
- $user->registered = true;
- $user->password = strtolower(str_random(RANDOM_KEY_LENGTH));
- $user->confirmation_code = strtolower(str_random(RANDOM_KEY_LENGTH));
- $user->public_id = $lastUser->public_id + 1;
- if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
- $user->is_admin = boolval(Input::get('is_admin'));
- $user->permissions = Input::get('permissions');
- }
- }
-
- $user->save();
-
- if (! $user->confirmed && Input::get('action') === 'email') {
- $this->userMailer->sendConfirmation($user, Auth::user());
- $message = trans('texts.sent_invite');
- } else {
- $message = trans('texts.updated_user');
- }
-
- Session::flash('message', $message);
+ if (! Auth::user()->hasFeature(FEATURE_USERS)) {
+ return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
+ $rules = [
+ 'first_name' => 'required',
+ 'last_name' => 'required',
+ ];
+
+ if ($userPublicId) {
+ $user = User::where('account_id', '=', Auth::user()->account_id)
+ ->where('public_id', '=', $userPublicId)
+ ->withTrashed()
+ ->firstOrFail();
+
+ $rules['email'] = 'required|email|unique:users,email,'.$user->id.',id';
+ } else {
+ $user = false;
+ $rules['email'] = 'required|email|unique:users';
+ }
+
+ $validator = Validator::make(Input::all(), $rules);
+
+ if ($validator->fails()) {
+ return Redirect::to($userPublicId ? 'users/edit' : 'users/create')
+ ->withErrors($validator)
+ ->withInput();
+ }
+
+ if (! \App\Models\LookupUser::validateField('email', Input::get('email'), $user)) {
+ return Redirect::to($userPublicId ? 'users/edit' : 'users/create')
+ ->withError(trans('texts.email_taken'))
+ ->withInput();
+ }
+
+ if ($userPublicId) {
+ $user->first_name = trim(Input::get('first_name'));
+ $user->last_name = trim(Input::get('last_name'));
+ $user->username = trim(Input::get('email'));
+ $user->email = trim(Input::get('email'));
+ if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
+ $user->is_admin = boolval(Input::get('is_admin'));
+ $user->permissions = Input::get('permissions');
+ }
+ } else {
+ $lastUser = User::withTrashed()->where('account_id', '=', Auth::user()->account_id)
+ ->orderBy('public_id', 'DESC')->first();
+
+ $user = new User();
+ $user->account_id = Auth::user()->account_id;
+ $user->first_name = trim(Input::get('first_name'));
+ $user->last_name = trim(Input::get('last_name'));
+ $user->username = trim(Input::get('email'));
+ $user->email = trim(Input::get('email'));
+ $user->registered = true;
+ $user->password = strtolower(str_random(RANDOM_KEY_LENGTH));
+ $user->confirmation_code = strtolower(str_random(RANDOM_KEY_LENGTH));
+ $user->public_id = $lastUser->public_id + 1;
+ if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
+ $user->is_admin = boolval(Input::get('is_admin'));
+ $user->permissions = Input::get('permissions');
+ }
+ }
+
+ $user->save();
+
+ if (! $user->confirmed && Input::get('action') === 'email') {
+ $this->userMailer->sendConfirmation($user, Auth::user());
+ $message = trans('texts.sent_invite');
+ } else {
+ $message = trans('texts.updated_user');
+ }
+
+ Session::flash('message', $message);
+
return Redirect::to('users/' . $user->public_id . '/edit');
}
diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php
index 828cb9f542ba..654a725a7bb3 100644
--- a/app/Http/Controllers/VendorController.php
+++ b/app/Http/Controllers/VendorController.php
@@ -151,7 +151,6 @@ class VendorController extends BaseController
return [
'data' => Input::old('data'),
'account' => Auth::user()->account,
- 'currencies' => Cache::get('currencies'),
];
}
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
index fef40c5cfdab..ccd3a90c4a08 100644
--- a/app/Http/Middleware/Authenticate.php
+++ b/app/Http/Middleware/Authenticate.php
@@ -68,7 +68,7 @@ class Authenticate
}
$account = $contact->account;
- if (Auth::guard('user')->check() && Auth::user('user')->account_id === $account->id) {
+ if (Auth::guard('user')->check() && Auth::user('user')->account_id == $account->id) {
// This is an admin; let them pretend to be a client
$authenticated = true;
}
diff --git a/app/Http/Requests/CreatePaymentAPIRequest.php b/app/Http/Requests/CreatePaymentAPIRequest.php
index f7a521c4c918..c6b4657080c3 100644
--- a/app/Http/Requests/CreatePaymentAPIRequest.php
+++ b/app/Http/Requests/CreatePaymentAPIRequest.php
@@ -33,7 +33,11 @@ class CreatePaymentAPIRequest extends PaymentRequest
$this->invoice = $invoice = Invoice::scope($this->invoice_id)
->withArchived()
->invoices()
- ->firstOrFail();
+ ->first();
+
+ if (! $this->invoice) {
+ abort(404, 'Invoice was not found');
+ }
$this->merge([
'invoice_id' => $invoice->id,
diff --git a/app/Http/Requests/RegisterRequest.php b/app/Http/Requests/RegisterRequest.php
index 17840be5983e..6275a8ce300d 100644
--- a/app/Http/Requests/RegisterRequest.php
+++ b/app/Http/Requests/RegisterRequest.php
@@ -31,7 +31,7 @@ class RegisterRequest extends Request
public function rules()
{
$rules = [
- 'email' => 'required|unique:users',
+ 'email' => 'email|required|unique:users',
'first_name' => 'required',
'last_name' => 'required',
'password' => 'required',
diff --git a/app/Http/ViewComposers/TranslationComposer.php b/app/Http/ViewComposers/TranslationComposer.php
index ff2dc954199f..89f3fb267049 100644
--- a/app/Http/ViewComposers/TranslationComposer.php
+++ b/app/Http/ViewComposers/TranslationComposer.php
@@ -2,6 +2,7 @@
namespace App\Http\ViewComposers;
+use Str;
use Cache;
use Illuminate\View\View;
@@ -44,5 +45,11 @@ class TranslationComposer
})->sortBy(function ($lang) {
return $lang->name;
}));
+
+ $view->with('currencies', Cache::get('currencies')->each(function ($currency) {
+ $currency->name = trans('texts.currency_' . Str::slug($currency->name, '_'));
+ })->sortBy(function ($currency) {
+ return $currency->name;
+ }));
}
}
diff --git a/app/Http/routes.php b/app/Http/routes.php
index 54a6b7333dca..98f1b869c10c 100644
--- a/app/Http/routes.php
+++ b/app/Http/routes.php
@@ -144,7 +144,7 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
Route::get('api/clients', 'ClientController@getDatatable');
Route::get('api/activities/{client_id?}', 'ActivityController@getDatatable');
Route::post('clients/bulk', 'ClientController@bulk');
- Route::get('clients/statement/{client_id}', 'ClientController@statement');
+ Route::get('clients/statement/{client_id}/{status_id?}/{start_date?}/{end_date?}', 'ClientController@statement');
Route::resource('tasks', 'TaskController');
Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable');
@@ -224,6 +224,7 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
// Expense
Route::resource('expenses', 'ExpenseController');
Route::get('expenses/create/{vendor_id?}/{client_id?}/{category_id?}', 'ExpenseController@create');
+ Route::get('expenses/{expenses}/clone', 'ExpenseController@clone');
Route::get('api/expenses', 'ExpenseController@getDatatable');
Route::get('api/expenses/{id}', 'ExpenseController@getDatatableVendor');
Route::post('expenses/bulk', 'ExpenseController@bulk');
diff --git a/app/Jobs/DownloadInvoices.php b/app/Jobs/DownloadInvoices.php
new file mode 100644
index 000000000000..651d0012837c
--- /dev/null
+++ b/app/Jobs/DownloadInvoices.php
@@ -0,0 +1,87 @@
+user = $user;
+ $this->invoices = $invoices;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @param ContactMailer $mailer
+ */
+ public function handle(UserMailer $userMailer)
+ {
+ $zip = Archive::instance_by_useragent(date('Y-m-d') . '-Invoice_PDFs');
+
+ foreach ($this->invoices as $invoice) {
+ $zip->add_file($invoice->getFileName(), $invoice->getPDFString());
+ }
+
+ $zip->finish();
+ exit;
+
+ /*
+ // if queues are disabled download a zip file
+ if (config('queue.default') === 'sync' || count($this->invoices) <= 10) {
+ $zip = Archive::instance_by_useragent(date('Y-m-d') . '-Invoice_PDFs');
+ foreach ($this->invoices as $invoice) {
+ $zip->add_file($invoice->getFileName(), $invoice->getPDFString());
+ }
+ $zip->finish();
+ exit;
+
+ // otherwise sends the PDFs in an email
+ } else {
+ $data = [];
+ foreach ($this->invoices as $invoice) {
+ $data[] = [
+ 'name' => $invoice->getFileName(),
+ 'data' => $invoice->getPDFString(),
+ ];
+ }
+
+ $subject = trans('texts.invoices_are_attached');
+ $data = [
+ 'documents' => $data
+ ];
+
+ $userMailer->sendMessage($this->user, $subject, false, $data);
+ }
+ */
+ }
+}
diff --git a/app/Jobs/PurgeAccountData.php b/app/Jobs/PurgeAccountData.php
index 6e928420adbd..2430292c76f1 100644
--- a/app/Jobs/PurgeAccountData.php
+++ b/app/Jobs/PurgeAccountData.php
@@ -61,6 +61,8 @@ class PurgeAccountData extends Job
$account->client_number_counter = $account->client_number_counter > 0 ? 1 : 0;
$account->save();
+ session([RECENTLY_VIEWED => false]);
+
if (env('MULTI_DB_ENABLED')) {
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
diff --git a/app/Jobs/SendNotificationEmail.php b/app/Jobs/SendNotificationEmail.php
index 2b1213bb1bec..cf475f77db4c 100644
--- a/app/Jobs/SendNotificationEmail.php
+++ b/app/Jobs/SendNotificationEmail.php
@@ -7,13 +7,16 @@ use App\Ninja\Mailers\UserMailer;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use App\Models\Traits\SerialisesDeletedModels;
/**
* Class SendInvoiceEmail.
*/
class SendNotificationEmail extends Job implements ShouldQueue
{
- use InteractsWithQueue, SerializesModels;
+ use InteractsWithQueue, SerializesModels, SerialisesDeletedModels {
+ SerialisesDeletedModels::getRestoredPropertyValue insteadof SerializesModels;
+ }
/**
* @var User
diff --git a/app/Libraries/HTMLUtils.php b/app/Libraries/HTMLUtils.php
index 24dd67a6b5da..ad8ea6c8cb53 100644
--- a/app/Libraries/HTMLUtils.php
+++ b/app/Libraries/HTMLUtils.php
@@ -39,6 +39,8 @@ class HTMLUtils
public static function sanitizeHTML($html)
{
+ $html = html_entity_decode($html);
+
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php
index df0c26a8a9c6..0e932c450514 100644
--- a/app/Libraries/Utils.php
+++ b/app/Libraries/Utils.php
@@ -916,7 +916,7 @@ class Utils
$str = '';
if (property_exists($model, 'is_deleted')) {
- $str = $model->is_deleted || ($model->deleted_at && $model->deleted_at != '0000-00-00') ? 'DISABLED ' : '';
+ $str = $model->is_deleted ? 'DISABLED ' : '';
if ($model->is_deleted) {
$str .= 'ENTITY_DELETED ';
@@ -1052,20 +1052,15 @@ class Utils
}
}
- public static function formatWebsite($website)
+ public static function formatWebsite($link)
{
- if (! $website) {
+ if (! $link) {
return '';
}
- $link = $website;
- $title = $website;
- $prefix = 'http://';
-
- if (strlen($link) > 7 && substr($link, 0, 7) === $prefix) {
- $title = substr($title, 7);
- } else {
- $link = $prefix.$link;
+ $title = $link;
+ if (substr($link, 0, 4) != 'http') {
+ $link = 'http://' . $link;
}
return link_to($link, $title, ['target' => '_blank']);
@@ -1246,4 +1241,64 @@ class Utils
fclose($handle);
return( ord($contents[28]) != 0 );
}
+
+ //Source: https://stackoverflow.com/questions/3302857/algorithm-to-get-the-excel-like-column-name-of-a-number
+ public static function num2alpha($n)
+ {
+ for($r = ""; $n >= 0; $n = intval($n / 26) - 1)
+ $r = chr($n%26 + 0x41) . $r;
+ return $r;
+ }
+
+ /**
+ * Replace language-specific characters by ASCII-equivalents.
+ * @param string $s
+ * @return string
+ * Source: https://stackoverflow.com/questions/3371697/replacing-accented-characters-php/16427125#16427125
+ */
+ public static function normalizeChars($s) {
+ $replace = array(
+ 'ъ'=>'-', 'Ь'=>'-', 'Ъ'=>'-', 'ь'=>'-',
+ 'Ă'=>'A', 'Ą'=>'A', 'À'=>'A', 'Ã'=>'A', 'Á'=>'A', 'Æ'=>'A', 'Â'=>'A', 'Å'=>'A', 'Ä'=>'Ae',
+ 'Þ'=>'B',
+ 'Ć'=>'C', 'ץ'=>'C', 'Ç'=>'C',
+ 'È'=>'E', 'Ę'=>'E', 'É'=>'E', 'Ë'=>'E', 'Ê'=>'E',
+ 'Ğ'=>'G',
+ 'İ'=>'I', 'Ï'=>'I', 'Î'=>'I', 'Í'=>'I', 'Ì'=>'I',
+ 'Ł'=>'L',
+ 'Ñ'=>'N', 'Ń'=>'N',
+ 'Ø'=>'O', 'Ó'=>'O', 'Ò'=>'O', 'Ô'=>'O', 'Õ'=>'O', 'Ö'=>'Oe',
+ 'Ş'=>'S', 'Ś'=>'S', 'Ș'=>'S', 'Š'=>'S',
+ 'Ț'=>'T',
+ 'Ù'=>'U', 'Û'=>'U', 'Ú'=>'U', 'Ü'=>'Ue',
+ 'Ý'=>'Y',
+ 'Ź'=>'Z', 'Ž'=>'Z', 'Ż'=>'Z',
+ 'â'=>'a', 'ǎ'=>'a', 'ą'=>'a', 'á'=>'a', 'ă'=>'a', 'ã'=>'a', 'Ǎ'=>'a', 'а'=>'a', 'А'=>'a', 'å'=>'a', 'à'=>'a', 'א'=>'a', 'Ǻ'=>'a', 'Ā'=>'a', 'ǻ'=>'a', 'ā'=>'a', 'ä'=>'ae', 'æ'=>'ae', 'Ǽ'=>'ae', 'ǽ'=>'ae',
+ 'б'=>'b', 'ב'=>'b', 'Б'=>'b', 'þ'=>'b',
+ 'ĉ'=>'c', 'Ĉ'=>'c', 'Ċ'=>'c', 'ć'=>'c', 'ç'=>'c', 'ц'=>'c', 'צ'=>'c', 'ċ'=>'c', 'Ц'=>'c', 'Č'=>'c', 'č'=>'c', 'Ч'=>'ch', 'ч'=>'ch',
+ 'ד'=>'d', 'ď'=>'d', 'Đ'=>'d', 'Ď'=>'d', 'đ'=>'d', 'д'=>'d', 'Д'=>'D', 'ð'=>'d',
+ 'є'=>'e', 'ע'=>'e', 'е'=>'e', 'Е'=>'e', 'Ə'=>'e', 'ę'=>'e', 'ĕ'=>'e', 'ē'=>'e', 'Ē'=>'e', 'Ė'=>'e', 'ė'=>'e', 'ě'=>'e', 'Ě'=>'e', 'Є'=>'e', 'Ĕ'=>'e', 'ê'=>'e', 'ə'=>'e', 'è'=>'e', 'ë'=>'e', 'é'=>'e',
+ 'ф'=>'f', 'ƒ'=>'f', 'Ф'=>'f',
+ 'ġ'=>'g', 'Ģ'=>'g', 'Ġ'=>'g', 'Ĝ'=>'g', 'Г'=>'g', 'г'=>'g', 'ĝ'=>'g', 'ğ'=>'g', 'ג'=>'g', 'Ґ'=>'g', 'ґ'=>'g', 'ģ'=>'g',
+ 'ח'=>'h', 'ħ'=>'h', 'Х'=>'h', 'Ħ'=>'h', 'Ĥ'=>'h', 'ĥ'=>'h', 'х'=>'h', 'ה'=>'h',
+ 'î'=>'i', 'ï'=>'i', 'í'=>'i', 'ì'=>'i', 'į'=>'i', 'ĭ'=>'i', 'ı'=>'i', 'Ĭ'=>'i', 'И'=>'i', 'ĩ'=>'i', 'ǐ'=>'i', 'Ĩ'=>'i', 'Ǐ'=>'i', 'и'=>'i', 'Į'=>'i', 'י'=>'i', 'Ї'=>'i', 'Ī'=>'i', 'І'=>'i', 'ї'=>'i', 'і'=>'i', 'ī'=>'i', 'ij'=>'ij', 'IJ'=>'ij',
+ 'й'=>'j', 'Й'=>'j', 'Ĵ'=>'j', 'ĵ'=>'j', 'я'=>'ja', 'Я'=>'ja', 'Э'=>'je', 'э'=>'je', 'ё'=>'jo', 'Ё'=>'jo', 'ю'=>'ju', 'Ю'=>'ju',
+ 'ĸ'=>'k', 'כ'=>'k', 'Ķ'=>'k', 'К'=>'k', 'к'=>'k', 'ķ'=>'k', 'ך'=>'k',
+ 'Ŀ'=>'l', 'ŀ'=>'l', 'Л'=>'l', 'ł'=>'l', 'ļ'=>'l', 'ĺ'=>'l', 'Ĺ'=>'l', 'Ļ'=>'l', 'л'=>'l', 'Ľ'=>'l', 'ľ'=>'l', 'ל'=>'l',
+ 'מ'=>'m', 'М'=>'m', 'ם'=>'m', 'м'=>'m',
+ 'ñ'=>'n', 'н'=>'n', 'Ņ'=>'n', 'ן'=>'n', 'ŋ'=>'n', 'נ'=>'n', 'Н'=>'n', 'ń'=>'n', 'Ŋ'=>'n', 'ņ'=>'n', 'ʼn'=>'n', 'Ň'=>'n', 'ň'=>'n',
+ 'о'=>'o', 'О'=>'o', 'ő'=>'o', 'õ'=>'o', 'ô'=>'o', 'Ő'=>'o', 'ŏ'=>'o', 'Ŏ'=>'o', 'Ō'=>'o', 'ō'=>'o', 'ø'=>'o', 'ǿ'=>'o', 'ǒ'=>'o', 'ò'=>'o', 'Ǿ'=>'o', 'Ǒ'=>'o', 'ơ'=>'o', 'ó'=>'o', 'Ơ'=>'o', 'œ'=>'oe', 'Œ'=>'oe', 'ö'=>'oe',
+ 'פ'=>'p', 'ף'=>'p', 'п'=>'p', 'П'=>'p',
+ 'ק'=>'q',
+ 'ŕ'=>'r', 'ř'=>'r', 'Ř'=>'r', 'ŗ'=>'r', 'Ŗ'=>'r', 'ר'=>'r', 'Ŕ'=>'r', 'Р'=>'r', 'р'=>'r',
+ 'ș'=>'s', 'с'=>'s', 'Ŝ'=>'s', 'š'=>'s', 'ś'=>'s', 'ס'=>'s', 'ş'=>'s', 'С'=>'s', 'ŝ'=>'s', 'Щ'=>'sch', 'щ'=>'sch', 'ш'=>'sh', 'Ш'=>'sh', 'ß'=>'ss',
+ 'т'=>'t', 'ט'=>'t', 'ŧ'=>'t', 'ת'=>'t', 'ť'=>'t', 'ţ'=>'t', 'Ţ'=>'t', 'Т'=>'t', 'ț'=>'t', 'Ŧ'=>'t', 'Ť'=>'t', '™'=>'tm',
+ 'ū'=>'u', 'у'=>'u', 'Ũ'=>'u', 'ũ'=>'u', 'Ư'=>'u', 'ư'=>'u', 'Ū'=>'u', 'Ǔ'=>'u', 'ų'=>'u', 'Ų'=>'u', 'ŭ'=>'u', 'Ŭ'=>'u', 'Ů'=>'u', 'ů'=>'u', 'ű'=>'u', 'Ű'=>'u', 'Ǖ'=>'u', 'ǔ'=>'u', 'Ǜ'=>'u', 'ù'=>'u', 'ú'=>'u', 'û'=>'u', 'У'=>'u', 'ǚ'=>'u', 'ǜ'=>'u', 'Ǚ'=>'u', 'Ǘ'=>'u', 'ǖ'=>'u', 'ǘ'=>'u', 'ü'=>'ue',
+ 'в'=>'v', 'ו'=>'v', 'В'=>'v',
+ 'ש'=>'w', 'ŵ'=>'w', 'Ŵ'=>'w',
+ 'ы'=>'y', 'ŷ'=>'y', 'ý'=>'y', 'ÿ'=>'y', 'Ÿ'=>'y', 'Ŷ'=>'y',
+ 'Ы'=>'y', 'ž'=>'z', 'З'=>'z', 'з'=>'z', 'ź'=>'z', 'ז'=>'z', 'ż'=>'z', 'ſ'=>'z', 'Ж'=>'zh', 'ж'=>'zh'
+ );
+ return strtr($s, $replace);
+ }
}
diff --git a/app/Listeners/HandleUserLoggedIn.php b/app/Listeners/HandleUserLoggedIn.php
index 10208ab1639d..030d73251e33 100644
--- a/app/Listeners/HandleUserLoggedIn.php
+++ b/app/Listeners/HandleUserLoggedIn.php
@@ -71,19 +71,31 @@ class HandleUserLoggedIn
Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize() . 'KB']));
}
- // check custom gateway id is correct
if (! Utils::isNinja()) {
+ // check custom gateway id is correct
$gateway = Gateway::find(GATEWAY_CUSTOM);
if (! $gateway || $gateway->name !== 'Custom') {
Session::flash('error', trans('texts.error_incorrect_gateway_ids'));
}
- /*
- if (! env('APP_KEY')) {
- Session::flash('error', trans('texts.error_app_key_not_set'));
- } elseif (strstr(env('APP_KEY'), 'SomeRandomString')) {
+
+ // make sure APP_KEY and APP_CIPHER are in the .env file
+ $appKey = env('APP_KEY');
+ $appCipher = env('APP_CIPHER');
+ if (! $appKey || ! $appCipher) {
+ $fp = fopen(base_path().'/.env', 'a');
+ if (! $appKey) {
+ fwrite($fp, "\nAPP_KEY=" . config('app.key'));
+ }
+ if (! $appCipher) {
+ fwrite($fp, "\nAPP_CIPHER=" . config('app.cipher'));
+ }
+ fclose($fp);
+ }
+
+ // warn if using the default app key
+ if (in_array(config('app.key'), ['SomeRandomString', 'SomeRandomStringSomeRandomString', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'])) {
Session::flash('error', trans('texts.error_app_key_set_to_default'));
}
- */
}
}
}
diff --git a/app/Models/Account.php b/app/Models/Account.php
index e50c0d36fcc7..3e415e65dfa6 100644
--- a/app/Models/Account.php
+++ b/app/Models/Account.php
@@ -332,6 +332,14 @@ class Account extends Eloquent
return $this->hasMany('App\Models\Product');
}
+ /**
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function defaultDocuments()
+ {
+ return $this->hasMany('App\Models\Document')->whereIsDefault(true);
+ }
+
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
diff --git a/app/Models/AccountEmailSettings.php b/app/Models/AccountEmailSettings.php
index 752b2aea1e19..8de75d1d3451 100644
--- a/app/Models/AccountEmailSettings.php
+++ b/app/Models/AccountEmailSettings.php
@@ -27,6 +27,12 @@ class AccountEmailSettings extends Eloquent
'email_template_reminder1',
'email_template_reminder2',
'email_template_reminder3',
+ 'late_fee1_amount',
+ 'late_fee1_percent',
+ 'late_fee2_amount',
+ 'late_fee2_percent',
+ 'late_fee3_amount',
+ 'late_fee3_percent',
];
}
diff --git a/app/Models/Activity.php b/app/Models/Activity.php
index 8d9fe10e9316..c637f467913f 100644
--- a/app/Models/Activity.php
+++ b/app/Models/Activity.php
@@ -123,11 +123,11 @@ class Activity extends Eloquent
$data = [
'client' => $client ? link_to($client->getRoute(), $client->getDisplayName()) : null,
- 'user' => $isSystem ? '' . trans('texts.system') . '' : $user->getDisplayName(),
+ 'user' => $isSystem ? '' . trans('texts.system') . '' : e($user->getDisplayName()),
'invoice' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
- 'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(),
- 'payment' => $payment ? $payment->transaction_reference : null,
+ 'contact' => $contactId ? e($client->getDisplayName()) : e($user->getDisplayName()),
+ 'payment' => $payment ? e($payment->transaction_reference) : null,
'payment_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null,
'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null,
'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null,
diff --git a/app/Models/BankAccount.php b/app/Models/BankAccount.php
index 2cd4a65646ef..a5365ee9f8e2 100644
--- a/app/Models/BankAccount.php
+++ b/app/Models/BankAccount.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Crypt;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@@ -33,6 +34,22 @@ class BankAccount extends EntityModel
return ENTITY_BANK_ACCOUNT;
}
+ /**
+ * @return mixed
+ */
+ public function getUsername()
+ {
+ return Crypt::decrypt($this->username);
+ }
+
+ /**
+ * @param $config
+ */
+ public function setUsername($value)
+ {
+ $this->username = Crypt::encrypt($value);
+ }
+
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
diff --git a/app/Models/Client.php b/app/Models/Client.php
index 31009197ff9a..4a6d8486e67e 100644
--- a/app/Models/Client.php
+++ b/app/Models/Client.php
@@ -54,54 +54,7 @@ class Client extends EntityModel
'public_notes',
];
- /**
- * @var string
- */
- public static $fieldName = 'name';
- /**
- * @var string
- */
- public static $fieldPhone = 'work_phone';
- /**
- * @var string
- */
- public static $fieldAddress1 = 'address1';
- /**
- * @var string
- */
- public static $fieldAddress2 = 'address2';
- /**
- * @var string
- */
- public static $fieldCity = 'city';
- /**
- * @var string
- */
- public static $fieldState = 'state';
- /**
- * @var string
- */
- public static $fieldPostalCode = 'postal_code';
- /**
- * @var string
- */
- public static $fieldNotes = 'notes';
- /**
- * @var string
- */
- public static $fieldCountry = 'country';
- /**
- * @var string
- */
- public static $fieldWebsite = 'website';
- /**
- * @var string
- */
- public static $fieldVatNumber = 'vat_number';
- /**
- * @var string
- */
- public static $fieldIdNumber = 'id_number';
+
/**
* @return array
@@ -109,22 +62,28 @@ class Client extends EntityModel
public static function getImportColumns()
{
return [
- self::$fieldName,
- self::$fieldPhone,
- self::$fieldAddress1,
- self::$fieldAddress2,
- self::$fieldCity,
- self::$fieldState,
- self::$fieldPostalCode,
- self::$fieldCountry,
- self::$fieldNotes,
- self::$fieldWebsite,
- self::$fieldVatNumber,
- self::$fieldIdNumber,
- Contact::$fieldFirstName,
- Contact::$fieldLastName,
- Contact::$fieldPhone,
- Contact::$fieldEmail,
+ 'name',
+ 'work_phone',
+ 'address1',
+ 'address2',
+ 'city',
+ 'state',
+ 'postal_code',
+ 'public_notes',
+ 'private_notes',
+ 'country',
+ 'website',
+ 'currency',
+ 'vat_number',
+ 'id_number',
+ 'custom1',
+ 'custom2',
+ 'contact_first_name',
+ 'contact_last_name',
+ 'contact_phone',
+ 'contact_email',
+ 'contact_custom1',
+ 'contact_custom2',
];
}
@@ -134,10 +93,11 @@ class Client extends EntityModel
public static function getImportMap()
{
return [
- 'first' => 'first_name',
- 'last' => 'last_name',
- 'email' => 'email',
- 'mobile|phone' => 'phone',
+ 'first' => 'contact_first_name',
+ 'last' => 'contact_last_name',
+ 'email' => 'contact_email',
+ 'work|office' => 'work_phone',
+ 'mobile|phone' => 'contact_phone',
'name|organization' => 'name',
'apt|street2|address2' => 'address2',
'street|address|address1' => 'address1',
@@ -145,8 +105,10 @@ class Client extends EntityModel
'state|province' => 'state',
'zip|postal|code' => 'postal_code',
'country' => 'country',
- 'note' => 'notes',
+ 'public' => 'public_notes',
+ 'private|note' => 'private_notes',
'site|website' => 'website',
+ 'currency' => 'currency',
'vat' => 'vat_number',
'number' => 'id_number',
];
@@ -282,7 +244,9 @@ class Client extends EntityModel
{
$publicId = isset($data['public_id']) ? $data['public_id'] : (isset($data['id']) ? $data['id'] : false);
- if ($publicId && $publicId != '-1') {
+ // check if this client wasRecentlyCreated to ensure a new contact is
+ // always created even if the request includes a contact id
+ if (! $this->wasRecentlyCreated && $publicId && $publicId != '-1') {
$contact = Contact::scope($publicId)->firstOrFail();
} else {
$contact = Contact::createNew();
diff --git a/app/Models/Document.php b/app/Models/Document.php
index f1f83436fbf3..90de300f5b81 100644
--- a/app/Models/Document.php
+++ b/app/Models/Document.php
@@ -24,6 +24,7 @@ class Document extends EntityModel
protected $fillable = [
'invoice_id',
'expense_id',
+ 'is_default',
];
/**
diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php
index c186d027ac3e..c5582c808291 100644
--- a/app/Models/Invoice.php
+++ b/app/Models/Invoice.php
@@ -115,6 +115,8 @@ class Invoice extends EntityModel implements BalanceAffecting
'terms',
'product',
'quantity',
+ 'tax1',
+ 'tax2',
];
}
@@ -135,6 +137,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'notes' => 'notes',
'product|item' => 'product',
'quantity|qty' => 'quantity',
+ 'tax' => 'tax1',
];
}
@@ -305,6 +308,23 @@ class Invoice extends EntityModel implements BalanceAffecting
return $this->hasMany('App\Models\Document')->orderBy('id');
}
+ /**
+ * @return mixed
+ */
+ public function allDocuments()
+ {
+ $documents = $this->documents;
+ $documents = $documents->merge($this->account->defaultDocuments);
+
+ foreach ($this->expenses as $expense) {
+ if ($expense->invoice_documents) {
+ $documents = $documents->merge($expense->documents);
+ }
+ }
+
+ return $documents;
+ }
+
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
@@ -1319,7 +1339,7 @@ class Invoice extends EntityModel implements BalanceAffecting
/**
* @return int
*/
- public function countDocuments()
+ public function countDocuments($expenses = false)
{
$count = count($this->documents);
@@ -1329,6 +1349,14 @@ class Invoice extends EntityModel implements BalanceAffecting
}
}
+ if ($expenses) {
+ foreach ($expenses as $expense) {
+ if ($expense->invoice_documents) {
+ $count += count($expense->documents);
+ }
+ }
+ }
+
return $count;
}
@@ -1337,7 +1365,11 @@ class Invoice extends EntityModel implements BalanceAffecting
*/
public function hasDocuments()
{
- if (count($this->documents)) {
+ if ($this->documents->count()) {
+ return true;
+ }
+
+ if ($this->account->defaultDocuments->count()) {
return true;
}
diff --git a/app/Models/LookupModel.php b/app/Models/LookupModel.php
index 1e406191628b..023287306bfc 100644
--- a/app/Models/LookupModel.php
+++ b/app/Models/LookupModel.php
@@ -34,7 +34,7 @@ class LookupModel extends Eloquent
if ($lookupAccount) {
$data['lookup_account_id'] = $lookupAccount->id;
} else {
- abort('Lookup account not found for ' . $accountKey);
+ abort(500, 'Lookup account not found for ' . $accountKey);
}
static::create($data);
@@ -96,7 +96,7 @@ class LookupModel extends Eloquent
->first();
}
if (! $isFound) {
- abort("Looked up {$className} not found: {$field} => {$value}");
+ abort(500, "Looked up {$className} not found: {$field} => {$value}");
}
Cache::put($key, $server, 120);
diff --git a/app/Models/Traits/PresentsInvoice.php b/app/Models/Traits/PresentsInvoice.php
index 641e6bdb7ab4..53647cf9675a 100644
--- a/app/Models/Traits/PresentsInvoice.php
+++ b/app/Models/Traits/PresentsInvoice.php
@@ -290,7 +290,7 @@ trait PresentsInvoice
'contact.custom_value1' => 'custom_contact_label1',
'contact.custom_value2' => 'custom_contact_label2',
] as $field => $property) {
- $data[$field] = $this->$property ?: trans('texts.custom_field');
+ $data[$field] = e($this->$property) ?: trans('texts.custom_field');
}
return $data;
diff --git a/app/Models/Traits/SendsEmails.php b/app/Models/Traits/SendsEmails.php
index 2e5ecc11e98b..0d9abb2c7b43 100644
--- a/app/Models/Traits/SendsEmails.php
+++ b/app/Models/Traits/SendsEmails.php
@@ -4,6 +4,7 @@ namespace App\Models\Traits;
use App\Constants\Domain;
use Utils;
+use HTMLUtils;
/**
* Class SendsEmails.
@@ -36,7 +37,8 @@ trait SendsEmails
$value = $this->account_email_settings->$field;
if ($value) {
- return preg_replace("/\r\n|\r|\n/", ' ', $value);
+ $value = preg_replace("/\r\n|\r|\n/", ' ', $value);
+ return HTMLUtils::sanitizeHTML($value);
}
}
@@ -94,7 +96,9 @@ trait SendsEmails
$template = preg_replace("/\r\n|\r|\n/", ' ', $template);
//
is causing page breaks with the email designs
- return str_replace('/>', ' />', $template);
+ $template = str_replace('/>', ' />', $template);
+
+ return HTMLUtils::sanitizeHTML($template);
}
/**
@@ -125,9 +129,9 @@ trait SendsEmails
*
* @return bool
*/
- public function getReminderDate($reminder)
+ public function getReminderDate($reminder, $filterEnabled = true)
{
- if (! $this->{"enable_reminder{$reminder}"}) {
+ if ($filterEnabled && ! $this->{"enable_reminder{$reminder}"}) {
return false;
}
@@ -142,10 +146,10 @@ trait SendsEmails
*
* @return bool|string
*/
- public function getInvoiceReminder($invoice)
+ public function getInvoiceReminder($invoice, $filterEnabled = true)
{
for ($i = 1; $i <= 3; $i++) {
- if ($date = $this->getReminderDate($i)) {
+ if ($date = $this->getReminderDate($i, $filterEnabled)) {
$field = $this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';
if ($invoice->$field == $date) {
return "reminder{$i}";
diff --git a/app/Models/Traits/SerialisesDeletedModels.php b/app/Models/Traits/SerialisesDeletedModels.php
new file mode 100644
index 000000000000..fc963368977b
--- /dev/null
+++ b/app/Models/Traits/SerialisesDeletedModels.php
@@ -0,0 +1,36 @@
+id)) {
+ return $this->restoreCollection($value);
+ }
+
+ $instance = new $value->class;
+ $query = $instance->newQuery()->useWritePdo();
+
+ if (property_exists($instance, 'forceDeleting')) {
+ return $query->withTrashed()->find($value->id);
+ }
+
+ return $query->findOrFail($value->id);
+ }
+}
diff --git a/app/Ninja/Datatables/CreditDatatable.php b/app/Ninja/Datatables/CreditDatatable.php
index 2fd96e09155e..53ff00b58845 100644
--- a/app/Ninja/Datatables/CreditDatatable.php
+++ b/app/Ninja/Datatables/CreditDatatable.php
@@ -50,13 +50,13 @@ class CreditDatatable extends EntityDatatable
[
'public_notes',
function ($model) {
- return $model->public_notes;
+ return e($model->public_notes);
},
],
[
'private_notes',
function ($model) {
- return $model->private_notes;
+ return e($model->private_notes);
},
],
];
diff --git a/app/Ninja/Datatables/ExpenseDatatable.php b/app/Ninja/Datatables/ExpenseDatatable.php
index 30ccffab4fd5..c3c344e92099 100644
--- a/app/Ninja/Datatables/ExpenseDatatable.php
+++ b/app/Ninja/Datatables/ExpenseDatatable.php
@@ -84,7 +84,7 @@ class ExpenseDatatable extends EntityDatatable
[
'public_notes',
function ($model) {
- return $model->public_notes != null ? substr($model->public_notes, 0, 100) : '';
+ return $model->public_notes != null ? e(substr($model->public_notes, 0, 100)) : '';
},
],
[
@@ -108,6 +108,15 @@ class ExpenseDatatable extends EntityDatatable
return Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id]);
},
],
+ [
+ trans("texts.clone_expense"),
+ function ($model) {
+ return URL::to("expenses/{$model->public_id}/clone");
+ },
+ function ($model) {
+ return Auth::user()->can('create', ENTITY_EXPENSE);
+ },
+ ],
[
trans('texts.view_invoice'),
function ($model) {
diff --git a/app/Ninja/Datatables/InvoiceDatatable.php b/app/Ninja/Datatables/InvoiceDatatable.php
index f76144b3faeb..efbd4e9905cf 100644
--- a/app/Ninja/Datatables/InvoiceDatatable.php
+++ b/app/Ninja/Datatables/InvoiceDatatable.php
@@ -181,14 +181,18 @@ class InvoiceDatatable extends EntityDatatable
public function bulkActions()
{
- $actions = parent::bulkActions();
+ $actions = [];
if ($this->entityType == ENTITY_INVOICE || $this->entityType == ENTITY_QUOTE) {
- $actions[] = \DropdownButton::DIVIDER;
+ $actions[] = [
+ 'label' => mtrans($this->entityType, 'download_' . $this->entityType),
+ 'url' => 'javascript:submitForm_'.$this->entityType.'("download")',
+ ];
$actions[] = [
'label' => mtrans($this->entityType, 'email_' . $this->entityType),
'url' => 'javascript:submitForm_'.$this->entityType.'("emailInvoice")',
];
+ $actions[] = \DropdownButton::DIVIDER;
$actions[] = [
'label' => mtrans($this->entityType, 'mark_sent'),
'url' => 'javascript:submitForm_'.$this->entityType.'("markSent")',
@@ -202,6 +206,9 @@ class InvoiceDatatable extends EntityDatatable
];
}
+ $actions[] = \DropdownButton::DIVIDER;
+ $actions = array_merge($actions, parent::bulkActions());
+
return $actions;
}
}
diff --git a/app/Ninja/Datatables/PaymentDatatable.php b/app/Ninja/Datatables/PaymentDatatable.php
index 2da386dc106d..05d23b59e297 100644
--- a/app/Ninja/Datatables/PaymentDatatable.php
+++ b/app/Ninja/Datatables/PaymentDatatable.php
@@ -46,7 +46,7 @@ class PaymentDatatable extends EntityDatatable
[
'transaction_reference',
function ($model) {
- return $model->transaction_reference ? $model->transaction_reference : ''.trans('texts.manual_entry').'';
+ return $model->transaction_reference ? e($model->transaction_reference) : ''.trans('texts.manual_entry').'';
},
],
[
diff --git a/app/Ninja/Datatables/ProductDatatable.php b/app/Ninja/Datatables/ProductDatatable.php
index 467efedbc347..43343f0cf753 100644
--- a/app/Ninja/Datatables/ProductDatatable.php
+++ b/app/Ninja/Datatables/ProductDatatable.php
@@ -24,7 +24,7 @@ class ProductDatatable extends EntityDatatable
[
'notes',
function ($model) {
- return nl2br(Str::limit($model->notes, 100));
+ return e(nl2br(Str::limit($model->notes, 100)));
},
],
[
diff --git a/app/Ninja/Datatables/RecurringInvoiceDatatable.php b/app/Ninja/Datatables/RecurringInvoiceDatatable.php
index 797d3ca0cf1c..a7d20ad355a8 100644
--- a/app/Ninja/Datatables/RecurringInvoiceDatatable.php
+++ b/app/Ninja/Datatables/RecurringInvoiceDatatable.php
@@ -64,7 +64,7 @@ class RecurringInvoiceDatatable extends EntityDatatable
[
'private_notes',
function ($model) {
- return $model->private_notes;
+ return e($model->private_notes);
},
],
[
diff --git a/app/Ninja/Datatables/TaskDatatable.php b/app/Ninja/Datatables/TaskDatatable.php
index a63460a31b1d..85722feec296 100644
--- a/app/Ninja/Datatables/TaskDatatable.php
+++ b/app/Ninja/Datatables/TaskDatatable.php
@@ -55,7 +55,7 @@ class TaskDatatable extends EntityDatatable
[
'description',
function ($model) {
- return $model->description;
+ return e($model->description);
},
],
[
diff --git a/app/Ninja/Datatables/UserDatatable.php b/app/Ninja/Datatables/UserDatatable.php
index ecc47039b9a1..4841dea1ea33 100644
--- a/app/Ninja/Datatables/UserDatatable.php
+++ b/app/Ninja/Datatables/UserDatatable.php
@@ -14,7 +14,7 @@ class UserDatatable extends EntityDatatable
[
'first_name',
function ($model) {
- return $model->public_id ? link_to('users/'.$model->public_id.'/edit', $model->first_name.' '.$model->last_name)->toHtml() : ($model->first_name.' '.$model->last_name);
+ return $model->public_id ? link_to('users/'.$model->public_id.'/edit', $model->first_name.' '.$model->last_name)->toHtml() : e($model->first_name.' '.$model->last_name);
},
],
[
diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php
index d136b159f1ff..f83112c6a7ce 100644
--- a/app/Ninja/Import/BaseTransformer.php
+++ b/app/Ninja/Import/BaseTransformer.php
@@ -101,35 +101,17 @@ class BaseTransformer extends TransformerAbstract
*
* @return null
*/
- public function getProductId($name)
+ public function getProduct($data, $key, $field, $default = false)
{
- $name = strtolower(trim($name));
+ $productKey = trim(strtolower($data->$key));
- return isset($this->maps[ENTITY_PRODUCT][$name]) ? $this->maps[ENTITY_PRODUCT][$name] : null;
- }
+ if (! isset($this->maps['product'][$productKey])) {
+ return $default;
+ }
- /**
- * @param $name
- *
- * @return null
- */
- public function getProductNotes($name)
- {
- $name = strtolower(trim($name));
+ $product = $this->maps['product'][$productKey];
- return isset($this->maps['product_notes'][$name]) ? $this->maps['product_notes'][$name] : null;
- }
-
- /**
- * @param $name
- *
- * @return null
- */
- public function getProductCost($name)
- {
- $name = strtolower(trim($name));
-
- return isset($this->maps['product_cost'][$name]) ? $this->maps['product_cost'][$name] : null;
+ return $product->$field ?: $default;
}
/**
@@ -156,6 +138,30 @@ class BaseTransformer extends TransformerAbstract
return isset($this->maps['countries2'][$name]) ? $this->maps['countries2'][$name] : null;
}
+ /**
+ * @param $name
+ *
+ * @return null
+ */
+ public function getTaxRate($name)
+ {
+ $name = strtolower(trim($name));
+
+ return isset($this->maps['tax_rates'][$name]) ? $this->maps['tax_rates'][$name] : 0;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return null
+ */
+ public function getTaxName($name)
+ {
+ $name = strtolower(trim($name));
+
+ return isset($this->maps['tax_names'][$name]) ? $this->maps['tax_names'][$name] : '';
+ }
+
/**
* @param $name
*
diff --git a/app/Ninja/Import/CSV/ClientTransformer.php b/app/Ninja/Import/CSV/ClientTransformer.php
index b53a714530af..07ccab2ea5d4 100644
--- a/app/Ninja/Import/CSV/ClientTransformer.php
+++ b/app/Ninja/Import/CSV/ClientTransformer.php
@@ -26,22 +26,29 @@ class ClientTransformer extends BaseTransformer
'name' => $this->getString($data, 'name'),
'work_phone' => $this->getString($data, 'work_phone'),
'address1' => $this->getString($data, 'address1'),
+ 'address2' => $this->getString($data, 'address2'),
'city' => $this->getString($data, 'city'),
'state' => $this->getString($data, 'state'),
'postal_code' => $this->getString($data, 'postal_code'),
- 'private_notes' => $this->getString($data, 'notes'),
+ 'public_notes' => $this->getString($data, 'public_notes'),
+ 'private_notes' => $this->getString($data, 'private_notes'),
'website' => $this->getString($data, 'website'),
'vat_number' => $this->getString($data, 'vat_number'),
'id_number' => $this->getString($data, 'id_number'),
+ 'custom_value1' => $this->getString($data, 'custom1'),
+ 'custom_value2' => $this->getString($data, 'custom2'),
'contacts' => [
[
- 'first_name' => $this->getString($data, 'first_name'),
- 'last_name' => $this->getString($data, 'last_name'),
- 'email' => $this->getString($data, 'email'),
- 'phone' => $this->getString($data, 'phone'),
+ 'first_name' => $this->getString($data, 'contact_first_name'),
+ 'last_name' => $this->getString($data, 'contact_last_name'),
+ 'email' => $this->getString($data, 'contact_email'),
+ 'phone' => $this->getString($data, 'contact_phone'),
+ 'custom_value1' => $this->getString($data, 'contact_custom1'),
+ 'custom_value2' => $this->getString($data, 'contact_custom2'),
],
],
'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ 'currency_code' => $this->getString($data, 'currency'),
];
});
}
diff --git a/app/Ninja/Import/CSV/InvoiceTransformer.php b/app/Ninja/Import/CSV/InvoiceTransformer.php
index 0692badfb3fc..6d8f2e414888 100644
--- a/app/Ninja/Import/CSV/InvoiceTransformer.php
+++ b/app/Ninja/Import/CSV/InvoiceTransformer.php
@@ -38,9 +38,13 @@ class InvoiceTransformer extends BaseTransformer
'invoice_items' => [
[
'product_key' => $this->getString($data, 'product'),
- 'notes' => $this->getString($data, 'notes') ?: $this->getProductNotes($this->getString($data, 'product')),
- 'cost' => $this->getFloat($data, 'amount') ?: $this->getProductCost($this->getString($data, 'product')),
+ 'notes' => $this->getString($data, 'notes') ?: $this->getProduct($data, 'product', 'notes', ''),
+ 'cost' => $this->getFloat($data, 'amount') ?: $this->getProduct($data, 'product', 'cost', 0),
'qty' => $this->getFloat($data, 'quantity') ?: 1,
+ 'tax_name1' => $this->getTaxName($this->getString($data, 'tax1')),
+ 'tax_rate1' => $this->getTaxRate($this->getString($data, 'tax1')),
+ 'tax_name2' => $this->getTaxName($this->getString($data, 'tax2')),
+ 'tax_rate2' => $this->getTaxRate($this->getString($data, 'tax2')),
],
],
];
diff --git a/app/Ninja/Import/CSV/ProductTransformer.php b/app/Ninja/Import/CSV/ProductTransformer.php
index 8b372e28baa5..22e146e2c98a 100644
--- a/app/Ninja/Import/CSV/ProductTransformer.php
+++ b/app/Ninja/Import/CSV/ProductTransformer.php
@@ -23,6 +23,7 @@ class ProductTransformer extends BaseTransformer
return new Item($data, function ($data) {
return [
+ 'public_id' => $this->getProduct($data, 'product_key', 'public_id'),
'product_key' => $this->getString($data, 'product_key'),
'notes' => $this->getString($data, 'notes'),
'cost' => $this->getFloat($data, 'cost'),
diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php
index a9803550ebfe..5507ef2ccfce 100644
--- a/app/Ninja/Mailers/ContactMailer.php
+++ b/app/Ninja/Mailers/ContactMailer.php
@@ -68,14 +68,7 @@ class ContactMailer extends Mailer
$documentStrings = [];
if ($account->document_email_attachment && $invoice->hasDocuments()) {
- $documents = $invoice->documents;
-
- foreach ($invoice->expenses as $expense) {
- if ($expense->invoice_documents) {
- $documents = $documents->merge($expense->documents);
- }
- }
-
+ $documents = $invoice->allDocuments();
$documents = $documents->sortBy('size');
$size = 0;
@@ -238,17 +231,25 @@ class ContactMailer extends Mailer
/**
* @param Payment $payment
*/
- public function sendPaymentConfirmation(Payment $payment)
+ public function sendPaymentConfirmation(Payment $payment, $refunded = 0)
{
$account = $payment->account;
$client = $payment->client;
$account->loadLocalizationSettings($client);
-
$invoice = $payment->invoice;
$accountName = $account->getDisplayName();
- $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT);
- $emailSubject = $invoice->account->getEmailSubject(ENTITY_PAYMENT);
+
+ if ($refunded > 0) {
+ $emailSubject = trans('texts.refund_subject');
+ $emailTemplate = trans('texts.refund_body', [
+ 'amount' => $account->formatMoney($refunded, $client),
+ 'invoice_number' => $invoice->invoice_number,
+ ]);
+ } else {
+ $emailSubject = $invoice->account->getEmailSubject(ENTITY_PAYMENT);
+ $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT);
+ }
if ($payment->invitation) {
$user = $payment->invitation->user;
@@ -277,9 +278,10 @@ class ContactMailer extends Mailer
'entityType' => ENTITY_INVOICE,
'bccEmail' => $account->getBccEmail(),
'fromEmail' => $account->getFromEmail(),
+ 'isRefund' => $refunded > 0,
];
- if ($account->attachPDF()) {
+ if (! $refunded && $account->attachPDF()) {
$data['pdfString'] = $invoice->getPDFString();
$data['pdfFileName'] = $invoice->getFileName();
}
diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php
index e4698897195f..aacfb530fadb 100644
--- a/app/Ninja/Mailers/UserMailer.php
+++ b/app/Ninja/Mailers/UserMailer.php
@@ -119,14 +119,17 @@ class UserMailer extends Mailer
/**
* @param Invitation $invitation
*/
- public function sendMessage($user, $subject, $message, $invoice = false)
+ public function sendMessage($user, $subject, $message, $data = false)
{
if (! $user->email) {
return;
}
+ $invoice = $data && isset($data['invoice']) ? $data['invoice'] : false;
$view = 'user_message';
- $data = [
+
+ $data = $data ?: [];
+ $data += [
'userName' => $user->getDisplayName(),
'primaryMessage' => $subject,
'secondaryMessage' => $message,
diff --git a/app/Ninja/PaymentDrivers/StripePaymentDriver.php b/app/Ninja/PaymentDrivers/StripePaymentDriver.php
index c97d3ebfc2b1..fc93b1325069 100644
--- a/app/Ninja/PaymentDrivers/StripePaymentDriver.php
+++ b/app/Ninja/PaymentDrivers/StripePaymentDriver.php
@@ -119,6 +119,18 @@ class StripePaymentDriver extends BasePaymentDriver
$data = $this->paymentDetails();
$data['description'] = $client->getDisplayName();
+ // if a customer already exists link the token to it
+ if ($customer = $this->customer()) {
+ $data['customerReference'] = $customer->token;
+ // otherwise create a new customer
+ } else {
+ $response = $this->gateway()->createCustomer([
+ 'description' => $client->getDisplayName(),
+ 'email' => $this->contact()->email,
+ ])->send();
+ $data['customerReference'] = $response->getCustomerReference();
+ }
+
if (! empty($data['plaidPublicToken'])) {
$plaidResult = $this->getPlaidToken($data['plaidPublicToken'], $data['plaidAccountId']);
unset($data['plaidPublicToken']);
@@ -126,11 +138,6 @@ class StripePaymentDriver extends BasePaymentDriver
$data['token'] = $plaidResult['stripe_bank_account_token'];
}
- // if a customer already exists link the token to it
- if ($customer = $this->customer()) {
- $data['customerReference'] = $customer->token;
- }
-
$tokenResponse = $this->gateway()
->createCard($data)
->send();
@@ -146,7 +153,11 @@ class StripePaymentDriver extends BasePaymentDriver
public function creatingCustomer($customer)
{
- $customer->token = $this->tokenResponse['id'];
+ if (isset($this->tokenResponse['customer'])) {
+ $customer->token = $this->tokenResponse['customer'];
+ } else {
+ $customer->token = $this->tokenResponse['id'];
+ }
return $customer;
}
diff --git a/app/Ninja/Presenters/AccountPresenter.php b/app/Ninja/Presenters/AccountPresenter.php
index 156aade6237c..5cfd0e70dd68 100644
--- a/app/Ninja/Presenters/AccountPresenter.php
+++ b/app/Ninja/Presenters/AccountPresenter.php
@@ -22,6 +22,28 @@ class AccountPresenter extends Presenter
return $this->entity->name ?: trans('texts.untitled_account');
}
+ /**
+ * @return string
+ */
+ public function address()
+ {
+ $account = $this->entity;
+
+ $str = $account->address1 ?: '';
+
+ if ($account->address2 && $str) {
+ $str .= ', ';
+ }
+
+ $str .= $account->address2;
+
+ if ($account->getCityState() && $str) {
+ $str .= ' - ';
+ }
+
+ return $str . $account->getCityState();
+ }
+
/**
* @return string
*/
@@ -144,7 +166,7 @@ class AccountPresenter extends Presenter
if ($rate->is_inclusive) {
$name .= ' - ' . trans('texts.inclusive');
}
- $options[($rate->is_inclusive ? '1 ' : '0 ') . $rate->rate . ' ' . $rate->name] = $name;
+ $options[($rate->is_inclusive ? '1 ' : '0 ') . $rate->rate . ' ' . $rate->name] = e($name);
}
return $options;
diff --git a/app/Ninja/Presenters/EntityPresenter.php b/app/Ninja/Presenters/EntityPresenter.php
index 863199b05a18..89dc552817f3 100644
--- a/app/Ninja/Presenters/EntityPresenter.php
+++ b/app/Ninja/Presenters/EntityPresenter.php
@@ -33,7 +33,9 @@ class EntityPresenter extends Presenter
{
$class = $text = '';
- if ($this->entity->is_deleted) {
+ if (! $this->entity->id) {
+ return '';
+ } elseif ($this->entity->is_deleted) {
$class = 'danger';
$label = trans('texts.deleted');
} elseif ($this->entity->trashed()) {
diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php
index 1d7e969a3e1c..2063675aca0a 100644
--- a/app/Ninja/Presenters/InvoicePresenter.php
+++ b/app/Ninja/Presenters/InvoicePresenter.php
@@ -285,7 +285,11 @@ class InvoicePresenter extends EntityPresenter
$label = trans('texts.fee');
}
- return ' - ' . $fee . ' ' . $label;
+ $label = ' - ' . $fee . ' ' . $label;
+
+ $label .= ' ';
+
+ return $label;
}
public function multiAccountLink()
diff --git a/app/Ninja/Reports/AbstractReport.php b/app/Ninja/Reports/AbstractReport.php
index e327c60b7170..701147b15b91 100644
--- a/app/Ninja/Reports/AbstractReport.php
+++ b/app/Ninja/Reports/AbstractReport.php
@@ -52,9 +52,8 @@ class AbstractReport
$this->totals[$currencyId][$dimension][$field] += $value;
}
- public function tableHeader()
- {
- $str = '';
+ public function tableHeaderArray() {
+ $columns_labeled = [];
foreach ($this->columns as $key => $val) {
if (is_array($val)) {
@@ -75,9 +74,21 @@ class AbstractReport
$class = count($class) ? implode(' ', $class) : 'group-false';
$label = trans("texts.{$field}");
- $str .= "
t |
"+this.options.dictFallbackText+"
"),i+='',e=n.createElement(i),"FORM"!==this.element.tagName?(o=n.createElement(''),o.appendChild(e)):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=o?o:e)},n.prototype.getExistingFallback=function(){var t,e,n,i,o,a;for(e=function(t){var e,n,i;for(n=0,i=t.length;n0){for(s=["TB","GB","MB","KB","b"],n=r=0,c=s.length;rt<"F"ip>'),C.renderer?i.isPlainObject(C.renderer)&&!C.renderer.header&&(C.renderer.header="jqueryui"):C.renderer="jqueryui"):i.extend(O,Yt.ext.classes,m.oClasses),i(this).addClass(O.sTable),""===C.oScroll.sX&&""===C.oScroll.sY||(C.oScroll.iBarWidth=wt()),C.oScroll.sX===!0&&(C.oScroll.sX="100%"),C.iInitDisplayStart===n&&(C.iInitDisplayStart=m.iDisplayStart,C._iDisplayStart=m.iDisplayStart),null!==m.iDeferLoading){C.bDeferLoading=!0;var N=i.isArray(m.iDeferLoading);C._iRecordsDisplay=N?m.iDeferLoading[0]:m.iDeferLoading,C._iRecordsTotal=N?m.iDeferLoading[1]:m.iDeferLoading}var S=C.oLanguage;i.extend(!0,S,m.oLanguage),""!==S.sUrl&&(i.ajax({dataType:"json",url:S.sUrl,success:function(t){s(t),a(_.oLanguage,t),i.extend(!0,S,t),rt(C)},error:function(){rt(C)}}),v=!0),null===m.asStripeClasses&&(C.asStripeClasses=[O.sStripeOdd,O.sStripeEven]);var x=C.asStripeClasses,L=i("tbody tr:eq(0)",this);i.inArray(!0,i.map(x,function(t,e){return L.hasClass(t)}))!==-1&&(i("tbody tr",this).removeClass(x.join(" ")),C.asDestroyStripes=x.slice());var D,q=[],W=this.getElementsByTagName("thead");if(0!==W.length&&(R(C.aoHeader,W[0]),q=F(C)),null===m.aoColumns)for(D=[],g=0,p=q.length;g
").appendTo(this)),C.nTHead=X[0];var H=i(this).children("tbody");0===H.length&&(H=i("
").appendTo(this)),C.nTBody=H[0];var j=i(this).children("tfoot");if(0===j.length&&P.length>0&&(""!==C.oScroll.sX||""!==C.oScroll.sY)&&(j=i("").appendTo(this)),0===j.length||0===j.children().length?i(this).addClass(O.sNoFooter):j.length>0&&(C.nTFoot=j[0],R(C.aoFooter,C.nTFoot)),m.aaData)for(g=0;g