Merge branch 'release-2.7.0'

Conflicts:
	app/Http/Controllers/DashboardController.php
	app/Http/Requests/UpdateInvoiceRequest.php
	app/Http/routes.php
	composer.lock
	resources/views/payments/credit_card.blade.php
This commit is contained in:
Hillel Coren 2016-09-12 13:36:53 +03:00
commit ebd258d655
230 changed files with 21977 additions and 45793 deletions

View File

@ -1,3 +1,3 @@
{
"directory": "./public/vendor"
"directory": "./resources/assets/bower"
}

3
.gitignore vendored
View File

@ -8,6 +8,7 @@
/public/build
/public/packages
/public/vendor
/resources/assets/bower
/storage
/bootstrap/compiled.php
/bootstrap/environment.php
@ -18,7 +19,7 @@ Thumbs.db
/.env
/.env.development.php
/.env.php
/docs/_build
/error_log
/auth.json
/public/error_log

View File

@ -8,7 +8,7 @@
[![Docs](https://readthedocs.org/projects/pip/badge/?version=latest)](http://docs.invoiceninja.com/en/latest/)
[![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## [Hosted](https://www.invoiceninja.com) | [Self-hosted](https://invoiceninja.org)
## [Hosted](https://www.invoiceninja.com) | [Self-hosted](https://www.invoiceninja.org)
All Pro and Enterprise features from our hosted app are included in both the [self-host zip](https://www.invoiceninja.com/self-host/) and the GitHub repository.
@ -42,6 +42,7 @@ Our [iPhone app](https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware
* Integrates with 50+ payment providers with [Omnipay](https://github.com/thephpleague/omnipay)
* Recurring invoices with auto-billing
* Expenses and vendors
* Import bank statements with [OFX](http://www.ofxhome.com/)
* Tasks with time-tracking
* File Attachments
* Multi-user/multi-company support

View File

@ -6,6 +6,7 @@ use App\Ninja\Repositories\AccountRepository;
use App\Services\PaymentService;
use App\Models\Invoice;
use App\Models\Account;
use Exception;
/**
* Class ChargeRenewalInvoices
@ -80,8 +81,12 @@ class ChargeRenewalInvoices extends Command
continue;
}
try {
$this->info("Charging invoice {$invoice->invoice_number}");
$this->paymentService->autoBillInvoice($invoice);
} catch (Exception $exception) {
$this->info('Error: ' . $exception->getMessage());
}
}
$this->info('Done');

View File

@ -57,15 +57,28 @@ class CheckData extends Command {
if (!$this->option('client_id')) {
$this->checkPaidToDate();
$this->checkBlankInvoiceHistory();
}
$this->checkBalances();
if (!$this->option('client_id')) {
$this->checkAccountData();
}
$this->info('Done');
}
private function checkBlankInvoiceHistory()
{
$count = DB::table('activities')
->where('activity_type_id', '=', 5)
->where('json_backup', '=', '')
->count();
$this->info($count . ' activities with blank invoice backup');
}
private function checkAccountData()
{
$tables = [
@ -97,6 +110,12 @@ class CheckData extends Command {
ENTITY_CLIENT,
ENTITY_USER
],
'expenses' => [
ENTITY_CLIENT,
ENTITY_VENDOR,
ENTITY_INVOICE,
ENTITY_USER
]
];
foreach ($tables as $table => $entityTypes) {
@ -136,9 +155,11 @@ class CheckData extends Command {
->join('payments', 'payments.client_id', '=', 'clients.id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('payments.is_deleted', '=', 0)
->where('payments.payment_status_id', '!=', 2)
->where('payments.payment_status_id', '!=', 3)
->where('invoices.is_deleted', '=', 0)
->groupBy('clients.id')
->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999')
->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');
@ -156,24 +177,24 @@ class CheckData extends Command {
// find all clients where the balance doesn't equal the sum of the outstanding invoices
$clients = DB::table('clients')
->join('invoices', 'invoices.client_id', '=', 'clients.id')
->join('accounts', 'accounts.id', '=', 'clients.account_id');
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
} else {
$clients->where('invoices.is_deleted', '=', 0)
->join('accounts', 'accounts.id', '=', 'clients.account_id')
->where('clients.is_deleted', '=', 0)
->where('invoices.is_deleted', '=', 0)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', 0)
->havingRaw('abs(clients.balance - sum(invoices.balance)) > .01 and clients.balance != 999999999.9999');
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
}
$clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at')
->orderBy('clients.id', 'DESC')
->get(['clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]);
->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');
foreach ($clients as $client) {
$this->info("=== Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ===");
$this->info("=== 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;
@ -228,8 +249,14 @@ class CheckData extends Command {
&& $activity->adjustment == 0
&& $invoice->amount > 0;
// **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");
$foundProblem = true;
$clientFix += $invoice->amount;
$activityFix = $invoice->amount;
// **Fix for allowing converting a recurring invoice to a normal one without updating the balance**
if ($noAdjustment && $invoice->invoice_type_id == INVOICE_TYPE_STANDARD && !$invoice->is_recurring) {
} 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}");
$foundProblem = true;
$clientFix += $invoice->amount;

View File

@ -109,6 +109,8 @@ class CreateTestData extends Command
for ($i=0; $i<$this->count; $i++) {
$data = [
'client_id' => $client->id,
'invoice_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
'due_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
'invoice_items' => [[
'product_key' => $this->faker->word,
'qty' => $this->faker->randomDigit + 1,
@ -133,7 +135,8 @@ class CreateTestData extends Command
$data = [
'invoice_id' => $invoice->id,
'client_id' => $client->id,
'amount' => $this->faker->randomFloat(2, 0, $invoice->amount)
'amount' => $this->faker->randomFloat(2, 0, $invoice->amount),
'payment_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
];
$payment = $this->paymentRepo->save($data);

View File

@ -52,5 +52,10 @@ class Kernel extends ConsoleKernel
->sendOutputTo($logFile)
->daily();
}
$schedule
->command('updater:check-for-update --prefixVersionWith=v')
->sendOutputTo($logFile)
->daily();
}
}

View File

@ -3,11 +3,13 @@
use Redirect;
use Utils;
use Exception;
use Crawler;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Validation\ValidationException;
/**
@ -39,7 +41,11 @@ class Handler extends ExceptionHandler
public function report(Exception $e)
{
// don't show these errors in the logs
if ($e instanceof HttpResponseException) {
if ($e instanceof NotFoundHttpException) {
if (Crawler::isCrawler()) {
return false;
}
} elseif ($e instanceof HttpResponseException) {
return false;
}

View File

@ -139,8 +139,6 @@ class AccountController extends BaseController
$account = $this->accountRepo->create();
$user = $account->users()->first();
Session::forget(RECENTLY_VIEWED);
if ($prevUserId) {
$users = $this->accountRepo->associateAccounts($user->id, $prevUserId);
Session::put(SESSION_USER_ACCOUNTS, $users);
@ -257,6 +255,10 @@ class AccountController extends BaseController
*/
public function showSection($section = false)
{
if ( ! Auth::user()->is_admin) {
return Redirect::to('/settings/user_details');
}
if (!$section) {
return Redirect::to('/settings/'.ACCOUNT_COMPANY_DETAILS, 301);
}
@ -541,6 +543,8 @@ class AccountController extends BaseController
$invoice->terms = trim($account->invoice_terms);
$invoice->invoice_footer = trim($account->invoice_footer);
$contact->first_name = 'Test';
$contact->last_name = 'Contact';
$contact->email = 'contact@gmail.com';
$client->contacts = [$contact];
@ -563,58 +567,7 @@ class AccountController extends BaseController
$data['invoiceDesigns'] = InvoiceDesign::getDesigns();
$data['invoiceFonts'] = Cache::get('fonts');
$data['section'] = $section;
$pageSizes = [
'A0',
'A1',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9',
'A10',
'B0',
'B1',
'B2',
'B3',
'B4',
'B5',
'B6',
'B7',
'B8',
'B9',
'B10',
'C0',
'C1',
'C2',
'C3',
'C4',
'C5',
'C6',
'C7',
'C8',
'C9',
'C10',
'RA0',
'RA1',
'RA2',
'RA3',
'RA4',
'SRA0',
'SRA1',
'SRA2',
'SRA3',
'SRA4',
'Executive',
'Folio',
'Legal',
'Letter',
'Tabloid',
];
$data['pageSizes'] = array_combine($pageSizes, $pageSizes);
$data['pageSizes'] = array_combine(InvoiceDesign::$pageSizes, InvoiceDesign::$pageSizes);
$design = false;
foreach ($data['invoiceDesigns'] as $item) {
@ -901,7 +854,26 @@ class AccountController extends BaseController
if (Input::get('custom_link') == 'subdomain') {
$subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH));
$exclude = ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner', 'info', 'ninja'];
$exclude = [
'www',
'app',
'mail',
'admin',
'blog',
'user',
'contact',
'payment',
'payments',
'billing',
'invoice',
'business',
'owner',
'info',
'ninja',
'docs',
'doc',
'documents'
];
$rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id|not_in:" . implode(',', $exclude);
} else {
$iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH));
@ -1052,10 +1024,11 @@ class AccountController extends BaseController
}
$labels = [];
foreach (['item', 'description', 'unit_cost', 'quantity', 'line_total', 'terms', 'balance_due', 'partial_due', 'subtotal', 'paid_to_date', 'discount'] as $field) {
foreach (['item', 'description', 'unit_cost', 'quantity', 'line_total', 'terms', 'balance_due', 'partial_due', 'subtotal', 'paid_to_date', 'discount', 'tax'] as $field) {
$labels[$field] = Input::get("labels_{$field}");
}
$account->invoice_labels = json_encode($labels);
$account->invoice_fields = Input::get('invoice_fields_json');
$account->save();

View File

@ -8,6 +8,7 @@ use Session;
use View;
use Crypt;
use File;
use Utils;
use App\Models\Account;
use App\Models\BankAccount;
use App\Ninja\Repositories\BankAccountRepository;
@ -130,6 +131,7 @@ class BankAccountController extends BaseController
$data = $this->bankAccountService->parseOFX($file);
} catch (\Exception $e) {
Session::flash('error', trans('texts.ofx_parse_failed'));
Utils::logError($e);
return view('accounts.import_ofx');
}

View File

@ -66,9 +66,9 @@ class BotController extends Controller
}
// regular chat message
} else {
if ($message === 'help') {
if ($text === 'help') {
$response = SkypeResponse::message(trans('texts.bot_help_message'));
} elseif ($message == 'status') {
} elseif ($text == 'status') {
$response = SkypeResponse::message(trans('texts.intent_not_supported'));
} else {
if ( ! $user = User::whereBotUserId($botUserId)->with('account')->first()) {
@ -98,8 +98,7 @@ class BotController extends Controller
private function authenticate($input)
{
$headers = getallheaders();
$token = isset($headers['Authorization']) ? $headers['Authorization'] : false;
$token = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : false;
if (Utils::isNinjaDev()) {
// skip validation for testing
@ -261,6 +260,8 @@ class BotController extends Controller
return false;
}
$token = explode(' ', $token)[1];
// https://blogs.msdn.microsoft.com/tsmatsuz/2016/07/12/developing-skype-bot/
// 0:Invalid, 1:Valid
$token_valid = 0;
@ -319,5 +320,4 @@ class BotController extends Controller
$res = base64_decode($res);
return $res;
}
}

View File

@ -89,9 +89,7 @@ class ClientController extends BaseController
public function show(ClientRequest $request)
{
$client = $request->entity();
$user = Auth::user();
Utils::trackViewed($client->getDisplayName(), ENTITY_CLIENT);
$actionLinks = [];
if($user->can('create', ENTITY_TASK)){

View File

@ -2,155 +2,34 @@
use Auth;
use DB;
use App\Ninja\Repositories\DashboardRepository;
use App\Ninja\Transformers\ActivityTransformer;
class DashboardApiController extends BaseAPIController
{
public function __construct(DashboardRepository $dashboardRepo)
{
parent::__construct();
$this->dashboardRepo = $dashboardRepo;
}
public function index()
{
$view_all = Auth::user()->hasPermission('view_all');
$user_id = Auth::user()->id;
$user = Auth::user();
$viewAll = $user->hasPermission('view_all');
$userId = $user->id;
$accountId = $user->account->id;
// total_income, billed_clients, invoice_sent and active_clients
$select = DB::raw('COUNT(DISTINCT CASE WHEN invoices.id IS NOT NULL THEN clients.id ELSE null END) billed_clients,
SUM(CASE WHEN invoices.invoice_status_id >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent,
COUNT(DISTINCT clients.id) active_clients');
$metrics = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->leftJoin('invoices', 'clients.id', '=', 'invoices.client_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false)
->where('invoices.invoice_type_id', '=', false);
if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){
$query->where('invoices.user_id', '=', $user_id);
$query->orwhere(function($query) use($user_id){
$query->where('invoices.user_id', '=', null);
$query->where('clients.user_id', '=', $user_id);
});
});
}
$metrics = $metrics->groupBy('accounts.id')
->first();
$select = DB::raw('SUM(clients.paid_to_date) as value, clients.currency_id as currency_id');
$paidToDate = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false);
if(!$view_all){
$paidToDate = $paidToDate->where('clients.user_id', '=', $user_id);
}
$paidToDate = $paidToDate->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END'))
->get();
$select = DB::raw('AVG(invoices.amount) as invoice_avg, clients.currency_id as currency_id');
$averageInvoice = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->leftJoin('invoices', 'clients.id', '=', 'invoices.client_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false);
if(!$view_all){
$averageInvoice = $averageInvoice->where('invoices.user_id', '=', $user_id);
}
$averageInvoice = $averageInvoice->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END'))
->get();
$select = DB::raw('SUM(clients.balance) as value, clients.currency_id as currency_id');
$balances = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END'));
if (!$view_all) {
$balances->where('clients.user_id', '=', $user_id);
}
$balances = $balances->get();
$pastDue = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
//->where('invoices.is_quote', '=', false)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('invoices.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '<', date('Y-m-d'));
if(!$view_all){
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
}
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->orderBy('invoices.due_date', 'asc')
->take(50)
->get();
$upcoming = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
//->where('invoices.is_quote', '=', false)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '>=', date('Y-m-d'))
->orderBy('invoices.due_date', 'asc');
if(!$view_all){
$upcoming = $upcoming->where('invoices.user_id', '=', $user_id);
}
$upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->get();
$payments = DB::table('payments')
->leftJoin('clients', 'clients.id', '=', 'payments.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('payments.account_id', '=', Auth::user()->account_id)
->where('payments.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('clients.is_deleted', '=', false)
->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true);
if(!$view_all){
$payments = $payments->where('payments.user_id', '=', $user_id);
}
$payments = $payments->select(['payments.payment_date', 'payments.amount', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id'])
->orderBy('payments.payment_date', 'desc')
->take(50)
->get();
$dashboardRepo = $this->dashboardRepo;
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
$paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll);
$averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll);
$balances = $dashboardRepo->balances($accountId, $userId, $viewAll);
$activities = $dashboardRepo->activities($accountId, $userId, $viewAll);
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);
$payments = $dashboardRepo->payments($accountId, $userId, $viewAll);
$hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) {
@ -167,15 +46,13 @@ class DashboardApiController extends BaseAPIController
'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' => $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0,
'averageInvoice' => (count($averageInvoice) && $averageInvoice[0]->invoice_avg) ? $averageInvoice[0]->invoice_avg : 0,
'averageInvoiceCurrency' => $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),
];
return $this->response($data);
}
}

View File

@ -1,192 +1,49 @@
<?php namespace App\Http\Controllers;
use stdClass;
use Auth;
use DB;
use View;
use App\Models\Activity;
use Utils;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Ninja\Repositories\DashboardRepository;
/**
* Class DashboardController
*/
class DashboardController extends BaseController
{
public function __construct(DashboardRepository $dashboardRepo)
{
$this->dashboardRepo = $dashboardRepo;
}
/**
* @return \Illuminate\Contracts\View\View
*/
public function index()
{
$view_all = Auth::user()->hasPermission('view_all');
$user_id = Auth::user()->id;
$user = Auth::user();
$viewAll = $user->hasPermission('view_all');
$userId = $user->id;
$account = $user->account;
$accountId = $account->id;
// total_income, billed_clients, invoice_sent and active_clients
$select = DB::raw(
'COUNT(DISTINCT CASE WHEN '.DB::getQueryGrammar()->wrap('invoices.id', true).' IS NOT NULL THEN '.DB::getQueryGrammar()->wrap('clients.id', true).' ELSE null END) billed_clients,
SUM(CASE WHEN '.DB::getQueryGrammar()->wrap('invoices.invoice_status_id', true).' >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent,
COUNT(DISTINCT '.DB::getQueryGrammar()->wrap('clients.id', true).') active_clients'
);
$metrics = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->leftJoin('invoices', 'clients.id', '=', 'invoices.client_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD);
if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){
$query->where('invoices.user_id', '=', $user_id);
$query->orwhere(function($query) use($user_id){
$query->where('invoices.user_id', '=', null);
$query->where('clients.user_id', '=', $user_id);
});
});
}
$metrics = $metrics->groupBy('accounts.id')
->first();
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('clients.paid_to_date', true).') as value,'
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$paidToDate = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false);
if(!$view_all){
$paidToDate = $paidToDate->where('clients.user_id', '=', $user_id);
}
$paidToDate = $paidToDate->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'))
->get();
$select = DB::raw(
'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, '
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$averageInvoice = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->leftJoin('invoices', 'clients.id', '=', 'invoices.client_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false);
if(!$view_all){
$averageInvoice = $averageInvoice->where('invoices.user_id', '=', $user_id);
}
$averageInvoice = $averageInvoice->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'))
->get();
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('clients.balance', true).') as value, '
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$balances = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'));
if (!$view_all) {
$balances->where('clients.user_id', '=', $user_id);
}
$balances = $balances->get();
$activities = Activity::where('activities.account_id', '=', Auth::user()->account_id)
->where('activities.activity_type_id', '>', 0);
if(!$view_all){
$activities = $activities->where('activities.user_id', '=', $user_id);
}
$activities = $activities->orderBy('activities.created_at', 'desc')
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task')
->take(50)
->get();
$pastDue = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
//->where('invoices.is_quote', '=', false)
->where('invoices.quote_invoice_id', '=', null)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('invoices.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '<', date('Y-m-d'));
if(!$view_all){
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
}
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->orderBy('invoices.due_date', 'asc')
->take(50)
->get();
$upcoming = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
//->where('invoices.is_quote', '=', false)
->where('invoices.quote_invoice_id', '=', null)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '>=', date('Y-m-d'))
->orderBy('invoices.due_date', 'asc');
if(!$view_all){
$upcoming = $upcoming->where('invoices.user_id', '=', $user_id);
}
$upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->get();
$payments = DB::table('payments')
->leftJoin('clients', 'clients.id', '=', 'payments.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('payments.account_id', '=', Auth::user()->account_id)
->where('payments.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('clients.is_deleted', '=', false)
->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true);
if(!$view_all){
$payments = $payments->where('payments.user_id', '=', $user_id);
}
$payments = $payments->select(['payments.payment_date', 'payments.amount', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id'])
->orderBy('payments.payment_date', 'desc')
->take(50)
->get();
$dashboardRepo = $this->dashboardRepo;
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
$paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll);
$averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll);
$balances = $dashboardRepo->balances($accountId, $userId, $viewAll);
$activities = $dashboardRepo->activities($accountId, $userId, $viewAll);
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);
$payments = $dashboardRepo->payments($accountId, $userId, $viewAll);
$expenses = $dashboardRepo->expenses($accountId, $userId, $viewAll);
$tasks = $dashboardRepo->tasks($accountId, $userId, $viewAll);
// check if the account has quotes
$hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) {
foreach ($data as $invoice) {
@ -196,8 +53,28 @@ class DashboardController extends BaseController
}
}
// check if the account has multiple curencies
$currencyIds = $account->currency_id ? [$account->currency_id] : [DEFAULT_CURRENCY];
$data = Client::scope()
->withArchived()
->distinct()
->get(['currency_id'])
->toArray();
array_map(function ($item) use (&$currencyIds) {
$currencyId = intval($item['currency_id']);
if ($currencyId && ! in_array($currencyId, $currencyIds)) {
$currencyIds[] = $currencyId;
}
}, $data);
$currencies = [];
foreach ($currencyIds as $currencyId) {
$currencies[$currencyId] = Utils::getFromCache($currencyId, 'currencies')->code;
}
$data = [
'account' => Auth::user()->account,
'account' => $user->account,
'paidToDate' => $paidToDate,
'balances' => $balances,
'averageInvoice' => $averageInvoice,
@ -209,8 +86,20 @@ class DashboardController extends BaseController
'payments' => $payments,
'title' => trans('texts.dashboard'),
'hasQuotes' => $hasQuotes,
'showBreadcrumbs' => false,
'currencies' => $currencies,
'expenses' => $expenses,
'tasks' => $tasks,
];
return View::make('dashboard', $data);
}
public function chartData($groupBy, $startDate, $endDate, $currencyCode, $includeExpenses)
{
$includeExpenses = filter_var($includeExpenses, FILTER_VALIDATE_BOOLEAN);
$data = $this->dashboardRepo->chartData(Auth::user()->account, $groupBy, $startDate, $endDate, $currencyCode, $includeExpenses);
return json_encode($data);
}
}

View File

@ -13,6 +13,7 @@ use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\Client;
use App\Models\TaxRate;
use App\Ninja\Repositories\InvoiceRepository;
use App\Services\ExpenseService;
use App\Ninja\Repositories\ExpenseRepository;
use App\Http\Requests\ExpenseRequest;
@ -26,12 +27,18 @@ class ExpenseController extends BaseController
protected $expenseService;
protected $entityType = ENTITY_EXPENSE;
public function __construct(ExpenseRepository $expenseRepo, ExpenseService $expenseService)
/**
* @var InvoiceRepository
*/
protected $invoiceRepo;
public function __construct(ExpenseRepository $expenseRepo, ExpenseService $expenseService, InvoiceRepository $invoiceRepo)
{
// parent::__construct();
$this->expenseRepo = $expenseRepo;
$this->expenseService = $expenseService;
$this->invoiceRepo = $invoiceRepo;
}
/**
@ -106,6 +113,14 @@ class ExpenseController extends BaseController
$actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')];
} else {
$actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans('texts.invoice_expense')];
// check for any open invoices
$invoices = $expense->client_id ? $this->invoiceRepo->findOpenInvoices($expense->client_id, ENTITY_EXPENSE) : [];
foreach ($invoices as $invoice) {
$actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans('texts.add_to_invoice', ['invoice' => $invoice->invoice_number])];
}
}
$actions[] = \DropdownButton::DIVIDER;
@ -151,7 +166,7 @@ class ExpenseController extends BaseController
Session::flash('message', trans('texts.updated_expense'));
$action = Input::get('action');
if (in_array($action, ['archive', 'delete', 'restore', 'invoice'])) {
if (in_array($action, ['archive', 'delete', 'restore', 'invoice', 'add_to_invoice'])) {
return self::bulk();
}
@ -178,6 +193,7 @@ class ExpenseController extends BaseController
switch($action)
{
case 'invoice':
case 'add_to_invoice':
$expenses = Expense::scope($ids)->with('client')->get();
$clientPublicId = null;
$currencyId = null;
@ -207,9 +223,17 @@ class ExpenseController extends BaseController
}
}
if ($action == 'invoice') {
return Redirect::to("invoices/create/{$clientPublicId}")
->with('expenseCurrencyId', $currencyId)
->with('expenses', $ids);
} else {
$invoiceId = Input::get('invoice_id');
return Redirect::to("invoices/{$invoiceId}/edit")
->with('expenseCurrencyId', $currencyId)
->with('expenses', $ids);
}
break;
default:

View File

@ -128,6 +128,7 @@ class ExportController extends BaseController
if ($key === 'quotes') {
$key = 'invoices';
$data['entityType'] = ENTITY_QUOTE;
$data['invoices'] = $data['quotes'];
}
$sheet->loadView("export.{$key}", $data);
});

View File

@ -161,6 +161,7 @@ class InvoiceApiController extends BaseAPIController
$invoice = $this->invoiceService->save($data);
$payment = false;
if ($invoice->isInvoice()) {
if (isset($data['auto_bill']) && boolval($data['auto_bill'])) {
$payment = $this->paymentService->autoBillInvoice($invoice);
} else if (isset($data['paid']) && $data['paid']) {
@ -170,6 +171,7 @@ class InvoiceApiController extends BaseAPIController
'amount' => $data['paid']
]);
}
}
if (isset($data['email_invoice']) && $data['email_invoice']) {
if ($payment) {

View File

@ -115,7 +115,6 @@ class InvoiceController extends BaseController
$method = 'POST';
$url = "{$entityType}s";
} else {
Utils::trackViewed($invoice->getDisplayName().' - '.$invoice->client->getDisplayName(), $invoice->getEntityType());
$method = 'PUT';
$url = "{$entityType}s/{$invoice->public_id}";
$clients->whereId($invoice->client_id);
@ -244,11 +243,6 @@ class InvoiceController extends BaseController
$invoice = $account->createInvoice($entityType, $clientId);
$invoice->public_id = 0;
if (Session::get('expenses')) {
$invoice->expenses = Expense::scope(Session::get('expenses'))->with('documents', 'expense_category')->get();
}
$clients = Client::scope()->with('contacts', 'country')->orderBy('name');
if (!Auth::user()->hasPermission('view_all')) {
$clients = $clients->where('clients.user_id', '=', Auth::user()->id);
@ -384,6 +378,7 @@ class InvoiceController extends BaseController
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null,
'expenses' => Session::get('expenses') ? Expense::scope(Session::get('expenses'))->with('documents', 'expense_category')->get() : [],
];
}
@ -405,14 +400,10 @@ class InvoiceController extends BaseController
$entityType = $invoice->getEntityType();
$message = trans("texts.created_{$entityType}");
// check if we created a new client with the invoice
// TODO: replace with HistoryListener
$input = $request->input();
$clientPublicId = isset($input['client']['public_id']) ? $input['client']['public_id'] : false;
if ($clientPublicId == '-1') {
$message = $message.' '.trans('texts.and_created_client');
$trackUrl = URL::to('clients/' . $invoice->client->public_id);
Utils::trackViewed($invoice->client->getDisplayName(), ENTITY_CLIENT, $trackUrl);
}
Session::flash('message', $message);
@ -582,7 +573,7 @@ class InvoiceController extends BaseController
$lastId = false;
foreach ($activities as $activity) {
$backup = json_decode($activity->json_backup);
if ($backup = json_decode($activity->json_backup)) {
$backup->invoice_date = Utils::fromSqlDate($backup->invoice_date);
$backup->due_date = Utils::fromSqlDate($backup->due_date);
$backup->features = [
@ -597,9 +588,14 @@ class InvoiceController extends BaseController
$key = Utils::timestampToDateTimeString(strtotime($activity->created_at)) . ' - ' . $activity->user->getDisplayName();
$versionsSelect[$lastId ? $lastId : 0] = $key;
$lastId = $activity->id;
} else {
Utils::logError('Failed to parse invoice backup');
}
}
if ($lastId) {
$versionsSelect[$lastId] = Utils::timestampToDateTimeString(strtotime($invoice->created_at)) . ' - ' . $invoice->user->getDisplayName();
}
$data = [
'invoice' => $invoice,

View File

@ -6,6 +6,7 @@ use Utils;
use View;
use Auth;
use URL;
use Crawler;
use Exception;
use Validator;
use App\Models\Invitation;
@ -133,7 +134,7 @@ class OnlinePaymentController extends BaseController
if ($paymentDriver->completeOffsitePurchase(Input::all())) {
Session::flash('message', trans('texts.applied_payment'));
}
return redirect()->to('view/' . $invitation->invitation_key);
return redirect()->to($invitation->getLink());
} catch (Exception $exception) {
return $this->error($paymentDriver, $exception);
}
@ -229,6 +230,10 @@ class OnlinePaymentController extends BaseController
public function handleBuyNow(ClientRepository $clientRepo, InvoiceService $invoiceService, $gatewayType = false)
{
if (Crawler::isCrawler()) {
return redirect()->to(NINJA_WEB_URL, 301);
}
$account = Account::whereAccountKey(Input::get('account_key'))->first();
$redirectUrl = Input::get('redirect_url', URL::previous());

View File

@ -5,8 +5,6 @@ use Config;
use Input;
use Utils;
use DB;
use DateInterval;
use DatePeriod;
use Session;
use View;
use App\Models\Account;
@ -56,186 +54,50 @@ class ReportController extends BaseController
$action = Input::get('action');
if (Input::all()) {
$groupBy = Input::get('group_by');
$chartType = Input::get('chart_type');
$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);
$enableReport = boolval(Input::get('enable_report'));
$enableChart = boolval(Input::get('enable_chart'));
} else {
$groupBy = 'MONTH';
$chartType = 'Bar';
$reportType = ENTITY_INVOICE;
$dateField = FILTER_INVOICE_DATE;
$startDate = Utils::today(false)->modify('-3 month');
$endDate = Utils::today(false);
$enableReport = true;
$enableChart = true;
}
$dateTypes = [
'DAYOFYEAR' => 'Daily',
'WEEK' => 'Weekly',
'MONTH' => 'Monthly',
];
$chartTypes = [
'Bar' => 'Bar',
'Line' => 'Line',
];
$reportTypes = [
ENTITY_CLIENT => trans('texts.client'),
ENTITY_INVOICE => trans('texts.invoice'),
ENTITY_PRODUCT => trans('texts.product'),
ENTITY_PAYMENT => trans('texts.payment'),
ENTITY_EXPENSE => trans('texts.expenses'),
ENTITY_TAX_RATE => trans('texts.taxes'),
ENTITY_EXPENSE => trans('texts.expense'),
ENTITY_TAX_RATE => trans('texts.tax'),
];
$params = [
'dateTypes' => $dateTypes,
'chartTypes' => $chartTypes,
'chartType' => $chartType,
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)),
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)),
'groupBy' => $groupBy,
'reportTypes' => $reportTypes,
'reportType' => $reportType,
'enableChart' => $enableChart,
'enableReport' => $enableReport,
'title' => trans('texts.charts_and_reports'),
];
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
if ($enableReport) {
$isExport = $action == 'export';
$params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport));
if ($isExport) {
self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']);
}
}
if ($enableChart) {
$params = array_merge($params, self::generateChart($groupBy, $startDate, $endDate));
}
} else {
$params['columns'] = [];
$params['displayData'] = [];
$params['reportTotals'] = [];
$params['labels'] = [];
$params['datasets'] = [];
$params['scaleStepWidth'] = 100;
}
return View::make('reports.chart_builder', $params);
}
/**
* @param $groupBy
* @param $startDate
* @param $endDate
* @return array
*/
private function generateChart($groupBy, $startDate, $endDate)
{
$width = 10;
$datasets = [];
$labels = [];
$maxTotals = 0;
foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) {
// SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions.
// Let's see if SQLite is being used.
if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') {
// Replace the unsupported function with it's date format counterpart
switch ($groupBy) {
case 'MONTH':
$dateFormat = '%m'; // returns 01-12
break;
case 'WEEK':
$dateFormat = '%W'; // returns 00-53
break;
case 'DAYOFYEAR':
$dateFormat = '%j'; // returns 001-366
break;
default:
$dateFormat = '%m'; // MONTH by default
break;
}
// Concatenate the year and the chosen timeframe (Month, Week or Day)
$timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)';
} else {
// Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL)
$timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))';
}
$records = DB::table($entityType.'s')
->select(DB::raw('sum('.$entityType.'s.amount) as total, '.$timeframe.' as '.$groupBy))
->join('clients', 'clients.id', '=', $entityType.'s.client_id')
->where('clients.is_deleted', '=', false)
->where($entityType.'s.account_id', '=', Auth::user()->account_id)
->where($entityType.'s.is_deleted', '=', false)
->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d'))
->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d'))
->groupBy($groupBy);
if ($entityType == ENTITY_INVOICE) {
$records->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) {
$records->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('invoices.is_deleted', '=', false);
}
$totals = $records->lists('total');
$dates = $records->lists($groupBy);
$data = array_combine($dates, $totals);
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
$period = new DatePeriod($startDate, $interval, $endDate);
$endDate->modify('-1 '.$padding);
$totals = [];
foreach ($period as $d) {
$dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n');
// MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365
$date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat);
$totals[] = isset($data[$date]) ? $data[$date] : 0;
if ($entityType == ENTITY_INVOICE) {
$labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F');
$label = $d->format($labelFormat);
$labels[] = $label;
}
}
$max = max($totals);
if ($max > 0) {
$datasets[] = [
'totals' => $totals,
'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'),
];
$maxTotals = max($max, $maxTotals);
}
}
$width = (ceil($maxTotals / 100) * 100) / 10;
$width = max($width, 10);
return [
'datasets' => $datasets,
'scaleStepWidth' => $width,
'labels' => $labels,
];
}
/**
* @param $reportType
* @param $startDate
@ -250,6 +112,8 @@ class ReportController extends BaseController
return $this->generateClientReport($startDate, $endDate, $isExport);
} elseif ($reportType == ENTITY_INVOICE) {
return $this->generateInvoiceReport($startDate, $endDate, $isExport);
} elseif ($reportType == ENTITY_PRODUCT) {
return $this->generateProductReport($startDate, $endDate, $isExport);
} elseif ($reportType == ENTITY_PAYMENT) {
return $this->generatePaymentReport($startDate, $endDate, $isExport);
} elseif ($reportType == ENTITY_TAX_RATE) {
@ -358,8 +222,8 @@ class ReportController extends BaseController
$reportTotals = [];
$payments = Payment::scope()
->withTrashed()
->where('is_deleted', '=', false)
->withArchived()
->excludeFailed()
->whereHas('client', function($query) {
$query->where('is_deleted', '=', false);
})
@ -379,12 +243,12 @@ class ReportController extends BaseController
$invoice->present()->invoice_date,
$account->formatMoney($invoice->amount, $client),
$payment->present()->payment_date,
$account->formatMoney($payment->amount, $client),
$account->formatMoney($payment->getCompletedAmount(), $client),
$payment->present()->method,
];
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount);
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment->amount);
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment->getCompletedAmount());
}
return [
@ -413,15 +277,14 @@ class ReportController extends BaseController
->with('contacts')
->where('is_deleted', '=', false)
->with(['invoices' => function($query) use ($startDate, $endDate) {
$query->where('invoice_date', '>=', $startDate)
$query->invoices()
->withArchived()
->where('invoice_date', '>=', $startDate)
->where('invoice_date', '<=', $endDate)
->where('is_deleted', '=', false)
->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false)
->with(['payments' => function($query) {
$query->withTrashed()
->with('payment_type', 'account_gateway.gateway')
->where('is_deleted', '=', false);
$query->withArchived()
->excludeFailed()
->with('payment_type', 'account_gateway.gateway');
}, 'invoice_items'])
->withTrashed();
}]);
@ -437,10 +300,10 @@ class ReportController extends BaseController
$invoice->present()->invoice_date,
$account->formatMoney($invoice->amount, $client),
$payment ? $payment->present()->payment_date : '',
$payment ? $account->formatMoney($payment->amount, $client) : '',
$payment ? $account->formatMoney($payment->getCompletedAmount(), $client) : '',
$payment ? $payment->present()->method : '',
];
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->amount : 0);
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
}
$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount);
@ -455,6 +318,60 @@ class ReportController extends BaseController
];
}
/**
* @param $startDate
* @param $endDate
* @param $isExport
* @return array
*/
private function generateProductReport($startDate, $endDate, $isExport)
{
$columns = ['client', 'invoice_number', 'invoice_date', 'quantity', 'product'];
$account = Auth::user()->account;
$displayData = [];
$reportTotals = [];
$clients = Client::scope()
->withTrashed()
->with('contacts')
->where('is_deleted', '=', false)
->with(['invoices' => function($query) use ($startDate, $endDate) {
$query->where('invoice_date', '>=', $startDate)
->where('invoice_date', '<=', $endDate)
->where('is_deleted', '=', false)
->where('is_recurring', '=', false)
->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->with(['invoice_items'])
->withTrashed();
}]);
foreach ($clients->get() as $client) {
foreach ($client->invoices as $invoice) {
foreach ($invoice->invoice_items as $invoiceItem) {
$displayData[] = [
$isExport ? $client->getDisplayName() : $client->present()->link,
$isExport ? $invoice->invoice_number : $invoice->present()->link,
$invoice->present()->invoice_date,
$invoiceItem->qty,
$invoiceItem->product_key,
];
//$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->amount : 0);
}
//$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount);
//$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'balance', $invoice->balance);
}
}
return [
'columns' => $columns,
'displayData' => $displayData,
'reportTotals' => [],
];
}
/**
* @param $startDate
* @param $endDate

View File

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use Codedge\Updater\UpdaterManager;
use App\Http\Requests;
use Redirect;
class SelfUpdateController extends BaseController
{
/**
* @var UpdaterManager
*/
protected $updater;
/**
* SelfUpdateController constructor.
*
* @param UpdaterManager $updater
*/
public function __construct(UpdaterManager $updater)
{
$this->updater = $updater;
}
/**
* Show default update page
*
* @return mixed
*/
public function index()
{
$versionInstalled = $this->updater->source()->getVersionInstalled('v');
$updateAvailable = $this->updater->source()->isNewVersionAvailable($versionInstalled);
return view(
'vendor.self-update.self-update',
[
'versionInstalled' => $versionInstalled,
'versionAvailable' => $this->updater->source()->getVersionAvailable(),
'updateAvailable' => $updateAvailable
]
);
}
/**
* Run the actual update
*
* @return \Illuminate\Http\RedirectResponse
*/
public function update()
{
$this->updater->source()->update();
return Redirect::to('/');
}
public function download()
{
$this->updater->source()->fetch();
}
}

