Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
David Bomba 2016-09-20 21:20:26 +10:00
commit 58ffb9b4a3
55 changed files with 493 additions and 106149 deletions

View File

@ -1,6 +1,7 @@
<?php namespace App\Console\Commands;
use DB;
use Mail;
use Carbon;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
@ -51,9 +52,12 @@ class CheckData extends Command {
*/
protected $description = 'Check/fix data';
protected $log = '';
protected $isValid = true;
public function fire()
{
$this->info(date('Y-m-d') . ' Running CheckData...');
$this->logMessage(date('Y-m-d') . ' Running CheckData...');
if (!$this->option('client_id')) {
$this->checkPaidToDate();
@ -66,7 +70,21 @@ class CheckData extends Command {
$this->checkAccountData();
}
$this->info('Done');
$this->logMessage('Done');
$errorEmail = env('ERROR_EMAIL');
if ( ! $this->isValid && $errorEmail) {
Mail::raw($this->log, function ($message) use ($errorEmail) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject('Check-Data');
});
}
}
private function logMessage($str)
{
$this->log .= $str . "\n";
}
private function checkBlankInvoiceHistory()
@ -76,7 +94,11 @@ class CheckData extends Command {
->where('json_backup', '=', '')
->count();
$this->info($count . ' activities with blank invoice backup');
if ($count > 0) {
$this->isValid = false;
}
$this->logMessage($count . ' activities with blank invoice backup');
}
private function checkAccountData()
@ -131,7 +153,8 @@ class CheckData extends Command {
->get(["{$table}.id", 'clients.account_id', 'clients.user_id']);
if (count($records)) {
$this->info(count($records) . " {$table} records with incorrect {$entityType} account id");
$this->isValid = false;
$this->logMessage(count($records) . " {$table} records with incorrect {$entityType} account id");
if ($this->option('fix') == 'true') {
foreach ($records as $record) {
@ -161,7 +184,11 @@ class CheckData extends Command {
->groupBy('clients.id')
->havingRaw('clients.paid_to_date != sum(payments.amount - payments.refunded) and clients.paid_to_date != 999999999.9999')
->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]);
$this->info(count($clients) . ' clients with incorrect paid to date');
$this->logMessage(count($clients) . ' clients with incorrect paid to date');
if (count($clients) > 0) {
$this->isValid = false;
}
if ($this->option('fix') == 'true') {
foreach ($clients as $client) {
@ -178,6 +205,7 @@ class CheckData extends Command {
$clients = DB::table('clients')
->join('invoices', 'invoices.client_id', '=', 'clients.id')
->join('accounts', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '!=', 20432)
->where('clients.is_deleted', '=', 0)
->where('invoices.is_deleted', '=', 0)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
@ -187,14 +215,18 @@ class CheckData extends Command {
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
}
$clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at')
->orderBy('accounts.company_id', 'DESC')
->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]);
$this->info(count($clients) . ' clients with incorrect balance/activities');
$this->logMessage(count($clients) . ' clients with incorrect balance/activities');
if (count($clients) > 0) {
$this->isValid = false;
}
foreach ($clients as $client) {
$this->info("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ===");
$this->logMessage("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ===");
$foundProblem = false;
$lastBalance = 0;
$lastAdjustment = 0;
@ -204,7 +236,7 @@ class CheckData extends Command {
->where('client_id', '=', $client->id)
->orderBy('activities.id')
->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']);
//$this->info(var_dump($activities));
//$this->logMessage(var_dump($activities));
foreach ($activities as $activity) {
@ -251,19 +283,19 @@ class CheckData extends Command {
// **Fix for ninja invoices which didn't have the invoice_type_id value set
if ($noAdjustment && $client->account_id == 20432) {
$this->info("No adjustment for ninja invoice");
$this->logMessage("No adjustment for ninja invoice");
$foundProblem = true;
$clientFix += $invoice->amount;
$activityFix = $invoice->amount;
// **Fix for allowing converting a recurring invoice to a normal one without updating the balance**
} elseif ($noAdjustment && $invoice->invoice_type_id == INVOICE_TYPE_STANDARD && !$invoice->is_recurring) {
$this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}");
$this->logMessage("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}");
$foundProblem = true;
$clientFix += $invoice->amount;
$activityFix = $invoice->amount;
// **Fix for updating balance when creating a quote or recurring invoice**
} elseif ($activity->adjustment != 0 && ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE || $invoice->is_recurring)) {
$this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}");
$this->logMessage("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}");
$foundProblem = true;
$clientFix -= $activity->adjustment;
$activityFix = 0;
@ -271,7 +303,7 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE) {
// **Fix for updating balance when deleting a recurring invoice**
if ($activity->adjustment != 0 && $invoice->is_recurring) {
$this->info("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}");
$this->logMessage("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}");
$foundProblem = true;
if ($activity->balance != $lastBalance) {
$clientFix -= $activity->adjustment;
@ -281,7 +313,7 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE) {
// **Fix for updating balance when archiving an invoice**
if ($activity->adjustment != 0 && !$invoice->is_recurring) {
$this->info("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}");
$this->logMessage("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}");
$foundProblem = true;
$activityFix = 0;
$clientFix += $activity->adjustment;
@ -289,12 +321,12 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE) {
// **Fix for updating balance when updating recurring invoice**
if ($activity->adjustment != 0 && $invoice->is_recurring) {
$this->info("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}");
$this->logMessage("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}");
$foundProblem = true;
$clientFix -= $activity->adjustment;
$activityFix = 0;
} else if ((strtotime($activity->created_at) - strtotime($lastCreatedAt) <= 1) && $activity->adjustment > 0 && $activity->adjustment == $lastAdjustment) {
$this->info("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}");
$this->logMessage("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}");
$foundProblem = true;
$clientFix -= $activity->adjustment;
$activityFix = 0;
@ -302,7 +334,7 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) {
// **Fix for updating balance when updating a quote**
if ($activity->balance != $lastBalance) {
$this->info("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}");
$this->logMessage("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}");
$foundProblem = true;
$clientFix += $lastBalance - $activity->balance;
$activityFix = 0;
@ -310,7 +342,7 @@ class CheckData extends Command {
} else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) {
// **Fix for deleting payment after deleting invoice**
if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) {
$this->info("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}");
$this->logMessage("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}");
$foundProblem = true;
$activityFix = 0;
$clientFix -= $activity->adjustment;
@ -339,7 +371,7 @@ class CheckData extends Command {
}
if ($activity->balance + $clientFix != $client->actual_balance) {
$this->info("** Creating 'recovered update' activity **");
$this->logMessage("** Creating 'recovered update' activity **");
if ($this->option('fix') == 'true') {
DB::table('activities')->insert([
'created_at' => new Carbon,
@ -353,7 +385,7 @@ class CheckData extends Command {
}
$data = ['balance' => $client->actual_balance];
$this->info("Corrected balance:{$client->actual_balance}");
$this->logMessage("Corrected balance:{$client->actual_balance}");
if ($this->option('fix') == 'true') {
DB::table('clients')
->where('id', $client->id)

View File

@ -1,5 +1,6 @@
<?php namespace App\Http\Controllers;
use Utils;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@ -20,4 +21,20 @@ class BaseController extends Controller
$this->layout = View::make($this->layout);
}
}
protected function returnBulk($entityType, $action, $ids)
{
$isDatatable = filter_var(request()->datatable, FILTER_VALIDATE_BOOLEAN);
$entityTypes = Utils::pluralizeEntityType($entityType);
if ($action == 'restore' && count($ids) == 1) {
return redirect("{$entityTypes}/" . $ids[0]);
} elseif ($isDatatable || ($action == 'archive' || $action == 'delete')) {
return redirect("{$entityTypes}");
} elseif (count($ids)) {
return redirect("{$entityTypes}/" . $ids[0]);
} else {
return redirect("{$entityTypes}");
}
}
}

View File

@ -221,10 +221,6 @@ class ClientController extends BaseController
$message = Utils::pluralize($action.'d_client', $count);
Session::flash('message', $message);
if ($action == 'restore' && $count == 1) {
return Redirect::to('clients/'.Utils::getFirst($ids));
} else {
return Redirect::to('clients');
}
return $this->returnBulk(ENTITY_CLIENT, $action, $ids);
}
}

View File

@ -42,12 +42,12 @@ class DashboardApiController extends BaseAPIController
$data = [
'id' => 1,
'paidToDate' => $paidToDate[0]->value ? $paidToDate[0]->value : 0,
'paidToDateCurrency' => $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : 0,
'balances' => $balances[0]->value ? $balances[0]->value : 0,
'balancesCurrency' => $balances[0]->currency_id ? $balances[0]->currency_id : 0,
'averageInvoice' => (count($averageInvoice) && $averageInvoice[0]->invoice_avg) ? $averageInvoice[0]->invoice_avg : 0,
'averageInvoiceCurrency' => $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : 0,
'paidToDate' => count($paidToDate) && $paidToDate[0]->value ? $paidToDate[0]->value : 0,
'paidToDateCurrency' => count($paidToDate) && $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : 0,
'balances' => count($balances) && $balances[0]->value ? $balances[0]->value : 0,
'balancesCurrency' => count($balances) && $balances[0]->currency_id ? $balances[0]->currency_id : 0,
'averageInvoice' => count($averageInvoice) && $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0,
'averageInvoiceCurrency' => count($averageInvoice) && $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : 0,
'invoicesSent' => $metrics ? $metrics->invoices_sent : 0,
'activeClients' => $metrics ? $metrics->active_clients : 0,
'activities' => $this->createCollection($activities, new ActivityTransformer(), ENTITY_ACTIVITY),

View File

@ -245,7 +245,7 @@ class ExpenseController extends BaseController
Session::flash('message', $message);
}
return Redirect::to('expenses');
return $this->returnBulk($this->entityType, $action, $ids);
}
private static function getViewModel()

View File

@ -330,7 +330,7 @@ class InvoiceController extends BaseController
}
}
}
// Tax rate $options
$account = Auth::user()->account;
$rates = TaxRate::scope()->orderBy('name')->get();
@ -531,11 +531,7 @@ class InvoiceController extends BaseController
Session::flash('message', $message);
}
if ($action == 'restore' && $count == 1) {
return Redirect::to("{$entityType}s/".Utils::getFirst($ids));
} else {
return Redirect::to("{$entityType}s");
}
return $this->returnBulk($entityType, $action, $ids);
}
public function convertQuote(InvoiceRequest $request)
@ -612,8 +608,10 @@ class InvoiceController extends BaseController
return View::make('invoices.history', $data);
}
public function checkInvoiceNumber($invoiceNumber)
public function checkInvoiceNumber()
{
$invoiceNumber = request()->invoice_number;
$count = Invoice::scope()
->whereInvoiceNumber($invoiceNumber)
->withTrashed()

View File

@ -154,11 +154,7 @@ class QuoteController extends BaseController
Session::flash('message', $message);
}
if ($action == 'restore' && $count == 1) {
return Redirect::to('quotes/'.Utils::getFirst($ids));
} else {
return Redirect::to('quotes');
}
return $this->returnBulk(ENTITY_QUOTE, $action, $ids);
}
public function approve($invitationKey)

View File

@ -11,6 +11,7 @@ use App\Models\Account;
use App\Models\Client;
use App\Models\Payment;
use App\Models\Expense;
use App\Models\Task;
/**
* Class ReportController
@ -56,8 +57,8 @@ class ReportController extends BaseController
if (Input::all()) {
$reportType = Input::get('report_type');
$dateField = Input::get('date_field');
$startDate = Utils::toSqlDate(Input::get('start_date'), false);
$endDate = Utils::toSqlDate(Input::get('end_date'), false);
$startDate = date_create(Input::get('start_date'));
$endDate = date_create(Input::get('end_date'));
} else {
$reportType = ENTITY_INVOICE;
$dateField = FILTER_INVOICE_DATE;
@ -71,15 +72,17 @@ class ReportController extends BaseController
ENTITY_PRODUCT => trans('texts.product'),
ENTITY_PAYMENT => trans('texts.payment'),
ENTITY_EXPENSE => trans('texts.expense'),
ENTITY_TASK => trans('texts.task'),
ENTITY_TAX_RATE => trans('texts.tax'),
];
$params = [
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)),
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)),
'startDate' => $startDate->format('Y-m-d'),
'endDate' => $endDate->format('Y-m-d'),
'reportTypes' => $reportTypes,
'reportType' => $reportType,
'title' => trans('texts.charts_and_reports'),
'account' => Auth::user()->account,
];
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
@ -120,9 +123,37 @@ class ReportController extends BaseController
return $this->generateTaxRateReport($startDate, $endDate, $dateField, $isExport);
} elseif ($reportType == ENTITY_EXPENSE) {
return $this->generateExpenseReport($startDate, $endDate, $isExport);
} elseif ($reportType == ENTITY_TASK) {
return $this->generateTaskReport($startDate, $endDate, $isExport);
}
}
private function generateTaskReport($startDate, $endDate, $isExport)
{
$columns = ['client', 'date', 'description', 'duration'];
$displayData = [];
$tasks = Task::scope()
->with('client.contacts')
->withArchived()
->dateRange($startDate, $endDate);
foreach ($tasks->get() as $task) {
$displayData[] = [
$task->client ? ($isExport ? $task->client->getDisplayName() : $task->client->present()->link) : trans('texts.unassigned'),
link_to($task->present()->url, $task->getStartTime()),
$task->present()->description,
Utils::formatTime($task->getDuration()),
];
}
return [
'columns' => $columns,
'displayData' => $displayData,
'reportTotals' => [],
];
}
/**
* @param $startDate
* @param $endDate

View File

@ -300,11 +300,7 @@ class TaskController extends BaseController
$message = Utils::pluralize($action.'d_task', $count);
Session::flash('message', $message);
if ($action == 'restore' && $count == 1) {
return Redirect::to('tasks/'.$ids[0].'/edit');
} else {
return Redirect::to('tasks');
}
return $this->returnBulk($this->entityType, $action, $ids);
}
}

View File

@ -81,7 +81,7 @@ class VendorController extends BaseController
public function show(VendorRequest $request)
{
$vendor = $request->entity();
$actionLinks = [
['label' => trans('texts.new_vendor'), 'url' => URL::to('/vendors/create/' . $vendor->public_id)]
];
@ -185,10 +185,6 @@ class VendorController extends BaseController
$message = Utils::pluralize($action.'d_vendor', $count);
Session::flash('message', $message);
if ($action == 'restore' && $count == 1) {
return Redirect::to('vendors/' . Utils::getFirst($ids));
} else {
return Redirect::to('vendors');
}
return $this->returnBulk($this->entityType, $action, $ids);
}
}

View File

@ -25,6 +25,7 @@ class ApiCheck {
{
$loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register');
$headers = Utils::getApiHeaders();
$hasApiSecret = false;
if ($secret = env(API_SECRET)) {
$hasApiSecret = hash_equals($request->api_secret ?: '', $secret);
@ -34,7 +35,7 @@ class ApiCheck {
// check API secret
if ( ! $hasApiSecret) {
sleep(ERROR_DELAY);
return Response::json('Invalid secret', 403, $headers);
return Response::json('Invalid value for API_SECRET', 403, $headers);
}
} else {
// check for a valid token

View File

@ -13,7 +13,7 @@ class VerifyCsrfToken extends BaseVerifier
* @var array
*/
private $openRoutes = [
'complete',
'complete/*',
'signup/register',
'api/v1/*',
'api/v1/login',

View File

@ -127,7 +127,7 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']);
Route::get('check_invoice_number/{invoice_number}', 'InvoiceController@checkInvoiceNumber');
Route::get('check_invoice_number', 'InvoiceController@checkInvoiceNumber');
Route::get('save_sidebar_state', 'UserController@saveSidebarState');
Route::get('settings/user_details', 'AccountController@showUserDetails');
@ -623,7 +623,7 @@ if (!defined('CONTACT_EMAIL')) {
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', '2.7.0' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '2.7.1' . 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'));

View File

@ -48,9 +48,15 @@ class HistoryUtils
$entity = $activity->client;
} else if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_TASK || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_TASK) {
$entity = $activity->task;
if ( ! $entity) {
continue;
}
$entity->setRelation('client', $activity->client);
} else {
$entity = $activity->invoice;
if ( ! $entity) {
continue;
}
$entity->setRelation('client', $activity->client);
}

View File

@ -165,6 +165,14 @@ class Task extends EntityModel
return '#' . $this->public_id;
}
public function scopeDateRange($query, $startDate, $endDate)
{
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) >= ' . $startDate->format('U'));
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->format('U'));
return $query;
}
}