View File

@ -160,7 +160,7 @@ class TaskController extends BaseController
$actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans('texts.invoice_task')];
// check for any open invoices
$invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : [];
$invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id, ENTITY_TASK) : [];
foreach ($invoices as $invoice) {
$actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans('texts.add_to_invoice', ['invoice' => $invoice->invoice_number])];

View File

@ -354,4 +354,16 @@ class UserController extends BaseController
return View::make('users.account_management');
}
public function saveSidebarState()
{
if (Input::has('show_left')) {
Session::put(SESSION_LEFT_SIDEBAR, boolval(Input::get('show_left')));
}
if (Input::has('show_right')) {
Session::put(SESSION_RIGHT_SIDEBAR, boolval(Input::get('show_right')));
}
return RESULT_SUCCESS;
}
}

View File

@ -82,8 +82,6 @@ class VendorController extends BaseController
{
$vendor = $request->entity();
Utils::trackViewed($vendor->getDisplayName(), 'vendor');
$actionLinks = [
['label' => trans('texts.new_vendor'), 'url' => URL::to('/vendors/create/' . $vendor->public_id)]
];

View File

@ -18,9 +18,11 @@ class Kernel extends HttpKernel {
'App\Http\Middleware\VerifyCsrfToken',
'App\Http\Middleware\DuplicateSubmissionCheck',
'App\Http\Middleware\QueryLogging',
'App\Http\Middleware\SessionDataCheckMiddleware',
'App\Http\Middleware\StartupCheck',
];
/**
* The application's route middleware.
*

View File

@ -42,7 +42,7 @@ class ApiCheck {
// check if user is archived
if ($token && $token->user) {
Auth::loginUsingId($token->user_id);
Auth::onceUsingId($token->user_id);
Session::set('token_id', $token->id);
} else {
sleep(ERROR_DELAY);

View File

@ -22,10 +22,6 @@ class DuplicateSubmissionCheck
$path = $request->path();
if (strpos($path, 'charts_and_reports') !== false) {
return $next($request);
}
if (in_array($request->method(), ['POST', 'PUT', 'DELETE'])) {
$lastPage = session(SESSION_LAST_REQUEST_PAGE);
$lastTime = session(SESSION_LAST_REQUEST_TIME);

View File

@ -33,6 +33,7 @@ class QueryLogging
$queries = DB::getQueryLog();
$count = count($queries);
Log::info($request->method() . ' - ' . $request->url() . ": $count queries");
//Log::info($queries);
}
}

View File

@ -0,0 +1,31 @@
<?php namespace App\Http\Middleware;
use Closure;
use Auth;
use Session;
// https://arjunphp.com/laravel5-inactivity-idle-session-logout/
class SessionDataCheckMiddleware {
/**
* Check session data, if role is not valid logout the request
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$bag = Session::getMetadataBag();
$max = env('IDLE_TIMEOUT_MINUTES', 6 * 60) * 60; // minute to second conversion
$elapsed = time() - $bag->getLastUsed();
if ( ! $bag || $elapsed > $max) {
$request->session()->flush();
Auth::logout();
$request->session()->flash('warning', trans('texts.inactive_logout'));
}
return $next($request);
}
}

View File

@ -14,6 +14,7 @@ use Schema;
use App\Models\Language;
use App\Models\InvoiceDesign;
use App\Events\UserSettingsChanged;
use App\Libraries\CurlUtils;
/**
* Class StartupCheck
@ -71,7 +72,7 @@ class StartupCheck
if (Utils::isNinja()) {
$data = Utils::getNewsFeedResponse();
} else {
$file = @file_get_contents(NINJA_APP_URL.'/news_feed/'.Utils::getUserType().'/'.NINJA_VERSION);
$file = @CurlUtils::get(NINJA_APP_URL.'/news_feed/'.Utils::getUserType().'/'.NINJA_VERSION);
$data = @json_decode($file);
}
if ($data) {
@ -128,7 +129,7 @@ class StartupCheck
$productId = Input::get('product_id');
$url = (Utils::isNinjaDev() ? SITE_URL : NINJA_APP_URL) . "/claim_license?license_key={$licenseKey}&product_id={$productId}&get_date=true";
$data = trim(file_get_contents($url));
$data = trim(CurlUtils::get($url));
if ($productId == PRODUCT_INVOICE_DESIGNS) {
if ($data = json_decode($data)) {

View File

@ -26,11 +26,14 @@ class VerifyCsrfToken extends BaseVerifier
'api/v1/tasks',
'api/v1/email_invoice',
'api/v1/hooks',
'api/v1/users',
'api/v1/users/*',
'hook/email_opened',
'hook/email_bounced',
'reseller_stats',
'payment_hook/*',
'buy_now/*',
'hook/bot/*',
];
/**

View File

@ -38,7 +38,7 @@ class CreatePaymentAPIRequest extends PaymentRequest
]);
$rules = [
'amount' => "required|less_than:{$invoice->balance}|positive",
'amount' => "required|numeric|between:0.01,{$invoice->balance}",
];
if ($this->payment_type_id == PAYMENT_TYPE_CREDIT) {

View File

@ -29,7 +29,8 @@ class CreatePaymentRequest extends PaymentRequest
$rules = [
'client' => 'required', // TODO: change to client_id once views are updated
'invoice' => 'required', // TODO: change to invoice_id once views are updated
'amount' => "required|less_than:{$invoice->balance}|positive",
'amount' => "required|numeric|between:0.01,{$invoice->balance}",
'payment_date' => 'required',
];
if ( ! empty($input['payment_type_id']) && $input['payment_type_id'] == PAYMENT_TYPE_CREDIT) {

View File

@ -2,6 +2,7 @@
use Input;
use Utils;
use App\Libraries\HistoryUtils;
class EntityRequest extends Request {
@ -52,7 +53,10 @@ class EntityRequest extends Request {
public function authorize()
{
if ($this->entity()) {
return $this->user()->can('view', $this->entity());
if ($this->user()->can('view', $this->entity())) {
HistoryUtils::trackViewed($this->entity());
return true;
}
} else {
return $this->user()->can('create', $this->entityType);
}
@ -62,4 +66,5 @@ class EntityRequest extends Request {
{
return [];
}
}

View File

@ -26,7 +26,7 @@ class UpdateInvoiceRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive',
//'invoice_date' => 'date',
'invoice_date' => 'required',
//'due_date' => 'date',
//'start_date' => 'date',
//'end_date' => 'date',

View File

@ -2,7 +2,7 @@
use Auth;
class UpdateUserRequest extends Request
class UpdateUserRequest extends EntityRequest
{
// Expenses
/**
@ -12,7 +12,7 @@ class UpdateUserRequest extends Request
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return Auth::user()->is_admin || $this->user()->id == Auth::user()->id;
}
/**

View File

@ -122,11 +122,13 @@ if (Utils::isReseller()) {
Route::group(['middleware' => 'auth:user'], function() {
Route::get('dashboard', 'DashboardController@index');
Route::get('dashboard_chart_data/{group_by}/{start_date}/{end_date}/{currency_id}/{include_expenses}', 'DashboardController@chartData');
Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible');
Route::get('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']);
Route::get('check_invoice_number/{invoice_number}', 'InvoiceController@checkInvoiceNumber');
Route::get('save_sidebar_state', 'UserController@saveSidebarState');
Route::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails');
@ -152,6 +154,7 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('invoices/create/{client_id?}', 'InvoiceController@create');
Route::get('recurring_invoices/create/{client_id?}', 'InvoiceController@createRecurring');
Route::get('recurring_invoices', 'RecurringInvoiceController@index');
Route::get('recurring_invoices/{invoices}/edit', 'InvoiceController@edit');
Route::get('invoices/{invoices}/clone', 'InvoiceController@cloneInvoice');
Route::post('invoices/bulk', 'InvoiceController@bulk');
Route::post('recurring_invoices/bulk', 'InvoiceController@bulk');
@ -235,13 +238,12 @@ Route::group([
Route::get('settings/email_preview', 'AccountController@previewEmail');
Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy');
Route::get('settings/data_visualizations', 'ReportController@d3');
Route::get('settings/charts_and_reports', 'ReportController@showReports');
Route::post('settings/charts_and_reports', 'ReportController@showReports');
Route::get('settings/reports', 'ReportController@showReports');
Route::post('settings/reports', 'ReportController@showReports');
Route::post('settings/change_plan', 'AccountController@changePlan');
Route::post('settings/cancel_account', 'AccountController@cancelAccount');
Route::post('settings/company_details', 'AccountController@updateDetails');
Route::get('settings/{section?}', 'AccountController@showSection');
Route::post('settings/{section?}', 'AccountController@doSection');
Route::post('user/setTheme', 'UserController@setTheme');
@ -264,6 +266,13 @@ Route::group([
Route::post('bank_accounts/bulk', 'BankAccountController@bulk');
Route::post('bank_accounts/validate', 'BankAccountController@validateAccount');
Route::post('bank_accounts/import_expenses/{bank_id}', 'BankAccountController@importExpenses');
Route::get('self-update', 'SelfUpdateController@index');
Route::post('self-update', 'SelfUpdateController@update');
Route::get('self-update/download', 'SelfUpdateController@download');
});
Route::group(['middleware' => 'auth:user'], function() {
Route::get('settings/{section?}', 'AccountController@showSection');
});
// Route groups for API
@ -346,7 +355,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ENV_DEVELOPMENT', 'local');
define('ENV_STAGING', 'staging');
define('RECENTLY_VIEWED', 'RECENTLY_VIEWED');
define('RECENTLY_VIEWED', 'recent_history');
define('ENTITY_CLIENT', 'client');
define('ENTITY_CONTACT', 'contact');
@ -402,7 +411,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACCOUNT_INVOICE_DESIGN', 'invoice_design');
define('ACCOUNT_CLIENT_PORTAL', 'client_portal');
define('ACCOUNT_EMAIL_SETTINGS', 'email_settings');
define('ACCOUNT_CHARTS_AND_REPORTS', 'charts_and_reports');
define('ACCOUNT_REPORTS', 'reports');
define('ACCOUNT_USER_MANAGEMENT', 'user_management');
define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations');
define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders');
@ -472,7 +481,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACTIVITY_TYPE_UPDATE_TASK', 43);
define('DEFAULT_INVOICE_NUMBER', '0001');
define('RECENTLY_VIEWED_LIMIT', 8);
define('RECENTLY_VIEWED_LIMIT', 20);
define('LOGGED_ERROR_LIMIT', 100);
define('RANDOM_KEY_LENGTH', 32);
define('MAX_NUM_USERS', 20);
@ -544,6 +553,8 @@ if (!defined('CONTACT_EMAIL')) {
define('SESSION_LOCALE', 'sessionLocale');
define('SESSION_USER_ACCOUNTS', 'userAccounts');
define('SESSION_REFERRAL_CODE', 'referralCode');
define('SESSION_LEFT_SIDEBAR', 'showLeftSidebar');
define('SESSION_RIGHT_SIDEBAR', 'showRightSidebar');
define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE');
define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME');
@ -608,8 +619,9 @@ if (!defined('CONTACT_EMAIL')) {
define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG');
define('NINJA_WEB_URL', env('NINJA_WEB_URL', 'https://www.invoiceninja.com'));
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '2.6.11' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '2.7.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'));
@ -792,10 +804,6 @@ if (!defined('CONTACT_EMAIL')) {
define('WEPAY_ENABLE_CANADA', env('WEPAY_ENABLE_CANADA', false));
define('WEPAY_THEME', env('WEPAY_THEME','{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}'));
define('WEPAY_FEE_PAYER', env('WEPAY_FEE_PAYER', 'payee'));
define('WEPAY_APP_FEE_MULTIPLIER', env('WEPAY_APP_FEE_MULTIPLIER', 0.002));
define('WEPAY_APP_FEE_FIXED', env('WEPAY_APP_FEE_MULTIPLIER', 0.00));
define('SKYPE_CARD_RECEIPT', 'message/card.receipt');
define('SKYPE_CARD_CAROUSEL', 'message/card.carousel');
define('SKYPE_CARD_HERO', '');
@ -815,6 +823,10 @@ if (!defined('CONTACT_EMAIL')) {
define('SKYPE_BUTTON_SHOW_IMAGE', 'showImage');
define('SKYPE_BUTTON_DOWNLOAD_FILE', 'downloadFile');
define('INVOICE_FIELDS_CLIENT', 'client_fields');
define('INVOICE_FIELDS_INVOICE', 'invoice_fields');
define('INVOICE_FIELDS_ACCOUNT', 'account_fields');
$creditCards = [
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],
2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'],
@ -868,6 +880,7 @@ if (!defined('CONTACT_EMAIL')) {
if (Utils::isNinjaDev())
{
//ini_set('memory_limit','1024M');
//Auth::loginUsingId(1);
//set_time_limit(0);
Auth::loginUsingId(1);
}
*/

View File

@ -0,0 +1,173 @@
<?php namespace App\Libraries;
use Request;
use stdClass;
use Session;
use App\Models\EntityModel;
use App\Models\Activity;
class HistoryUtils
{
public static function loadHistory($users)
{
$userIds = [];
if (is_array($users)) {
foreach ($users as $user) {
$userIds[] = $user->user_id;
}
} else {
$userIds[] = $users;
}
$activityTypes = [
ACTIVITY_TYPE_CREATE_CLIENT,
ACTIVITY_TYPE_CREATE_TASK,
ACTIVITY_TYPE_UPDATE_TASK,
ACTIVITY_TYPE_CREATE_INVOICE,
ACTIVITY_TYPE_UPDATE_INVOICE,
ACTIVITY_TYPE_EMAIL_INVOICE,
ACTIVITY_TYPE_CREATE_QUOTE,
ACTIVITY_TYPE_UPDATE_QUOTE,
ACTIVITY_TYPE_EMAIL_QUOTE,
ACTIVITY_TYPE_VIEW_INVOICE,
ACTIVITY_TYPE_VIEW_QUOTE,
];
$activities = Activity::scope()
->with(['client.contacts', 'invoice', 'task'])
->whereIn('user_id', $userIds)
->whereIn('activity_type_id', $activityTypes)
->orderBy('id', 'asc')
->limit(100)
->get();
foreach ($activities as $activity)
{
if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_CLIENT) {
$entity = $activity->client;
} else if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_TASK || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_TASK) {
$entity = $activity->task;
$entity->setRelation('client', $activity->client);
} else {
$entity = $activity->invoice;
$entity->setRelation('client', $activity->client);
}
static::trackViewed($entity);
}
}
public static function trackViewed(EntityModel $entity)
{
if ($entity->isEntityType(ENTITY_CREDIT)
|| $entity->isEntityType(ENTITY_PAYMENT)
|| $entity->isEntityType(ENTITY_VENDOR)) {
return;
}
$object = static::convertToObject($entity);
$history = Session::get(RECENTLY_VIEWED) ?: [];
$accountHistory = isset($history[$entity->account_id]) ? $history[$entity->account_id] : [];
$data = [];
// Add to the list and make sure to only show each item once
for ($i = 0; $i<count($accountHistory); $i++) {
$item = $accountHistory[$i];
if ($object->url == $item->url) {
continue;
}
array_push($data, $item);
if (isset($counts[$item->accountId])) {
$counts[$item->accountId]++;
} else {
$counts[$item->accountId] = 1;
}
}
array_unshift($data, $object);
if (isset($counts[$entity->account_id]) && $counts[$entity->account_id] > RECENTLY_VIEWED_LIMIT) {
array_pop($data);
}
$history[$entity->account_id] = $data;
Session::put(RECENTLY_VIEWED, $history);
}
private static function convertToObject($entity)
{
$object = new stdClass();
$object->accountId = $entity->account_id;
$object->url = $entity->present()->url;
$object->entityType = $entity->subEntityType();
$object->name = $entity->present()->titledName;
$object->timestamp = time();
if ($entity->isEntityType(ENTITY_CLIENT)) {
$object->client_id = $entity->public_id;
$object->client_name = $entity->getDisplayName();
} elseif (method_exists($entity, 'client') && $entity->client) {
$object->client_id = $entity->client->public_id;
$object->client_name = $entity->client->getDisplayName();
} else {
$object->client_id = 0;
$object->client_name = 0;
}
return $object;
}
public static function renderHtml($accountId)
{
$lastClientId = false;
$clientMap = [];
$str = '';
$history = Session::get(RECENTLY_VIEWED, []);
$history = isset($history[$accountId]) ? $history[$accountId] : [];
foreach ($history as $item)
{
if ($item->entityType == ENTITY_CLIENT && isset($clientMap[$item->client_id])) {
continue;
}
$clientMap[$item->client_id] = true;
if ($lastClientId === false || $item->client_id != $lastClientId)
{
$icon = '<i class="fa fa-users" style="width:32px"></i>';
if ($item->client_id) {
$link = url('/clients/' . $item->client_id);
$name = $item->client_name ;
$buttonLink = url('/invoices/create/' . $item->client_id);
$button = '<a type="button" class="btn btn-primary btn-sm pull-right" href="' . $buttonLink . '">
<i class="fa fa-plus-circle" style="width:20px" title="' . trans('texts.create_invoice') . '"></i>
</a>';
} else {
$link = '#';
$name = trans('texts.unassigned');
$button = '';
}
$str .= sprintf('<li>%s<a href="%s"><div>%s %s</div></a></li>', $button, $link, $icon, $name);
$lastClientId = $item->client_id;
}
if ($item->entityType == ENTITY_CLIENT) {
continue;
}
$icon = '<i class="fa fa-' . EntityModel::getIcon($item->entityType . 's') . '" style="width:24px"></i>';
$str .= sprintf('<li style="text-align:right; padding-right:18px;"><a href="%s">%s %s</a></li>', $item->url, $item->name, $icon);
}
return $str;
}
}