View File

@ -184,6 +184,7 @@ trait PresentsInvoice
'quote_to',
'details',
'invoice_no',
'quote_no',
'valid_until',
'client_name',
'address1',

View File

@ -557,16 +557,16 @@ class BasePaymentDriver
$paymentMethod->setRelation('account_gateway_token', $customer);
$paymentMethod = $this->creatingPaymentMethod($paymentMethod);
// archive the old payment method
$oldPaymentMethod = PaymentMethod::clientId($this->client()->id)
->wherePaymentTypeId($paymentMethod->payment_type_id)
->first();
if ($oldPaymentMethod) {
$oldPaymentMethod->delete();
}
if ($paymentMethod) {
// archive the old payment method
$oldPaymentMethod = PaymentMethod::clientId($this->client()->id)
->wherePaymentTypeId($paymentMethod->payment_type_id)
->first();
if ($oldPaymentMethod) {
$oldPaymentMethod->delete();
}
$paymentMethod->save();
}
@ -833,8 +833,8 @@ class BasePaymentDriver
return true;
}
$accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id',
'=', $gatewayTypeId)->first();
$accountGatewaySettings = AccountGatewaySettings::scope(false, $this->invitation->account_id)
->where('account_gateway_settings.gateway_type_id', '=', $gatewayTypeId)->first();
if ($accountGatewaySettings) {
$invoice = $this->invoice();

View File

@ -21,6 +21,11 @@ class TaskPresenter extends EntityPresenter
return $this->entity->user->getDisplayName();
}
public function description()
{
return substr($this->entity->description, 0, 40) . (strlen($this->entity->description) > 40 ? '...' : '');
}
/**
* @param $account
* @return mixed

View File

@ -78,7 +78,6 @@ class ClientRepository extends BaseRepository
$client = Client::createNew();
} else {
$client = Client::scope($publicId)->with('contacts')->firstOrFail();
\Log::warning('Entity not set in client repo save');
}
// convert currency code to id

View File

@ -68,7 +68,6 @@ class TaskRepository
// do nothing
} elseif ($publicId) {
$task = Task::scope($publicId)->firstOrFail();
\Log::warning('Entity not set in task repo save');
} else {
$task = Task::createNew();
}

View File

@ -24,7 +24,7 @@ class ActivityTransformer extends EntityTransformer
return [
'id' => $activity->key(),
'activity_type_id' => $activity->activity_type_id,
'client_id' => $activity->client->public_id,
'client_id' => $activity->client ? $activity->client->public_id : null,
'user_id' => $activity->user->public_id + 1,
'invoice_id' => $activity->invoice ? $activity->invoice->public_id : null,
'payment_id' => $activity->payment ? $activity->payment->public_id : null,

View File

@ -36,3 +36,11 @@ Want to find out everything there is to know about how to use your Invoice Ninja
data_visualizations
api_tokens
user_management
.. _self_host:
.. toctree::
:maxdepth: 1
:caption: Self Host
iphone_app

46
docs/iphone_app.rst Normal file
View File

@ -0,0 +1,46 @@
iPhone Application
==================
The Invoice Ninja iPhone application allows a user to connect to their self-hosted Invoice Ninja web application.
Connecting your iPhone to your self-hosted invoice ninja installation requires a couple of easy steps.
Web app configuration
"""""""""""""""""""""
Firstly you'll need to add an additional field to your .env file which is located in the root directory of your self-hosted Invoice Ninja installation.
The additional field to add is API_SECRET, set this to your own defined alphanumeric string.
Save your .env file and now open Invoice Ninja on your iPhone.
iPhone configuration
""""""""""""""""""""
Once you have completed the in-app purchase to unlock the iPhone to connect to your own server, you'll be presented with two fields.
The first is the Base URL of your self-hosted installation, ie http://ninja.yourapp.com
The second field is the API_SECRET, enter in the API_SECRET you used in your .env file.
Click SAVE.
You should be able to login now from your iPhone!
FAQ:
""""
Q: I get a HTTP 500 error.
A: Most likely you have not entered your API_SECRET in your .env file
Q: I get a HTTP 403 error when i attempt to login with the iPhone.
A: Most likely your API_SECRET on the iPhone does not match that on your self-hosted installation.
Q: Do I need to create a token on the server?
A: No, this is not required. The server will automagically create a token if one does not exist on first login.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10530
public/js/Chart.min.js vendored

File diff suppressed because one or more lines are too long

9559
public/js/d3.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -44,6 +44,7 @@
.twitter-typeahead .tt-suggestion {
padding: 3px 20px;
white-space: nowrap;
cursor: pointer;
}
/*Added to menu element when it contains no content*/
@ -68,4 +69,4 @@
/*Added to the element that wraps highlighted text*/
.twitter-typeahead .tt-highlight {
}
}