View File

@ -41,7 +41,7 @@ class InvoiceCard
$this->setTotal($invoice->present()->requestedAmount);
if (floatval($invoice->amount)) {
$this->addButton(SKYPE_BUTTON_OPEN_URL, trans('texts.download_pdf'), $invoice->getInvitationLink('view', true));
$this->addButton(SKYPE_BUTTON_OPEN_URL, trans('texts.download_pdf'), $invoice->getInvitationLink('download', true));
$this->addButton(SKYPE_BUTTON_IM_BACK, trans('texts.email_invoice'), trans('texts.email_invoice'));
} else {
$this->addButton(SKYPE_BUTTON_IM_BACK, trans('texts.list_products'), trans('texts.list_products'));

View File

@ -191,19 +191,6 @@ class Utils
return $response;
}
public static function getLastURL()
{
if (!count(Session::get(RECENTLY_VIEWED))) {
return '#';
}
$history = Session::get(RECENTLY_VIEWED);
$last = $history[0];
$penultimate = count($history) > 1 ? $history[1] : $last;
return Request::url() == $last->url ? $penultimate->url : $last->url;
}
public static function getProLabel($feature)
{
if (Auth::check()
@ -369,9 +356,7 @@ class Utils
public static function formatMoney($value, $currencyId = false, $countryId = false, $showCode = false)
{
if (!$value) {
$value = 0;
}
$value = floatval($value);
if (!$currencyId) {
$currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY);
@ -602,51 +587,6 @@ class Utils
}
}
public static function trackViewed($name, $type, $url = false)
{
if (!$url) {
$url = Request::url();
}
$viewed = Session::get(RECENTLY_VIEWED);
if (!$viewed) {
$viewed = [];
}
$object = new stdClass();
$object->accountId = Auth::user()->account_id;
$object->url = $url;
$object->name = ucwords($type).': '.$name;
$data = [];
$counts = [];
for ($i = 0; $i<count($viewed); $i++) {
$item = $viewed[$i];
if ($object->url == $item->url || $object->name == $item->name) {
continue;
}
array_push($data, $item);
if (isset($counts[$item->accountId])) {
$counts[$item->accountId]++;
} else {
$counts[$item->accountId] = 1;
}
}
array_unshift($data, $object);
if (isset($counts[Auth::user()->account_id]) && $counts[Auth::user()->account_id] > RECENTLY_VIEWED_LIMIT) {
array_pop($data);
}
Session::put(RECENTLY_VIEWED, $data);
}
public static function processVariables($str)
{
if (!$str) {

View File

@ -389,8 +389,8 @@ class ActivityListener
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_DELETE_PAYMENT,
$payment->getCompletedAmount(),
$payment->getCompletedAmount() * -1
$payment->isFailedOrVoided() ? 0 : $payment->getCompletedAmount(),
$payment->isFailedOrVoided() ? 0 : $payment->getCompletedAmount() * -1
);
}
@ -419,8 +419,8 @@ class ActivityListener
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_VOIDED_PAYMENT,
$payment->amount,
$payment->amount * -1
$payment->is_deleted ? 0 : $payment->getCompletedAmount(),
$payment->is_deleted ? 0 : $payment->getCompletedAmount() * -1
);
}
@ -434,8 +434,8 @@ class ActivityListener
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_FAILED_PAYMENT,
$payment->getCompletedAmount(),
$payment->getCompletedAmount() * -1
$payment->is_deleted ? 0 : $payment->getCompletedAmount(),
$payment->is_deleted ? 0 : $payment->getCompletedAmount() * -1
);
}

View File

@ -6,6 +6,7 @@ use Session;
use App\Events\UserLoggedIn;
use App\Events\UserSignedUp;
use App\Ninja\Repositories\AccountRepository;
use App\Libraries\HistoryUtils;
/**
* Class HandleUserLoggedIn
@ -47,6 +48,7 @@ class HandleUserLoggedIn {
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
Session::put(SESSION_USER_ACCOUNTS, $users);
HistoryUtils::loadHistory($users ?: Auth::user()->id);
$account->loadLocalizationSettings();

View File

@ -11,6 +11,7 @@ use App\Events\UserSettingsChanged;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
use App\Models\Traits\PresentsInvoice;
/**
* Class Account
@ -19,6 +20,7 @@ class Account extends Eloquent
{
use PresentableTrait;
use SoftDeletes;
use PresentsInvoice;
/**
* @var string
@ -65,6 +67,7 @@ class Account extends Eloquent
'show_item_taxes',
'default_tax_rate_id',
'enable_second_tax_rate',
'include_item_taxes_inline',
'start_of_week',
];
@ -93,7 +96,7 @@ class Account extends Eloquent
ACCOUNT_TEMPLATES_AND_REMINDERS,
ACCOUNT_BANKS,
ACCOUNT_CLIENT_PORTAL,
ACCOUNT_CHARTS_AND_REPORTS,
ACCOUNT_REPORTS,
ACCOUNT_DATA_VISUALIZATIONS,
ACCOUNT_API_TOKENS,
ACCOUNT_USER_MANAGEMENT,
@ -398,11 +401,7 @@ class Account extends Eloquent
}
}
/**
* @param string $date
* @return DateTime|null|string
*/
public function getDateTime($date = 'now')
public function getDate($date = 'now')
{
if ( ! $date) {
return null;
@ -410,6 +409,16 @@ class Account extends Eloquent
$date = new \DateTime($date);
}
return $date;
}
/**
* @param string $date
* @return DateTime|null|string
*/
public function getDateTime($date = 'now')
{
$date = $this->getDate($date);
$date->setTimeZone(new \DateTimeZone($this->getTimezone()));
return $date;
@ -466,7 +475,7 @@ class Account extends Eloquent
*/
public function formatDate($date)
{
$date = $this->getDateTime($date);
$date = $this->getDate($date);
if ( ! $date) {
return null;
@ -1028,68 +1037,6 @@ class Account extends Eloquent
Session::put('start_of_week', $this->start_of_week);
}
/**
* @return array
*/
public function getInvoiceLabels()
{
$data = [];
$custom = (array) json_decode($this->invoice_labels);
$fields = [
'invoice',
'invoice_date',
'due_date',
'invoice_number',
'po_number',
'discount',
'taxes',
'tax',
'item',
'description',
'unit_cost',
'quantity',
'line_total',
'subtotal',
'paid_to_date',
'balance_due',
'partial_due',
'terms',
'your_invoice',
'quote',
'your_quote',
'quote_date',
'quote_number',
'total',
'invoice_issued_to',
'quote_issued_to',
//'date',
'rate',
'hours',
'balance',
'from',
'to',
'invoice_to',
'details',
'invoice_no',
'valid_until',
];
foreach ($fields as $field) {
if (isset($custom[$field]) && $custom[$field]) {
$data[$field] = $custom[$field];
} else {
$data[$field] = $this->isEnglish() ? uctrans("texts.$field") : trans("texts.$field");
}
}
foreach (['item', 'quantity', 'unit_cost'] as $field) {
$data["{$field}_orig"] = $data[$field];
}
return $data;
}
/**
* @return bool
*/

View File

@ -83,6 +83,11 @@ class Activity extends Eloquent
return $this->belongsTo('App\Models\Task')->withTrashed();
}
public function key()
{
return sprintf('%s-%s-%s', $this->activity_type_id, $this->client_id, $this->created_at->timestamp);
}
/**
* @return mixed
*/

View File

@ -328,7 +328,8 @@ class Client extends EntityModel
}
$contact = $this->contacts[0];
return $contact->getDisplayName();
return $contact->getDisplayName() ?: trans('texts.unnamed_client');
}
/**

View File

@ -92,6 +92,16 @@ class EntityModel extends Eloquent
return $this->public_id . ':' . $this->getEntityType();
}
public function subEntityType()
{
return $this->getEntityType();
}
public function isEntityType($type)
{
return $this->getEntityType() === $type;
}
/*
public function getEntityType()
{
@ -229,4 +239,23 @@ class EntityModel extends Eloquent
}
}
public static function getIcon($entityType)
{
$icons = [
'dashboard' => 'tachometer',
'clients' => 'users',
'invoices' => 'file-pdf-o',
'payments' => 'credit-card',
'recurring_invoices' => 'files-o',
'credits' => 'credit-card',
'quotes' => 'file-text-o',
'tasks' => 'clock-o',
'expenses' => 'file-image-o',
'vendors' => 'building',
'settings' => 'cog',
];
return array_get($icons, $entityType);
}
}

View File

@ -107,10 +107,7 @@ class Expense extends EntityModel
*/
public function getName()
{
if($this->expense_number)
return $this->expense_number;
return $this->public_id;
return $this->transaction_id ?: '#' . $this->public_id;
}
/**

View File

@ -131,7 +131,12 @@ class Invoice extends EntityModel implements BalanceAffecting
*/
public function getRoute()
{
if ($this->is_recurring) {
$entityType = 'recurring_invoice';
} else {
$entityType = $this->getEntityType();
}
return "/{$entityType}s/{$this->public_id}/edit";
}
@ -211,7 +216,10 @@ class Invoice extends EntityModel implements BalanceAffecting
if ($calculate) {
$amount = 0;
foreach ($this->payments as $payment) {
$amount += $payment->amount;
if ($payment->payment_status_id == PAYMENT_STATUS_VOIDED || $payment->payment_status_id == PAYMENT_STATUS_FAILED) {
continue;
}
$amount += $payment->getCompletedAmount();
}
return $amount;
} else {
@ -380,6 +388,13 @@ class Invoice extends EntityModel implements BalanceAffecting
return $this->isType(INVOICE_TYPE_QUOTE);
}
/**
* @return bool
*/
public function isInvoice() {
return $this->isType(INVOICE_TYPE_STANDARD) && ! $this->is_recurring;
}
/**
* @param bool $notify
*/
@ -533,6 +548,15 @@ class Invoice extends EntityModel implements BalanceAffecting
return $this->isType(INVOICE_TYPE_QUOTE) ? ENTITY_QUOTE : ENTITY_INVOICE;
}
public function subEntityType()
{
if ($this->is_recurring) {
return ENTITY_RECURRING_INVOICE;
} else {
return $this->getEntityType();
}
}
/**
* @return bool
*/
@ -696,6 +720,8 @@ class Invoice extends EntityModel implements BalanceAffecting
'custom_invoice_item_label2',
'invoice_embed_documents',
'page_size',
'include_item_taxes_inline',
'invoice_fields',
]);
foreach ($this->invoice_items as $invoiceItem) {

View File

@ -14,6 +14,58 @@ class InvoiceDesign extends Eloquent
*/
public $timestamps = false;
public static $pageSizes = [
'A0',
'A1',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9',
'A10',
'B0',
'B1',
'B2',
'B3',
'B4',
'B5',
'B6',
'B7',
'B8',
'B9',
'B10',
'C0',
'C1',
'C2',
'C3',
'C4',
'C5',
'C6',
'C7',
'C8',
'C9',
'C10',
'RA0',
'RA1',
'RA2',
'RA3',
'RA4',
'SRA0',
'SRA1',
'SRA2',
'SRA3',
'SRA4',
'Executive',
'Folio',
'Legal',
'Letter',
'Tabloid',
];
/**
* @return mixed
*/

View File

@ -121,6 +121,13 @@ class Payment extends EntityModel
}
*/
public function scopeExcludeFailed($query)
{
$query->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
return $query;
}
/**
* @return mixed
*/
@ -177,6 +184,11 @@ class Payment extends EntityModel
return $this->payment_status_id == PAYMENT_STATUS_VOIDED;
}
public function isFailedOrVoided()
{
return $this->isFailed() || $this->isVoided();
}
/**
* @param null $amount
* @return bool

View File

@ -151,9 +151,24 @@ class Task extends EntityModel
{
return "/tasks/{$this->public_id}/edit";
}
public function getName()
{
return '#' . $this->public_id;
}
public function getDisplayName()
{
if ($this->description) {
return mb_strimwidth($this->description, 0, 16, "...");
}
return '#' . $this->public_id;
}
}
Task::created(function ($task) {
event(new TaskWasCreated($task));
});

View File

@ -0,0 +1,229 @@
<?php namespace App\Models\Traits;
/**
* Class PresentsInvoice
*/
trait PresentsInvoice
{
public function getInvoiceFields()
{
if ($this->invoice_fields) {
$fields = json_decode($this->invoice_fields, true);
return $this->applyLabels($fields);
} else {
return $this->getDefaultInvoiceFields();
}
}
public function getDefaultInvoiceFields()
{
$fields = [
INVOICE_FIELDS_INVOICE => [
'invoice.invoice_number',
'invoice.po_number',
'invoice.invoice_date',
'invoice.due_date',
'invoice.balance_due',
'invoice.partial_due',
],
INVOICE_FIELDS_CLIENT => [
'client.client_name',
'client.id_number',
'client.vat_number',
'client.address1',
'client.address2',
'client.city_state_postal',
'client.country',
'client.email',
],
'account_fields1' => [
'account.company_name',
'account.id_number',
'account.vat_number',
'account.website',
'account.email',
'account.phone',
],
'account_fields2' => [
'account.address1',
'account.address2',
'account.city_state_postal',
'account.country',
],
];
if ($this->custom_invoice_text_label1) {
$fields[INVOICE_FIELDS_INVOICE][] = 'invoice.custom_text_value1';
}
if ($this->custom_invoice_text_label2) {
$fields[INVOICE_FIELDS_INVOICE][] = 'invoice.custom_text_value2';
}
if ($this->custom_client_label1) {
$fields[INVOICE_FIELDS_CLIENT][] = 'client.custom_value1';
}
if ($this->custom_client_label2) {
$fields[INVOICE_FIELDS_CLIENT][] = 'client.custom_value2';
}
if ($this->custom_label1) {
$fields['account_fields2'][] = 'account.custom_value1';
}
if ($this->custom_label2) {
$fields['account_fields2'][] = 'account.custom_value2';
}
return $this->applyLabels($fields);
}
public function getAllInvoiceFields()
{
$fields = [
INVOICE_FIELDS_INVOICE => [
'invoice.invoice_number',
'invoice.po_number',
'invoice.invoice_date',
'invoice.due_date',
'invoice.balance_due',
'invoice.partial_due',
'invoice.custom_text_value1',
'invoice.custom_text_value2',
],
INVOICE_FIELDS_CLIENT => [
'client.client_name',
'client.id_number',
'client.vat_number',
'client.address1',
'client.address2',
'client.city_state_postal',
'client.country',
'client.email',
'client.contact_name',
'client.custom_value1',
'client.custom_value2',
],
INVOICE_FIELDS_ACCOUNT => [
'account.company_name',
'account.id_number',
'account.vat_number',
'account.website',
'account.email',
'account.phone',
'account.address1',
'account.address2',
'account.city_state_postal',
'account.country',
'account.custom_value1',
'account.custom_value2',
]
];
return $this->applyLabels($fields);
}
private function applyLabels($fields)
{
$labels = $this->getInvoiceLabels();
foreach ($fields as $section => $sectionFields) {
foreach ($sectionFields as $index => $field) {
list($entityType, $fieldName) = explode('.', $field);
if (substr($fieldName, 0, 6) == 'custom') {
$fields[$section][$field] = $labels[$field];
} else {
$fields[$section][$field] = $labels[$fieldName];
}
unset($fields[$section][$index]);
}
}
return $fields;
}
/**
* @return array
*/
public function getInvoiceLabels()
{
$data = [];
$custom = (array) json_decode($this->invoice_labels);
$fields = [
'invoice',
'invoice_date',
'due_date',
'invoice_number',
'po_number',
'discount',
'taxes',
'tax',
'item',
'description',
'unit_cost',
'quantity',
'line_total',
'subtotal',
'paid_to_date',
'balance_due',
'partial_due',
'terms',
'your_invoice',
'quote',
'your_quote',
'quote_date',
'quote_number',
'total',
'invoice_issued_to',
'quote_issued_to',
'rate',
'hours',
'balance',
'from',
'to',
'invoice_to',
'quote_to',
'details',
'invoice_no',
'valid_until',
'client_name',
'address1',
'address2',
'id_number',
'vat_number',
'city_state_postal',
'country',
'email',
'contact_name',
'company_name',
'website',
'phone',
];
foreach ($fields as $field) {
if (isset($custom[$field]) && $custom[$field]) {
$data[$field] = $custom[$field];
} else {
$data[$field] = $this->isEnglish() ? uctrans("texts.$field") : trans("texts.$field");
}
}
foreach (['item', 'quantity', 'unit_cost'] as $field) {
$data["{$field}_orig"] = $data[$field];
}
foreach ([
'invoice.custom_text_value1' => 'custom_invoice_text_label1',
'invoice.custom_text_value2' => 'custom_invoice_text_label2',
'client.custom_value1' => 'custom_client_label1',
'client.custom_value2' => 'custom_client_label2',
'account.custom_value1' => 'custom_label1',
'account.custom_value2' => 'custom_label2'
] as $field => $property) {
$data[$field] = $this->$property ?: trans('texts.custom_field');
}
return $data;
}
}

View File

@ -286,7 +286,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
public function clearSession()
{
$keys = [
RECENTLY_VIEWED,
SESSION_USER_ACCOUNTS,
SESSION_TIMEZONE,
SESSION_DATE_FORMAT,

View File

@ -67,7 +67,7 @@ class BaseIntent
public function process()
{
// do nothing by default
throw new Exception(trans('texts.intent_not_supported'));
}
public function setStateEntities($entityType, $entities)

View File

@ -44,6 +44,7 @@ class InvoiceIntent extends BaseIntent
$productRepo = app('App\Ninja\Repositories\ProductRepository');
$invoiceItems = [];
$offset = 0;
if ( ! isset($this->data->compositeEntities) || ! count($this->data->compositeEntities)) {
return [];
@ -55,7 +56,19 @@ class InvoiceIntent extends BaseIntent
$qty = 1;
foreach ($entity->children as $child) {
if ($child->type == 'Product') {
$product = $productRepo->findPhonetically($child->value);
// check additional words in product name
// https://social.msdn.microsoft.com/Forums/azure/en-US/a508e039-0f76-4280-8156-4a017bcfc6dd/none-of-your-composite-entities-contain-all-of-the-highlighted-entities?forum=LUIS
$query = $this->data->query;
$startIndex = strpos($query, $child->value, $offset);
$endIndex = strlen($query);
$offset = $startIndex + 1;
foreach ($this->data->entities as $indexChild) {
if ($indexChild->startIndex > $startIndex) {
$endIndex = min($endIndex, $indexChild->startIndex);
}
}
$productName = substr($query, $startIndex, ($endIndex - $startIndex));
$product = $productRepo->findPhonetically($productName);
} else {
$qty = $child->value;
}

View File

@ -12,12 +12,12 @@ class ListProductsIntent extends ProductIntent
$account = Auth::user()->account;
$products = Product::scope()
->orderBy('product_key')
->limit(10)
->limit(5)
->get()
->transform(function($item, $key) use ($account) {
$card = $item->present()->skypeBot($account);
if ($this->stateEntity(ENTITY_INVOICE)) {
$card->addButton('imBack', trans('texts.add_to_invoice'), trans('texts.add_product_to_invoice', ['product' => $item->product_key]));
$card->addButton('imBack', trans('texts.add_to_invoice', ['invoice' => '']), trans('texts.add_product_to_invoice', ['product' => $item->product_key]));
}
return $card;
});

View File

@ -606,8 +606,11 @@ class BasePaymentDriver
$term = strtolower($matches[2]);
$price = $invoice_item->cost;
if ($plan == PLAN_ENTERPRISE) {
preg_match('/###[\d] [\w]* (\d*)/', $invoice_item->notes, $matches);
if (count($matches)) {
$numUsers = $matches[1];
} else {
$numUsers = 5;
}
} else {
$numUsers = 1;
}

View File

@ -6,8 +6,8 @@ class PayFastPaymentDriver extends BasePaymentDriver
public function completeOffsitePurchase($input)
{
if ($accountGateway->isGateway(GATEWAY_PAYFAST) && Request::has('pt')) {
$token = Request::query('pt');
}
parent::completeOffsitePurchase([
'token' => Request::query('pt')
]);
}
}

View File

@ -402,7 +402,7 @@ class StripePaymentDriver extends BasePaymentDriver
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first();
if (!$paymentMethod) {
throw new Exception('Unknown payment method');
return false;
}
if ($eventType == 'customer.source.deleted' || $eventType == 'customer.bank_account.deleted') {

View File

@ -53,8 +53,8 @@ class WePayPaymentDriver extends BasePaymentDriver
$data['transaction_id'] = $transactionId;
}
$data['applicationFee'] = (WEPAY_APP_FEE_MULTIPLIER * $data['amount']) + WEPAY_APP_FEE_FIXED;
$data['feePayer'] = WEPAY_FEE_PAYER;
$data['applicationFee'] = (env('WEPAY_APP_FEE_MULTIPLIER') * $data['amount']) + env('WEPAY_APP_FEE_FIXED');
$data['feePayer'] = env('WEPAY_FEE_PAYER');
$data['callbackUri'] = $this->accountGateway->getWebhookUrl();
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod)) {

View File

@ -27,4 +27,12 @@ class EntityPresenter extends Presenter
return link_to($link, $name)->toHtml();
}
public function titledName()
{
$entity = $this->entity;
$entityType = $entity->getEntityType();
return sprintf('%s: %s', trans('texts.' . $entityType), $entity->getDisplayName());
}
}

View File