View File

@ -210,7 +210,11 @@ NINJA.decodeJavascript = function(invoice, javascript)
if (invoice.partial > 0 && field == 'balance_due') {
field = 'partial_due';
} else if (invoice.is_quote) {
field = field.replace('invoice', 'quote');
if (field == 'due_date') {
field = 'valid_until';
} else {
field = field.replace('invoice', 'quote');
}
}
var label = invoiceLabels[field];
if (match.indexOf('UC') >= 0) {

View File

@ -643,6 +643,7 @@ $LANG = array(
'custom' => 'Custom',
'invoice_to' => 'Invoice to',
'invoice_no' => 'Invoice No.',
'quote_no' => 'Quote No.',
'recent_payments' => 'Recent Payments',
'outstanding' => 'Outstanding',
'manage_companies' => 'Manage Companies',
@ -2068,7 +2069,7 @@ $LANG = array(
'bot_emailed_notify_viewed' => 'I\'ll email you when it\'s viewed.',
'bot_emailed_notify_paid' => 'I\'ll email you when it\'s paid.',
'add_product_to_invoice' => 'Add 1 :product',
'not_authorized' => 'Your are not authorized',
'not_authorized' => 'You are not authorized',
'bot_get_email' => 'Hi! (wave)<br/>Thanks for trying the Invoice Ninja Bot.<br/>Send me your account email to get started.',
'bot_get_code' => 'Thanks! I\'ve sent a you an email with your security code.',
'bot_welcome' => 'That\'s it, your account is verified.<br/>',
@ -2129,6 +2130,11 @@ $LANG = array(
'max' => 'Max',
'limits_not_met' => 'This invoice does not meet the limits for that payment type.',
'date_range' => 'Date Range',
'raw' => 'Raw',
'raw_html' => 'Raw HTML',
'update' => 'Update',
);
return $LANG;

View File

@ -37,7 +37,7 @@
{{ Session::get("show_trash:gateway") ? 'checked' : ''}}/>&nbsp; {{ trans('texts.show_archived_deleted')}} {{ Utils::transFlowText('gateways') }}
</label>
-->
@if ($showAdd)
{!! Button::primary(trans('texts.add_gateway'))
->asLinkTo(URL::to('/gateways/create'))
@ -72,39 +72,41 @@
</div>
<div class="modal-body">
<div class="row" style="text-align:center">
<div class="col-xs-12">
<div id="payment-limits-slider"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div id="payment-limit-min-container">
<label for="payment-limit-min">{{ trans('texts.min') }}</label><br>
<div class="input-group">
<span class="input-group-addon">{{ $currency->symbol }}</span>
<input type="number" class="form-control" min="0" id="payment-limit-min"
name="limit_min">
</div>
<label><input type="checkbox" id="payment-limit-min-enable"
name="limit_min_enable"> {{ trans('texts.enable_min') }}</label>
<div class="panel-body">
<div class="row" style="text-align:center">
<div class="col-xs-12">
<div id="payment-limits-slider"></div>
</div>
</div>
<div class="col-md-6">
<div id="payment-limit-max-container">
<label for="payment-limit-max">{{ trans('texts.max') }}</label><br>
</div><br/>
<div class="row">
<div class="col-md-6">
<div id="payment-limit-min-container">
<label for="payment-limit-min">{{ trans('texts.min') }}</label><br>
<div class="input-group" style="padding-bottom:8px">
<span class="input-group-addon">{{ $currency->symbol }}</span>
<input type="number" class="form-control" min="0" id="payment-limit-min"
name="limit_min">
</div>
<label><input type="checkbox" id="payment-limit-min-enable"
name="limit_min_enable"> {{ trans('texts.enable_min') }}</label>
</div>
</div>
<div class="col-md-6">
<div id="payment-limit-max-container">
<label for="payment-limit-max">{{ trans('texts.max') }}</label><br>
<div class="input-group">
<span class="input-group-addon">{{ $currency->symbol }}</span>
<input type="number" class="form-control" min="0" id="payment-limit-max"
name="limit_max">
<div class="input-group" style="padding-bottom:8px">
<span class="input-group-addon">{{ $currency->symbol }}</span>
<input type="number" class="form-control" min="0" id="payment-limit-max"
name="limit_max">
</div>
<label><input type="checkbox" id="payment-limit-max-enable"
name="limit_max_enable"> {{ trans('texts.enable_max') }}</label>
</div>
<label><input type="checkbox" id="payment-limit-max-enable"
name="limit_max_enable"> {{ trans('texts.enable_max') }}</label>
</div>
</div>
<input type="hidden" name="gateway_type_id" id="payment-limit-gateway-type">
</div>
<input type="hidden" name="gateway_type_id" id="payment-limit-gateway-type">
</div>
<div class="modal-footer" style="margin-top: 0px">
@ -168,24 +170,24 @@
});
limitsSlider.noUiSlider.on('update', function (values, handle) {
var value = values[handle];
var value = Math.round(values[handle]);
if (handle == 1) {
$('#payment-limit-max').val(Math.round(value)).removeAttr('disabled');
$('#payment-limit-max').val(value).removeAttr('disabled');
$('#payment-limit-max-enable').prop('checked', true);
} else {
$('#payment-limit-min').val(Math.round(value)).removeAttr('disabled');
$('#payment-limit-min').val(value).removeAttr('disabled');
$('#payment-limit-min-enable').prop('checked', true);
}
});
$('#payment-limit-min').on('change keyup', function () {
$('#payment-limit-min').on('change input', function () {
setTimeout(function () {
limitsSlider.noUiSlider.set([$('#payment-limit-min').val(), null]);
}, 100);
$('#payment-limit-min-enable').attr('checked', 'checked');
});
$('#payment-limit-max').on('change keyup', function () {
$('#payment-limit-max').on('change input', function () {
setTimeout(function () {
limitsSlider.noUiSlider.set([null, $('#payment-limit-max').val()]);
}, 100);

View File

@ -64,10 +64,11 @@
</div>
<p>&nbsp;<p/>
<div class="row">
<div class="col-md-10 show-when-ready" style="display:none">
<div class="col-md-9 show-when-ready" style="display:none">
@include('partials/quill_toolbar', ['name' => $field])
</div>
<div class="col-md-2 pull-right" style="padding-top:10px">
<div class="col-md-3 pull-right" style="padding-top:10px">
{!! Button::normal(trans('texts.raw'))->withAttributes(['onclick' => 'showRaw("'.$field.'")'])->small() !!}
{!! Button::primary(trans('texts.preview'))->withAttributes(['onclick' => 'serverPreview("'.$field.'")'])->small() !!}
</div>
</div>

View File

@ -104,6 +104,25 @@
</div>
</div>
<div class="modal fade" id="rawModal" tabindex="-1" role="dialog" aria-labelledby="rawModalLabel" aria-hidden="true">
<div class="modal-dialog" style="width:800px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="rawModalLabel">{{ trans('texts.raw_html') }}</h4>
</div>
<div class="modal-body">
<textarea id="raw-textarea" rows="20" style="width:100%"></textarea>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.close') }}</button>
<button type="button" onclick="updateRaw()" class="btn btn-success" data-dismiss="modal">{{ trans('texts.update') }}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="templateHelpModal" tabindex="-1" role="dialog" aria-labelledby="templateHelpModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
@ -184,7 +203,6 @@
}
function serverPreview(field) {
console.log(field);
$('#templatePreviewModal').modal('show');
var template = $('#email_template_' + field).val();
var url = '{{ URL::to('settings/email_preview') }}?template=' + template;
@ -314,6 +332,55 @@
});
}
function showRaw(field) {
window.rawHtmlField = field;
var template = $('#email_template_' + field).val();
$('#raw-textarea').val(formatXml(template));
$('#rawModal').modal('show');
}
function updateRaw() {
var value = $('#raw-textarea').val();
var field = window.rawHtmlField;
editors[field].setHTML(value);
value = editors[field].getHTML();
var fieldName = 'email_template_' + field;
$('#' + fieldName).val(value);
refreshPreview();
}
// https://gist.github.com/sente/1083506
function formatXml(xml) {
var formatted = '';
var reg = /(>)(<)(\/*)/g;
xml = xml.replace(reg, '$1\r\n$2$3');
var pad = 0;
jQuery.each(xml.split('\r\n'), function(index, node) {
var indent = 0;
if (node.match( /.+<\/\w[^>]*>$/ )) {
indent = 0;
} else if (node.match( /^<\/\w/ )) {
if (pad != 0) {
pad -= 1;
}
} else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
indent = 1;
} else {
indent = 0;
}
var padding = '';
for (var i = 0; i < pad; i++) {
padding += ' ';
}
formatted += padding + node + '\r\n';
pad += indent;
});
return formatted;
}
</script>
@stop

View File

@ -45,4 +45,14 @@
@include('export.payments')
@endif
</html>
@if (isset($vendors) && $vendors && count($vendors))
<tr><td>{{ strtoupper(trans('texts.vendors')) }}</td></tr>
@include('export.vendors')
@endif
@if (isset($vendor_contacts) && $vendor_contacts && count($vendor_contacts))
<tr><td>{{ strtoupper(trans('texts.vendor_contacts')) }}</td></tr>
@include('export.vendor_contacts')
@endif
</html>

View File

@ -9,10 +9,10 @@
<td>{{ trans('texts.phone') }}</td>
</tr>
@foreach ($contacts as $contact)
@if (!$contact->client->is_deleted)
@foreach ($vendor_contacts as $contact)
@if (!$contact->vendor->is_deleted)
<tr>
<td>{{ $contact->client->getDisplayName() }}</td>
<td>{{ $contact->vendor->getDisplayName() }}</td>
@if ($multiUser)
<td>{{ $contact->user->getDisplayName() }}</td>
@endif
@ -24,4 +24,4 @@
@endif
@endforeach
<tr><td></td></tr>
<tr><td></td></tr>

View File

@ -3,43 +3,27 @@
@if ($multiUser)
<td>{{ trans('texts.user') }}</td>
@endif
<td>{{ trans('texts.balance') }}</td>
<td>{{ trans('texts.paid_to_date') }}</td>
<td>{{ trans('texts.address1') }}</td>
<td>{{ trans('texts.address2') }}</td>
<td>{{ trans('texts.city') }}</td>
<td>{{ trans('texts.state') }}</td>
<td>{{ trans('texts.postal_code') }}</td>
<td>{{ trans('texts.country') }}</td>
@if ($account->custom_client_label1)
<td>{{ $account->custom_client_label1 }}</td>
@endif
@if ($account->custom_client_label2)
<td>{{ $account->custom_client_label2 }}</td>
@endif
</tr>
@foreach ($clients as $client)
@foreach ($vendors as $vendor)
<tr>
<td>{{ $client->getDisplayName() }}</td>
<td>{{ $vendor->getDisplayName() }}</td>
@if ($multiUser)
<td>{{ $client->user->getDisplayName() }}</td>
@endif
<td>{{ $account->formatMoney($client->balance, $client) }}</td>
<td>{{ $account->formatMoney($client->paid_to_date, $client) }}</td>
<td>{{ $client->address1 }}</td>
<td>{{ $client->address2 }}</td>
<td>{{ $client->city }}</td>
<td>{{ $client->state }}</td>
<td>{{ $client->postal_code }}</td>
<td>{{ $client->present()->country }}</td>
@if ($account->custom_client_label1)
<td>{{ $client->custom_value1 }}</td>
@endif
@if ($account->custom_client_label2)
<td>{{ $client->custom_value2 }}</td>
<td>{{ $vendor->user->getDisplayName() }}</td>
@endif
<td>{{ $vendor->address1 }}</td>
<td>{{ $vendor->address2 }}</td>
<td>{{ $vendor->city }}</td>
<td>{{ $vendor->state }}</td>
<td>{{ $vendor->postal_code }}</td>
<td>{{ $vendor->present()->country }}</td>
</tr>
@endforeach
<tr><td></td></tr>
<tr><td></td></tr>

View File

@ -199,8 +199,11 @@
}
window.loadedSearchData = false;
function onSearchBlur() {
$('#search').typeahead('val', '');
}
function onSearchFocus() {
$('#search').typeahead('val', '');
$('#search-form').show();
if (!window.loadedSearchData) {
@ -319,6 +322,7 @@
// Focus the search input if the user clicks forward slash
$('#search').focusin(onSearchFocus);
$('#search').blur(onSearchBlur);
$('body').keypress(function(event) {
if (event.which == 47 && !$('*:focus').length) {

View File

@ -262,8 +262,8 @@
</div>
</td>
<td>
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
rows="1" cols="60" style="resize: vertical" class="form-control word-wrap"></textarea>
<textarea data-bind="value: notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
rows="1" cols="60" style="resize: vertical;height:42px" class="form-control word-wrap"></textarea>
<input type="text" data-bind="value: task_public_id, attr: {name: 'invoice_items[' + $index() + '][task_public_id]'}" style="display: none"/>
<input type="text" data-bind="value: expense_public_id, attr: {name: 'invoice_items[' + $index() + '][expense_public_id]'}" style="display: none"/>
</td>
@ -340,11 +340,11 @@
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="notes" style="padding-bottom:44px">
{!! Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'")
{!! Former::textarea('public_notes')->data_bind("value: public_notes, valueUpdate: 'afterkeydown'")
->label(null)->style('resize: none; width: 500px;')->rows(4) !!}
</div>
<div role="tabpanel" class="tab-pane" id="terms">
{!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'")
{!! Former::textarea('terms')->data_bind("value:terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'")
->label(false)->style('resize: none; width: 500px')->rows(4)
->help('<div class="checkbox">
<label>
@ -356,7 +356,7 @@
</div>') !!}
</div>
<div role="tabpanel" class="tab-pane" id="footer">
{!! Former::textarea('invoice_footer')->data_bind("value:wrapped_footer, placeholder: footer_placeholder, valueUpdate: 'afterkeydown'")
{!! Former::textarea('invoice_footer')->data_bind("value:invoice_footer, placeholder: footer_placeholder, valueUpdate: 'afterkeydown'")
->label(false)->style('resize: none; width: 500px')->rows(4)
->help('<div class="checkbox">
<label>
@ -1108,10 +1108,9 @@
});
$('textarea').on('keyup focus', function(e) {
while($(this).outerHeight() < this.scrollHeight + parseFloat($(this).css("borderTopWidth")) + parseFloat($(this).css("borderBottomWidth"))) {
$(this).height($(this).height()+1);
};
$(this).height(0).height(this.scrollHeight-18);
});
}
function createInvoiceModel() {

View File

@ -296,40 +296,6 @@ function InvoiceModel(data) {
}
})
self.wrapped_terms = ko.computed({
read: function() {
return this.terms();
},
write: function(value) {
value = wordWrapText(value, 300);
self.terms(value);
},
owner: this
});
self.wrapped_notes = ko.computed({
read: function() {
return this.public_notes();
},
write: function(value) {
value = wordWrapText(value, 300);
self.public_notes(value);
},
owner: this
});
self.wrapped_footer = ko.computed({
read: function() {
return this.invoice_footer();
},
write: function(value) {
value = wordWrapText(value, 600);
self.invoice_footer(value);
},
owner: this
});
self.removeItem = function(item) {
self.invoice_items.remove(item);
refreshPDF(true);
@ -745,18 +711,6 @@ function ItemModel(data) {
ko.mapping.fromJS(data, {}, this);
}
self.wrapped_notes = ko.computed({
read: function() {
return this.notes();
},
write: function(value) {
value = wordWrapText(value, 235);
self.notes(value);
onItemChange();
},
owner: this
});
this.totals = ko.observable();
this.totals.rawTotal = ko.computed(function() {
@ -899,7 +853,7 @@ ko.bindingHandlers.productTypeahead = {
};
function checkInvoiceNumber() {
var url = '{{ url('check_invoice_number') }}/' + $('#invoice_number').val();
var url = '{{ url('check_invoice_number') }}?invoice_number=' + encodeURIComponent($('#invoice_number').val());
$.get(url, function(data) {
var isValid = data == '{{ RESULT_SUCCESS }}' ? true : false;
if (isValid) {

View File

@ -7,6 +7,7 @@
<div style="display:none">
{!! Former::text('action') !!}
{!! Former::text('public_id') !!}
{!! Former::text('datatable')->value('true') !!}
</div>
@can('create', 'invoice')
@ -88,7 +89,7 @@
@endif
{!! Former::close() !!}
<script type="text/javascript">
function submitForm(action) {

View File

@ -1,5 +1,23 @@
@extends('payments.payment_method')
@section('head')
@parent
<script type="text/javascript">
$(function() {
$('.payment-form').submit(function(event) {
var $form = $(this);
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
return true;
});
});
</script>
@stop
@section('payment_details')
{!! Former::vertical_open($url)
@ -256,18 +274,4 @@
{!! Former::close() !!}
<script type="text/javascript">
$(function() {
$('.payment-form').submit(function(event) {
var $form = $(this);
// Disable the submit button to prevent repeated clicks
$form.find('button').prop('disabled', true);
return true;
});
});
</script>
@stop

View File

@ -9,7 +9,7 @@
Stripe.setPublishableKey('{{ $accountGateway->getPublishableStripeKey() }}');
$(function() {
var countries = {!! Cache::get('countries')->pluck('iso_3166_2','id') !!};
$('.payment-form').submit(function(event) {
$('.payment-form').unbind('submit').submit(function(event) {
if($('[name=plaidAccountId]').length)return;
var $form = $(this);

View File

@ -6,6 +6,7 @@
<script type="text/javascript" src="https://static.wepay.com/min/js/tokenization.v2.js"></script>
<script type="text/javascript">
$(function() {
$("#state").attr('maxlength', '3');
var countries = {!! Cache::get('countries')->pluck('iso_3166_2','id') !!};
WePay.set_endpoint('{{ WEPAY_ENVIRONMENT }}');
var $form = $('.payment-form');

View File

@ -1,9 +1,52 @@
@extends('header')
@section('head')
@parent
<script src="{{ asset('js/daterangepicker.min.js') }}" type="text/javascript"></script>
<link href="{{ asset('css/daterangepicker.css') }}" rel="stylesheet" type="text/css"/>
@stop
@section('content')
@parent
@include('accounts.nav', ['selected' => ACCOUNT_REPORTS, 'advanced' => true])
<script type="text/javascript">
$(function() {
var chartStartDate = moment("{{ $startDate }}");
var chartEndDate = moment("{{ $endDate }}");
// Initialize date range selector
function cb(start, end) {
$('#reportrange span').html(start.format('{{ $account->getMomentDateFormat() }}') + ' - ' + end.format('{{ $account->getMomentDateFormat() }}'));
$('#start_date').val(start.format('YYYY-MM-DD'));
$('#end_date').val(end.format('YYYY-MM-DD'));
}
$('#reportrange').daterangepicker({
locale: {
"format": "{{ $account->getMomentDateFormat() }}",
},
startDate: chartStartDate,
endDate: chartEndDate,
linkedCalendars: false,
ranges: {
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
}, cb);
cb(chartStartDate, chartEndDate);
});
</script>
{!! Former::open()->rules(['start_date' => 'required', 'end_date' => 'required'])->addClass('warn-on-exit') !!}
@ -24,13 +67,25 @@
<div class="row">
<div class="col-md-6">
{!! Former::text('start_date')->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT))
->addGroupClass('start_date')
->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'start_date\')"></i>') !!}
{!! Former::text('end_date')->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT))
->addGroupClass('end_date')
->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'end_date\')"></i>') !!}
<div class="form-group">
<label for="reportrange" class="control-label col-lg-4 col-sm-4">
{{ trans('texts.date_range') }}
</label>
<div class="col-lg-8 col-sm-8">
<div id="reportrange" style="background: #f9f9f9; cursor: pointer; padding: 9px 14px; border: 1px solid #dfe0e1; margin-top: 0px; margin-left:18px">
<i class="glyphicon glyphicon-calendar fa fa-calendar"></i>&nbsp;
<span></span> <b class="caret"></b>
</div>
<div style="display:none">
{!! Former::text('start_date') !!}
{!! Former::text('end_date') !!}
</div>
</div>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
{!! Former::actions(
Button::primary(trans('texts.export'))->withAttributes(array('onclick' => 'onExportClick()'))->appendIcon(Icon::create('export')),

View File

@ -12,7 +12,7 @@
<style type="text/css">
input.time-input {
width: 110px;
width: 100%;
font-size: 14px !important;
}

View File

@ -7,7 +7,7 @@
},
{
"stack": "$accountDetails",
"margin": [40, 0, 0, 0]
"margin": [7, 0, 0, 0]
},
{
"stack": "$accountAddress"
@ -215,4 +215,4 @@
}
},
"pageMargins": [40, 40, 40, 60]
}
}