@ -203,7 +203,8 @@ class AccountRepository
['new_user', '/users/create'],
['custom_fields', '/settings/invoice_settings'],
['invoice_number', '/settings/invoice_settings'],
['buy_now_buttons', '/settings/client_portal#buyNow']
['buy_now_buttons', '/settings/client_portal#buy_now'],
['invoice_fields', '/settings/invoice_design#invoice_fields'],
]);
$settings = array_merge(Account::$basicSettings, Account::$advancedSettings);
@ -279,6 +280,7 @@ class AccountRepository
$invoice->invoice_number = $account->getNextInvoiceNumber($invoice);
$invoice->invoice_date = $clientAccount->getRenewalDate();
$invoice->amount = $invoice->balance = $plan_cost - $credit;
$invoice->invoice_type_id = INVOICE_TYPE_STANDARD;
$invoice->save();
if ($credit) {

View File

@ -144,11 +144,12 @@ class ClientRepository extends BaseRepository
$clients = Client::scope()->get(['id', 'name', 'public_id']);
foreach ($clients as $client) {
$map[$client->id] = $client;
if ( ! $client->name) {
continue;
}
$map[$client->id] = $client;
$similar = similar_text($clientNameMeta, metaphone($client->name), $percent);
if ($percent > $max) {

View File

@ -0,0 +1,364 @@
<?php namespace App\Ninja\Repositories;
use stdClass;
use DB;
use App\Models\Activity;
use App\Models\Invoice;
use App\Models\Task;
use DateInterval;
use DatePeriod;
class DashboardRepository
{
/**
* @param $groupBy
* @param $startDate
* @param $endDate
* @return array
*/
public function chartData($account, $groupBy, $startDate, $endDate, $currencyId, $includeExpenses)
{
$accountId = $account->id;
$startDate = date_create($startDate);
$endDate = date_create($endDate);
$groupBy = strtoupper($groupBy);
if ($groupBy == 'DAY') {
$groupBy = 'DAYOFYEAR';
}
$datasets = [];
$labels = [];
$totals = new stdClass;
$entitTypes = [ENTITY_INVOICE, ENTITY_PAYMENT];
if ($includeExpenses) {
$entitTypes[] = ENTITY_EXPENSE;
}
foreach ($entitTypes as $entityType) {
$data = [];
$count = 0;
$records = $this->rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId);
array_map(function ($item) use (&$data, &$count, $groupBy) {
$data[$item->$groupBy] = $item->total;
$count += $item->count;
}, $records->get());
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
$period = new DatePeriod($startDate, $interval, $endDate);
$endDate->modify('-1 '.$padding);
$records = [];
foreach ($period as $d) {
$dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n');
// MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365
$date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat);
$records[] = isset($data[$date]) ? $data[$date] : 0;
if ($entityType == ENTITY_INVOICE) {
$labels[] = $d->format('r');
}
}
if ($entityType == ENTITY_INVOICE) {
$color = '51,122,183';
} elseif ($entityType == ENTITY_PAYMENT) {
$color = '54,193,87';
} elseif ($entityType == ENTITY_EXPENSE) {
$color = '128,128,128';
}
$record = new stdClass;
$record->data = $records;
$record->label = trans("texts.{$entityType}s");
$record->lineTension = 0;
$record->borderWidth = 4;
$record->borderColor = "rgba({$color}, 1)";
$record->backgroundColor = "rgba({$color}, 0.05)";
$datasets[] = $record;
if ($entityType == ENTITY_INVOICE) {
$totals->invoices = array_sum($data);
$totals->average = $count ? round($totals->invoices / $count, 2) : 0;
} elseif ($entityType == ENTITY_PAYMENT) {
$totals->revenue = array_sum($data);
$totals->balance = $totals->invoices - $totals->revenue;
} elseif ($entityType == ENTITY_EXPENSE) {
//$totals->profit = $totals->revenue - array_sum($data);
$totals->expenses = array_sum($data);
}
}
$data = new stdClass;
$data->labels = $labels;
$data->datasets = $datasets;
$response = new stdClass;
$response->data = $data;
$response->totals = $totals;
return $response;
}
private function rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId)
{
$accountId = $account->id;
$currencyId = intval($currencyId);
$timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))';
$records = DB::table($entityType.'s')
->leftJoin('clients', 'clients.id', '=', $entityType.'s.client_id')
->whereRaw('(clients.id IS NULL OR clients.is_deleted = 0)')
->where($entityType.'s.account_id', '=', $accountId)
->where($entityType.'s.is_deleted', '=', false)
->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d'))
->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d'))
->groupBy($groupBy);
if ($entityType == ENTITY_EXPENSE) {
$records->where('expenses.expense_currency_id', '=', $currencyId);
} elseif ($currencyId == $account->getCurrencyId()) {
$records->whereRaw("(clients.currency_id = {$currencyId} or coalesce(clients.currency_id, 0) = 0)");
} else {
$records->where('clients.currency_id', '=', $currencyId);
}
if ($entityType == ENTITY_INVOICE) {
$records->select(DB::raw('sum(invoices.amount) as total, count(invoices.id) as count, '.$timeframe.' as '.$groupBy))
->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) {
$records->select(DB::raw('sum(payments.amount - payments.refunded) as total, count(payments.id) as count, '.$timeframe.' as '.$groupBy))
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('invoices.is_deleted', '=', false)
->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
} elseif ($entityType == ENTITY_EXPENSE) {
$records->select(DB::raw('sum(expenses.amount) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy));
}
return $records;
}
public function totals($accountId, $userId, $viewAll)
{
// total_income, billed_clients, invoice_sent and active_clients
$select = DB::raw(
'COUNT(DISTINCT CASE WHEN '.DB::getQueryGrammar()->wrap('invoices.id', true).' IS NOT NULL THEN '.DB::getQueryGrammar()->wrap('clients.id', true).' ELSE null END) billed_clients,
SUM(CASE WHEN '.DB::getQueryGrammar()->wrap('invoices.invoice_status_id', true).' >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent,
COUNT(DISTINCT '.DB::getQueryGrammar()->wrap('clients.id', true).') active_clients'
);
$metrics = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->leftJoin('invoices', 'clients.id', '=', 'invoices.client_id')
->where('accounts.id', '=', $accountId)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD);
if (!$viewAll){
$metrics = $metrics->where(function($query) use($userId){
$query->where('invoices.user_id', '=', $userId);
$query->orwhere(function($query) use($userId){
$query->where('invoices.user_id', '=', null);
$query->where('clients.user_id', '=', $userId);
});
});
}
return $metrics->groupBy('accounts.id')->first();
}
public function paidToDate($accountId, $userId, $viewAll)
{
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('clients.paid_to_date', true).') as value,'
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$paidToDate = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', $accountId)
->where('clients.is_deleted', '=', false);
if (!$viewAll){
$paidToDate = $paidToDate->where('clients.user_id', '=', $userId);
}
return $paidToDate->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'))
->get();
}
public function averages($accountId, $userId, $viewAll)
{
$select = DB::raw(
'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, '
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$averageInvoice = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->leftJoin('invoices', 'clients.id', '=', 'invoices.client_id')
->where('accounts.id', '=', $accountId)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false);
if (!$viewAll){
$averageInvoice = $averageInvoice->where('invoices.user_id', '=', $userId);
}
return $averageInvoice->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'))
->get();
}
public function balances($accountId, $userId, $viewAll)
{
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('clients.balance', true).') as value, '
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$balances = DB::table('accounts')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', $accountId)
->where('clients.is_deleted', '=', false)
->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'));
if (!$viewAll) {
$balances->where('clients.user_id', '=', $userId);
}
return $balances->get();
}
public function activities($accountId, $userId, $viewAll)
{
$activities = Activity::where('activities.account_id', '=', $accountId)
->where('activities.activity_type_id', '>', 0);
if (!$viewAll){
$activities = $activities->where('activities.user_id', '=', $userId);
}
return $activities->orderBy('activities.created_at', 'desc')
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task')
->take(50)
->get();
}
public function pastDue($accountId, $userId, $viewAll)
{
$pastDue = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', $accountId)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
->where('invoices.quote_invoice_id', '=', null)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('invoices.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '<', date('Y-m-d'));
if (!$viewAll){
$pastDue = $pastDue->where('invoices.user_id', '=', $userId);
}
return $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->orderBy('invoices.due_date', 'asc')
->take(50)
->get();
}
public function upcoming($accountId, $userId, $viewAll)
{
$upcoming = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', $accountId)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('invoices.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
->where('invoices.quote_invoice_id', '=', null)
->where('invoices.balance', '>', 0)
->where('invoices.is_deleted', '=', false)
->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '>=', date('Y-m-d'))
->orderBy('invoices.due_date', 'asc');
if (!$viewAll){
$upcoming = $upcoming->where('invoices.user_id', '=', $userId);
}
return $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->get();
}
public function payments($accountId, $userId, $viewAll)
{
$payments = DB::table('payments')
->leftJoin('clients', 'clients.id', '=', 'payments.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('payments.account_id', '=', $accountId)
->where('payments.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('clients.is_deleted', '=', false)
->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->whereNotIn('payments.payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
if (!$viewAll){
$payments = $payments->where('payments.user_id', '=', $userId);
}
return $payments->select(['payments.payment_date', DB::raw('(payments.amount - payments.refunded) as amount'), 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id'])
->orderBy('payments.payment_date', 'desc')
->take(50)
->get();
}
public function expenses($accountId, $userId, $viewAll)
{
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('expenses.amount', true).') as value,'
.DB::getQueryGrammar()->wrap('expenses.expense_currency_id', true).' as currency_id'
);
$paidToDate = DB::table('accounts')
->select($select)
->leftJoin('expenses', 'accounts.id', '=', 'expenses.account_id')
->where('accounts.id', '=', $accountId)
->where('expenses.is_deleted', '=', false);
if (!$viewAll){
$paidToDate = $paidToDate->where('expenses.user_id', '=', $userId);
}
return $paidToDate->groupBy('accounts.id')
->groupBy('expenses.expense_currency_id')
->get();
}
public function tasks($accountId, $userId, $viewAll)
{
return Task::scope()
->withArchived()
->whereIsRunning(true)
->get();
}
}

View File

@ -541,7 +541,7 @@ class InvoiceRepository extends BaseRepository
}
if ($productKey = trim($item['product_key'])) {
if (\Auth::user()->account->update_products && ! strtotime($productKey) && ! $task && ! $expense) {
if (\Auth::user()->account->update_products && ! $invoice->has_tasks && ! $invoice->has_expenses) {
$product = Product::findProductByKey($productKey);
if (!$product) {
if (Auth::user()->can('create', ENTITY_PRODUCT)) {
@ -735,15 +735,21 @@ class InvoiceRepository extends BaseRepository
* @param $clientId
* @return mixed
*/
public function findOpenInvoices($clientId)
public function findOpenInvoices($clientId, $entityType = false)
{
return Invoice::scope()
$query = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->whereClientId($clientId)
->whereIsRecurring(false)
->whereDeletedAt(null)
->whereHasTasks(true)
->where('invoice_status_id', '<', 5)
->whereDeletedAt(null);
if ($entityType == ENTITY_TASK) {
$query->whereHasTasks(true);
} elseif ($entityType == ENTITY_EXPENSE) {
$query->whereHasExpenses(true);
}
return $query->where('invoice_status_id', '<', 5)
->select(['public_id', 'invoice_number'])
->get();
}

View File

@ -0,0 +1,35 @@
<?php namespace App\Ninja\Transformers;
use App\Models\Activity;
/**
* @SWG\Definition(definition="Activity", @SWG\Xml(name="Activity"))
*/
class ActivityTransformer extends EntityTransformer
{
protected $defaultIncludes = [ ];
/**
* @var array
*/
protected $availableIncludes = [ ];
/**
* @param Client $client
* @return array
*/
public function transform(Activity $activity)
{
return [
'id' => $activity->key(),
'activity_type_id' => $activity->activity_type_id,
'client_id' => $activity->client->public_id,
'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,
'credit_id' => $activity->credit ? $activity->credit->public_id : null,
'updated_at' => $this->getTimestamp($activity->updated_at)
];
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace App\Policies;
class RecurringInvoicePolicy extends EntityPolicy {}

View File

@ -32,15 +32,12 @@ class AppServiceProvider extends ServiceProvider
return 'data:image/jpeg;base64,' . base64_encode($contents);
});
Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') {
$capitalize = config('former.capitalize_translations');
$class = ( Request::is($url) || Request::is($url.'/*') || Request::is($url2.'/*') ) ? ' class="active"' : '';
if ($capitalize) {
$title = ucwords(trans("texts.$text")) . Utils::getProLabel($text);
} else {
Form::macro('nav_link', function($url, $text) {
//$class = ( Request::is($url) || Request::is($url.'/*') || Request::is($url2.'/*') ) ? ' class="active"' : '';
$class = ( Request::is($url) || Request::is($url.'/*') ) ? ' class="active"' : '';
$title = trans("texts.$text") . Utils::getProLabel($text);
}
return '<li'.$class.'><a href="'.URL::to($url).'" '.$extra.'>'.$title.'</a></li>';
return '<li'.$class.'><a href="'.URL::to($url).'">'.$title.'</a></li>';
});
Form::macro('tab_link', function($url, $text, $active = false) {
@ -53,41 +50,10 @@ class AppServiceProvider extends ServiceProvider
$Type = ucfirst($type);
$Types = ucfirst($types);
$class = ( Request::is($types) || Request::is('*'.$type.'*')) && !Request::is('*settings*') ? ' active' : '';
$user = Auth::user();
$str = '<li class="dropdown '.$class.'">
<a href="'.URL::to($types).'" class="dropdown-toggle">'.trans("texts.$types").'</a>';
$items = [];
if ($user->can('create', $type)) {
$items[] = '<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>';
}
if ($type == ENTITY_INVOICE) {
if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('recurring_invoices').'">'.trans('texts.recurring_invoices').'</a></li>';
if($user->can('create', ENTITY_INVOICE))$items[] = '<li><a href="'.URL::to('recurring_invoices/create').'">'.trans('texts.new_recurring_invoice').'</a></li>';
$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('quotes').'">'.trans('texts.quotes').'</a></li>';
if($user->can('create', ENTITY_QUOTE))$items[] = '<li><a href="'.URL::to('quotes/create').'">'.trans('texts.new_quote').'</a></li>';
} else if ($type == ENTITY_CLIENT) {
if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('credits').'">'.trans('texts.credits').'</a></li>';
if($user->can('create', ENTITY_CREDIT))$items[] = '<li><a href="'.URL::to('credits/create').'">'.trans('texts.new_credit').'</a></li>';
} else if ($type == ENTITY_EXPENSE) {
if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('vendors').'">'.trans('texts.vendors').'</a></li>';
if($user->can('create', ENTITY_VENDOR))$items[] = '<li><a href="'.URL::to('vendors/create').'">'.trans('texts.new_vendor').'</a></li>';
}
if(!empty($items)){
$str.= '<ul class="dropdown-menu" id="menu1">'.implode($items).'</ul>';
}
$str .= '</li>';
return $str;
return '<li class="dropdown '.$class.'">
<a href="'.URL::to($types).'" class="dropdown-toggle">'.trans("texts.$types").'</a>
</li>';
});
Form::macro('flatButton', function($label, $color) {
@ -199,14 +165,6 @@ class AppServiceProvider extends ServiceProvider
return true;
});
Validator::extend('less_than', function($attribute, $value, $parameters) {
return floatval($value) <= floatval($parameters[0]);
});
Validator::replacer('less_than', function($message, $attribute, $rule, $parameters) {
return str_replace(':value', $parameters[0], $message);
});
Validator::extend('has_counter', function($attribute, $value, $parameters) {
return !$value || strstr($value, '{$counter}');
});

View File

@ -11,6 +11,7 @@ class EventServiceProvider extends ServiceProvider {
* @var array
*/
protected $listen = [
// Clients
'App\Events\ClientWasCreated' => [
'App\Listeners\ActivityListener@createdClient',
@ -159,6 +160,11 @@ class EventServiceProvider extends ServiceProvider {
'App\Events\TaskWasUpdated' => [
'App\Listeners\ActivityListener@updatedTask',
],
// Update events
\Codedge\Updater\Events\UpdateAvailable::class => [
\Codedge\Updater\Listeners\SendUpdateAvailableNotification::class
],
];
/**

View File

@ -646,7 +646,7 @@ class ImportService
{
EntityModel::$notifySubscriptions = false;
foreach ([ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_PAYMENT] as $entityType) {
foreach ([ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_QUOTE] as $entityType) {
$this->results[$entityType] = [
RESULT_SUCCESS => [],
RESULT_FAILURE => [],

View File

@ -29,7 +29,8 @@
"stacktrace-js": "~1.0.1",
"fuse.js": "~2.0.2",
"dropzone": "~4.3.0",
"sweetalert": "~1.1.3"
"sweetalert": "~1.1.3",
"bootstrap-daterangepicker": "~2.1.24"
},
"resolutions": {
"jquery": "~1.11"

View File

@ -80,7 +80,9 @@
"collizo4sky/omnipay-wepay": "dev-additional-calls",
"barryvdh/laravel-ide-helper": "~2.2",
"barryvdh/laravel-debugbar": "~2.2",
"fzaninotto/faker": "^1.5"
"fzaninotto/faker": "^1.5",
"jaybizzle/laravel-crawler-detect": "1.*",
"codedge/laravel-selfupdater": "5.x-dev"
},
"require-dev": {
"phpunit/phpunit": "~4.0",

558
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -154,6 +154,8 @@ return [
'Jlapp\Swaggervel\SwaggervelServiceProvider',
'Maatwebsite\Excel\ExcelServiceProvider',
Websight\GcsProvider\CloudStorageServiceProvider::class,
'Jaybizzle\LaravelCrawlerDetect\LaravelCrawlerDetectServiceProvider',
Codedge\Updater\UpdaterServiceProvider::class,
/*
* Application Service Providers...
@ -255,7 +257,8 @@ return [
'Socialite' => 'Laravel\Socialite\Facades\Socialite',
'Excel' => 'Maatwebsite\Excel\Facades\Excel',
'PushNotification' => 'Davibennun\LaravelPushNotification\Facades\PushNotification',
'Crawler' => 'Jaybizzle\LaravelCrawlerDetect\Facades\LaravelCrawlerDetect',
'Updater' => Codedge\Updater\UpdaterFacade::class,
],
];

73
config/self-update.php Normal file
View File

@ -0,0 +1,73 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default source repository type
|--------------------------------------------------------------------------
|
| The default source repository type you want to pull your updates from.
|
*/
'default' => env('SELF_UPDATER_SOURCE', 'github'),
/*
|--------------------------------------------------------------------------
| Version installed
|--------------------------------------------------------------------------
|
| Set this to the version of your software installed on your system.
|
*/
'version_installed' => env('SELF_UPDATER_VERSION_INSTALLED', '2.6.9'),
/*
|--------------------------------------------------------------------------
| Repository types
|--------------------------------------------------------------------------
|
| A repository can be of different types, which can be specified here.
| Current options:
| - github
|
*/
'repository_types' => [
'github' => [
'type' => 'github',
'repository_vendor' => env('SELF_UPDATER_REPO_VENDOR', 'invoiceninja'),
'repository_name' => env('SELF_UPDATER_REPO_NAME', 'invoiceninja'),
'repository_url' => '',
'download_path' => env('SELF_UPDATER_DOWNLOAD_PATH', '/tmp'),
],
],
/*
|--------------------------------------------------------------------------
| Event Logging
|--------------------------------------------------------------------------
|
| Configure if fired events should be logged
|
*/
'log_events' => env('SELF_UPDATER_LOG_EVENTS', false),
/*
|--------------------------------------------------------------------------
| Mail To Settings
|--------------------------------------------------------------------------
|
| Configure if fired events should be logged
|
*/
'mail_to' => [
'address' => env('SELF_UPDATER_MAILTO_ADDRESS', ''),
'name' => env('SELF_UPDATER_MAILTO_NAME', ''),
],
];

View File

@ -29,7 +29,7 @@ return [
|
*/
'lifetime' => env('SESSION_LIFETIME', 120),
'lifetime' => env('SESSION_LIFETIME', (60 * 8)),
'expire_on_close' => false,

View File

@ -41,38 +41,6 @@ class ConstantsSeeder extends Seeder
Frequency::create(array('name' => 'Six months'));
Frequency::create(array('name' => 'Annually'));
Industry::create(array('name' => 'Accounting & Legal'));
Industry::create(array('name' => 'Advertising'));
Industry::create(array('name' => 'Aerospace'));
Industry::create(array('name' => 'Agriculture'));
Industry::create(array('name' => 'Automotive'));
Industry::create(array('name' => 'Banking & Finance'));
Industry::create(array('name' => 'Biotechnology'));
Industry::create(array('name' => 'Broadcasting'));
Industry::create(array('name' => 'Business Services'));
Industry::create(array('name' => 'Commodities & Chemicals'));
Industry::create(array('name' => 'Communications'));
Industry::create(array('name' => 'Computers & Hightech'));
Industry::create(array('name' => 'Defense'));
Industry::create(array('name' => 'Energy'));
Industry::create(array('name' => 'Entertainment'));
Industry::create(array('name' => 'Government'));
Industry::create(array('name' => 'Healthcare & Life Sciences'));
Industry::create(array('name' => 'Insurance'));
Industry::create(array('name' => 'Manufacturing'));
Industry::create(array('name' => 'Marketing'));
Industry::create(array('name' => 'Media'));
Industry::create(array('name' => 'Nonprofit & Higher Ed'));
Industry::create(array('name' => 'Pharmaceuticals'));
Industry::create(array('name' => 'Professional Services & Consulting'));
Industry::create(array('name' => 'Real Estate'));
Industry::create(array('name' => 'Retail & Wholesale'));
Industry::create(array('name' => 'Sports'));
Industry::create(array('name' => 'Transportation'));
Industry::create(array('name' => 'Travel & Luxury'));
Industry::create(array('name' => 'Other'));
Industry::create(array('name' => 'Photography'));
Size::create(array('name' => '1 - 3'));
Size::create(array('name' => '4 - 10'));
Size::create(array('name' => '11 - 50'));

View File

@ -11,7 +11,7 @@ class CurrenciesSeeder extends Seeder
// http://www.localeplanet.com/icu/currency.html
$currencies = [
['name' => 'US Dollar', 'code' => 'USD', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['name' => 'Pound Sterling', 'code' => 'GBP', 'symbol' => '£', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['name' => 'British Pound', 'code' => 'GBP', 'symbol' => '£', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['name' => 'Euro', 'code' => 'EUR', 'symbol' => '€', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
['name' => 'South African Rand', 'code' => 'ZAR', 'symbol' => 'R', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
['name' => 'Danish Krone', 'code' => 'DKK', 'symbol' => 'kr ', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ',', 'swap_currency_symbol' => true],

View File

@ -26,5 +26,6 @@ class DatabaseSeeder extends Seeder
$this->call('PaymentTermsSeeder');
$this->call('PaymentTypesSeeder');
$this->call('LanguageSeeder');
$this->call('IndustrySeeder');
}
}

View File

@ -46,7 +46,7 @@ class DateFormatsSeeder extends Seeder
['format' => 'M j, Y g:i a', 'format_moment' => 'MMM D, YYYY h:mm:ss a'],
['format' => 'F j, Y g:i a', 'format_moment' => 'MMMM D, YYYY h:mm:ss a'],
['format' => 'D M jS, Y g:i a', 'format_moment' => 'ddd MMM Do, YYYY h:mm:ss a'],
['format' => 'Y-m-d g:i a', 'format_moment' => 'YYYY-MMM-DD h:mm:ss a'],
['format' => 'Y-m-d g:i a', 'format_moment' => 'YYYY-MM-DD h:mm:ss a'],
['format' => 'd-m-Y g:i a', 'format_moment' => 'DD-MM-YYYY h:mm:ss a'],
['format' => 'm/d/Y g:i a', 'format_moment' => 'MM/DD/YYYY h:mm:ss a'],
['format' => 'd.m.Y g:i a', 'format_moment' => 'D.MM.YYYY h:mm:ss a'],

View File

@ -0,0 +1,55 @@
<?php
use App\Models\Industry;
class IndustrySeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$industries = [
['name' => 'Accounting & Legal'],
['name' => 'Advertising'],
['name' => 'Aerospace'],
['name' => 'Agriculture'],
['name' => 'Automotive'],
['name' => 'Banking & Finance'],
['name' => 'Biotechnology'],
['name' => 'Broadcasting'],
['name' => 'Business Services'],
['name' => 'Commodities & Chemicals'],
['name' => 'Communications'],
['name' => 'Computers & Hightech'],
['name' => 'Defense'],
['name' => 'Energy'],
['name' => 'Entertainment'],
['name' => 'Government'],
['name' => 'Healthcare & Life Sciences'],
['name' => 'Insurance'],
['name' => 'Manufacturing'],
['name' => 'Marketing'],
['name' => 'Media'],
['name' => 'Nonprofit & Higher Ed'],
['name' => 'Pharmaceuticals'],
['name' => 'Professional Services & Consulting'],
['name' => 'Real Estate'],
['name' => 'Retail & Wholesale'],
['name' => 'Sports'],
['name' => 'Transportation'],
['name' => 'Travel & Luxury'],
['name' => 'Other'],
['name' => 'Photography'],
['name' => 'Construction'],
];
foreach ($industries as $industry) {
$record = Industry::whereName($industry['name'])->first();
if ( ! $record) {
Industry::create($industry);
}
}
Eloquent::reguard();
}
}

View File

@ -8,6 +8,9 @@ class LanguageSeeder extends Seeder
{
Eloquent::unguard();
// https://github.com/caouecs/Laravel-lang
// https://www.loc.gov/standards/iso639-2/php/code_list.php
$languages = [
['name' => 'English', 'locale' => 'en'],
['name' => 'Italian', 'locale' => 'it'],
@ -26,6 +29,7 @@ class LanguageSeeder extends Seeder
['name' => 'Polish', 'locale' => 'pl'],
['name' => 'Czech', 'locale' => 'cs'],
['name' => 'Croatian', 'locale' => 'hr'],
['name' => 'Albanian', 'locale' => 'sq'],
];
foreach ($languages as $language) {

View File

@ -22,5 +22,6 @@ class UpdateSeeder extends Seeder
$this->call('PaymentTermsSeeder');
$this->call('PaymentTypesSeeder');
$this->call('LanguageSeeder');
$this->call('IndustrySeeder');
}
}

View File

@ -10,6 +10,7 @@ use App\Models\InvoiceDesign;
use App\Models\Client;
use App\Models\Contact;
use App\Models\Product;
use App\Models\DateFormat;
use Faker\Factory;
class UserTableSeeder extends Seeder
@ -42,6 +43,7 @@ class UserTableSeeder extends Seeder
'primary_color' => $faker->hexcolor,
'timezone_id' => 1,
'company_id' => $company->id,
//'date_format_id' => DateFormat::all()->random()->id,
]);
$user = User::create([

225
docs/Makefile Normal file
View File

@ -0,0 +1,225 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/InvoiceName.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/InvoiceName.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/InvoiceName"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/InvoiceName"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."

38
docs/api_tokens.rst Normal file
View File

@ -0,0 +1,38 @@
API Tokens
==========
Invoice Ninja uses API tokens to enable access to third party providers, so you can streamline many of your invoicing and payments functions with our partners.
Tokens
""""""
The API Tokens page displays a table listing your current API tokens.
To add a new API token, click the blue Add Token + button at the top right of the page. The Tokens/ Create page will open.
Create Token
^^^^^^^^^^^^
Enter the token name in the field and click the green Save button. A new token number will be automatically generated. You will then be redirected to the API Tokens page, where the new token will display in the table next to the relevant Name entry.
If you want to cancel the new entry before saving it, click the gray Cancel button.
Edit Token
^^^^^^^^^^
To edit an existing token, click on the gray Select button in the Action column of the API Tokens table, and a drop down menu will open. Select Edit Token from the menu, and the Tokens/ Edit page will open. You can now edit the token name. Click Save to apply the changes, or Cancel.
Archive Token
^^^^^^^^^^^^^
To archive an existing token, click on the gray Select button in the Action column of the API Tokens table, and a drop down menu will open. Select Archive Token from the menu, and the token will automatically be sent to archives. It will no longer be viewable in the API Tokens table.
Documentation
^^^^^^^^^^^^^
Need some extra help? Click on the gray Documentation button directly above the API Tokens table, and you'll be redirected to https://www.invoiceninja.com/api-documentation/. Here, you can read all about using API documentation in your Invoice Ninja account.
Zapier
""""""
Invoice Ninja proudly partners with https://zapier.com/ to provide seamless app connections for your Invoice Ninja activity. Check out various Zaps that deliver integrated functionality between Invoice Ninja and other apps to help you streamline your accounting. Click on the gray Zapier button, just to the right of the Documentation button, to find out more.

15
docs/client_portal.rst Normal file
View File

@ -0,0 +1,15 @@
Client Portal
=============
The invoicing process is a two-way street. You bill the client; the client needs to view the invoice and make the payment. Why not make it as easy as possible for you and for your clients? This is the purpose of Invoice Ninja's Client Portal. With Invoice Ninja, you can provide a portal for your clients where they can open and view your invoices, and even make payments, all via the Invoice Ninja website pages.
- **Dashboard**: The Client Portal Dashboard is a summary page that shows all your invoicing activity with the specific client. Do you want to display the Dashboard page for your clients when they use the Client Portal? You can choose to show or hide the dashboard. Check the Enable box to show the dashboard. Uncheck to hide the dashboard.
- **Password protect invoices**: To increase security of invoice viewing, you can opt to make the invoices password protected for each individual client. To view your invoice, the client will need to enter a specific password. Check the box to enable the password protect function.
- **Generate password automatically**: If your client does not already have a password for the portal, you can ensure they get one by enabling the system to automatically generate a password. The password will be sent to the client together with the first invoice. To enable this function, check the box.
Custom CSS
""""""""""
Do you have some experience in web design? Want to put your individual fingerprint on your client portal? You can control the look and feel of your client portal by entering custom CSS in your portal layout. Enter the CSS is the Custom CSS field.
To apply all changes to the Client Portal, click the green Save button at the bottom of the page.

117
docs/clients.rst Normal file
View File

@ -0,0 +1,117 @@
Clients
=======
They come in all shapes and sizes. Theyre the reason you go to work in the morning. There are new ones coming up, and old ones coming back. What you need is a well-maintained, up-to-date, comprehensive client list to keep your invoicing in order.
Your clients are the core of your freelance business, and your Clients page is the core of your activity on Invoice Ninja.
List Clients
""""""""""""
The Clients page is a list page that presents a summary of all your clients in a user-friendly table. Think of your Clients page as the “central station” of your invoicing activity. Most of your day-to-day invoicing actions can be taken from the various links and buttons that appear on the Clients list page. Now, well take a closer look at the setup of the Clients page, and the range of actions available to you on the Clients page.
To view your client list page, go to the main taskbar and click the Clients tab.
Overview
^^^^^^^^
The Clients page presents a list summary of all your current clients in a table format. The main elements of the table include:
- **Client:** The name of the client
- **Contact:** The name of the primary contact person
- **Email:** The client email address
- **Date Created:** The date the client was created in the system
- **Last Login:** The date an action was last taken for this client
- **Balance:** The clients payment balance
- **Action:** A range of actions you can take to manage activity relating to the selected client
Actions
^^^^^^^
To select an action for a particular client, hover with your mouse anywhere in the row entry of the client. A gray Select button will appear. Click on the Select arrow and a drop-down list will open.
When you click on an action, you will be automatically redirected to the relevant action page for the selected client. Here are the available actions in the drop-down list of the Action button, and the corresponding action pages that will open:
- **Edit Client** Edit the clients details on the Clients / Edit page
- **New Task** Enter a new task on the Tasks / Create page
- **New Invoice** Enter a new invoice on the Invoices / Create page
- **New Quote** Enter a new quote on the Quotes / Create page
- **Enter Payment** Enter a new payment on the Payments / Create page
- **Enter Credit** Enter a new credit on the Credits / Create page
- **Archive client** Click to archive the client
- **Delete client** Click to delete the client
Credits
^^^^^^^
You can manage your credits by visiting the Credits page directly from the Clients page. To open the Credits page, click on the gray Credits button that appears at the top right side of the page, to the left of the New Client + button.
Sorting & Filtering Clients
The sort and filter functions make it easy for you to manage and view your client information.
Sort the clients table via any of the following data columns: Client, Contact, Email, Date Created, Last Login, or Balance. To sort, click on the tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction. (If you click on the Client, Contact or Email arrow, the data will be displayed in alphabetical or reverse alphabetical order.)
Filter the clients list by completing the Filter field, situated at the top right of the page, to the left of the gray Credits button. Clients can be filtered according to the client name, contact person name, or elements of the client name or contact person name. Heres an example: Lets filter the table for a client named “Joe Smith” of “Best Ninja” company. You can type “best ninja”, or “best” or “ninja”, or even “bes”, or “nin”, or “ja”, or “Joe”, “Smith”, “Jo” “oe”, “th” or any other grouping of letters in the client name or contact person name. The filter function will automatically locate and present all the relevant entries. This function makes it easy to find clients with even minimal input of information.
.. Tip:: Need to search for a specific client in your Clients list? Start typing the first letters of the client's name and the filter will automatically present the relevant listings.
Archiving/Deleting
^^^^^^^^^^^^^^^^^^
To archive or delete a specific client, hover over the client entry row, and open the Action drop-down list. Select Archive client or Delete client from the list. The Clients table will automatically refresh. Archived clients will appear in the table with a lighter gray font color, while deleted clients are hidden from view.
Note: You can also archive or delete one or more clients via the gray Archive button that appears at the top left side of the Clients table. To archive or delete clients, check the relevant clients in the check boxes that appear in the far left column next to the client name. Then click on the Archive button, open the drop-down list and select the desired action.
Want to view archived or deleted clients? Check the box marked Show archived/deleted clients, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of clients, including current, archived and deleted clients.
- **Deleted clients** are displayed with a strike through.
- **Archived clients** are displayed with a lighter gray font color.
You can choose to restore or delete the archived client. To restore an archived client, hover with your mouse over the Action area for the relevant archived client. A gray Select button will appear. Click on the Select arrow, and choose Restore client from the drop-down list. To delete an archived client, select Delete client from the drop-down list of the Select button.
Create Client
"""""""""""""
So, youve taken on a new client? Congratulations!
Your Clients list is at the heart of your invoicing activity, so it's really important to maintain current information on all your clients. When you start working with a new client, the first thing youll need to do is to add the new client by entering their contact information and business details.
When creating and saving a new client to your Clients list, make sure to have the relevant, up-to-date information at hand. You are only required to enter the information one time. Invoice Ninja automatically tracks all invoicing activity for each client. Need to create an invoice, schedule a task or update a payment status? Simply select the clients name from the Client list.
There are two ways to enter a new client:
1. Via the Create Client page.
2. Or, while creating a new invoice.
Here, were going to focus on entering a new client via the Create Client page.
**Lets Begin**
To enter a new client, go to the Clients tab, open the drop-down menu, and click on New Client. This will open the Create Client page.
The Create Client page is divided into four sections. Enter the information in the relevant fields.
.. Note:: You dont have to complete every field. Enter the information that is important or necessary for your needs.
Lets take a closer look at each section:
- **Organization**: Enter details about your clients business/company/organization, including the company name, ID number, VAT number, website address and telephone number.
- **Contacts**: Enter the name, email address and phone number of your contact person for this client. You can enter as many contact people as you like. To add more contact people, click +Add Contact.
- **Address**: Enter the street address of your client. This will be of particular importance if you need to send hard-copy invoices or payment receipts.
- **Additional Info**: Enter the payment currency, language, payment due date, company size (no. of employees), the relevant industry sector, and any other private notes or reminders you wish to add (dont worry - no one can see them but you.)
Once you have filled in the page, click Save to save the new client information. From now on, when you click the Client field, the clients name will appear in the drop down menu. Simply select the client you need and let the invoicing begin!
How to Edit Client Information
The information you enter on the Create Client page acts as your default settings for this client. You can change these settings at any time. How? There are two methods:
Via the Clients list
1. Select the Clients tab to view your client list.
2. Select the relevant client from the list. The summary page of the client will open.
3. Click on the gray Edit Client button, at the top right corner of the page. You will now be taken to the Clients/Edit page, where you can edit any of the fields.
During the invoicing process
1. Open the New Invoice page.
2. Click on the arrow at the right end of the Client field, and select the client name from the drop down menu.
3. Click Edit Client, which appears below the Client field. This will open the Client window. You can now edit the clients information.

338
docs/conf.py Normal file
View File

@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
#
# Invoice Ninja documentation build configuration file, created by
# sphinx-quickstart on Fri Aug 19 12:02:54 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Invoice Ninja'
copyright = u'2016, Invoice Ninja'
author = u'Invoice Ninja'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'2.6'
# The full version, including alpha/beta/rc tags.
release = u'2.6.10'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'Invoice Ninja v2.6.10'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'InvoiceNamedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'InvoiceName.tex', u'Invoice Ninja Documentation',
u'Hillel Coren', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'invoicename', u'Invoice Ninja Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'InvoiceName', u'Invoice Ninja Documentation',
author, 'InvoiceName', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

84
docs/credits.rst Normal file
View File

@ -0,0 +1,84 @@
Credits
=======
List Credits
""""""""""""
Your workload and fee structure vary from client to client. Whats more, your work schedule may change from one month to the next, or even from week to week and often for the same client.
At times, payments can get really tricky. Perhaps youve been paid in advance for work that has been delayed, or maybe the scope of work changed and you were overpaid. Whatever the reason, there will come a time when you will need to issue a credit to your clients account. This is where the Credits function comes in handy.
Overview
^^^^^^^^
The Credits list page is a summary of all credits issued to all clients.
To open the Credits page, click on the Clients tab, open the drop-down menu, and click on Credits.
On the Credits page, credit information is presented in a simple, user-friendly table. Lets take a closer look at the table elements:
- **Client**: The clients name
- **Credit**: Amount The amount of the individual credit
- **Credit Balance**: The balance of the individual credit
- **Date of Issue**: The date the individual credit was issued
- **Private Notes**: Comments or reminders that you included FYI
- **Action**: Option to archive or delete the credit
Archiving/Deleting
^^^^^^^^^^^^^^^^^^
To archive or delete a credit, hover over the credit entry row, and open the Action drop-down list. Select Archive credit or Delete credit from the list. The Credits table will automatically refresh, and archived or deleted credits will no longer appear in the list.
You can also archive or delete one or more credit via the gray Archive button that appears at the top left side of the Credits list page. To archive or delete credits, check the relevant credits in the check boxes that appear in the far left column next to the client field. The number of credits selected for archiving/deleting will automatically update and show on the Archive button. Then click on the Archive button, open the drop-down list and select the desired action.
Want to view archived or deleted credits? Check the box marked Show archived/deleted credits, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of credits, including current, archived and deleted credits. The status of the archived or deleted credits will appear in the column at the far right of the table.
- Deleted credits are displayed with a red Deleted button. To restore deleted credits, hover on the red Deleted button. A gray Select button will appear. Click on the Select arrow, and select Restore credit in the drop-down list.
- Archived credits are displayed with an orange Archived button. To restore or delete the archived credit, hover on the orange Archived button. A gray Select button will appear. Click on the Select arrow, and choose Restore credit from the drop-down list. To delete an archived credit, select Delete credit from the drop-down list of the Select button.
Sorting & Filtering
^^^^^^^^^^^^^^^^^^^
The sort and filter functions make it easy for you to manage and analyze your credits.
- Sort the credits table via any of the following data columns: Client, Credit Amount, Credit Balance, Credit Date and Private Notes. To sort, click on the tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction. (If you click on the Client or Private Notes arrow, the data will be displayed in alphabetical or /reverse alphabetical order.)
- Filter the credits table by completing the Filter field, situated on the top right of the page, to the left of the blue Enter Credit button. Credits can be filtered according to the client name, or elements of the client name. Heres an example: Lets filter the table for credits issued to “Best Ninja”. You can type “best ninja”, or “best” or “ninja”, or even “bes”, or “nin”, or “ja”, or any other grouping of letters in the client name. The filter function will automatically locate and present all the relevant entries. This function makes it easy to find credit entries with even minimal input of information.
.. TIP:: Need to search for a specific client in your Credits list? Start typing the first letters of the client's name and the filter will automatically present the relevant listings.
Enter Credit
""""""""""""
Creating a new credit is fast and simple. Remember, all credits you create will appear in the Credits list page.
**Lets Begin**
To issue a credit, youll need to open the Credits / Create page.
There are three ways to open this page:
1. Go to the Clients tab, open the drop-down menu, and click on Enter Credit. This will open the Credits / Create page. TIP: This is the quickest way to reach the Enter Credit page.
2. Go to the Clients tab, open the drop-down menu, and click on Credits. This will open the Credits list page, which displays a summary of all your clients credits. To issue a new credit, click the blue Enter Credit + button in the upper right corner, above the orange bar.
3. Go to the Client's summary page. Locate the blue New Invoice button at the top right of the page. Click on the arrow at the right side of the button. A drop-down list will open. Select Enter Credit. This will open the Credits / Create page. TIP: This method will automatically pre-select the client's name in the Client field of the Enter Credit page.
Ok, so youve successfully landed on the Credits / Create page.
Overview
^^^^^^^^
The Credits / Create page has four fields for you to complete. Lets take a closer look at each field:
- **Client**: Click on the arrow at the right end of the Client field. Select the relevant client from the client list.
- **Amount**: Enter the credit amount. The currency is determined by the currency setting for the specific client.
- **Credit Date**: Select the appropriate date of issue for the credit. It may be todays date or any other date.
- **Private Notes**: [Optional] Enter any private comments or reminders (dont worry - no one can see them but you.)
Saving the Credit
^^^^^^^^^^^^^^^^^
To save your new credit, click Save.
Youve now completed the process of issuing a credit to your clients account.
When you click Save, youll be automatically redirected to the clients summary page. The credit balance will appear in the Standing column, under Credit.
If you wish to view the clients credit details in full, click on the gray Credits tab at the right side of the clients summary page. This will open a table displaying information about all credits issued to the client, including amount, balance, date of issue and private notes.

View File

@ -0,0 +1,10 @@
Data Visualizations
===================
Who says analyzing your invoicing data can't be fun? Data Visualizations is an interactive, intuitive and practical feature that helps you understand exactly what is going on with your invoicing numbers. The visualization function takes your data, according to the data group you select, and creates a visual pattern that demonstrates your income proportions according to various parameters. What's more, it's not just a graphical display; it also links to the various data in the illustration, making it easy for you to dig deeper into your numbers, simply by hovering your mouse.
- **Group by**: To generate a Data Visualization, select the required data group (Clients, Invoices, Products). The visualization will be automatically generated below.
To view particular data, hover your mouse over the circles within the illustration. Text boxes will automatically appear displaying information about the data grouping.
.. TIP:: For the Invoices and Clients data visualizations, each display box includes a View link at the top right. To view the specific client or invoice, click on the View link. The Edit Invoice or View Client page will open in another window.

24
docs/email_settings.rst Normal file
View File

@ -0,0 +1,24 @@
Email Settings
==============
Email communication with your clients is an important part of the Invoice Ninja invoicing process sending invoices via email, notifying clients that an invoice is due, reminding clients about overdue payments, and more.
With the Email Settings feature, you can specify certain settings and designs for the notification emails your clients receive from your Invoice Ninja account.
The Email Settings page includes two sections: **Email Settings** and **Email Designs**.
Email Settings
""""""""""""""
- **Attach PDFs**: Want to be able to attach PDF files to your emails? Check the Enable box.
- **Invoice Link**: When you email an invoice to a client, the invoice is viewable via a web link that is embedded in the notification email. The default link uses a URL from the Invoice Ninja site. If you wish to change the link to your own website or another subdomain, check the relevant box and enter the URL details.
Email Design
""""""""""""
- **Email Style**: You can make your emails look more professional by choosing a design layout. Select the desired style by opening the drop-down menu. Available styles are Plain (regular email layout), Light (graphical layout featuring light border) and Dark (graphical layout featuring dark border). To preview the different styles, click the question mark icon at the right end of the field.
- **Enable markup**: Want to give your clients the convenient option to pay you online with a direct link from the invoice notification email? Check Enable markup to add a payment link to the invoice email. Then, your clients can click through to submit an online payment.
Finished customizing your email settings? Click the green Save button to apply the new settings.

115
docs/expenses.rst Normal file
View File

@ -0,0 +1,115 @@
Expenses
========
Running a freelance business isn't just about the money that's coming in. You also need to take care of the money going out. With Invoice Ninja, all your earnings, expenses, clients and vendors are stored and managed in one, smart system designed to keep you on top of things. What's more, the Expenses part of your Invoice Ninja account streamlines with your invoicing via a click of the mouse, across multiples currencies, so you get the complete bigger picture of your business expenses - with simplicity and ease.
List Expenses
"""""""""""""
To view the Expenses list page, click on the Expenses tab on the main taskbar.
Overview
^^^^^^^^
The Expenses list page displays a summary of all business expenses that you choose to enter. Apart from giving an overview of all your recorded expenses in a table format, you can also carry out a number of important actions from the Expenses page. First, let's take a look at the various columns in the Expenses table from left to right:
- **Vendor**: The name of the vendor
- **Client**: The name of the client for whom the expense is relevant
- **Expense**: Date The date the expense occurred
- **Amount**: The expense amount
- **Category**: The assigned category of the expense
- **Public Notes**: The notes entered when creating the expense (this becomes the item description if the expense is converted to an invoice)
- **Status**: The current status of the expense: Logged (blue), Pending (orange), Invoiced (gray), Paid (green)
The final column to the right is the Action column. To view the actions, hover your mouse over the Action area of the relevant expense entry and a gray Select button will appear. Click on the arrow at the right side of the button to open a drop-down list. These are the action options:
- **Edit Expense**: Edit the expense information on the Edit Expenses page.
- **Invoice Expense**: Convert the expense to a client invoice.
- **Archive Expense**: Click here to archive the expense. It will be archived and removed from the Expenses list page.
- **Delete Expense**: Click here to delete the expense. It will be deleted and removed from the Expenses list page.
.. TIP:: To sort the Expenses list according to any of the columns, click on the orange column tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction.
Expense Categories
""""""""""""""""""
In order to better manage your business expenses, you can create custom categories and assign them to various expenses. For example, you may decide to create a category for "Office Supplies", or "Events" or perhaps "Salaries". Whichever categories you decide you need, creating them is super simple. First, you'll need to open the Expense Categories page.
To open the Expense Categories page:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Click on the gray Categories button that appears above the Expenses table. The page features a list of all existing categories.
To create a new Expense Category:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Click on the blue New Expense Category + button located at the top right of the Expense Categories page. On the Expense Categories/ Create page, enter the name of the category, and click Save. The new category will now appear in the list on the Expense Categories page. When you create a new expense, you can apply a category from your list to the expense.
To edit an Expense Category:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
On the Expense Categories page, click on the gray Select button in the far right column of the category entry, and select Edit Category.
To archive an Expense Category:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
On the Expense Categories page, select the relevant category, and click the gray Archive button above left of the Expense Categories table. The category will be permanently removed from the Categories list. To view archived categories, check the Show archived/deleted box next to the Archive button. The category will now be viewable again in the Expense category table, marked with an orange Archived status. To restore the archived category, click on the gray Select button in the far right column of the category entry, and select Restore expense category.
.. TIP:: There's another fast way to archive an Expense Category. Click on the gray Select button in the far right column of the category entry, and select Archive Category.
Here's a fast, easy way to get to the Vendors page from the Expenses list page. Click the gray Vendors button located at the top right of the page, to the right of the Categories button.
Filter
^^^^^^
To filter the Expenses list, enter the filter data in the Filter field, situated at the top right of the page, to the left of the gray Categories button. Expenses can be filtered according to Vendor name. Enter the name or parts of the name, and the filter function will automatically locate and present the relevant entries.
Archiving/Deleting Expenses
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To archive or delete an expense, hover over the expense entry row, and open the Action drop-down list. Select Archive Expense or Delete Expense from the list. The Expenses table will automatically refresh, and archived or deleted expenses will no longer appear in the list.
You can also archive or delete one or more expense via the gray Archive button that appears at the top left side of the Expenses list page. To archive or delete expenses, check the relevant expenses in the check boxes that appear in the far left column next to the vendor name. The number of expenses selected for archiving/deleting will automatically update and show on the Archive button. Then click on the Archive button, open the drop-down list and select the desired action.
.. NOTE:: Want to view archived or deleted expenses? Check the box marked Show archived/deleted, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of expenses, including current, archived and deleted expenses. The status of the archived and deleted expenses will be displayed in the far right column.
- Deleted expenses are displayed with a strikethrough line and a red Deleted button in the right hand column of the expense entry. To restore deleted expenses, hover on the red Deleted button. A gray Select button will appear. Click on the Select arrow, and select Restore expense in the drop-down list.
- Archived expenses are displayed with an orange Archived button. To restore the archived expense, hover on the orange Archived button. A gray Select button will appear. Click on the Select arrow, and choose Restore expense from the drop-down list. To delete an archived expense, select Delete expense from the drop-down list of the Select button.
Invoice
^^^^^^^
Are you billing a client directly for an expense? With the Invoice button on the Expenses list page, you can automatically convert an expense to an invoice in one simple click. To create an invoice for an expense, first you'll need to select the expense in question. Select an expense by checking the box located in the far left column of the relevant entry. Then click the blue Invoice button located at the top left of the Expenses table. The expense will be converted automatically to a new invoice.
.. TIP:: If you want to invoice an expense, you need to enable the invoicing function when you create the expense, or later by editing the expense. To enable the invoicing function, check the Should be invoiced box that appears on the Expenses/ Create or Expenses/ Edit page.
Create Expense
""""""""""""""
You can create a new expense directly from the Expenses list page by clicking on the blue New Expense + button located at the top right side of the page. The Expenses / Create page will open.
To ensure your business records are meticulous and organized, enter all your expenses in to your Invoice Ninja account. It's the perfect way to keep track, keep up to date and even invoice clients directly for expenses you've accrued while on the job. Managing and invoicing expenses on Invoice Ninja is so easy but the first step is logging the expense. Here's how to do it.
To create an expense, click on the Expenses tab on the main taskbar. Select New Expense from the drop-down menu and the Expenses / Create page will open.
Overview
^^^^^^^^
The Expenses / Create page features a range of fields and checkboxes for you to complete.
- **Vendor**: Click on the arrow on the right side of the Vendor field and select the vendor from the drop-down list.
- **Category**: Click on the arrow on the right side of the Category field and select the category from the drop-down list. Note: you don't have to apply a category. This is totally optional.
- **Date**: Enter the relevant date of the expense.
- **Currency**: Select the currency of the expense. This is a fantastic feature for complicated cross-border invoicing of overseas clients and/or vendors.
- **Amount**: The amount of the expense.
- **Client**: Click on the arrow on the right side of the Client field and select the relevant client from the drop-down list. TIP: Selecting a client is optional. If the expense is not attached to a particular client, leave this field blank.
- **Should be invoiced**: Do you need to invoice a particular client for this expense? If yes, check the Should be invoiced box to enable invoicing later.
- **Convert currency**: If the expense was paid in a different currency, check the Convert currency box. Then, when the expense is converted to an invoice, you can convert the amount to the currency with which you normally invoice the client.
- **Apply taxes**: If you need to apply taxes to the expense when invoicing the client, check the Apply taxes box. Then, when you create the invoice for the expense, the taxes feature will be enabled.
- **Public Notes**: Enter a description of the expense. When the expense is converted to an invoice, the text you enter here will feature as the line item description for the expense on the invoice. TIP: This is the description of the expense that your client will see on the invoice. Make sure to include the relevant details.
- **Private Notes**: Enter comments or notes that you wish to include about the expense as a personal reminder. Remember, the Private Notes section is for your eyes only, so feel free to enter anything you like.
- **Attached documents**: If you need to provide documentation relevant to the expense, such as receipts, stubs or other items, you can attach as many documents as you need here. File types can include Word documents, Excel spreadsheets, scanned PDF files and more. Click on the Attached documents box to open the Browse window, and select the relevant files.
To save the new expense, click the green Save button at the bottom of the page. Then, the expense you created will appear as an entry in the Expenses list page.
.. TIP:: After you click Save, the Expenses/ Create page will automatically refresh, and you'll see a gray More Actions button featured to the right of the Save button. Click on the More Actions button, and you can take any of three actions directly from the new expense page: Invoice Expense, Archive Expense or Delete Expense.

37
docs/index.rst Normal file
View File

@ -0,0 +1,37 @@
Invoice Ninja User Guide
========================
Want to find out everything there is to know about how to use your Invoice Ninja account? Look no further than our User Guide, where youll learn all about creating and sending invoices, receiving payments, creating tasks, converting quotes to invoices, recurring invoices, entering credits and much, much more.
.. _basic-features:
.. toctree::
:maxdepth: 2
:caption: Basic Features
introduction
clients
invoices
payments
recurring_invoices
credits
quotes
tasks
expenses
settings
.. _advanced-settings:
.. toctree::
:maxdepth: 2
:caption: Advanced Settings
invoice_settings
invoice_design
email_settings
templates_and_reminders
client_portal
reports
data_visualizations
api_tokens
user_management

111
docs/introduction.rst Normal file
View File

@ -0,0 +1,111 @@
Introduction
============
Lets get acquainted with a basic overview of the structure of the Invoice Ninja website. Once youve wrapped your mind around a few central concepts, its as easy as ABC to effectively manage your freelance business accounts.
The Invoice Ninja system is based on two main pillars:
- **List pages** are summary pages of the various activities in your account. These include the Clients list page, Tasks list page, Invoices list page and Payments list page. List pages are located in the main taskbar of the Invoice Ninja site. Simply click on Clients, Tasks, Invoices or Payments to open the list page you need. The list pages provide a centralized overview and management station for the particular component of your account. For example, the Clients list page is a list of all your clients, with accompanying information and handy links, so you can manage all your clients on one page.
- **Action pages** are pages dedicated to a specific action you can take in your Invoice Ninja account. Examples include Create New Client, Enter Credit, Create New Invoice, Create Recurring Invoice, Enter Credit, Enter Payment and Create New Task. All actions you take will be recorded in the List pages, updated in real time to reflect your invoicing activity from minute to minute.
So remember the ninja rule: **list pages provide a summary overview**, and **action pages facilitate your invoicing activity**.
Youre invited to browse the user guide for more detailed information. Were always looking to take Invoice Ninja one step ahead, so if you have any comments, queries or suggestions, wed love to hear from you.
Dashboard
^^^^^^^^^
Welcome to the Dashboard page of your Invoice Ninja account. This is the first page youll see when you login. It provides a general overview of the invoicing status of your freelance business. It is easy on the eye, and easy to digest. When you arrive at the Dashboard page, youll glimpse a comprehensive and user-friendly snapshot of whats going on right now in your invoicing world.
So lets jump right in and take a look at the different elements that make up your invoicing dashboard.
When you login to your Invoice Ninja account, youll automatically arrive on the Dashboard page. To go to the Dashboard page from anywhere in the site, click the Dashboard tab on the main taskbar.
The first thing youll notice about the Dashboard page is the three large data boxes at the top of the screen. These are designed to offer a simple yet powerful overview of your total business accounts:
- **Total Revenue**: The total amount that your business has brought in to date.
- **Average Invoice**: The amount of the current average invoice. Note this will change over time, depending upon your income.
- **Outstanding**: The total amount of all unpaid invoices.
.. TIP:: If you are being paid in a range of currencies, the three data boxes on your dashboard will display the relevant amounts in all currencies.
Below the three main data boxes, there are four windows summarizing different aspects of your invoicing activity.
Window 1: Notifications
"""""""""""""""""""""""
The Notifications list is incredibly useful as it presents an up-to-date, action-packed summary of what is happening across your entire invoicing account. Every action taken, whether by you or by one of your clients, is listed in chronological order, together with the date the action occurred. The list is updated in real time, with more recent actions showing first, so you get a minute-to-minute understanding of your invoicing activity.
The Notifications list includes all possible actions occurring within your Invoice Ninja account, including:
- Creating an invoice
- Sending an invoice
- Creating a new client
- Archiving/deleting a client
- Creating a credit
- Your client viewing your invoice
- Your client sending a payment
- And many, many more
.. TIP:: You can view a real-time tally of the number of invoices sent, displayed at the top right side of the blue Notifications header bar.
Window 2: Recent Payments
"""""""""""""""""""""""""
The Recent Payments list provides a summary of your clients payments, with the most recent payments showing at the top of the list. The Recent Payments list presents an overview of the following key information:
- **Invoice #**: The invoice reference number
- **Client**: The clients name
- **Payment Date**: The date the payment was made
- **Amount**: The amount of the payment. Note that the amount will be displayed in the currency in which it was paid.
.. NOTE:: In order for Invoices or Quotes to appear on the Dashboard page, the Due Date and Valid Until fields must be completed. Invoices or Quotes lacking this information will not be viewable on the Dashboard.
Window 3: Upcoming Invoices
"""""""""""""""""""""""""""
The Upcoming Invoices list provides a summary of all invoices with due dates approaching. The Upcoming Invoices list presents an overview of the following key information:
- **Invoice #**: The invoice reference number
- **Client**: The clients name
- **Due Date**: The due date of the payment
- **Balance Due**: The amount due
Window 4: Invoices Past Due
"""""""""""""""""""""""""""
The Invoices Past Due list provides a summary of all unpaid invoices. The Invoices Past Due list presents an overview of the following key information:
- **Invoice #**: The invoice reference number
- **Client**: The clients name
- **Due Date**: The original due date of the overdue payment
- **Balance**: Due The amount overdue
.. NOTE:: Archived invoices, payments and quotes will appear on the dashboard, and their amounts will be included in the account totals at the top of the page. Deleted invoices, payments and quotes will not appear, nor will their amounts be included on the Dashboard page.
Window 5: Upcoming Quotes
"""""""""""""""""""""""""
If you have a Pro account, the Dashboard will also include two extra windows displaying your Upcoming Quotes and Expired Quotes.
The Upcoming Quotes list provides a summary of all quotes with "Valid Until" dates approaching. The Upcoming Quotes list presents an overview of the following key information:
- **Quote**: # The quote reference number
- **Client**: The clients name
- **Due Date**: The valid until date
- **Balance Due**: The amount of the quote
Window 6: Expired Quotes
""""""""""""""""""""""""
The Expired Quotes list provides a summary of all quotes that have already passed their "Valid Until" date. The Expired Quotes list presents an overview of the following key information:
- **Quote #**: The quote reference number
- **Client**: The clients name
- **Due Date**: The valid until date
- **Balance Due**: The amount of the quote
.. TIP:: In addition to displaying a helpful overview of your invoicing activity, the Dashboard page is rich in clickable links, providing you with a shortcut to relevant pages you may wish to view. For example, all invoice numbers are clickable, taking you directly to the specific invoice page, and all client names are clickable, taking you directly to the specific client summary page.

57
docs/invoice_design.rst Normal file
View File

@ -0,0 +1,57 @@
Invoice Design
==============
Whether you're a design novice or programming pro, Invoice Ninja gives you the power to customize your invoice design, to give your invoices the exact look you want. The design tool is the perfect way to match your invoices to your company's graphical look and feel, including colors, fonts, logos, margin sizes, column names, headers, footers and much more.
You can design you invoice using the simple selection tools, or go deeper and customize it from the foundations with our customization feature.
The Invoice Design page is divided into two sections. The top section presents the various options for customization and the bottom section displays a real time PDF sample of your invoice, so you can see your changes as you go along.
General Settings
""""""""""""""""
To customize the invoice design general settings, click on the General Settings tab. You have a number of options for setting changes:
Design Invoice Ninja provides a selection of design templates to choose from. Examples include 'Bold', 'Elegant', 'Hipster' and 'Business'. The default template setting is 'Clean'. To change the template setting, select a design from the drop down menu. The PDF invoice display below will automatically update to show you a preview of the design template.
.. TIP:: Your chosen design will only apply after you click the Save button. Feel free to play with the various designs and explore your options before saving.
- **Body Font**: Select the desired font for the body text of the invoice.
- **Header Font**: Select the desired font for the header text of the invoice.
- **Font Size**: Select the desired font size for the text that appears in the invoice.
- **Primary Color**: Select the desired primary color of the invoice design.
- **Secondary Color**: Select the desired secondary color of the invoice design.
.. TIP:: The invoice design templates are based on a two-tone color scheme. Make sure to select primary and secondary colors that are complementary and reflect your design taste and your company's design theme.
Invoice Labels
^^^^^^^^^^^^^^
Want to change the column names or terms on your invoice template? To customize the names of the Item, Description, Unit Cost, Quantity, Line Total and Terms on your invoice, enter the desired text in the relevant field.
Invoice Options
^^^^^^^^^^^^^^^
Hide Quantity If your line items are always 1, then you can opt to hide the Quantity field. To hide Quantity, check the box.
Hide Paid to Date If you wish to hide the Paid to Date column until payment has been made, check the box. Then, your invoices won't ever display 'Paid to Date 0.00'.
Header/Footer
^^^^^^^^^^^^^
Want your header and footer to appear on all pages of the invoice, or the first page only? Select the desired setting here.
Once you've selected all your settings, click the green Save button and the new settings will apply.
Customize
"""""""""
If you have design experience, you can customize the invoice exactly as you want, beyond the options provided by Invoice Ninja. To work on the invoice document, click the blue Customize button. You'll be redirected to the programming page, where you'll have the option of working on the invoice Content, Styles, Defaults, Margins, Header and Footer layout. Simply click on the relevant tab and work away. The design changes you make will be reflected in the sample PDF invoice on the right side of the page.
To change the invoice design template, select the desired design from the drop down menu at the bottom left of the page.
To save your customized changes, click the green Save button at the bottom of the page.
To cancel your customized changes, click the gray Cancel button at the bottom of the page.
Need help with your customized coding? Click the gray Help button at the bottom of the page. You'll be provided with a link to our support forum and to the website of the software provider, where you can explore how to use it.

82
docs/invoice_settings.rst Normal file
View File

@ -0,0 +1,82 @@
Invoice Settings
================
You can customize your invoice template by adding new details about the client, your company, header and footer information, adjusting the numbering format and more. Any changes you make to the Invoice Settings will apply to all your invoices.
The Invoice Settings page has four sections:
- Invoice and Quote Numbers
- Custom Fields
- Quote Settings
- Default Messages
Invoice and Quote Numbers
"""""""""""""""""""""""""
Want to create your own numbering system for invoices and quotes?
To customize your invoice numbering system, click on the Invoice Number tab on the left.
There are two ways to customize the invoice number: by adding a prefix or creating a pattern.
To add a prefix, select the Prefix button. In the field immediately below, add your chosen prefix. For example, you may choose to add your company initials, such as M&D. The current invoice number appears in the Counter field.
All your invoices will automatically include the numbering change. So if you chose the prefix M&D, your invoice numbers will appear as M&D001, and so on. 3
To create a pattern, select the Pattern button. In the pattern field, enter the custom variable of your choice. For example, if you create a pattern of {$year}-{$counter}, then your invoices will be numbered with the current year and latest invoice number. To view available options for custom patterns, click on the question mark icon at the right end of the Pattern field.
All your invoices will automatically display invoice numbers according to your customized pattern.
To customize your quote numbering system, click on the Quote Number tab on the right.
There are two ways to customize the quote number: by adding a prefix or creating a pattern.
- To add a prefix, select the Prefix button. In the field immediately below, add your chosen prefix. The prefix will appear before the quote number on all your quotes.
- To create a pattern, select the Pattern button. In the pattern field, enter the custom variable of your choice. To view available options for custom patterns, click on the question mark icon at the right end of the Pattern field. 4
All your quotes will automatically display quote numbers according to your customized pattern.
.. TIP:: You can choose to integrate your quote numbers with the invoice number counter. This is an important feature as it allows you to keep the same number when converting a quote to an invoice. So, Quote-001 will automatically become Invoice-001. To number your quotes with your invoice numbering system, check the Share invoice counter button. To number your quotes separately, uncheck the Share invoice counter button. 5
Custom Fields
"""""""""""""
You can create new fields for your client entries, company details and invoices by assigning new field values and labels in the Custom Fields section. All field changes will automatically appear in the PDF invoice.
Client Fields
^^^^^^^^^^^^^
To add fields to your client entries, click on the Client Fields tab.
You have the option of adding one or two new fields which will appear on the Client/Create and Client/Edit pages. When creating or editing a client, complete these fields if they are relevant to the client. The field name and information you enter will appear in the Client details section of the PDF invoice.
Company Fields
^^^^^^^^^^^^^^
To add fields to your company details, click on the Company Fields tab. Enter the Field Label and Field Value information in the relevant fields. The information you enter will automatically appear in the Company details section of the PDF invoice.
Invoice Fields
^^^^^^^^^^^^^^
Want to include customized information in your invoices? To add fields to your invoice entry, click on the Invoice Fields tab. Enter the new field name in the Field Label field. You can add one or two new invoice fields. The new fields will appear in the top part of the Create/Invoice page, and will automatically be included in the PDF invoice.
Invoice Charges
^^^^^^^^^^^^^^^
To add new invoice charge fields, click on the Invoice Charge tab. Enter the new charge in the fields provided. You can add one or two new invoice charge fields. The new charge field/s will appear in the Invoice Subtotals section. Amounts entered into these fields during the Create or Edit Invoice process will automatically appear in the PDF invoice. To apply the Tax feature for the new charge, check the Charge tax button.
Quote Settings
""""""""""""""
Want to convert accepted quotes into invoices at a click of a button? Check the Enable button and the auto convert function will apply. So, when a client approves a quote, it will automatically convert into a quote, saving you time and hassle.
.. TIP:: This feature is extra-helpful if you linked your quote and invoice number counters in the Invoice and Quote Numbers section of the Invoice Settings page.
To disable the auto convert function, uncheck the Enable button.
Default Messages
""""""""""""""""
Set any customized default text you want to Invoice Terms, Invoice Footer and Quote Terms.
Completed all your Invoice Settings? Click the green Save button at the bottom of the page, and your customized changes will appear on all your invoices.

138
docs/invoices.rst Normal file
View File

@ -0,0 +1,138 @@
Invoices
========
Well, its time to bill, and the Invoices function of Invoice Ninja lets you get the job done fast and with perfect accuracy.
With a bustling freelance business, youre going to be sending out a lot of invoices. Creating an invoice is simple with the New Invoice page. Once youve entered the client, job and rates information, youll see a preview of your invoice, and youll have a range of actions at your fingertips from saving a draft, to sending the invoice to the client via email, to printing a PDF hard copy.
List Invoices
"""""""""""""
With numerous invoices to keep track of, the Invoices list page is your go-to guide for the entire history and status of all your invoices including those that have been paid, those sent but unpaid, and those still in the drafting and editing stage.
Overview
^^^^^^^^
The life of an invoice in the Invoice Ninja system is made up of a number of stages:
- **Draft**: When youve created an invoice, but have not yet sent it. You may still need to edit.
- **Sent**: Youve sent the invoice, but the client has not yet paid.
- **Viewed**: The client has opened the invoice email and viewed the invoice.
- **Partial**: The invoice has been partially paid.
- **Paid**: Congratulations! The client has paid the full invoice amount.
In order to understand exactly how the Invoices list page works, well take you through it step by step.
To view your Invoices list page, click the Invoices tab on the main taskbar. This will open the Invoices list page.
The Invoices list page displays a table of all your invoices, at every stage, from the moment you create a new invoice, to the moment you archive or delete an invoice.
Let's explore the invoices list according to the tabs on the main header bar of the table from left to right:
- **Invoice #**: The number of the invoice
- **Client**: The client name
- **Invoice Date**: The date the invoice was issued
- **Invoice Total**: The total amount of the invoice
- **Balance Due**: The amount owed by the client (after credits and other adjustments are calculated)
- **Due Date**: The date the payment is due
- **Status**: The current status of the invoice (Gray = Draft, Blue = Sent, XX = Viewed, XX = Partial, Green = Paid)
- **Action**: The Action button provides a range of possible actions, depending upon the status of the invoice.
To view the actions, hover your mouse over the Action area of the relevant invoice entry and a gray Select button will appear. Click on the arrow at the right side of the button to open a drop-down list. For invoices with “Draft” status, the drop-down list presents the following action items:
- **Edit Invoice**: Edit the invoice information on the Edit Invoice page.
- **Clone Invoice**: Duplicate the invoice. Then you can make minor adjustments. This is a fast way to create a new invoice that is identical or similar to this invoice.
- **View History**: You'll be redirected to the Invoices / History page, where you can view a history of all the actions taken from the time the invoice was created. The Invoices / History page displays a copy of the latest version of the invoice and a drop-down list of all actions and the corresponding version of the invoice. Select the version you wish to view. Click on the blue Edit Invoice button at the top right of the page to edit the invoice.
- **Mark Sent**: When you have sent the invoice to your customer, mark as Sent. This will update the invoice status in the Invoices list, so you can keep track.
- **Enter Payment**: Enter the payment relevant to this invoice. You'll be redirected to the Payments / Create page.
- **Archive Invoice**: Click here to archive the invoice. It will be archived and removed from the Invoices list page.
- **Delete Invoice**: Click here to delete the invoice. It will be deleted and removed from the Invoices list page.
.. TIP:: For invoices with “Viewed”, “Sent”, “Partial” or “Paid” status, only the relevant and applicable options from the above list will show in the Action drop-down list.
.. TIP:: To sort the invoices list according to any of the columns, click on the orange column tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction.
Create Invoice
""""""""""""""
Here, were going to focus on how to create a new invoice.
**Lets Begin**
To create a new invoice, go to the Invoices tab on the main taskbar, open the drop-down menu, and click on New Invoice. This will open the Invoices / Create page.
When you open the Invoices / Create page, the Invoice Ninja system will automatically create a new, empty invoice for you to complete. Note that each new invoice you create will be automatically numbered in chronological order. This will ensure your records are kept logical and organized. (You have the option to change the invoice number manually. We'll discuss that a little later.)
The top section of the invoice contains a range of important information specific to the client and the work you are invoicing. Lets explore them one by one:
- **Client**: Click on the arrow at the right end of the Client field. Select the relevant client from the client list. TIP: You can create a new client while creating a new invoice. Simply click on the Create new client link, situated below the Client field on the Invoices / Create page. A pop-up window will open, enabling you to complete the new clients details. Then continue creating the invoice for this new client.
- **Invoice Date**: The date of creation of the invoice. Click the calendar icon to select the relevant date.
- **Due Date**: The date the invoice payment is due. Click the calendar icon to select the relevant date.
- **Partial**: In the event that you need to bill the client for a partial amount of the total amount due, enter the amount in the Partial field. This will be automatically applied to the invoice.
- **Invoice #**: The invoice number is assigned automatically when you create a new invoice, in order of chronology. TIP: You can manually override the default invoice number by entering a different number in the Invoice # field.
- **PO #**: The purchase order number. Enter the purchase order number for easy reference.
- **Discount**: If you wish to apply a discount to the invoice, you can choose one of two methods: a monetary amount, or a percentage of the total amount due. To choose a method, click on the arrow at the right side of the box next to the Discount field. Select Percent or Amount from the drop-down list. Then enter the relevant figure. For example, to apply a 20% discount, enter the number 20, and select “Percent” from the drop-down list. To apply a $50 discount, enter the number 50, and select “Amount” from the drop-down list.
- **Taxes**: Manage the various tax rates that apply by clicking on the Manage rates link. The Tax Rates pop-up window will open. To apply a tax rate, enter the name of the tax in the Name field, and the percentage amount in the Rate field. For example, to apply a VAT of 17%, enter VAT in the Name field, and 17 in the Rate field. TIP: If you need to apply multiple taxes, add another Name and Rate to the new row. A new row will open automatically as soon as you begin typing in the current row.
The Tax Rates pop-up box offers various settings for presentation of taxes on the invoice. Check the boxes of the settings you wish to apply.
- **Enable specifying an invoice tax**: Check this box to apply the tax rate for the entire invoice. It will appear on the invoice above the Balance Due field.
- **Enable specifying line item taxes**: Check this box to apply various tax rates to specific items of the same invoice. This setting enables you to apply different taxes to the different line items.
- **Display line item taxes inline**: Check this box to include a Tax column on the invoice, so your customer can view the tax amounts that apply to each line item.
After selecting the desired tax settings, youll need to choose a tax rate for the invoice, or for each line item. To select a tax rate, click on the arrow at the right side of each Tax field that appears on the invoice. A drop-down list will open, featuring all the tax rates you created. Choose the relevant tax rate from the list. It will automatically apply and the figures in the invoice will adjust accordingly.
.. TIP:: The currency of the invoice will be according to the default currency specified for this client when you created the client.
Now that weve completed the general invoice information, its time to finish creating your invoice by specifying the job/s youre billing for, the amounts due for each job/line item, taxes, discounts and final balance due. Let's explore the various columns of the invoice, from left to right along the orange header bar:
- **Item**: This is the name of the item you are billing for. You can either enter the details manually, or by selecting one of the set items created by you at the Product Settings stage. To select a set item, click on the arrow at the right side of the item bar and choose the relevant item from the drop-down list. To enter the item manually, click inside the field and enter the item. Here are some examples of an item: 1 hour programming services OR 5 pages translation OR 1 hour consulting.
- **Description**: Add more information about the item. This will help the customer better understand the job completed, and is also useful for your own reference.
- **Unit Cost**: The amount you charge per unit of items. For example, let's say your item is "1 hour consulting", and you charge $80 for an hour of consulting that is, for 1 item unit. Then you'll enter 80 in the Unit Cost field. Note: If you have selected a set item, the unit cost that you pre-defined at the Product Settings stage will apply by default. You can manually override the default unit cost by clicking in the Unit Cost field and changing the value.
- **Quantity**: The number of units being charged. Continuing the above example, let's say you need to charge for 3 hours of consulting, enter the number 3 in the Quantity field.
- **Tax**: This field will only appear if you selected "Enable specifying line item taxes." To apply tax to the line item, click on the arrow at the right side of the Tax field and select the relevant tax from the drop-down list.
- **Line Total**: This is the amount due for the particular line item. Once you have entered the Unit Cost and Quantity, this figure will be calculated automatically. If you change either value at any time during creation of the invoice, the Line Total will adjust accordingly.
.. TIP:: You can enter as many line items as you need in the invoice. As soon as you enter any data in a line item, a fresh, blank line item will open in the row below.
Beneath and to the right of the line item section, you'll find the Balance Due section. It's made up of a number of figures, all leading to the golden number the final, total Balance Due.
- **Subtotal**: This is the amount due before other figures are taken into calculation, such as Tax, Partial payments, Credits, etc.
- **Tax**: The tax rate for the invoice. Here you can select the appropriate tax rate for the entire invoice by clicking the arrow at the right side of the Tax field and selecting the relevant tax from the drop-down list. Note: If you selected "Enable specifying line item taxes" in the Manage rates pop-up box, then the tax applied to each line item will appear here, listed individually.
- **Paid to Date**: The amount paid to date, including partial payments and credits.
- **Balance Due**: The final balance owed to you by your customer, after taxes, partial payments and credits have been deducted from the charged amount.
Directly to the left of the Balance Due section, you'll see a text box with three tabs to choose from:
- **Note to Client**: Want to write a personal or explanatory note to the client? Enter it here.
- **Invoice Terms**: Want to set terms to the invoice? Enter them here. The terms will appear on the invoice. If you want to make these the default terms for all invoices, check the Save as default terms box. Then these terms will automatically appear on each invoice you create. Need to change the default terms? Click Reset Terms, and the text box will clear. You can enter new terms or leave blank.
- **Invoice Footer**: Want to enter information to appear as a footer on the invoice? Enter it here. The text will appear at the bottom of the invoice. If you want to make this the default footer for all invoices, check the Save as default footer box. Then this footer will automatically appear on each invoice you create. Need to change the default footer? Click Reset footer, and the text box will clear. You can enter a new footer or leave blank.
.. TIP:: The Invoices page is rich in clickable links, providing you with a shortcut to relevant pages you may wish to view. For example, all invoice numbers are clickable, taking you directly to the specific invoice page, and all client names are clickable, taking you directly to the specific client summary page.
Invoice Preview
^^^^^^^^^^^^^^^
Did you know that all this time you've been creating the new invoice, a preview of the invoice appears below, and it changes in real time according to the data you've entered?
Scroll down below the invoice data fields to check out the invoice preview.
But before we get there you'll see a row of colorful buttons, giving you a range of options:
- **Blue button Download PDF**: Download the invoice as a PDF file. You can then print or save to your PC or mobile device.
- **Green button Save Invoice**: Save the last version of the invoice. The data is saved in your Invoice Ninja account. You can return to the invoice at any time to continue working on it.
- **Orange button Email Invoice**: Email the invoice directly via the Invoice Ninja system to the email address specified for the client.
- **Gray button More Actions**
Click on More Actions to open the following action list:
- **Clone**: Invoice Duplicate the current invoice. Then you can make minor adjustments. This is a fast way to create a new invoice that is identical or similar to a previous invoice.
- **View History**: You'll be redirected to the Invoices / History page, where you can view a history of all the actions taken from the time the invoice was created. The Invoices / History page displays a copy of the latest version of the invoice and a drop-down list of all actions and the corresponding version of the invoice. Select the version you wish to view. Click on the blue Edit Invoice button at the top right of the page to go back to the invoice page.
- **Mark Sent**: When you have sent the invoice to your customer, mark as Sent. This will update the invoice status in the Invoices list, so you can keep track.
- **Enter Payment**: Enter the payment relevant to this invoice. You'll be redirected to the Payments / Create page.
- **Archive Invoice**: Want to archive the invoice? Click here. The invoice will be archived and removed from the Invoices list page.
- **Delete Invoice**: Want to delete the invoice? Click here. The invoice will be deleted and removed from the Invoices list page.
.. TIP:: At the left of these colorful buttons, you'll see a field with an arrow that opens a drop-down menu. This field provides you with template options for the invoice design. Click on the arrow to select the desired template. When selected, the invoice preview will change to reflect the new template.
.. IMPORTANT:: Remember to click the green Save Invoice button every time you finish working on an invoice. If you don't click Save, you will lose the changes made. (But don't worry if you forget to click Save, a dialog box with a reminder to save will open when you try to leave the page.)

87
docs/payments.rst Normal file
View File

@ -0,0 +1,87 @@
Payments
========
The Invoice Ninja system handles your entire invoicing process from sending a quote (Pro Plan only), to invoicing your client, to receiving payment. Whats more, you can receive payments directly and automatically via Invoice Ninjas 45+ payment gateway partners, enabling totally smooth management of your customer accounts using your choice of payment provider. To learn more about Invoice Ninjas payment gateway partners `click here <https://www.invoiceninja.com/partners>`
List Payments
"""""""""""""
In the meantime, were going to take you through the Payments list page to give you an idea of the complete payment picture.
To view the Payments list page, click on the Payments tab on the main taskbar. This will open the Payments list page.
Understanding the Payments List Page
Overview
^^^^^^^^
The Payments list page displays a summary of all payments once they have been received. Payments are recorded in two ways:
1. **Automatic payment**: If your client has paid you via any of Invoice Ninjas 45+ payment gateways, the payment will be automatically recorded in the Payments list. You will be notified by Invoice Ninja on your Dashboard page in the Payments table, and also via email (if you have chosen the relevant notification setting on the Settings / Notifications page).
2. **Manual payment**: If your client has paid you via cash, check, bank transfer, credit card or any other payment system not linked to Invoice Ninja, you will need to enter the payment manually on the Payments / Create page.
Whether automatic or manual entry, the Payments list page presents an overview of all payments received in a user-friendly table format. Now, well take you through the various columns in the Payments table from left to right:
- **Invoice**: The invoice number for the payment
- **Client**: The client name
- **Transaction Reference**: If you have been paid automatically via one of the payment gateways, the reference number of the payment generated by the payment gateway system will be automatically recorded here. If you have entered a manual payment, the Transaction Reference will display the information you entered in the Transaction Reference field when entering the payment. If you left the Transaction Reference field blank when entering the payment, the system will automatically display “Manual entry” as the Transaction Reference on the Payments list.
- **Method**: The method of payment used, ie. PayPal, Bank Transfer, Visa, etc
- **Payment**: Amount The payment amount that was received
- **Payment Date**: The date the payment was received
The final column to the right is the Action column. To view the actions, hover your mouse over the Action area of the relevant payment entry and a gray Select button will appear. Click on the arrow at the right side of the button to open a drop-down list. These are the action options:
- **Edit Payment**: Edit the payment information on the Edit Payment page.
- **Archive Payment**: Click here to archive the payment. It will be archived and removed from the Payments list page.
- **Delete Payment**: Click here to delete the payment. It will be archived and removed from the Payments list page.
.. TIP:: To sort the Payments list according to any of the columns, click on the orange column tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction.
Archiving/Deleting
^^^^^^^^^^^^^^^^^^
To archive or delete a payment, hover over the payment entry row, and open the Action drop-down list. Select Archive Payment or Delete Payment from the list. The Payments table will automatically refresh, and archived or deleted payments will no longer appear in the list.
You can also archive or delete one or more payment via the gray Archive button that appears at the top left side of the Payments list page. To archive or delete payments, check the relevant payments in the check boxes that appear in the far left column next to the invoice number. The number of payments selected for archiving/deleting will automatically update and show on the Archive button. Then click on the Archive button, open the drop-down list and select the desired action.
Want to view archived or deleted payments? Check the box marked Show archived/deleted payments, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of payments, including current, archived and deleted payments. The status of the archived and deleted payments will be displayed in the far right column.
- **Deleted payments** are displayed with a red Deleted button. To restore deleted payments, hover on the red Deleted button. A gray Select button will appear. Click on the Select arrow, and select Restore payment in the drop-down list.
- **Archived payments** are displayed with an orange Archived button. To restore the archived payment, hover on the orange Archived button. A gray Select button will appear. Click on the Select arrow, and choose Restore payment from the drop-down list. To delete an archived payment, select Delete payment from the drop-down list of the Select button.
Enter Payment
^^^^^^^^^^^^^
You can enter a new payment directly from the Payments list page by clicking on the blue Enter Payment + button located at the top right side of the page. The Payments / Create page will open.
Filter
^^^^^^
To filter the Payments list, enter the filter data in the Filter field, situated at the top right of the page, to the left of the blue Enter Payment + button. Payments can be filtered according to Client name. Enter the name or parts of the name, and the filter function will automatically locate and present the relevant entries.
Enter Payment
"""""""""""""
Once youve received payment, youll need to enter the payment on the Invoice Ninja system. If your client paid via one of Invoice Ninjas 45+ payment partners, the system will automatically record the payment and send a downloadable PDF receipt to the clients email address. If you were paid via cash, check, bank transfer, credit card, credit or any other payment method not automatically linked to the Invoice Ninja system, you will be required to enter the payment. The procedure of entering a payment is simple and lightning fast. Now, well take you through the payment entry process, step by step.
**Lets Begin**
The Payments / Create page features a number of fields that youll need to complete.
- **Client**: Click on the arrow on the right side of the Client field and select the client from the drop-down list.
- **Invoice**: Enter the invoice corresponding to the received payment. Click on the arrow on the right side of the Invoice field and select the invoice number from the drop-down list.
- **Amount**: Enter the amount of the payment received. TIP: The amount may not correspond to the amount on the invoice. It may be a partial payment, or it may be a higher amount than listed on the invoice, in which case the extra payment will be recorded as a credit on the clients account balance.
- **Payment**: Type Select the method of payment used. Click on the arrow on the right side of the Payment Type field, and a drop-down list featuring a range of payment methods will open. Select the appropriate method from the list. Options include Bank Transfer, Cash, Debit, all credit cards, Google Wallet, PayPal, check and more.
Apply Credit: Here, you can apply credit from the clients existing credit accrued in their account balance with your company. To do so, select Apply credit, which is the first option in the Payment Type drop-down list. If the client does not have enough credit to cover the payment, a red warning message will appear to notify you.
- **Payment Date**: The date the payment was received. Click on the calendar icon to the right side of the Payment Date field and select the appropriate date.
- **Transaction Reference**: Enter any information you wish for your future reference. This information can be useful when managing your accounts.
Email payment receipt
^^^^^^^^^^^^^^^^^^^^^
If you wish to send a receipt to your client for the payment received, check the “Email payment receipt to the client” box. A downloadable PDF receipt will be generated and sent to the clients email address.
Saving the Payment
^^^^^^^^^^^^^^^^^^
Once youve completed all the payment details, click Save and the payment will be saved to the Payments list. To cancel the payment entry, click Cancel.
.. TIP:: When you click Save, youll be automatically redirected to the clients individual summary page. Here you can view all the client details and actions recently taken in relation to this client, including the payment just entered and the balance adjustment.

135
docs/quotes.rst Normal file
View File

@ -0,0 +1,135 @@
Quotes
======
For Pro Plan users, the Quotes function streamlines your invoicing activity, from the moment you create and send a price quote for a particular job, until the quote is accepted, you invoice the job, receive payment and provide a receipt. With the Quotes function, you can automatically convert accepted quotes into invoices in a swift one-click action. Keeping track of your projected work schedule and potential income, the Quotes feature gives you even greater control of your freelance activity.
List Quotes
"""""""""""
.. Note:: The Quotes feature is only available for Pro Plan users.
As a freelancer, many jobs whether big or small will begin as a quote. How so? Well, a client expresses interest in your skills or product, but wants to know how much its going to cost, in advance. With the Quotes feature of Invoice Ninja, available to Pro Plan users only, you can easily create a price quote and send it to the client up front, helping them make the decision to commit, and helping you keep track of your projected work schedule and income.
Once the price quote is accepted by the client, the Invoice Ninja system enables automatic creation of an invoice that imports all the quote data, so your quotes and invoices are linked, organized and easily traceable. An advanced Invoice Ninja feature, Quotes is truly the cherry on top, giving you maximum functionality from A to Z in your invoicing activity.
To view your Quotes list page, click the Invoices tab on the main taskbar, and select Quotes from the drop-down menu. This will open the Quotes list page.
Overview
^^^^^^^^
The Quotes list page displays a table of all your quotes, at every stage, whether at the drafting stage, sent to the client, accepted by the client and converted to invoice, or archived/ deleted quotes. Use your Quotes list to get a better grasp of where you stand in terms of possible future projects and income.
Now, well explore all the tabs on the main header bar of the table from left to right:
.. TIP:: To sort the quotes list according to any of the columns, click on the orange column tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction.
- **Quote #**: The number of the quote
- **Client**: The client name
- **Quote Date**: The date the quote was created
- **Quote Total**: The total amount of the quote, after adjustments for credits and partial payments
- **Valid Until**: The last date that the quote is valid and can be accepted by the client
- **Status**: The current status of the quote (Gray = Draft, Blue = Sent, Converted [clickable link to the invoice])
Action The Action button provides a range of possible actions, depending upon the status of the quote. To view the actions, hover your mouse over the Action area of the relevant quote entry and a gray Select button will appear. Click on the arrow at the right side of the button to open a drop-down list. Here are the available actions:
- **Edit Quote**: Edit the quote information on the Edit Quote page.
- **Clone Quote**: Duplicate the quote. Then you can make minor adjustments. This is a fast way to create a new quote that is identical or similar to this quote.
- **View History**: You'll be redirected to the Quotes / Quote History page, where you can view a history of all the actions taken from the time the quote was created. The Quote History page displays a copy of the latest version of the quote and a drop-down list of all actions and the corresponding version of the quote. Select the version you wish to view. Click on the blue Edit Quote button at the top right of the page to edit the quote.
- **Mark Sent**: When you have sent the quote to your customer, mark as Sent. This will update the quote status in the Quotes list, so you can keep track.
- **Convert to Invoice**: Select this action to automatically convert the quote to an invoice. You'll be redirected to the Invoices / Create page. All the quote information will be imported to the invoice. Continue working on the invoice, save or send it to the client.
- **Archive Quote**: Click here to archive the quote. It will be archived and removed from the Quotes list page.
- **Delete Quote**: Click here to delete the quote. It will be deleted and removed from the Quotes list page.
.. TIP:: For quotes with “Converted” status, there is also the option to View Invoice in the Action drop-down list. Click View Invoice to see the invoice page linked to the specific quote.
You can create a new quote directly from the Quotes list page by clicking on the blue New Quote + button located at the top right side of the page. The Quotes / Create page will open.
Filter
^^^^^^
To filter the Quotes list, enter the filter data in the Filter field, situated at the top right of the page, to the left of the blue New Quote + button. Quotes can be filtered according to the client name or quote number, or elements of these data. Lets filter the table for a client named “Best Ninja”. You can type “best ninja”, or “best” or “ninja”, or even “bes”, or “nin”, or “ja”, or any other grouping of letters in the client name. Alternatively, you can filter according to quote number. The filter function will automatically locate and present the relevant entries.
Archiving/Deleting
^^^^^^^^^^^^^^^^^^
To archive or delete a quote, hover over the quote entry row, and open the Action drop-down list. Select Archive quote or Delete quote from the list. The Quotes table will automatically refresh, and archived or deleted quotes will no longer appear in the list.
You can also archive or delete one or more quote via the gray Archive button that appears at the top left side of the Quotes list page. To archive or delete quotes, check the relevant quotes in the check boxes that appear in the far left column next to the quote number. The number of quotes selected for archiving/deleting will automatically update and show on the Archive button. Then click on the Archive button, open the drop-down list and select the desired action.
Want to view archived or deleted quotes? Check the box marked Show archived/deleted quotes, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of quotes, including current, archived and deleted quotes. The status of the archived or deleted quote will appear in the column at the far right of the table.
- **Deleted quotes** are displayed with a red Deleted button. To restore deleted quotes, hover on the red Deleted button. A gray Select button will appear. Click on the Select arrow, and select Restore quote in the drop-down list.
- **Archived quotes** are displayed with an orange Archived button. To restore or delete the archived quote, hover on the orange Archived button. A gray Select button will appear. Click on the Select arrow, and choose Restore quote from the drop-down list. To delete an archived quote, select Delete quote from the drop-down list of the Select button.
.. TIP:: The Quotes page features clickable links to relevant pages you may wish to view. For example, all quote numbers are clickable, taking you directly to the specific quote page, and all client names are clickable, taking you directly to the specific client summary page. In addition, if a quote has been converted to an invoice, you can click “Converted” in the status column of the quote entry. This will take you directly to the invoice page for this quote.
Create Quote
""""""""""""
To create a new quote, go to the Invoices tab on the main taskbar, open the drop-down menu, and click on New Quote. This will open the Quotes / Create page.
How to Create a New Quote
When you open the Quotes / Create page, the Invoice Ninja system will automatically create a new, empty quote for you to complete. Note that the quote entry page is very similar in format to the invoice entry page. This makes converting the quote to an invoice extremely logical and simple.
The top section of the quote contains a range of important information specific to the client and the quote. Lets explore them one by one:
- **Client**: Click on the arrow at the right end of the Client field. Select the relevant client from the client list. TIP: You can create a new client while creating a new quote. Simply click on the Create new client link, situated below the Client field on the Quotes / Create page. A pop-up window will open, enabling you to complete the new clients details. Then continue creating the quote for this new client.
Alternatively, once youve selected an existing client from the drop-down client list, you can edit the client or view the client details. Click the Edit Client or View Client links situated directly below the client field. If you click Edit Client, the Edit Client pop-up box will open. If you click View Client, the client summary page will open in a new window.
- **Quote Date**: The date of creation of the quote. Click the calendar icon to select the relevant date.
- **Valid Until**: The last date that the quote is valid and can be accepted by the client. Click the calendar icon to select the relevant date.
- **Partial**: In the event that you will be billing the client for a partial amount of the quote, enter the amount in the Partial field. This will be automatically applied to the quote, and later, to the invoice.
- **Quote #**: The quote number is assigned automatically when you create a new quote, in order of chronology. You can manually override the default quote number by entering a different number in the Quote # field.
- **PO #**: The purchase order number. Enter the purchase order number for easy reference.
- **Discount**: Applying a discount to a quote is the same as applying a discount to an invoice. To learn how to apply a discount, refer to section 5.11 of the User Guide.
.. TIP:: The currency of the quote will be according to the default currency specified for this client when you created the client.
Now that weve completed the general quote information, its time to finish creating your quote by specifying the job/s youre billing for, the amounts due for each job/line item, discounts and final balance. Let's explore the various columns of the quote, from left to right along the orange header bar:
- **Item**: This is the name of the item you are quoting for. You can either enter the details manually, or by selecting one of the set items created by you at the Product Settings stage. To select a set item, click on the arrow at the right side of the item bar and choose the relevant item from the drop-down list. To enter the item manually, click inside the field and enter the item. Here are some examples of an item: 1 hour programming services OR 5 pages translation OR 1 hour consulting.
- **Description**: Add more information about the item. This will help the customer better understand the scope of the price quote, and is also useful for your own reference.
- **Unit Cost**: The amount you intend to charge per unit of items. For example, let's say your item is "1 hour consulting", and you charge $80 for an hour of consulting that is, for 1 item unit. Then you'll enter 80 in the Unit Cost field. Note: If you have selected a set item, the unit cost that you pre-defined at the Product Settings stage will apply by default. You can manually override the default unit cost by clicking in the Unit Cost field and changing the value.
- **Quantity**: The number of units included in the quote. Continuing the above example, let's say you need to quote for 3 hours of consulting, enter the number 3 in the Quantity field.
- **Line Total**: This is the amount quoted for the particular line item. Once you have entered the Unit Cost and Quantity, this figure will be calculated automatically. If you change either value at any time during creation of the quote, the Line Total will adjust accordingly.
.. TIP:: You can enter as many line items as you need in the quote. As soon as you enter any data in a line item, a fresh, blank line item will open in the row below.
Beneath and to the right of the line item section, you'll find the Total value of the quote. It's made up of a number of figures, including Subtotal, Paid to Date and Total:
- **Subtotal**: This is the amount quoted before other payments made to date are included in the quote calculation, such as Partial payments, Credits, etc.
- **Paid to Date**: The amount paid to date towards the value of the quote, including partial payments and credits.
- **Total**: The final value of the quote for the specified job(s), after partial payments and credits have been deducted from the quoted amount.
Directly to the left of the Balance Due section, you'll see a text box with three tabs to choose from:
- **Note to Client**: Want to write a personal or explanatory note to the client? Enter it here.
- **Quote Terms**: Want to set terms to the quote? Enter them here. The terms will appear on the quote. If you want to make these the default terms for all quotes, check the Save as default terms box. Then these terms will automatically appear on each quote you create. Need to change the default terms? Click Reset Terms, and the text box will clear. You can enter new terms or leave blank.
- **Quote Footer**: Want to enter information to appear as a footer on the quote? Enter it here. The text will appear at the bottom of the quote. If you want to make this the default footer for all quotes, check the Save as default footer box. Then this footer will automatically appear on each quote you create. Need to change the default footer? Click Reset footer, and the text box will clear. You can enter a new footer or leave blank.
Below the quote data fields, you'll see a row of colorful buttons, giving you a range of options:
- **Blue button Download PDF**: Download the quote as a PDF file. You can then print or save to your PC or mobile device.
- **Green button Save Quote**: Save the last version of the quote. The data is saved in your Invoice Ninja account. You can return to the quote at any time to continue working on it.
- **Orange button Email Quote**: Email the quote directly via the Invoice Ninja system to the email address specified for the client.
- **Gray button More Actions**:
Click on More Actions to open the following action list:
- **Clone Quote**: Duplicate the current quote. Then you can make minor adjustments. This is a fast way to create a new quote that is identical or similar to a previous quote.
- **View History**: You'll be redirected to the Quotes / Quote History page, where you can view a history of all the actions taken from the time the quote was created. The Quote History page displays a copy of the latest version of the quote and a drop-down list of all actions and the corresponding version of the quote. Select the version you wish to view. Click on the blue Edit Quote button at the top right of the page to go back to the quote page.
- **Mark Sent**: When you have sent the quote to your customer, mark as Sent. This will update the quote status in the Quotes list, so you can keep track.
- **Convert to Invoice**: Select this action to automatically convert the quote to an invoice. You'll be redirected to the Invoices / Create page. All the quote information will be imported to the invoice. Continue working on the invoice, save or send it to the client.
- **Archive Quote**: Want to archive the quote? Click here. The quote will be archived and removed from the Quotes list page.
- **Delete Quote**: Want to delete the quote? Click here. The quote will be deleted and removed from the Quotes list page.
.. TIP:: At the left of these colorful buttons, you'll see a field with an arrow that opens a drop-down menu. This field provides you with template options for the quote design. Click on the arrow to select the desired template. When selected, the quote preview will change to reflect the new template.
Quote Preview
^^^^^^^^^^^^^
Did you know that all this time you've been creating the new quote, a preview of the quote appears below, and it changes in real time according to the data you've entered? The PDF is created in real time; all you have to do is click Save.
To check out the quote preview, scroll down below the invoice data fields.

Some files were not shown because too many files have changed in this diff Show More