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

This commit is contained in:
Joshua Dwire 2016-06-01 16:46:03 -04:00
commit 49d47bb9e3
62 changed files with 948 additions and 625 deletions

View File

@ -5,6 +5,7 @@ sudo: true
php: php:
- 5.5.9 - 5.5.9
# - 5.6 # - 5.6
# - 5.6
# - 7.0 # - 7.0
# - hhvm # - hhvm
@ -27,6 +28,7 @@ before_install:
# set GitHub token and update composer # set GitHub token and update composer
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi; - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
- composer self-update && composer -V - composer self-update && composer -V
# - export USE_ZEND_ALLOC=0
install: install:
# install Composer dependencies # install Composer dependencies
@ -66,6 +68,7 @@ before_script:
script: script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php - php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance ClientCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance ClientCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance ExpenseCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance ExpenseCest.php
@ -76,7 +79,6 @@ script:
- php ./vendor/codeception/codeception/codecept run acceptance OnlinePaymentCest.php - php ./vendor/codeception/codeception/codecept run acceptance OnlinePaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
#- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env #- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
#- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php #- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php
@ -96,4 +98,4 @@ after_script:
notifications: notifications:
email: email:
on_success: never on_success: never
on_failure: change on_failure: change

View File

@ -154,7 +154,7 @@ class CheckData extends Command {
$clients->where('clients.id', '=', $this->option('client_id')); $clients->where('clients.id', '=', $this->option('client_id'));
} else { } else {
$clients->where('invoices.is_deleted', '=', 0) $clients->where('invoices.is_deleted', '=', 0)
->where('invoices.is_quote', '=', 0) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', 0) ->where('invoices.is_recurring', '=', 0)
->havingRaw('abs(clients.balance - sum(invoices.balance)) > .01 and clients.balance != 999999999.9999'); ->havingRaw('abs(clients.balance - sum(invoices.balance)) > .01 and clients.balance != 999999999.9999');
} }
@ -184,7 +184,7 @@ class CheckData extends Command {
if ($activity->invoice_id) { if ($activity->invoice_id) {
$invoice = DB::table('invoices') $invoice = DB::table('invoices')
->where('id', '=', $activity->invoice_id) ->where('id', '=', $activity->invoice_id)
->first(['invoices.amount', 'invoices.is_recurring', 'invoices.is_quote', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']); ->first(['invoices.amount', 'invoices.is_recurring', 'invoices.invoice_type_id', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']);
// Check if this invoice was once set as recurring invoice // Check if this invoice was once set as recurring invoice
if ($invoice && !$invoice->is_recurring && DB::table('invoices') if ($invoice && !$invoice->is_recurring && DB::table('invoices')
@ -221,14 +221,14 @@ class CheckData extends Command {
&& $invoice->amount > 0; && $invoice->amount > 0;
// **Fix for allowing converting a recurring invoice to a normal one without updating the balance** // **Fix for allowing converting a recurring invoice to a normal one without updating the balance**
if ($noAdjustment && !$invoice->is_quote && !$invoice->is_recurring) { if ($noAdjustment && $invoice->invoice_type_id == INVOICE_TYPE_STANDARD && !$invoice->is_recurring) {
$this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} isQuote:{$invoice->is_quote} isRecurring:{$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; $foundProblem = true;
$clientFix += $invoice->amount; $clientFix += $invoice->amount;
$activityFix = $invoice->amount; $activityFix = $invoice->amount;
// **Fix for updating balance when creating a quote or recurring invoice** // **Fix for updating balance when creating a quote or recurring invoice**
} elseif ($activity->adjustment != 0 && ($invoice->is_quote || $invoice->is_recurring)) { } elseif ($activity->adjustment != 0 && ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE || $invoice->is_recurring)) {
$this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} isQuote:{$invoice->is_quote} isRecurring:{$invoice->is_recurring}"); $this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}");
$foundProblem = true; $foundProblem = true;
$clientFix -= $activity->adjustment; $clientFix -= $activity->adjustment;
$activityFix = 0; $activityFix = 0;

View File

@ -1,5 +1,7 @@
<?php namespace App\Console\Commands; <?php namespace App\Console\Commands;
use Utils;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;

View File

@ -597,8 +597,8 @@ class AccountController extends BaseController
// sample invoice to help determine variables // sample invoice to help determine variables
$invoice = Invoice::scope() $invoice = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('client', 'account') ->with('client', 'account')
->where('is_quote', '=', false)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->first(); ->first();
@ -799,11 +799,7 @@ class AccountController extends BaseController
private function saveTaxRates() private function saveTaxRates()
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$account->fill(Input::all());
$account->invoice_taxes = Input::get('invoice_taxes') ? true : false;
$account->invoice_item_taxes = Input::get('invoice_item_taxes') ? true : false;
$account->show_item_taxes = Input::get('show_item_taxes') ? true : false;
$account->default_tax_rate_id = Input::get('default_tax_rate_id');
$account->save(); $account->save();
Session::flash('message', trans('texts.updated_settings')); Session::flash('message', trans('texts.updated_settings'));
@ -1268,6 +1264,10 @@ class AccountController extends BaseController
$account = Auth::user()->account; $account = Auth::user()->account;
\Log::info("Canceled Account: {$account->name} - {$user->email}"); \Log::info("Canceled Account: {$account->name} - {$user->email}");
Document::scope()->each(function($item, $key) {
$item->delete();
});
$this->accountRepo->unlinkAccount($account); $this->accountRepo->unlinkAccount($account);
if ($account->company->accounts->count() == 1) { if ($account->company->accounts->count() == 1) {
$account->company->forceDelete(); $account->company->forceDelete();

View File

@ -262,6 +262,12 @@ class AppController extends BaseController
if (!Utils::isNinjaProd()) { if (!Utils::isNinjaProd()) {
try { try {
set_time_limit(60 * 5); set_time_limit(60 * 5);
Artisan::call('clear-compiled');
Artisan::call('cache:clear');
Artisan::call('debugbar:clear');
Artisan::call('route:clear');
Artisan::call('view:clear');
Artisan::call('config:clear');
Artisan::call('optimize', array('--force' => true)); Artisan::call('optimize', array('--force' => true));
Cache::flush(); Cache::flush();
Session::flush(); Session::flush();

View File

@ -136,7 +136,7 @@ class ClientController extends BaseController
'credit' => $client->getTotalCredit(), 'credit' => $client->getTotalCredit(),
'title' => trans('texts.view_client'), 'title' => trans('texts.view_client'),
'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0, 'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0,
'hasQuotes' => Invoice::scope()->where('is_quote', '=', true)->whereClientId($client->id)->count() > 0, 'hasQuotes' => Invoice::scope()->invoiceType(INVOICE_TYPE_QUOTE)->whereClientId($client->id)->count() > 0,
'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0, 'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0,
'gatewayLink' => $client->getGatewayLink($accountGateway), 'gatewayLink' => $client->getGatewayLink($accountGateway),
'gateway' => $accountGateway 'gateway' => $accountGateway

View File

@ -63,7 +63,7 @@ class ClientPortalController extends BaseController
if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey)
&& (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
if ($invoice->is_quote) { if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteInvitationWasViewed($invoice, $invitation)); event(new QuoteInvitationWasViewed($invoice, $invitation));
} else { } else {
event(new InvoiceInvitationWasViewed($invoice, $invitation)); event(new InvoiceInvitationWasViewed($invoice, $invitation));

View File

@ -9,7 +9,7 @@ class DashboardApiController extends BaseAPIController
{ {
public function index() public function index()
{ {
$view_all = !Auth::user()->hasPermission('view_all'); $view_all = Auth::user()->hasPermission('view_all');
$user_id = Auth::user()->id; $user_id = Auth::user()->id;
// total_income, billed_clients, invoice_sent and active_clients // total_income, billed_clients, invoice_sent and active_clients
@ -24,7 +24,7 @@ class DashboardApiController extends BaseAPIController
->where('clients.is_deleted', '=', false) ->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false) ->where('invoices.is_recurring', '=', false)
->where('invoices.is_quote', '=', false); ->where('invoices.invoice_type_id', '=', false);
if(!$view_all){ if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){ $metrics = $metrics->where(function($query) use($user_id){
@ -62,7 +62,7 @@ class DashboardApiController extends BaseAPIController
->where('accounts.id', '=', Auth::user()->account_id) ->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false) ->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->where('invoices.is_quote', '=', false) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false); ->where('invoices.is_recurring', '=', false);
if(!$view_all){ if(!$view_all){
@ -106,7 +106,7 @@ class DashboardApiController extends BaseAPIController
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id); $pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
} }
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) $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') ->orderBy('invoices.due_date', 'asc')
->take(50) ->take(50)
->get(); ->get();
@ -131,7 +131,7 @@ class DashboardApiController extends BaseAPIController
} }
$upcoming = $upcoming->take(50) $upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) ->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(); ->get();
$payments = DB::table('payments') $payments = DB::table('payments')
@ -157,13 +157,12 @@ class DashboardApiController extends BaseAPIController
$hasQuotes = false; $hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) { foreach ([$upcoming, $pastDue] as $data) {
foreach ($data as $invoice) { foreach ($data as $invoice) {
if ($invoice->is_quote) { if ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE) {
$hasQuotes = true; $hasQuotes = true;
} }
} }
} }
$data = [ $data = [
'id' => 1, 'id' => 1,
'paidToDate' => $paidToDate[0]->value ? $paidToDate[0]->value : 0, 'paidToDate' => $paidToDate[0]->value ? $paidToDate[0]->value : 0,

View File

@ -26,7 +26,7 @@ class DashboardController extends BaseController
->where('clients.is_deleted', '=', false) ->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false) ->where('invoices.is_recurring', '=', false)
->where('invoices.is_quote', '=', false); ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD);
if(!$view_all){ if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){ $metrics = $metrics->where(function($query) use($user_id){
@ -64,7 +64,7 @@ class DashboardController extends BaseController
->where('accounts.id', '=', Auth::user()->account_id) ->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false) ->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->where('invoices.is_quote', '=', false) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false); ->where('invoices.is_recurring', '=', false);
if(!$view_all){ if(!$view_all){
@ -121,7 +121,7 @@ class DashboardController extends BaseController
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id); $pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
} }
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) $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') ->orderBy('invoices.due_date', 'asc')
->take(50) ->take(50)
->get(); ->get();
@ -147,7 +147,7 @@ class DashboardController extends BaseController
} }
$upcoming = $upcoming->take(50) $upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) ->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(); ->get();
$payments = DB::table('payments') $payments = DB::table('payments')
@ -173,7 +173,7 @@ class DashboardController extends BaseController
$hasQuotes = false; $hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) { foreach ([$upcoming, $pastDue] as $data) {
foreach ($data as $invoice) { foreach ($data as $invoice) {
if ($invoice->is_quote) { if ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE) {
$hasQuotes = true; $hasQuotes = true;
} }
} }

View File

@ -2,13 +2,21 @@
use App\Models\Document; use App\Models\Document;
use App\Ninja\Repositories\DocumentRepository;
use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
class DocumentAPIController extends BaseAPIController class DocumentAPIController extends BaseAPIController
{ {
protected $documentRepo;
public function __construct() protected $entityType = ENTITY_DOCUMENT;
public function __construct(DocumentRepository $documentRepo)
{ {
parent::__construct(); parent::__construct();
$this->documentRepo = $documentRepo;
} }
public function index() public function index()
@ -16,16 +24,19 @@ class DocumentAPIController extends BaseAPIController
//stub //stub
} }
public function show($publicId) public function show(DocumentRequest $request)
{ {
$document = Document::scope($publicId)->firstOrFail(); $document = $request->entity();
return DocumentController::getDownloadResponse($document); return DocumentController::getDownloadResponse($document);
} }
public function store() public function store(CreateDocumentRequest $request)
{ {
//stub
$document = $this->documentRepo->upload($request->all());
return $this->itemResponse($document);
} }
public function update() public function update()

View File

@ -14,6 +14,7 @@ use App\Ninja\Repositories\DocumentRepository;
use App\Http\Requests\DocumentRequest; use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest; use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\UpdateDocumentRequest;
class DocumentController extends BaseController class DocumentController extends BaseController
{ {
@ -26,20 +27,20 @@ class DocumentController extends BaseController
$this->documentRepo = $documentRepo; $this->documentRepo = $documentRepo;
} }
public function get(DocumentRequest $request) public function get(DocumentRequest $request)
{ {
return static::getDownloadResponse($request->entity()); return static::getDownloadResponse($request->entity());
} }
public static function getDownloadResponse($document){ public static function getDownloadResponse($document){
$direct_url = $document->getDirectUrl(); $direct_url = $document->getDirectUrl();
if($direct_url){ if($direct_url){
return redirect($direct_url); return redirect($direct_url);
} }
$stream = $document->getStream(); $stream = $document->getStream();
if($stream){ if($stream){
$headers = [ $headers = [
'Content-Type' => Document::$types[$document->type]['mime'], 'Content-Type' => Document::$types[$document->type]['mime'],
@ -54,59 +55,55 @@ class DocumentController extends BaseController
$response = Response::make($document->getRaw(), 200); $response = Response::make($document->getRaw(), 200);
$response->header('content-type', Document::$types[$document->type]['mime']); $response->header('content-type', Document::$types[$document->type]['mime']);
} }
return $response; return $response;
} }
public function getPreview(DocumentRequest $request) public function getPreview(DocumentRequest $request)
{ {
$document = $request->entity(); $document = $request->entity();
if(empty($document->preview)){ if(empty($document->preview)){
return Response::view('error', array('error'=>'Preview does not exist!'), 404); return Response::view('error', array('error'=>'Preview does not exist!'), 404);
} }
$direct_url = $document->getDirectPreviewUrl(); $direct_url = $document->getDirectPreviewUrl();
if($direct_url){ if($direct_url){
return redirect($direct_url); return redirect($direct_url);
} }
$previewType = pathinfo($document->preview, PATHINFO_EXTENSION); $previewType = pathinfo($document->preview, PATHINFO_EXTENSION);
$response = Response::make($document->getRawPreview(), 200); $response = Response::make($document->getRawPreview(), 200);
$response->header('content-type', Document::$types[$previewType]['mime']); $response->header('content-type', Document::$types[$previewType]['mime']);
return $response; return $response;
} }
public function getVFSJS(DocumentRequest $request, $publicId, $name) public function getVFSJS(DocumentRequest $request, $publicId, $name)
{ {
$document = $request->entity(); $document = $request->entity();
if(substr($name, -3)=='.js'){ if(substr($name, -3)=='.js'){
$name = substr($name, 0, -3); $name = substr($name, 0, -3);
} }
if(!$document->isPDFEmbeddable()){ if(!$document->isPDFEmbeddable()){
return Response::view('error', array('error'=>'Image does not exist!'), 404); return Response::view('error', array('error'=>'Image does not exist!'), 404);
} }
$content = $document->preview?$document->getRawPreview():$document->getRaw(); $content = $document->preview?$document->getRawPreview():$document->getRaw();
$content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")'; $content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")';
$response = Response::make($content, 200); $response = Response::make($content, 200);
$response->header('content-type', 'text/javascript'); $response->header('content-type', 'text/javascript');
$response->header('cache-control', 'max-age=31536000'); $response->header('cache-control', 'max-age=31536000');
return $response; return $response;
} }
public function postUpload(CreateDocumentRequest $request) public function postUpload(CreateDocumentRequest $request)
{ {
if (!Utils::hasFeature(FEATURE_DOCUMENTS)) { $result = $this->documentRepo->upload($request->all(), $doc_array);
return;
}
$result = $this->documentRepo->upload(Input::all()['file'], $doc_array);
if(is_string($result)){ if(is_string($result)){
return Response::json([ return Response::json([
'error' => $result, 'error' => $result,
@ -120,4 +117,11 @@ class DocumentController extends BaseController
], 200); ], 200);
} }
} }
public function delete(UpdateDocumentRequest $request)
{
$request->entity()->delete();
return RESULT_SUCCESS;
}
} }

View File

@ -134,23 +134,23 @@ class ExportController extends BaseController
if ($request->input(ENTITY_INVOICE)) { if ($request->input(ENTITY_INVOICE)) {
$data['invoices'] = Invoice::scope() $data['invoices'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status') ->with('user', 'client.contacts', 'invoice_status')
->withArchived() ->withArchived()
->where('is_quote', '=', false)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->get(); ->get();
$data['quotes'] = Invoice::scope() $data['quotes'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_QUOTE)
->with('user', 'client.contacts', 'invoice_status') ->with('user', 'client.contacts', 'invoice_status')
->withArchived() ->withArchived()
->where('is_quote', '=', true)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->get(); ->get();
$data['recurringInvoices'] = Invoice::scope() $data['recurringInvoices'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status', 'frequency') ->with('user', 'client.contacts', 'invoice_status', 'frequency')
->withArchived() ->withArchived()
->where('is_quote', '=', false)
->where('is_recurring', '=', true) ->where('is_recurring', '=', true)
->get(); ->get();
} }

View File

@ -134,6 +134,7 @@ class InvoiceApiController extends BaseAPIController
'city', 'city',
'state', 'state',
'postal_code', 'postal_code',
'country_id',
'private_notes', 'private_notes',
'currency_code', 'currency_code',
] as $field) { ] as $field) {
@ -182,7 +183,7 @@ class InvoiceApiController extends BaseAPIController
$invoice = Invoice::scope($invoice->public_id) $invoice = Invoice::scope($invoice->public_id)
->with('client', 'invoice_items', 'invitations') ->with('client', 'invoice_items', 'invitations')
->first(); ->first();
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} }
@ -269,7 +270,7 @@ class InvoiceApiController extends BaseAPIController
$item[$key] = $val; $item[$key] = $val;
} }
} }
return $item; return $item;
} }
@ -308,7 +309,7 @@ class InvoiceApiController extends BaseAPIController
public function update(UpdateInvoiceAPIRequest $request, $publicId) public function update(UpdateInvoiceAPIRequest $request, $publicId)
{ {
if ($request->action == ACTION_CONVERT) { if ($request->action == ACTION_CONVERT) {
$quote = $request->entity(); $quote = $request->entity();
$invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id); $invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id);
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} elseif ($request->action) { } elseif ($request->action) {
@ -322,7 +323,7 @@ class InvoiceApiController extends BaseAPIController
$invoice = Invoice::scope($publicId) $invoice = Invoice::scope($publicId)
->with('client', 'invoice_items', 'invitations') ->with('client', 'invoice_items', 'invitations')
->firstOrFail(); ->firstOrFail();
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} }
@ -351,10 +352,23 @@ class InvoiceApiController extends BaseAPIController
public function destroy(UpdateInvoiceAPIRequest $request) public function destroy(UpdateInvoiceAPIRequest $request)
{ {
$invoice = $request->entity(); $invoice = $request->entity();
$this->invoiceRepo->delete($invoice); $this->invoiceRepo->delete($invoice);
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} }
public function download(InvoiceRequest $request)
{
$invoice = $request->entity();
$pdfString = $invoice->getPDFString();
header('Content-Type: application/pdf');
header('Content-Length: ' . strlen($pdfString));
header('Content-disposition: attachment; filename="' . $invoice->getFileName() . '"');
header('Cache-Control: public, must-revalidate, max-age=0');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
return $pdfString;
}
} }

View File

@ -574,9 +574,9 @@ class InvoiceController extends BaseController
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS),
]; ];
$invoice->is_quote = intval($invoice->is_quote); $invoice->invoice_type_id = intval($invoice->invoice_type_id);
$activityTypeId = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; $activityTypeId = $invoice->isType(INVOICE_TYPE_QUOTE) ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE;
$activities = Activity::scope(false, $invoice->account_id) $activities = Activity::scope(false, $invoice->account_id)
->where('activity_type_id', '=', $activityTypeId) ->where('activity_type_id', '=', $activityTypeId)
->where('invoice_id', '=', $invoice->id) ->where('invoice_id', '=', $invoice->id)
@ -596,7 +596,7 @@ class InvoiceController extends BaseController
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS),
]; ];
$backup->is_quote = isset($backup->is_quote) && intval($backup->is_quote); $backup->invoice_type_id = isset($backup->invoice_type_id) && intval($backup->invoice_type_id) == INVOICE_TYPE_QUOTE;
$backup->account = $invoice->account->toArray(); $backup->account = $invoice->account->toArray();
$versionsJson[$activity->id] = $backup; $versionsJson[$activity->id] = $backup;

View File

@ -77,8 +77,8 @@ class PaymentController extends BaseController
public function create(PaymentRequest $request) public function create(PaymentRequest $request)
{ {
$invoices = Invoice::scope() $invoices = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->where('is_quote', '=', false)
->where('invoices.balance', '>', 0) ->where('invoices.balance', '>', 0)
->with('client', 'invoice_status') ->with('client', 'invoice_status')
->orderBy('invoice_number')->get(); ->orderBy('invoice_number')->get();
@ -108,7 +108,7 @@ class PaymentController extends BaseController
$data = array( $data = array(
'client' => null, 'client' => null,
'invoice' => null, 'invoice' => null,
'invoices' => Invoice::scope()->where('is_recurring', '=', false)->where('is_quote', '=', false) 'invoices' => Invoice::scope()->invoiceType(INVOICE_TYPE_STANDARD)->where('is_recurring', '=', false)
->with('client', 'invoice_status')->orderBy('invoice_number')->get(), ->with('client', 'invoice_status')->orderBy('invoice_number')->get(),
'payment' => $payment, 'payment' => $payment,
'method' => 'PUT', 'method' => 'PUT',

View File

@ -113,16 +113,16 @@ class QuoteController extends BaseController
$rates = TaxRate::scope()->orderBy('name')->get(); $rates = TaxRate::scope()->orderBy('name')->get();
$options = []; $options = [];
$defaultTax = false; $defaultTax = false;
foreach ($rates as $rate) { foreach ($rates as $rate) {
$options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%'; $options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%';
// load default invoice tax // load default invoice tax
if ($rate->id == $account->default_tax_rate_id) { if ($rate->id == $account->default_tax_rate_id) {
$defaultTax = $rate; $defaultTax = $rate;
} }
} }
return [ return [
'entityType' => ENTITY_QUOTE, 'entityType' => ENTITY_QUOTE,
'account' => Auth::user()->account, 'account' => Auth::user()->account,
@ -130,7 +130,7 @@ class QuoteController extends BaseController
'taxRateOptions' => $options, 'taxRateOptions' => $options,
'defaultTax' => $defaultTax, 'defaultTax' => $defaultTax,
'countries' => Cache::get('countries'), 'countries' => Cache::get('countries'),
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), 'clients' => Client::scope()->viewable()->with('contacts', 'country')->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->orderBy('name')->get(), 'taxRates' => TaxRate::scope()->orderBy('name')->get(),
'currencies' => Cache::get('currencies'), 'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'), 'sizes' => Cache::get('sizes'),

View File

@ -168,7 +168,7 @@ class ReportController extends BaseController
->groupBy($groupBy); ->groupBy($groupBy);
if ($entityType == ENTITY_INVOICE) { if ($entityType == ENTITY_INVOICE) {
$records->where('is_quote', '=', false) $records->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false); ->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) { } elseif ($entityType == ENTITY_PAYMENT) {
$records->join('invoices', 'invoices.id', '=', 'payments.invoice_id') $records->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
@ -374,7 +374,7 @@ class ReportController extends BaseController
$query->where('invoice_date', '>=', $startDate) $query->where('invoice_date', '>=', $startDate)
->where('invoice_date', '<=', $endDate) ->where('invoice_date', '<=', $endDate)
->where('is_deleted', '=', false) ->where('is_deleted', '=', false)
->where('is_quote', '=', false) ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->with(['payments' => function($query) { ->with(['payments' => function($query) {
$query->withTrashed() $query->withTrashed()
@ -429,7 +429,7 @@ class ReportController extends BaseController
->with(['invoices' => function($query) use ($startDate, $endDate) { ->with(['invoices' => function($query) use ($startDate, $endDate) {
$query->where('invoice_date', '>=', $startDate) $query->where('invoice_date', '>=', $startDate)
->where('invoice_date', '<=', $endDate) ->where('invoice_date', '<=', $endDate)
->where('is_quote', '=', false) ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->withArchived(); ->withArchived();
}]); }]);

View File

@ -23,10 +23,11 @@ class ApiCheck {
{ {
$loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register'); $loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register');
$headers = Utils::getApiHeaders(); $headers = Utils::getApiHeaders();
$hasApiSecret = hash_equals($request->api_secret ?: '', env(API_SECRET));
if ($loggingIn) { if ($loggingIn) {
// check API secret // check API secret
if ( ! $request->api_secret || ! env(API_SECRET) || ! hash_equals($request->api_secret, env(API_SECRET))) { if ( ! $hasApiSecret) {
sleep(ERROR_DELAY); sleep(ERROR_DELAY);
return Response::json('Invalid secret', 403, $headers); return Response::json('Invalid secret', 403, $headers);
} }
@ -48,7 +49,7 @@ class ApiCheck {
return $next($request); return $next($request);
} }
if (!Utils::hasFeature(FEATURE_API) && !$loggingIn) { if (!Utils::hasFeature(FEATURE_API) && !$hasApiSecret) {
return Response::json('API requires pro plan', 403, $headers); return Response::json('API requires pro plan', 403, $headers);
} else { } else {
$key = Auth::check() ? Auth::user()->account->id : $request->getClientIp(); $key = Auth::check() ? Auth::user()->account->id : $request->getClientIp();
@ -59,7 +60,7 @@ class ApiCheck {
$hour_throttle = Cache::get("hour_throttle:{$key}", null); $hour_throttle = Cache::get("hour_throttle:{$key}", null);
$last_api_request = Cache::get("last_api_request:{$key}", 0); $last_api_request = Cache::get("last_api_request:{$key}", 0);
$last_api_diff = time() - $last_api_request; $last_api_diff = time() - $last_api_request;
if (is_null($hour_throttle)) { if (is_null($hour_throttle)) {
$new_hour_throttle = 0; $new_hour_throttle = 0;
} else { } else {
@ -83,4 +84,4 @@ class ApiCheck {
return $next($request); return $next($request);
} }
} }

View File

@ -1,7 +1,15 @@
<?php namespace App\Http\Requests; <?php namespace App\Http\Requests;
use App\Models\Invoice;
use App\Models\Expense;
class CreateDocumentRequest extends DocumentRequest class CreateDocumentRequest extends DocumentRequest
{ {
protected $autoload = [
ENTITY_INVOICE,
ENTITY_EXPENSE,
];
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
* *
@ -9,6 +17,18 @@ class CreateDocumentRequest extends DocumentRequest
*/ */
public function authorize() public function authorize()
{ {
if ( ! $this->user()->hasFeature(FEATURE_DOCUMENTS)) {
return false;
}
if ($this->invoice && $this->user()->cannot('edit', $this->invoice)) {
return false;
}
if ($this->expense && $this->user()->cannot('edit', $this->expense)) {
return false;
}
return $this->user()->can('create', ENTITY_DOCUMENT); return $this->user()->can('create', ENTITY_DOCUMENT);
} }
@ -20,7 +40,8 @@ class CreateDocumentRequest extends DocumentRequest
public function rules() public function rules()
{ {
return [ return [
//'file' => 'mimes:jpg'
]; ];
} }
} }

View File

@ -2,8 +2,50 @@
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
// https://laracasts.com/discuss/channels/general-discussion/laravel-5-modify-input-before-validation/replies/34366
abstract class Request extends FormRequest { abstract class Request extends FormRequest {
// // populate in subclass to auto load record
protected $autoload = [];
/**
* Validate the input.
*
* @param \Illuminate\Validation\Factory $factory
* @return \Illuminate\Validation\Validator
*/
public function validator($factory)
{
return $factory->make(
$this->sanitizeInput(), $this->container->call([$this, 'rules']), $this->messages()
);
}
/**
* Sanitize the input.
*
* @return array
*/
protected function sanitizeInput()
{
if (method_exists($this, 'sanitize')) {
$input = $this->container->call([$this, 'sanitize']);
} else {
$input = $this->all();
}
// autoload referenced entities
foreach ($this->autoload as $entityType) {
if ($id = $this->input("{$entityType}_public_id") ?: $this->input("{$entityType}_id")) {
$class = "App\\Models\\" . ucwords($entityType);
$entity = $class::scope($id)->firstOrFail();
$input[$entityType] = $entity;
$input[$entityType . '_id'] = $entity->id;
}
}
$this->replace($input);
return $this->all();
}
} }

View File

@ -0,0 +1,26 @@
<?php namespace App\Http\Requests;
class UpdateDocumentRequest extends DocumentRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
];
}
}

View File

@ -157,7 +157,8 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('documents/{documents}/{filename?}', 'DocumentController@get'); Route::get('documents/{documents}/{filename?}', 'DocumentController@get');
Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS');
Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview');
Route::post('document', 'DocumentController@postUpload'); Route::post('documents', 'DocumentController@postUpload');
Route::delete('documents/{documents}', 'DocumentController@delete');
Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/create/{client_id?}', 'QuoteController@create');
Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice');
@ -271,6 +272,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function()
//Route::get('quotes', 'QuoteApiController@index'); //Route::get('quotes', 'QuoteApiController@index');
//Route::resource('quotes', 'QuoteApiController'); //Route::resource('quotes', 'QuoteApiController');
Route::get('invoices', 'InvoiceApiController@index'); Route::get('invoices', 'InvoiceApiController@index');
Route::get('download/{invoice_id}', 'InvoiceApiController@download');
Route::resource('invoices', 'InvoiceApiController'); Route::resource('invoices', 'InvoiceApiController');
Route::get('payments', 'PaymentApiController@index'); Route::get('payments', 'PaymentApiController@index');
Route::resource('payments', 'PaymentApiController'); Route::resource('payments', 'PaymentApiController');
@ -360,6 +362,9 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_BANK_ACCOUNT', 'bank_account'); define('ENTITY_BANK_ACCOUNT', 'bank_account');
define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount'); define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
define('PERSON_CONTACT', 'contact'); define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user'); define('PERSON_USER', 'user');
define('PERSON_VENDOR_CONTACT','vendorcontact'); define('PERSON_VENDOR_CONTACT','vendorcontact');

View File

@ -18,7 +18,7 @@ class Account extends Eloquent
{ {
use PresentableTrait; use PresentableTrait;
use SoftDeletes; use SoftDeletes;
public static $plan_prices = array( public static $plan_prices = array(
PLAN_PRO => array( PLAN_PRO => array(
PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY, PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY,
@ -56,6 +56,11 @@ class Account extends Eloquent
'currency_id', 'currency_id',
'language_id', 'language_id',
'military_time', 'military_time',
'invoice_taxes',
'invoice_item_taxes',
'show_item_taxes',
'default_tax_rate_id',
'enable_second_tax_rate',
]; ];
public static $basicSettings = [ public static $basicSettings = [
@ -406,7 +411,7 @@ class Account extends Eloquent
return $gateway; return $gateway;
} }
} }
return false; return false;
} }
@ -426,27 +431,27 @@ class Account extends Eloquent
if($this->logo == ''){ if($this->logo == ''){
$this->calculateLogoDetails(); $this->calculateLogoDetails();
} }
return !empty($this->logo); return !empty($this->logo);
} }
public function getLogoDisk(){ public function getLogoDisk(){
return Storage::disk(env('LOGO_FILESYSTEM', 'logos')); return Storage::disk(env('LOGO_FILESYSTEM', 'logos'));
} }
protected function calculateLogoDetails(){ protected function calculateLogoDetails(){
$disk = $this->getLogoDisk(); $disk = $this->getLogoDisk();
if($disk->exists($this->account_key.'.png')){ if($disk->exists($this->account_key.'.png')){
$this->logo = $this->account_key.'.png'; $this->logo = $this->account_key.'.png';
} else if($disk->exists($this->account_key.'.jpg')) { } else if($disk->exists($this->account_key.'.jpg')) {
$this->logo = $this->account_key.'.jpg'; $this->logo = $this->account_key.'.jpg';
} }
if(!empty($this->logo)){ if(!empty($this->logo)){
$image = imagecreatefromstring($disk->get($this->logo)); $image = imagecreatefromstring($disk->get($this->logo));
$this->logo_width = imagesx($image); $this->logo_width = imagesx($image);
$this->logo_height = imagesy($image); $this->logo_height = imagesy($image);
$this->logo_size = $disk->size($this->logo); $this->logo_size = $disk->size($this->logo);
} else { } else {
$this->logo = null; $this->logo = null;
@ -458,33 +463,33 @@ class Account extends Eloquent
if(!$this->hasLogo()){ if(!$this->hasLogo()){
return null; return null;
} }
$disk = $this->getLogoDisk(); $disk = $this->getLogoDisk();
return $disk->get($this->logo); return $disk->get($this->logo);
} }
public function getLogoURL($cachebuster = false) public function getLogoURL($cachebuster = false)
{ {
if(!$this->hasLogo()){ if(!$this->hasLogo()){
return null; return null;
} }
$disk = $this->getLogoDisk(); $disk = $this->getLogoDisk();
$adapter = $disk->getAdapter(); $adapter = $disk->getAdapter();
if($adapter instanceof \League\Flysystem\Adapter\Local) { if($adapter instanceof \League\Flysystem\Adapter\Local) {
// Stored locally // Stored locally
$logo_url = str_replace(public_path(), url('/'), $adapter->applyPathPrefix($this->logo), $count); $logo_url = str_replace(public_path(), url('/'), $adapter->applyPathPrefix($this->logo), $count);
if ($cachebuster) { if ($cachebuster) {
$logo_url .= '?no_cache='.time(); $logo_url .= '?no_cache='.time();
} }
if($count == 1){ if($count == 1){
return str_replace(DIRECTORY_SEPARATOR, '/', $logo_url); return str_replace(DIRECTORY_SEPARATOR, '/', $logo_url);
} }
} }
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); return Document::getDirectFileUrl($this->logo, $this->getLogoDisk());
} }
@ -529,18 +534,18 @@ class Account extends Eloquent
$invoice = Invoice::createNew(); $invoice = Invoice::createNew();
$invoice->is_recurring = false; $invoice->is_recurring = false;
$invoice->is_quote = false; $invoice->invoice_type_id = INVOICE_TYPE_STANDARD;
$invoice->invoice_date = Utils::today(); $invoice->invoice_date = Utils::today();
$invoice->start_date = Utils::today(); $invoice->start_date = Utils::today();
$invoice->invoice_design_id = $this->invoice_design_id; $invoice->invoice_design_id = $this->invoice_design_id;
$invoice->client_id = $clientId; $invoice->client_id = $clientId;
if ($entityType === ENTITY_RECURRING_INVOICE) { if ($entityType === ENTITY_RECURRING_INVOICE) {
$invoice->invoice_number = microtime(true); $invoice->invoice_number = microtime(true);
$invoice->is_recurring = true; $invoice->is_recurring = true;
} else { } else {
if ($entityType == ENTITY_QUOTE) { if ($entityType == ENTITY_QUOTE) {
$invoice->is_quote = true; $invoice->invoice_type_id = INVOICE_TYPE_QUOTE;
} }
if ($this->hasClientNumberPattern($invoice) && !$clientId) { if ($this->hasClientNumberPattern($invoice) && !$clientId) {
@ -549,7 +554,7 @@ class Account extends Eloquent
$invoice->invoice_number = $this->getNextInvoiceNumber($invoice); $invoice->invoice_number = $this->getNextInvoiceNumber($invoice);
} }
} }
if (!$clientId) { if (!$clientId) {
$invoice->client = Client::createNew(); $invoice->client = Client::createNew();
$invoice->client->public_id = 0; $invoice->client->public_id = 0;
@ -558,34 +563,34 @@ class Account extends Eloquent
return $invoice; return $invoice;
} }
public function getNumberPrefix($isQuote) public function getNumberPrefix($invoice_type_id)
{ {
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) {
return ''; return '';
} }
return ($isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: ''; return ($invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: '';
} }
public function hasNumberPattern($isQuote) public function hasNumberPattern($invoice_type_id)
{ {
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) {
return false; return false;
} }
return $isQuote ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false); return $invoice_type_id == INVOICE_TYPE_QUOTE ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false);
} }
public function hasClientNumberPattern($invoice) public function hasClientNumberPattern($invoice)
{ {
$pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern;
return strstr($pattern, '$custom'); return strstr($pattern, '$custom');
} }
public function getNumberPattern($invoice) public function getNumberPattern($invoice)
{ {
$pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern;
if (!$pattern) { if (!$pattern) {
return false; return false;
@ -595,7 +600,7 @@ class Account extends Eloquent
$replace = [date('Y')]; $replace = [date('Y')];
$search[] = '{$counter}'; $search[] = '{$counter}';
$replace[] = str_pad($this->getCounter($invoice->is_quote), $this->invoice_number_padding, '0', STR_PAD_LEFT); $replace[] = str_pad($this->getCounter($invoice->invoice_type_id), $this->invoice_number_padding, '0', STR_PAD_LEFT);
if (strstr($pattern, '{$userId}')) { if (strstr($pattern, '{$userId}')) {
$search[] = '{$userId}'; $search[] = '{$userId}';
@ -638,9 +643,9 @@ class Account extends Eloquent
return str_replace($search, $replace, $pattern); return str_replace($search, $replace, $pattern);
} }
public function getCounter($isQuote) public function getCounter($invoice_type_id)
{ {
return $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; return $invoice_type_id == INVOICE_TYPE_QUOTE && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter;
} }
public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE) public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE)
@ -651,31 +656,35 @@ class Account extends Eloquent
public function getNextInvoiceNumber($invoice) public function getNextInvoiceNumber($invoice)
{ {
if ($this->hasNumberPattern($invoice->is_quote)) { if ($this->hasNumberPattern($invoice->invoice_type_id)) {
return $this->getNumberPattern($invoice); $number = $this->getNumberPattern($invoice);
} else {
$counter = $this->getCounter($invoice->invoice_type_id);
$prefix = $this->getNumberPrefix($invoice->invoice_type_id);
$counterOffset = 0;
// confirm the invoice number isn't already taken
do {
$number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT);
$check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first();
$counter++;
$counterOffset++;
} while ($check);
// update the invoice counter to be caught up
if ($counterOffset > 1) {
if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) {
$this->quote_number_counter += $counterOffset - 1;
} else {
$this->invoice_number_counter += $counterOffset - 1;
}
$this->save();
}
} }
$counter = $this->getCounter($invoice->is_quote); if ($invoice->recurring_invoice_id) {
$prefix = $this->getNumberPrefix($invoice->is_quote); $number = $this->recurring_invoice_number_prefix . $number;
$counterOffset = 0;
// confirm the invoice number isn't already taken
do {
$number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT);
$check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first();
$counter++;
$counterOffset++;
} while ($check);
// update the invoice counter to be caught up
if ($counterOffset > 1) {
if ($invoice->is_quote && !$this->share_counter) {
$this->quote_number_counter += $counterOffset - 1;
} else {
$this->invoice_number_counter += $counterOffset - 1;
}
$this->save();
} }
return $number; return $number;
@ -683,19 +692,17 @@ class Account extends Eloquent
public function incrementCounter($invoice) public function incrementCounter($invoice)
{ {
if ($invoice->is_quote && !$this->share_counter) { // if they didn't use the counter don't increment it
if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice)) {
return;
}
if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) {
$this->quote_number_counter += 1; $this->quote_number_counter += 1;
} else { } else {
$default = $this->invoice_number_counter; $this->invoice_number_counter += 1;
$actual = Utils::parseInt($invoice->invoice_number);
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $default != $actual) {
$this->invoice_number_counter = $actual + 1;
} else {
$this->invoice_number_counter += 1;
}
} }
$this->save(); $this->save();
} }
@ -718,7 +725,7 @@ class Account extends Eloquent
$query->where('updated_at', '>=', $updatedAt); $query->where('updated_at', '>=', $updatedAt);
} }
}]); }]);
} }
} }
public function loadLocalizationSettings($client = false) public function loadLocalizationSettings($client = false)
@ -731,8 +738,8 @@ class Account extends Eloquent
Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT); Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT);
Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT); Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT);
$currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY; $currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY;
$locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE);
Session::put(SESSION_CURRENCY, $currencyId); Session::put(SESSION_CURRENCY, $currencyId);
Session::put(SESSION_LOCALE, $locale); Session::put(SESSION_LOCALE, $locale);
@ -815,7 +822,7 @@ class Account extends Eloquent
if ( ! Utils::isNinja()) { if ( ! Utils::isNinja()) {
return; return;
} }
$this->company->trial_plan = $plan; $this->company->trial_plan = $plan;
$this->company->trial_started = date_create()->format('Y-m-d'); $this->company->trial_started = date_create()->format('Y-m-d');
$this->company->save(); $this->company->save();
@ -826,18 +833,18 @@ class Account extends Eloquent
if (Utils::isNinjaDev()) { if (Utils::isNinjaDev()) {
return true; return true;
} }
$planDetails = $this->getPlanDetails(); $planDetails = $this->getPlanDetails();
$selfHost = !Utils::isNinjaProd(); $selfHost = !Utils::isNinjaProd();
if (!$selfHost && function_exists('ninja_account_features')) { if (!$selfHost && function_exists('ninja_account_features')) {
$result = ninja_account_features($this, $feature); $result = ninja_account_features($this, $feature);
if ($result != null) { if ($result != null) {
return $result; return $result;
} }
} }
switch ($feature) { switch ($feature) {
// Pro // Pro
case FEATURE_CUSTOMIZE_INVOICE_DESIGN: case FEATURE_CUSTOMIZE_INVOICE_DESIGN:
@ -854,7 +861,7 @@ class Account extends Eloquent
case FEATURE_CLIENT_PORTAL_PASSWORD: case FEATURE_CLIENT_PORTAL_PASSWORD:
case FEATURE_CUSTOM_URL: case FEATURE_CUSTOM_URL:
return $selfHost || !empty($planDetails); return $selfHost || !empty($planDetails);
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan // Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case FEATURE_MORE_CLIENTS: case FEATURE_MORE_CLIENTS:
return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false))); return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false)));
@ -867,26 +874,26 @@ class Account extends Eloquent
// Fallthrough // Fallthrough
case FEATURE_CLIENT_PORTAL_CSS: case FEATURE_CLIENT_PORTAL_CSS:
return !empty($planDetails);// A plan is required even for self-hosted users return !empty($planDetails);// A plan is required even for self-hosted users
// Enterprise; No Trial allowed; grandfathered for old pro users // Enterprise; No Trial allowed; grandfathered for old pro users
case FEATURE_USERS:// Grandfathered for old Pro users case FEATURE_USERS:// Grandfathered for old Pro users
if($planDetails && $planDetails['trial']) { if($planDetails && $planDetails['trial']) {
// Do they have a non-trial plan? // Do they have a non-trial plan?
$planDetails = $this->getPlanDetails(false, false); $planDetails = $this->getPlanDetails(false, false);
} }
return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE)); return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE));
// Enterprise; No Trial allowed // Enterprise; No Trial allowed
case FEATURE_DOCUMENTS: case FEATURE_DOCUMENTS:
case FEATURE_USER_PERMISSIONS: case FEATURE_USER_PERMISSIONS:
return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial']; return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial'];
default: default:
return false; return false;
} }
} }
public function isPro(&$plan_details = null) public function isPro(&$plan_details = null)
{ {
if (!Utils::isNinjaProd()) { if (!Utils::isNinjaProd()) {
@ -898,7 +905,7 @@ class Account extends Eloquent
} }
$plan_details = $this->getPlanDetails(); $plan_details = $this->getPlanDetails();
return !empty($plan_details); return !empty($plan_details);
} }
@ -913,36 +920,36 @@ class Account extends Eloquent
} }
$plan_details = $this->getPlanDetails(); $plan_details = $this->getPlanDetails();
return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE; return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE;
} }
public function getPlanDetails($include_inactive = false, $include_trial = true) public function getPlanDetails($include_inactive = false, $include_trial = true)
{ {
if (!$this->company) { if (!$this->company) {
return null; return null;
} }
$plan = $this->company->plan; $plan = $this->company->plan;
$trial_plan = $this->company->trial_plan; $trial_plan = $this->company->trial_plan;
if(!$plan && (!$trial_plan || !$include_trial)) { if(!$plan && (!$trial_plan || !$include_trial)) {
return null; return null;
} }
$trial_active = false; $trial_active = false;
if ($trial_plan && $include_trial) { if ($trial_plan && $include_trial) {
$trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started); $trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started);
$trial_expires = clone $trial_started; $trial_expires = clone $trial_started;
$trial_expires->modify('+2 weeks'); $trial_expires->modify('+2 weeks');
if ($trial_expires >= date_create()) { if ($trial_expires >= date_create()) {
$trial_active = true; $trial_active = true;
} }
} }
$plan_active = false; $plan_active = false;
if ($plan) { if ($plan) {
if ($this->company->plan_expires == null) { if ($this->company->plan_expires == null) {
$plan_active = true; $plan_active = true;
$plan_expires = false; $plan_expires = false;
@ -953,11 +960,11 @@ class Account extends Eloquent
} }
} }
} }
if (!$include_inactive && !$plan_active && !$trial_active) { if (!$include_inactive && !$plan_active && !$trial_active) {
return null; return null;
} }
// Should we show plan details or trial details? // Should we show plan details or trial details?
if (($plan && !$trial_plan) || !$include_trial) { if (($plan && !$trial_plan) || !$include_trial) {
$use_plan = true; $use_plan = true;
@ -984,7 +991,7 @@ class Account extends Eloquent
$use_plan = $plan_expires >= $trial_expires; $use_plan = $plan_expires >= $trial_expires;
} }
} }
if ($use_plan) { if ($use_plan) {
return array( return array(
'trial' => false, 'trial' => false,
@ -1011,7 +1018,7 @@ class Account extends Eloquent
if (!Utils::isNinjaProd()) { if (!Utils::isNinjaProd()) {
return false; return false;
} }
$plan_details = $this->getPlanDetails(); $plan_details = $this->getPlanDetails();
return $plan_details && $plan_details['trial']; return $plan_details && $plan_details['trial'];
@ -1026,7 +1033,7 @@ class Account extends Eloquent
return array(PLAN_PRO, PLAN_ENTERPRISE); return array(PLAN_PRO, PLAN_ENTERPRISE);
} }
} }
if ($this->company->trial_plan == PLAN_PRO) { if ($this->company->trial_plan == PLAN_PRO) {
if ($plan) { if ($plan) {
return $plan != PLAN_PRO; return $plan != PLAN_PRO;
@ -1034,28 +1041,28 @@ class Account extends Eloquent
return array(PLAN_ENTERPRISE); return array(PLAN_ENTERPRISE);
} }
} }
return false; return false;
} }
public function getCountTrialDaysLeft() public function getCountTrialDaysLeft()
{ {
$planDetails = $this->getPlanDetails(true); $planDetails = $this->getPlanDetails(true);
if(!$planDetails || !$planDetails['trial']) { if(!$planDetails || !$planDetails['trial']) {
return 0; return 0;
} }
$today = new DateTime('now'); $today = new DateTime('now');
$interval = $today->diff($planDetails['expires']); $interval = $today->diff($planDetails['expires']);
return $interval ? $interval->d : 0; return $interval ? $interval->d : 0;
} }
public function getRenewalDate() public function getRenewalDate()
{ {
$planDetails = $this->getPlanDetails(); $planDetails = $this->getPlanDetails();
if ($planDetails) { if ($planDetails) {
$date = $planDetails['expires']; $date = $planDetails['expires'];
$date = max($date, date_create()); $date = max($date, date_create());
@ -1107,7 +1114,7 @@ class Account extends Eloquent
'invoice_items', 'invoice_items',
'created_at', 'created_at',
'is_recurring', 'is_recurring',
'is_quote', 'invoice_type_id',
]); ]);
foreach ($invoice->invoice_items as $invoiceItem) { foreach ($invoice->invoice_items as $invoiceItem) {
@ -1185,7 +1192,7 @@ class Account extends Eloquent
$field = "email_template_{$entityType}"; $field = "email_template_{$entityType}";
$template = $this->$field; $template = $this->$field;
} }
if (!$template) { if (!$template) {
$template = $this->getDefaultEmailTemplate($entityType, $message); $template = $this->getDefaultEmailTemplate($entityType, $message);
} }
@ -1275,7 +1282,7 @@ class Account extends Eloquent
{ {
$url = SITE_URL; $url = SITE_URL;
$iframe_url = $this->iframe_url; $iframe_url = $this->iframe_url;
if ($iframe_url) { if ($iframe_url) {
return "{$iframe_url}/?"; return "{$iframe_url}/?";
} else if ($this->subdomain) { } else if ($this->subdomain) {
@ -1310,18 +1317,18 @@ class Account extends Eloquent
if (!$entity) { if (!$entity) {
return false; return false;
} }
// convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1' // convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1'
$field = str_replace(['invoice_', 'label'], ['', 'value'], $field); $field = str_replace(['invoice_', 'label'], ['', 'value'], $field);
return Utils::isEmpty($entity->$field) ? false : true; return Utils::isEmpty($entity->$field) ? false : true;
} }
public function attatchPDF() public function attachPDF()
{ {
return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment; return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment;
} }
public function getEmailDesignId() public function getEmailDesignId()
{ {
return $this->hasFeature(FEATURE_CUSTOM_EMAILS) ? $this->email_design_id : EMAIL_DESIGN_PLAIN; return $this->hasFeature(FEATURE_CUSTOM_EMAILS) ? $this->email_design_id : EMAIL_DESIGN_PLAIN;
@ -1329,11 +1336,11 @@ class Account extends Eloquent
public function clientViewCSS(){ public function clientViewCSS(){
$css = ''; $css = '';
if ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { if ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) {
$bodyFont = $this->getBodyFontCss(); $bodyFont = $this->getBodyFontCss();
$headerFont = $this->getHeaderFontCss(); $headerFont = $this->getHeaderFontCss();
$css = 'body{'.$bodyFont.'}'; $css = 'body{'.$bodyFont.'}';
if ($headerFont != $bodyFont) { if ($headerFont != $bodyFont) {
$css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}'; $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}';
@ -1343,17 +1350,17 @@ class Account extends Eloquent
// For self-hosted users, a white-label license is required for custom CSS // For self-hosted users, a white-label license is required for custom CSS
$css .= $this->client_view_css; $css .= $this->client_view_css;
} }
return $css; return $css;
} }
public function getFontsUrl($protocol = ''){ public function getFontsUrl($protocol = ''){
$bodyFont = $this->getHeaderFontId(); $bodyFont = $this->getHeaderFontId();
$headerFont = $this->getBodyFontId(); $headerFont = $this->getBodyFontId();
$bodyFontSettings = Utils::getFromCache($bodyFont, 'fonts'); $bodyFontSettings = Utils::getFromCache($bodyFont, 'fonts');
$google_fonts = array($bodyFontSettings['google_font']); $google_fonts = array($bodyFontSettings['google_font']);
if($headerFont != $bodyFont){ if($headerFont != $bodyFont){
$headerFontSettings = Utils::getFromCache($headerFont, 'fonts'); $headerFontSettings = Utils::getFromCache($headerFont, 'fonts');
$google_fonts[] = $headerFontSettings['google_font']; $google_fonts[] = $headerFontSettings['google_font'];
@ -1361,7 +1368,7 @@ class Account extends Eloquent
return ($protocol?$protocol.':':'').'//fonts.googleapis.com/css?family='.implode('|',$google_fonts); return ($protocol?$protocol.':':'').'//fonts.googleapis.com/css?family='.implode('|',$google_fonts);
} }
public function getHeaderFontId() { public function getHeaderFontId() {
return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT;
} }
@ -1373,47 +1380,47 @@ class Account extends Eloquent
public function getHeaderFontName(){ public function getHeaderFontName(){
return Utils::getFromCache($this->getHeaderFontId(), 'fonts')['name']; return Utils::getFromCache($this->getHeaderFontId(), 'fonts')['name'];
} }
public function getBodyFontName(){ public function getBodyFontName(){
return Utils::getFromCache($this->getBodyFontId(), 'fonts')['name']; return Utils::getFromCache($this->getBodyFontId(), 'fonts')['name'];
} }
public function getHeaderFontCss($include_weight = true){ public function getHeaderFontCss($include_weight = true){
$font_data = Utils::getFromCache($this->getHeaderFontId(), 'fonts'); $font_data = Utils::getFromCache($this->getHeaderFontId(), 'fonts');
$css = 'font-family:'.$font_data['css_stack'].';'; $css = 'font-family:'.$font_data['css_stack'].';';
if($include_weight){ if($include_weight){
$css .= 'font-weight:'.$font_data['css_weight'].';'; $css .= 'font-weight:'.$font_data['css_weight'].';';
} }
return $css; return $css;
} }
public function getBodyFontCss($include_weight = true){ public function getBodyFontCss($include_weight = true){
$font_data = Utils::getFromCache($this->getBodyFontId(), 'fonts'); $font_data = Utils::getFromCache($this->getBodyFontId(), 'fonts');
$css = 'font-family:'.$font_data['css_stack'].';'; $css = 'font-family:'.$font_data['css_stack'].';';
if($include_weight){ if($include_weight){
$css .= 'font-weight:'.$font_data['css_weight'].';'; $css .= 'font-weight:'.$font_data['css_weight'].';';
} }
return $css; return $css;
} }
public function getFonts(){ public function getFonts(){
return array_unique(array($this->getHeaderFontId(), $this->getBodyFontId())); return array_unique(array($this->getHeaderFontId(), $this->getBodyFontId()));
} }
public function getFontsData(){ public function getFontsData(){
$data = array(); $data = array();
foreach($this->getFonts() as $font){ foreach($this->getFonts() as $font){
$data[] = Utils::getFromCache($font, 'fonts'); $data[] = Utils::getFromCache($font, 'fonts');
} }
return $data; return $data;
} }
public function getFontFolders(){ public function getFontFolders(){
return array_map(function($item){return $item['folder'];}, $this->getFontsData()); return array_map(function($item){return $item['folder'];}, $this->getFontsData());
} }

View File

@ -6,20 +6,25 @@ use Auth;
class Document extends EntityModel class Document extends EntityModel
{ {
protected $fillable = [
'invoice_id',
'expense_id',
];
public static $extraExtensions = array( public static $extraExtensions = array(
'jpg' => 'jpeg', 'jpg' => 'jpeg',
'tif' => 'tiff', 'tif' => 'tiff',
); );
public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts
'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain', 'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain',
'application/zip', 'application/msword', 'application/zip', 'application/msword',
'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel', 'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml',
'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint',
); );
public static $types = array( public static $types = array(
'png' => array( 'png' => array(
'mime' => 'image/png', 'mime' => 'image/png',
@ -70,18 +75,18 @@ class Document extends EntityModel
'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
), ),
); );
public function fill(array $attributes) public function fill(array $attributes)
{ {
parent::fill($attributes); parent::fill($attributes);
if(empty($this->attributes['disk'])){ if(empty($this->attributes['disk'])){
$this->attributes['disk'] = env('DOCUMENT_FILESYSTEM', 'documents'); $this->attributes['disk'] = env('DOCUMENT_FILESYSTEM', 'documents');
} }
return $this; return $this;
} }
public function account() public function account()
{ {
return $this->belongsTo('App\Models\Account'); return $this->belongsTo('App\Models\Account');
@ -101,7 +106,7 @@ class Document extends EntityModel
{ {
return $this->belongsTo('App\Models\Invoice')->withTrashed(); return $this->belongsTo('App\Models\Invoice')->withTrashed();
} }
public function getDisk(){ public function getDisk(){
return Storage::disk(!empty($this->disk)?$this->disk:env('DOCUMENT_FILESYSTEM', 'documents')); return Storage::disk(!empty($this->disk)?$this->disk:env('DOCUMENT_FILESYSTEM', 'documents'));
} }
@ -110,19 +115,19 @@ class Document extends EntityModel
{ {
$this->attributes['disk'] = $value?$value:env('DOCUMENT_FILESYSTEM', 'documents'); $this->attributes['disk'] = $value?$value:env('DOCUMENT_FILESYSTEM', 'documents');
} }
public function getDirectUrl(){ public function getDirectUrl(){
return static::getDirectFileUrl($this->path, $this->getDisk()); return static::getDirectFileUrl($this->path, $this->getDisk());
} }
public function getDirectPreviewUrl(){ public function getDirectPreviewUrl(){
return $this->preview?static::getDirectFileUrl($this->preview, $this->getDisk(), true):null; return $this->preview?static::getDirectFileUrl($this->preview, $this->getDisk(), true):null;
} }
public static function getDirectFileUrl($path, $disk, $prioritizeSpeed = false){ public static function getDirectFileUrl($path, $disk, $prioritizeSpeed = false){
$adapter = $disk->getAdapter(); $adapter = $disk->getAdapter();
$fullPath = $adapter->applyPathPrefix($path); $fullPath = $adapter->applyPathPrefix($path);
if($adapter instanceof \League\Flysystem\AwsS3v3\AwsS3Adapter) { if($adapter instanceof \League\Flysystem\AwsS3v3\AwsS3Adapter) {
$client = $adapter->getClient(); $client = $adapter->getClient();
$command = $client->getCommand('GetObject', [ $command = $client->getCommand('GetObject', [
@ -136,12 +141,12 @@ class Document extends EntityModel
$secret = env('RACKSPACE_TEMP_URL_SECRET'); $secret = env('RACKSPACE_TEMP_URL_SECRET');
if($secret){ if($secret){
$object = $adapter->getContainer()->getObject($fullPath); $object = $adapter->getContainer()->getObject($fullPath);
if(env('RACKSPACE_TEMP_URL_SECRET_SET')){ if(env('RACKSPACE_TEMP_URL_SECRET_SET')){
// Go ahead and set the secret too // Go ahead and set the secret too
$object->getService()->getAccount()->setTempUrlSecret($secret); $object->getService()->getAccount()->setTempUrlSecret($secret);
} }
$url = $object->getUrl(); $url = $object->getUrl();
$expiry = strtotime('+10 minutes'); $expiry = strtotime('+10 minutes');
$urlPath = urldecode($url->getPath()); $urlPath = urldecode($url->getPath());
@ -150,64 +155,64 @@ class Document extends EntityModel
return sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry); return sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry);
} }
} }
return null; return null;
} }
public function getRaw(){ public function getRaw(){
$disk = $this->getDisk(); $disk = $this->getDisk();
return $disk->get($this->path); return $disk->get($this->path);
} }
public function getStream(){ public function getStream(){
$disk = $this->getDisk(); $disk = $this->getDisk();
return $disk->readStream($this->path); return $disk->readStream($this->path);
} }
public function getRawPreview(){ public function getRawPreview(){
$disk = $this->getDisk(); $disk = $this->getDisk();
return $disk->get($this->preview); return $disk->get($this->preview);
} }
public function getUrl(){ public function getUrl(){
return url('documents/'.$this->public_id.'/'.$this->name); return url('documents/'.$this->public_id.'/'.$this->name);
} }
public function getClientUrl($invitation){ public function getClientUrl($invitation){
return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name); return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name);
} }
public function isPDFEmbeddable(){ public function isPDFEmbeddable(){
return $this->type == 'jpeg' || $this->type == 'png' || $this->preview; return $this->type == 'jpeg' || $this->type == 'png' || $this->preview;
} }
public function getVFSJSUrl(){ public function getVFSJSUrl(){
if(!$this->isPDFEmbeddable())return null; if(!$this->isPDFEmbeddable())return null;
return url('documents/js/'.$this->public_id.'/'.$this->name.'.js'); return url('documents/js/'.$this->public_id.'/'.$this->name.'.js');
} }
public function getClientVFSJSUrl(){ public function getClientVFSJSUrl(){
if(!$this->isPDFEmbeddable())return null; if(!$this->isPDFEmbeddable())return null;
return url('client/documents/js/'.$this->public_id.'/'.$this->name.'.js'); return url('client/documents/js/'.$this->public_id.'/'.$this->name.'.js');
} }
public function getPreviewUrl(){ public function getPreviewUrl(){
return $this->preview?url('documents/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null; return $this->preview?url('documents/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null;
} }
public function toArray() public function toArray()
{ {
$array = parent::toArray(); $array = parent::toArray();
if(empty($this->visible) || in_array('url', $this->visible))$array['url'] = $this->getUrl(); if(empty($this->visible) || in_array('url', $this->visible))$array['url'] = $this->getUrl();
if(empty($this->visible) || in_array('preview_url', $this->visible))$array['preview_url'] = $this->getPreviewUrl(); if(empty($this->visible) || in_array('preview_url', $this->visible))$array['preview_url'] = $this->getPreviewUrl();
return $array; return $array;
} }
public function cloneDocument(){ public function cloneDocument(){
$document = Document::createNew($this); $document = Document::createNew($this);
$document->path = $this->path; $document->path = $this->path;
@ -219,7 +224,7 @@ class Document extends EntityModel
$document->size = $this->size; $document->size = $this->size;
$document->width = $this->width; $document->width = $this->width;
$document->height = $this->height; $document->height = $this->height;
return $document; return $document;
} }
} }
@ -230,11 +235,11 @@ Document::deleted(function ($document) {
->where('documents.path', '=', $document->path) ->where('documents.path', '=', $document->path)
->where('documents.disk', '=', $document->disk) ->where('documents.disk', '=', $document->disk)
->count(); ->count();
if(!$same_path_count){ if(!$same_path_count){
$document->getDisk()->delete($document->path); $document->getDisk()->delete($document->path);
} }
if($document->preview){ if($document->preview){
$same_preview_count = DB::table('documents') $same_preview_count = DB::table('documents')
->where('documents.account_id', '=', $document->account_id) ->where('documents.account_id', '=', $document->account_id)
@ -245,5 +250,5 @@ Document::deleted(function ($document) {
$document->getDisk()->delete($document->preview); $document->getDisk()->delete($document->preview);
} }
} }
}); });

View File

@ -30,7 +30,7 @@ class EntityModel extends Eloquent
} else { } else {
$lastEntity = $className::scope(false, $entity->account_id); $lastEntity = $className::scope(false, $entity->account_id);
} }
$lastEntity = $lastEntity->orderBy('public_id', 'DESC') $lastEntity = $lastEntity->orderBy('public_id', 'DESC')
->first(); ->first();
@ -86,6 +86,15 @@ class EntityModel extends Eloquent
return $query; return $query;
} }
public function scopeViewable($query)
{
if (Auth::check() && ! Auth::user()->hasPermission('view_all')) {
$query->where($this->getEntityType(). 's.user_id', '=', Auth::user()->id);
}
return $query;
}
public function scopeWithArchived($query) public function scopeWithArchived($query)
{ {
return $query->withTrashed()->where('is_deleted', '=', false); return $query->withTrashed()->where('is_deleted', '=', false);
@ -110,7 +119,7 @@ class EntityModel extends Eloquent
{ {
return 'App\\Ninja\\Transformers\\' . ucwords(Utils::toCamelCase($entityType)) . 'Transformer'; return 'App\\Ninja\\Transformers\\' . ucwords(Utils::toCamelCase($entityType)) . 'Transformer';
} }
public function setNullValues() public function setNullValues()
{ {
foreach ($this->fillable as $field) { foreach ($this->fillable as $field) {

View File

@ -29,9 +29,9 @@ class Invoice extends EntityModel implements BalanceAffecting
'tax_name1', 'tax_name1',
'tax_rate1', 'tax_rate1',
'tax_name2', 'tax_name2',
'tax_rate2', 'tax_rate2',
]; ];
protected $casts = [ protected $casts = [
'is_recurring' => 'boolean', 'is_recurring' => 'boolean',
'has_tasks' => 'boolean', 'has_tasks' => 'boolean',
@ -96,7 +96,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function affectsBalance() public function affectsBalance()
{ {
return !$this->is_quote && !$this->is_recurring; return $this->isType(INVOICE_TYPE_STANDARD) && !$this->is_recurring;
} }
public function getAdjustment() public function getAdjustment()
@ -139,7 +139,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getAmountPaid($calculate = false) public function getAmountPaid($calculate = false)
{ {
if ($this->is_quote || $this->is_recurring) { if ($this->isType(INVOICE_TYPE_QUOTE) || $this->is_recurring) {
return 0; return 0;
} }
@ -230,10 +230,23 @@ class Invoice extends EntityModel implements BalanceAffecting
public function scopeInvoices($query) public function scopeInvoices($query)
{ {
return $query->where('is_quote', '=', false) return $query->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false); ->where('is_recurring', '=', false);
} }
public function scopeInvoiceType($query, $typeId)
{
return $query->where('invoice_type_id', '=', $typeId);
}
public function isType($typeId) {
return $this->invoice_type_id == $typeId;
}
public function isQuote() {
return $this->isType(INVOICE_TYPE_QUOTE);
}
public function markInvitationsSent($notify = false) public function markInvitationsSent($notify = false)
{ {
foreach ($this->invitations as $invitation) { foreach ($this->invitations as $invitation) {
@ -256,7 +269,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return; return;
} }
if ($this->is_quote) { if ($this->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteInvitationWasEmailed($invitation)); event(new QuoteInvitationWasEmailed($invitation));
} else { } else {
event(new InvoiceInvitationWasEmailed($invitation)); event(new InvoiceInvitationWasEmailed($invitation));
@ -292,7 +305,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function markApproved() public function markApproved()
{ {
if ($this->is_quote) { if ($this->isType(INVOICE_TYPE_QUOTE)) {
$this->invoice_status_id = INVOICE_STATUS_APPROVED; $this->invoice_status_id = INVOICE_STATUS_APPROVED;
$this->save(); $this->save();
} }
@ -341,7 +354,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getEntityType() public function getEntityType()
{ {
return $this->is_quote ? ENTITY_QUOTE : ENTITY_INVOICE; return $this->isType(INVOICE_TYPE_QUOTE) ? ENTITY_QUOTE : ENTITY_INVOICE;
} }
public function isSent() public function isSent()
@ -416,7 +429,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'invoice_design_id', 'invoice_design_id',
'invoice_fonts', 'invoice_fonts',
'features', 'features',
'is_quote', 'invoice_type_id',
'custom_value1', 'custom_value1',
'custom_value2', 'custom_value2',
'custom_taxes1', 'custom_taxes1',
@ -515,12 +528,12 @@ class Invoice extends EntityModel implements BalanceAffecting
'name', 'name',
]); ]);
} }
foreach ($this->expenses as $expense) { foreach ($this->expenses as $expense) {
$expense->setVisible([ $expense->setVisible([
'documents', 'documents',
]); ]);
foreach ($expense->documents as $document) { foreach ($expense->documents as $document) {
$document->setVisible([ $document->setVisible([
'public_id', 'public_id',
@ -579,12 +592,12 @@ class Invoice extends EntityModel implements BalanceAffecting
return $schedule[1]->getStart(); return $schedule[1]->getStart();
} }
public function getDueDate($invoice_date = null){ public function getDueDate($invoice_date = null){
if(!$this->is_recurring) { if(!$this->is_recurring) {
return $this->due_date ? $this->due_date : null; return $this->due_date ? $this->due_date : null;
} }
else{ else{
$now = time(); $now = time();
if($invoice_date) { if($invoice_date) {
// If $invoice_date is specified, all calculations are based on that date // If $invoice_date is specified, all calculations are based on that date
@ -598,7 +611,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$now = $invoice_date->getTimestamp(); $now = $invoice_date->getTimestamp();
} }
} }
if($this->due_date && $this->due_date != '0000-00-00'){ if($this->due_date && $this->due_date != '0000-00-00'){
// This is a recurring invoice; we're using a custom format here. // This is a recurring invoice; we're using a custom format here.
// The year is always 1998; January is 1st, 2nd, last day of the month. // The year is always 1998; January is 1st, 2nd, last day of the month.
@ -607,7 +620,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$monthVal = (int)date('n', $dueDateVal); $monthVal = (int)date('n', $dueDateVal);
$dayVal = (int)date('j', $dueDateVal); $dayVal = (int)date('j', $dueDateVal);
$dueDate = false; $dueDate = false;
if($monthVal == 1) {// January; day of month if($monthVal == 1) {// January; day of month
$currentDay = (int)date('j', $now); $currentDay = (int)date('j', $now);
$lastDayOfMonth = (int)date('t', $now); $lastDayOfMonth = (int)date('t', $now);
@ -634,7 +647,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if($dueDay > $lastDayOfMonth){ if($dueDay > $lastDayOfMonth){
// No later than the end of the month // No later than the end of the month
$dueDay = $lastDayOfMonth; $dueDay = $lastDayOfMonth;
} }
} }
$dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear); $dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear);
@ -663,7 +676,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return date('Y-m-d', strtotime('+'.$days.' day', $now)); return date('Y-m-d', strtotime('+'.$days.' day', $now));
} }
} }
// Couldn't calculate one // Couldn't calculate one
return null; return null;
} }
@ -681,11 +694,11 @@ class Invoice extends EntityModel implements BalanceAffecting
$dateStart = $date->getStart(); $dateStart = $date->getStart();
$date = $this->account->formatDate($dateStart); $date = $this->account->formatDate($dateStart);
$dueDate = $this->getDueDate($dateStart); $dueDate = $this->getDueDate($dateStart);
if($dueDate) { if($dueDate) {
$date .= ' <small>(' . trans('texts.due') . ' ' . $this->account->formatDate($dueDate) . ')</small>'; $date .= ' <small>(' . trans('texts.due') . ' ' . $this->account->formatDate($dueDate) . ')</small>';
} }
$dates[] = $date; $dates[] = $date;
} }
@ -799,16 +812,16 @@ class Invoice extends EntityModel implements BalanceAffecting
$invitation = $this->invitations[0]; $invitation = $this->invitations[0];
$link = $invitation->getLink('view', true); $link = $invitation->getLink('view', true);
$key = env('PHANTOMJS_CLOUD_KEY'); $key = env('PHANTOMJS_CLOUD_KEY');
if (Utils::isNinjaDev()) { if (Utils::isNinjaDev()) {
$link = env('TEST_LINK'); $link = env('TEST_LINK');
} }
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D"; $url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$pdfString = file_get_contents($url); $pdfString = file_get_contents($url);
$pdfString = strip_tags($pdfString); $pdfString = strip_tags($pdfString);
if ( ! $pdfString || strlen($pdfString) < 200) { if ( ! $pdfString || strlen($pdfString) < 200) {
Utils::logError("PhantomJSCloud - failed to create pdf: {$pdfString}"); Utils::logError("PhantomJSCloud - failed to create pdf: {$pdfString}");
return false; return false;
@ -861,14 +874,14 @@ class Invoice extends EntityModel implements BalanceAffecting
return $total; return $total;
} }
// if $calculatePaid is true we'll loop through each payment to // if $calculatePaid is true we'll loop through each payment to
// determine the sum, otherwise we'll use the cached paid_to_date amount // determine the sum, otherwise we'll use the cached paid_to_date amount
public function getTaxes($calculatePaid = false) public function getTaxes($calculatePaid = false)
{ {
$taxes = []; $taxes = [];
$taxable = $this->getTaxable(); $taxable = $this->getTaxable();
$paidAmount = $this->getAmountPaid($calculatePaid); $paidAmount = $this->getAmountPaid($calculatePaid);
if ($this->tax_name1) { if ($this->tax_name1) {
$invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2); $invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2);
$invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
@ -883,7 +896,7 @@ class Invoice extends EntityModel implements BalanceAffecting
foreach ($this->invoice_items as $invoiceItem) { foreach ($this->invoice_items as $invoiceItem) {
$itemTaxAmount = $this->getItemTaxable($invoiceItem, $taxable); $itemTaxAmount = $this->getItemTaxable($invoiceItem, $taxable);
if ($invoiceItem->tax_name1) { if ($invoiceItem->tax_name1) {
$itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2); $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2);
$itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
@ -896,20 +909,20 @@ class Invoice extends EntityModel implements BalanceAffecting
$this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount); $this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount);
} }
} }
return $taxes; return $taxes;
} }
private function calculateTax(&$taxes, $name, $rate, $amount, $paid) private function calculateTax(&$taxes, $name, $rate, $amount, $paid)
{ {
if ( ! $amount) { if ( ! $amount) {
return; return;
} }
$amount = round($amount, 2); $amount = round($amount, 2);
$paid = round($paid, 2); $paid = round($paid, 2);
$key = $rate . ' ' . $name; $key = $rate . ' ' . $name;
if ( ! isset($taxes[$key])) { if ( ! isset($taxes[$key])) {
$taxes[$key] = [ $taxes[$key] = [
'name' => $name, 'name' => $name,
@ -920,14 +933,14 @@ class Invoice extends EntityModel implements BalanceAffecting
} }
$taxes[$key]['amount'] += $amount; $taxes[$key]['amount'] += $amount;
$taxes[$key]['paid'] += $paid; $taxes[$key]['paid'] += $paid;
} }
public function hasDocuments(){ public function hasDocuments(){
if(count($this->documents))return true; if(count($this->documents))return true;
return $this->hasExpenseDocuments(); return $this->hasExpenseDocuments();
} }
public function hasExpenseDocuments(){ public function hasExpenseDocuments(){
foreach($this->expenses as $expense){ foreach($this->expenses as $expense){
if(count($expense->documents))return true; if(count($expense->documents))return true;
@ -957,7 +970,7 @@ Invoice::creating(function ($invoice) {
}); });
Invoice::created(function ($invoice) { Invoice::created(function ($invoice) {
if ($invoice->is_quote) { if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasCreated($invoice)); event(new QuoteWasCreated($invoice));
} else { } else {
event(new InvoiceWasCreated($invoice)); event(new InvoiceWasCreated($invoice));
@ -965,7 +978,7 @@ Invoice::created(function ($invoice) {
}); });
Invoice::updating(function ($invoice) { Invoice::updating(function ($invoice) {
if ($invoice->is_quote) { if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasUpdated($invoice)); event(new QuoteWasUpdated($invoice));
} else { } else {
event(new InvoiceWasUpdated($invoice)); event(new InvoiceWasUpdated($invoice));

View File

@ -16,6 +16,24 @@ class Product extends EntityModel
'default_tax_rate_id', 'default_tax_rate_id',
]; ];
public static function getImportColumns()
{
return [
'product_key',
'notes',
'cost',
];
}
public static function getImportMap()
{
return [
'product|item' => 'product_key',
'notes|description|details' => 'notes',
'cost|amount|price' => 'cost',
];
}
public function getEntityType() public function getEntityType()
{ {
return ENTITY_PRODUCT; return ENTITY_PRODUCT;

View File

@ -15,21 +15,38 @@ class BaseTransformer extends TransformerAbstract
protected function hasClient($name) protected function hasClient($name)
{ {
$name = strtolower($name); $name = trim(strtolower($name));
return isset($this->maps[ENTITY_CLIENT][$name]); return isset($this->maps[ENTITY_CLIENT][$name]);
} }
protected function hasProduct($key)
{
$key = trim(strtolower($key));
return isset($this->maps[ENTITY_PRODUCT][$key]);
}
protected function getString($data, $field) protected function getString($data, $field)
{ {
return (isset($data->$field) && $data->$field) ? $data->$field : ''; return (isset($data->$field) && $data->$field) ? $data->$field : '';
} }
protected function getNumber($data, $field)
{
return (isset($data->$field) && $data->$field) ? $data->$field : 0;
}
protected function getClientId($name) protected function getClientId($name)
{ {
$name = strtolower($name); $name = strtolower($name);
return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null; return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null;
} }
protected function getProductId($name)
{
$name = strtolower($name);
return isset($this->maps[ENTITY_PRODUCT][$name]) ? $this->maps[ENTITY_PRODUCT][$name] : null;
}
protected function getCountryId($name) protected function getCountryId($name)
{ {
$name = strtolower($name); $name = strtolower($name);
@ -53,7 +70,7 @@ class BaseTransformer extends TransformerAbstract
if ( ! $date instanceof DateTime) { if ( ! $date instanceof DateTime) {
$date = DateTime::createFromFormat($format, $date); $date = DateTime::createFromFormat($format, $date);
} }
return $date ? $date->format('Y-m-d') : null; return $date ? $date->format('Y-m-d') : null;
} }
@ -87,11 +104,11 @@ class BaseTransformer extends TransformerAbstract
return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null; return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null;
} }
protected function getVendorId($name) protected function getVendorId($name)
{ {
$name = strtolower($name); $name = strtolower($name);
return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null;
} }
} }

View File

@ -0,0 +1,22 @@
<?php namespace App\Ninja\Import\CSV;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
class ProductTransformer extends BaseTransformer
{
public function transform($data)
{
if (empty($data->product_key) || $this->hasProduct($data->product_key)) {
return false;
}
return new Item($data, function ($data) {
return [
'product_key' => $this->getString($data, 'product_key'),
'notes' => $this->getString($data, 'notes'),
'cost' => $this->getNumber($data, 'cost'),
];
});
}
}

View File

@ -60,7 +60,7 @@ class ContactMailer extends Mailer
$sent = false; $sent = false;
if ($account->attatchPDF() && !$pdfString) { if ($account->attachPDF() && !$pdfString) {
$pdfString = $invoice->getPDFString(); $pdfString = $invoice->getPDFString();
} }
@ -97,7 +97,7 @@ class ContactMailer extends Mailer
$account->loadLocalizationSettings(); $account->loadLocalizationSettings();
if ($sent === true) { if ($sent === true) {
if ($invoice->is_quote) { if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasEmailed($invoice)); event(new QuoteWasEmailed($invoice));
} else { } else {
event(new InvoiceWasEmailed($invoice)); event(new InvoiceWasEmailed($invoice));
@ -176,7 +176,7 @@ class ContactMailer extends Mailer
'documents' => $documentStrings, 'documents' => $documentStrings,
]; ];
if ($account->attatchPDF()) { if ($account->attachPDF()) {
$data['pdfString'] = $pdfString; $data['pdfString'] = $pdfString;
$data['pdfFileName'] = $invoice->getFileName(); $data['pdfFileName'] = $invoice->getFileName();
} }
@ -255,7 +255,7 @@ class ContactMailer extends Mailer
'entityType' => ENTITY_INVOICE, 'entityType' => ENTITY_INVOICE,
]; ];
if ($account->attatchPDF()) { if ($account->attachPDF()) {
$data['pdfString'] = $invoice->getPDFString(); $data['pdfString'] = $invoice->getPDFString();
$data['pdfFileName'] = $invoice->getFileName(); $data['pdfFileName'] = $invoice->getFileName();
} }

View File

@ -19,7 +19,7 @@ class InvoicePresenter extends EntityPresenter {
{ {
if ($this->entity->partial > 0) { if ($this->entity->partial > 0) {
return 'partial_due'; return 'partial_due';
} elseif ($this->entity->is_quote) { } elseif ($this->entity->isType(INVOICE_TYPE_QUOTE)) {
return 'total'; return 'total';
} else { } else {
return 'balance_due'; return 'balance_due';

View File

@ -57,43 +57,49 @@ class DocumentRepository extends BaseRepository
return $query; return $query;
} }
public function upload($uploaded, &$doc_array=null) public function upload($data, &$doc_array=null)
{ {
$uploaded = $data['file'];
$extension = strtolower($uploaded->getClientOriginalExtension()); $extension = strtolower($uploaded->getClientOriginalExtension());
if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){
$documentType = Document::$extraExtensions[$extension]; $documentType = Document::$extraExtensions[$extension];
} }
else{ else{
$documentType = $extension; $documentType = $extension;
} }
if(empty(Document::$types[$documentType])){ if(empty(Document::$types[$documentType])){
return 'Unsupported file type'; return 'Unsupported file type';
} }
$documentTypeData = Document::$types[$documentType]; $documentTypeData = Document::$types[$documentType];
$filePath = $uploaded->path(); $filePath = $uploaded->path();
$name = $uploaded->getClientOriginalName(); $name = $uploaded->getClientOriginalName();
$size = filesize($filePath); $size = filesize($filePath);
if($size/1000 > MAX_DOCUMENT_SIZE){ if($size/1000 > MAX_DOCUMENT_SIZE){
return 'File too large'; return 'File too large';
} }
// don't allow a document to be linked to both an invoice and an expense
if (array_get($data, 'invoice_id') && array_get($data, 'expense_id')) {
unset($data['expense_id']);
}
$hash = sha1_file($filePath); $hash = sha1_file($filePath);
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType; $filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType;
$document = Document::createNew(); $document = Document::createNew();
$document->fill($data);
$disk = $document->getDisk(); $disk = $document->getDisk();
if(!$disk->exists($filename)){// Have we already stored the same file if(!$disk->exists($filename)){// Have we already stored the same file
$stream = fopen($filePath, 'r'); $stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($filename, $stream, ['mimetype'=>$documentTypeData['mime']]); $disk->getDriver()->putStream($filename, $stream, ['mimetype'=>$documentTypeData['mime']]);
fclose($stream); fclose($stream);
} }
// This is an image; check if we need to create a preview // This is an image; check if we need to create a preview
if(in_array($documentType, array('jpeg','png','gif','bmp','tiff','psd'))){ if(in_array($documentType, array('jpeg','png','gif','bmp','tiff','psd'))){
$makePreview = false; $makePreview = false;
@ -105,32 +111,32 @@ class DocumentRepository extends BaseRepository
// Needs to be converted // Needs to be converted
$makePreview = true; $makePreview = true;
} else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){ } else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){
$makePreview = true; $makePreview = true;
} }
if(in_array($documentType,array('bmp','tiff','psd'))){ if(in_array($documentType,array('bmp','tiff','psd'))){
if(!class_exists('Imagick')){ if(!class_exists('Imagick')){
// Cant't read this // Cant't read this
$makePreview = false; $makePreview = false;
} else { } else {
$imgManagerConfig['driver'] = 'imagick'; $imgManagerConfig['driver'] = 'imagick';
} }
} }
if($makePreview){ if($makePreview){
$previewType = 'jpeg'; $previewType = 'jpeg';
if(in_array($documentType, array('png','gif','tiff','psd'))){ if(in_array($documentType, array('png','gif','tiff','psd'))){
// Has transparency // Has transparency
$previewType = 'png'; $previewType = 'png';
} }
$document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType.'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType; $document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType.'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType;
if(!$disk->exists($document->preview)){ if(!$disk->exists($document->preview)){
// We haven't created a preview yet // We haven't created a preview yet
$imgManager = new ImageManager($imgManagerConfig); $imgManager = new ImageManager($imgManagerConfig);
$img = $imgManager->make($filePath); $img = $imgManager->make($filePath);
if($width <= DOCUMENT_PREVIEW_SIZE && $height <= DOCUMENT_PREVIEW_SIZE){ if($width <= DOCUMENT_PREVIEW_SIZE && $height <= DOCUMENT_PREVIEW_SIZE){
$previewWidth = $width; $previewWidth = $width;
$previewHeight = $height; $previewHeight = $height;
@ -141,9 +147,9 @@ class DocumentRepository extends BaseRepository
$previewHeight = DOCUMENT_PREVIEW_SIZE; $previewHeight = DOCUMENT_PREVIEW_SIZE;
$previewWidth = $width * DOCUMENT_PREVIEW_SIZE / $height; $previewWidth = $width * DOCUMENT_PREVIEW_SIZE / $height;
} }
$img->resize($previewWidth, $previewHeight); $img->resize($previewWidth, $previewHeight);
$previewContent = (string) $img->encode($previewType); $previewContent = (string) $img->encode($previewType);
$disk->put($document->preview, $previewContent); $disk->put($document->preview, $previewContent);
$base64 = base64_encode($previewContent); $base64 = base64_encode($previewContent);
@ -153,23 +159,23 @@ class DocumentRepository extends BaseRepository
} }
}else{ }else{
$base64 = base64_encode(file_get_contents($filePath)); $base64 = base64_encode(file_get_contents($filePath));
} }
} }
$document->path = $filename; $document->path = $filename;
$document->type = $documentType; $document->type = $documentType;
$document->size = $size; $document->size = $size;
$document->hash = $hash; $document->hash = $hash;
$document->name = substr($name, -255); $document->name = substr($name, -255);
if(!empty($imageSize)){ if(!empty($imageSize)){
$document->width = $imageSize[0]; $document->width = $imageSize[0];
$document->height = $imageSize[1]; $document->height = $imageSize[1];
} }
$document->save(); $document->save();
$doc_array = $document->toArray(); $doc_array = $document->toArray();
if(!empty($base64)){ if(!empty($base64)){
$mime = Document::$types[!empty($previewType)?$previewType:$documentType]['mime']; $mime = Document::$types[!empty($previewType)?$previewType:$documentType]['mime'];
$doc_array['base64'] = 'data:'.$mime.';base64,'.$base64; $doc_array['base64'] = 'data:'.$mime.';base64,'.$base64;
@ -177,10 +183,10 @@ class DocumentRepository extends BaseRepository
return $document; return $document;
} }
public function getClientDatatable($contactId, $entityType, $search) public function getClientDatatable($contactId, $entityType, $search)
{ {
$query = DB::table('invitations') $query = DB::table('invitations')
->join('accounts', 'accounts.id', '=', 'invitations.account_id') ->join('accounts', 'accounts.id', '=', 'invitations.account_id')
->join('invoices', 'invoices.id', '=', 'invitations.invoice_id') ->join('invoices', 'invoices.id', '=', 'invitations.invoice_id')
@ -192,7 +198,7 @@ class DocumentRepository extends BaseRepository
->where('clients.deleted_at', '=', null) ->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false) ->where('invoices.is_recurring', '=', false)
// This needs to be a setting to also hide the activity on the dashboard page // This needs to be a setting to also hide the activity on the dashboard page
//->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT)
->select( ->select(
'invitations.invitation_key', 'invitations.invitation_key',
'invoices.invoice_number', 'invoices.invoice_number',
@ -205,22 +211,22 @@ class DocumentRepository extends BaseRepository
$table = \Datatable::query($query) $table = \Datatable::query($query)
->addColumn('invoice_number', function ($model) { ->addColumn('invoice_number', function ($model) {
return link_to( return link_to(
'/view/'.$model->invitation_key, '/view/'.$model->invitation_key,
$model->invoice_number $model->invoice_number
)->toHtml(); )->toHtml();
}) })
->addColumn('name', function ($model) { ->addColumn('name', function ($model) {
return link_to( return link_to(
'/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name, '/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name,
$model->name, $model->name,
['target'=>'_blank'] ['target'=>'_blank']
)->toHtml(); )->toHtml();
}) })
->addColumn('document_date', function ($model) { ->addColumn('document_date', function ($model) {
return Utils::fromSqlDate($model->created_at); return Utils::dateToString($model->created_at);
}) })
->addColumn('document_size', function ($model) { ->addColumn('document_size', function ($model) {
return Form::human_filesize($model->size); return Form::human_filesize($model->size);
}); });
return $table->make(); return $table->make();

View File

@ -148,33 +148,17 @@ class ExpenseRepository extends BaseRepository
// Documents // Documents
$document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();; $document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();;
foreach ($document_ids as $document_id){ foreach ($document_ids as $document_id){
$document = Document::scope($document_id)->first(); // check document completed upload before user submitted form
if($document && Auth::user()->can('edit', $document)){ if ($document_id) {
$document->invoice_id = null; $document = Document::scope($document_id)->first();
$document->expense_id = $expense->id; if($document && Auth::user()->can('edit', $document)){
$document->save(); $document->invoice_id = null;
} $document->expense_id = $expense->id;
} $document->save();
if(!empty($input['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){
// Fallback upload
$doc_errors = array();
foreach($input['documents'] as $upload){
$result = $this->documentRepo->upload($upload);
if(is_string($result)){
$doc_errors[] = $result;
}
else{
$result->expense_id = $expense->id;
$result->save();
$document_ids[] = $result->public_id;
} }
} }
if(!empty($doc_errors)){
Session::flash('error', implode('<br>',array_map('htmlentities',$doc_errors)));
}
} }
// prevent loading all of the documents if we don't have to // prevent loading all of the documents if we don't have to
if ( ! $expense->wasRecentlyCreated) { if ( ! $expense->wasRecentlyCreated) {
foreach ($expense->documents as $document){ foreach ($expense->documents as $document){

View File

@ -32,9 +32,9 @@ class InvoiceRepository extends BaseRepository
public function all() public function all()
{ {
return Invoice::scope() return Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status') ->with('user', 'client.contacts', 'invoice_status')
->withTrashed() ->withTrashed()
->where('is_quote', '=', false)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->get(); ->get();
} }
@ -106,7 +106,7 @@ class InvoiceRepository extends BaseRepository
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id') ->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', $accountId) ->where('invoices.account_id', '=', $accountId)
->where('invoices.is_quote', '=', false) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('contacts.deleted_at', '=', null) ->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true) ->where('invoices.is_recurring', '=', true)
->where('contacts.is_primary', '=', true) ->where('contacts.is_primary', '=', true)
@ -156,7 +156,7 @@ class InvoiceRepository extends BaseRepository
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->where('invitations.contact_id', '=', $contactId) ->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null) ->where('invitations.deleted_at', '=', null)
->where('invoices.is_quote', '=', false) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null) ->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true) ->where('invoices.is_recurring', '=', true)
@ -203,14 +203,14 @@ class InvoiceRepository extends BaseRepository
->join('contacts', 'contacts.client_id', '=', 'clients.id') ->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invitations.contact_id', '=', $contactId) ->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null) ->where('invitations.deleted_at', '=', null)
->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE) ->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD)
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null) ->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null) ->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true) ->where('contacts.is_primary', '=', true)
->where('invoices.is_recurring', '=', false) ->where('invoices.is_recurring', '=', false)
// This needs to be a setting to also hide the activity on the dashboard page // This needs to be a setting to also hide the activity on the dashboard page
//->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT)
->select( ->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -287,7 +287,7 @@ class InvoiceRepository extends BaseRepository
$account->invoice_footer = trim($data['invoice_footer']); $account->invoice_footer = trim($data['invoice_footer']);
} }
$account->save(); $account->save();
} }
if (isset($data['invoice_number']) && !$invoice->is_recurring) { if (isset($data['invoice_number']) && !$invoice->is_recurring) {
$invoice->invoice_number = trim($data['invoice_number']); $invoice->invoice_number = trim($data['invoice_number']);
@ -329,7 +329,7 @@ class InvoiceRepository extends BaseRepository
if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) { if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) {
$invoice->auto_bill = AUTO_BILL_OFF; $invoice->auto_bill = AUTO_BILL_OFF;
} }
if (isset($data['recurring_due_date'])) { if (isset($data['recurring_due_date'])) {
$invoice->due_date = $data['recurring_due_date']; $invoice->due_date = $data['recurring_due_date'];
} elseif (isset($data['due_date'])) { } elseif (isset($data['due_date'])) {
@ -351,7 +351,7 @@ class InvoiceRepository extends BaseRepository
} else { } else {
$invoice->terms = ''; $invoice->terms = '';
} }
$invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : ''); $invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : '');
$invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null; $invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null;
@ -370,8 +370,8 @@ class InvoiceRepository extends BaseRepository
// provide backwards compatability // provide backwards compatability
if (isset($data['tax_name']) && isset($data['tax_rate'])) { if (isset($data['tax_name']) && isset($data['tax_rate'])) {
$data['tax_name1'] = $data['tax_name']; $data['tax_name1'] = $data['tax_name'];
$data['tax_rate1'] = $data['tax_rate']; $data['tax_rate1'] = $data['tax_rate'];
} }
$total = 0; $total = 0;
@ -405,11 +405,11 @@ class InvoiceRepository extends BaseRepository
} }
if (isset($item['tax_rate1']) && Utils::parseFloat($item['tax_rate1']) > 0) { if (isset($item['tax_rate1']) && Utils::parseFloat($item['tax_rate1']) > 0) {
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']); $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']);
$itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2);
} }
if (isset($item['tax_rate2']) && Utils::parseFloat($item['tax_rate2']) > 0) { if (isset($item['tax_rate2']) && Utils::parseFloat($item['tax_rate2']) > 0) {
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']); $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']);
$itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2);
} }
} }
@ -453,7 +453,7 @@ class InvoiceRepository extends BaseRepository
$taxAmount1 = round($total * $invoice->tax_rate1 / 100, 2); $taxAmount1 = round($total * $invoice->tax_rate1 / 100, 2);
$taxAmount2 = round($total * $invoice->tax_rate2 / 100, 2); $taxAmount2 = round($total * $invoice->tax_rate2 / 100, 2);
$total = round($total + $taxAmount1 + $taxAmount2, 2); $total = round($total + $taxAmount1 + $taxAmount2, 2);
$total += $itemTax; $total += $itemTax;
// custom fields not charged taxes // custom fields not charged taxes
@ -476,43 +476,24 @@ class InvoiceRepository extends BaseRepository
if ($publicId) { if ($publicId) {
$invoice->invoice_items()->forceDelete(); $invoice->invoice_items()->forceDelete();
} }
$document_ids = !empty($data['document_ids'])?array_map('intval', $data['document_ids']):array();; $document_ids = !empty($data['document_ids'])?array_map('intval', $data['document_ids']):array();;
foreach ($document_ids as $document_id){ foreach ($document_ids as $document_id){
$document = Document::scope($document_id)->first(); $document = Document::scope($document_id)->first();
if($document && Auth::user()->can('edit', $document)){ if($document && Auth::user()->can('edit', $document)){
if($document->invoice_id && $document->invoice_id != $invoice->id){ if($document->invoice_id && $document->invoice_id != $invoice->id){
// From a clone // From a clone
$document = $document->cloneDocument(); $document = $document->cloneDocument();
$document_ids[] = $document->public_id;// Don't remove this document $document_ids[] = $document->public_id;// Don't remove this document
} }
$document->invoice_id = $invoice->id; $document->invoice_id = $invoice->id;
$document->expense_id = null; $document->expense_id = null;
$document->save(); $document->save();
} }
} }
if(!empty($data['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){
// Fallback upload
$doc_errors = array();
foreach($data['documents'] as $upload){
$result = $this->documentRepo->upload($upload);
if(is_string($result)){
$doc_errors[] = $result;
}
else{
$result->invoice_id = $invoice->id;
$result->save();
$document_ids[] = $result->public_id;
}
}
if(!empty($doc_errors)){
Session::flash('error', implode('<br>',array_map('htmlentities',$doc_errors)));
}
}
foreach ($invoice->documents as $document){ foreach ($invoice->documents as $document){
if(!in_array($document->public_id, $document_ids)){ if(!in_array($document->public_id, $document_ids)){
// Removed // Removed
@ -586,12 +567,12 @@ class InvoiceRepository extends BaseRepository
// provide backwards compatability // provide backwards compatability
if (isset($item['tax_name']) && isset($item['tax_rate'])) { if (isset($item['tax_name']) && isset($item['tax_rate'])) {
$item['tax_name1'] = $item['tax_name']; $item['tax_name1'] = $item['tax_name'];
$item['tax_rate1'] = $item['tax_rate']; $item['tax_rate1'] = $item['tax_rate'];
} }
$invoiceItem->fill($item); $invoiceItem->fill($item);
$invoice->invoice_items()->save($invoiceItem); $invoice->invoice_items()->save($invoiceItem);
} }
@ -642,7 +623,7 @@ class InvoiceRepository extends BaseRepository
'tax_name2', 'tax_name2',
'tax_rate2', 'tax_rate2',
'amount', 'amount',
'is_quote', 'invoice_type_id',
'custom_value1', 'custom_value1',
'custom_value2', 'custom_value2',
'custom_taxes1', 'custom_taxes1',
@ -654,7 +635,7 @@ class InvoiceRepository extends BaseRepository
} }
if ($quotePublicId) { if ($quotePublicId) {
$clone->is_quote = false; $clone->invoice_type_id = INVOICE_TYPE_STANDARD;
$clone->quote_id = $quotePublicId; $clone->quote_id = $quotePublicId;
} }
@ -675,9 +656,9 @@ class InvoiceRepository extends BaseRepository
'cost', 'cost',
'qty', 'qty',
'tax_name1', 'tax_name1',
'tax_rate1', 'tax_rate1',
'tax_name2', 'tax_name2',
'tax_rate2', 'tax_rate2',
] as $field) { ] as $field) {
$cloneItem->$field = $item->$field; $cloneItem->$field = $item->$field;
} }
@ -686,7 +667,7 @@ class InvoiceRepository extends BaseRepository
} }
foreach ($invoice->documents as $document) { foreach ($invoice->documents as $document) {
$cloneDocument = $document->cloneDocument(); $cloneDocument = $document->cloneDocument();
$invoice->documents()->save($cloneDocument); $invoice->documents()->save($cloneDocument);
} }
@ -731,8 +712,8 @@ class InvoiceRepository extends BaseRepository
public function findOpenInvoices($clientId) public function findOpenInvoices($clientId)
{ {
return Invoice::scope() return Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->whereClientId($clientId) ->whereClientId($clientId)
->whereIsQuote(false)
->whereIsRecurring(false) ->whereIsRecurring(false)
->whereDeletedAt(null) ->whereDeletedAt(null)
->whereHasTasks(true) ->whereHasTasks(true)
@ -760,7 +741,7 @@ class InvoiceRepository extends BaseRepository
$invoice = Invoice::createNew($recurInvoice); $invoice = Invoice::createNew($recurInvoice);
$invoice->client_id = $recurInvoice->client_id; $invoice->client_id = $recurInvoice->client_id;
$invoice->recurring_invoice_id = $recurInvoice->id; $invoice->recurring_invoice_id = $recurInvoice->id;
$invoice->invoice_number = $recurInvoice->account->recurring_invoice_number_prefix . $recurInvoice->account->getNextInvoiceNumber($recurInvoice); $invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber($invoice);
$invoice->amount = $recurInvoice->amount; $invoice->amount = $recurInvoice->amount;
$invoice->balance = $recurInvoice->amount; $invoice->balance = $recurInvoice->amount;
$invoice->invoice_date = date_create()->format('Y-m-d'); $invoice->invoice_date = date_create()->format('Y-m-d');
@ -839,9 +820,9 @@ class InvoiceRepository extends BaseRepository
} }
$sql = implode(' OR ', $dates); $sql = implode(' OR ', $dates);
$invoices = Invoice::whereAccountId($account->id) $invoices = Invoice::invoiceType(INVOICE_TYPE_STANDARD)
->whereAccountId($account->id)
->where('balance', '>', 0) ->where('balance', '>', 0)
->where('is_quote', '=', false)
->where('is_recurring', '=', false) ->where('is_recurring', '=', false)
->whereRaw('('.$sql.')') ->whereRaw('('.$sql.')')
->get(); ->get();

View File

@ -11,6 +11,13 @@ class ProductRepository extends BaseRepository
return 'App\Models\Product'; return 'App\Models\Product';
} }
public function all()
{
return Product::scope()
->withTrashed()
->get();
}
public function find($accountId) public function find($accountId)
{ {
return DB::table('products') return DB::table('products')
@ -30,11 +37,11 @@ class ProductRepository extends BaseRepository
'products.deleted_at' 'products.deleted_at'
); );
} }
public function save($data, $product = null) public function save($data, $product = null)
{ {
$publicId = isset($data['public_id']) ? $data['public_id'] : false; $publicId = isset($data['public_id']) ? $data['public_id'] : false;
if ($product) { if ($product) {
// do nothing // do nothing
} elseif ($publicId) { } elseif ($publicId) {
@ -50,4 +57,4 @@ class ProductRepository extends BaseRepository
return $product; return $product;
} }
} }

View File

@ -14,6 +14,7 @@ class DocumentTransformer extends EntityTransformer
'type' => $document->type, 'type' => $document->type,
'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null, 'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null,
'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null, 'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null,
'updated_at' => $this->getTimestamp($document->updated_at),
]); ]);
} }
} }

View File

@ -34,7 +34,7 @@ class InvoiceTransformer extends EntityTransformer
public function __construct($account = null, $serializer = null, $client = null) public function __construct($account = null, $serializer = null, $client = null)
{ {
parent::__construct($account, $serializer); parent::__construct($account, $serializer);
$this->client = $client; $this->client = $client;
} }
@ -93,7 +93,7 @@ class InvoiceTransformer extends EntityTransformer
'terms' => $invoice->terms, 'terms' => $invoice->terms,
'public_notes' => $invoice->public_notes, 'public_notes' => $invoice->public_notes,
'is_deleted' => (bool) $invoice->is_deleted, 'is_deleted' => (bool) $invoice->is_deleted,
'is_quote' => (bool) $invoice->is_quote, 'invoice_type_id' => (int) $invoice->invoice_type_id,
'is_recurring' => (bool) $invoice->is_recurring, 'is_recurring' => (bool) $invoice->is_recurring,
'frequency_id' => (int) $invoice->frequency_id, 'frequency_id' => (int) $invoice->frequency_id,
'start_date' => $invoice->start_date, 'start_date' => $invoice->start_date,
@ -119,6 +119,7 @@ class InvoiceTransformer extends EntityTransformer
'quote_invoice_id' => (int) $invoice->quote_invoice_id, 'quote_invoice_id' => (int) $invoice->quote_invoice_id,
'custom_text_value1' => $invoice->custom_text_value1, 'custom_text_value1' => $invoice->custom_text_value1,
'custom_text_value2' => $invoice->custom_text_value2, 'custom_text_value2' => $invoice->custom_text_value2,
'is_quote' => (bool) $invoice->isType(INVOICE_TYPE_QUOTE), // Temp to support mobile app
]); ]);
} }
} }

View File

@ -29,8 +29,8 @@ class AppServiceProvider extends ServiceProvider {
else{ else{
$contents = $image; $contents = $image;
} }
return 'data:image/jpeg;base64,' . base64_encode($contents); return 'data:image/jpeg;base64,' . base64_encode($contents);
}); });
Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') { Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') {
@ -58,11 +58,11 @@ class AppServiceProvider extends ServiceProvider {
$str = '<li class="dropdown '.$class.'"> $str = '<li class="dropdown '.$class.'">
<a href="'.URL::to($types).'" class="dropdown-toggle">'.trans("texts.$types").'</a>'; <a href="'.URL::to($types).'" class="dropdown-toggle">'.trans("texts.$types").'</a>';
$items = []; $items = [];
if($user->can('create', $type))$items[] = '<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>'; if($user->can('create', $type))$items[] = '<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>';
if ($type == ENTITY_INVOICE) { if ($type == ENTITY_INVOICE) {
if(!empty($items))$items[] = '<li class="divider"></li>'; if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('recurring_invoices').'">'.trans("texts.recurring_invoices").'</a></li>'; $items[] = '<li><a href="'.URL::to('recurring_invoices').'">'.trans("texts.recurring_invoices").'</a></li>';
@ -81,7 +81,7 @@ class AppServiceProvider extends ServiceProvider {
$items[] = '<li><a href="'.URL::to('vendors').'">'.trans("texts.vendors").'</a></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($user->can('create', ENTITY_VENDOR))$items[] = '<li><a href="'.URL::to('vendors/create').'">'.trans("texts.new_vendor").'</a></li>';
} }
if(!empty($items)){ if(!empty($items)){
$str.= '<ul class="dropdown-menu" id="menu1">'.implode($items).'</ul>'; $str.= '<ul class="dropdown-menu" id="menu1">'.implode($items).'</ul>';
} }
@ -157,14 +157,14 @@ class AppServiceProvider extends ServiceProvider {
return $str . '</ol>'; return $str . '</ol>';
}); });
Form::macro('human_filesize', function($bytes, $decimals = 1) { Form::macro('human_filesize', function($bytes, $decimals = 1) {
$size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB'); $size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
$factor = floor((strlen($bytes) - 1) / 3); $factor = floor((strlen($bytes) - 1) / 3);
if($factor == 0)$decimals=0;// There aren't fractional bytes if($factor == 0)$decimals=0;// There aren't fractional bytes
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor]; return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
}); });
Validator::extend('positive', function($attribute, $value, $parameters) { Validator::extend('positive', function($attribute, $value, $parameters) {
return Utils::parseFloat($value) >= 0; return Utils::parseFloat($value) >= 0;
}); });

View File

@ -13,6 +13,7 @@ use App\Ninja\Repositories\ContactRepository;
use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\ProductRepository;
use App\Ninja\Serializers\ArraySerializer; use App\Ninja\Serializers\ArraySerializer;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
@ -23,6 +24,7 @@ class ImportService
protected $invoiceRepo; protected $invoiceRepo;
protected $clientRepo; protected $clientRepo;
protected $contactRepo; protected $contactRepo;
protected $productRepo;
protected $processedRows = array(); protected $processedRows = array();
public static $entityTypes = [ public static $entityTypes = [
@ -31,6 +33,8 @@ class ImportService
ENTITY_INVOICE, ENTITY_INVOICE,
ENTITY_PAYMENT, ENTITY_PAYMENT,
ENTITY_TASK, ENTITY_TASK,
ENTITY_PRODUCT,
ENTITY_EXPENSE,
]; ];
public static $sources = [ public static $sources = [
@ -45,7 +49,14 @@ class ImportService
IMPORT_ZOHO, IMPORT_ZOHO,
]; ];
public function __construct(Manager $manager, ClientRepository $clientRepo, InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ContactRepository $contactRepo) public function __construct(
Manager $manager,
ClientRepository $clientRepo,
InvoiceRepository $invoiceRepo,
PaymentRepository $paymentRepo,
ContactRepository $contactRepo,
ProductRepository $productRepo
)
{ {
$this->fractal = $manager; $this->fractal = $manager;
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer());
@ -54,6 +65,7 @@ class ImportService
$this->invoiceRepo = $invoiceRepo; $this->invoiceRepo = $invoiceRepo;
$this->paymentRepo = $paymentRepo; $this->paymentRepo = $paymentRepo;
$this->contactRepo = $contactRepo; $this->contactRepo = $contactRepo;
$this->productRepo = $productRepo;
} }
public function import($source, $files) public function import($source, $files)
@ -216,8 +228,11 @@ class ImportService
'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,'.Auth::user()->account_id, 'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,'.Auth::user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
]; ];
} else { }
return true; if ($entityType === ENTITY_PRODUCT) {
$rules = [
'product_key' => 'required',
];
} }
$validator = Validator::make($data, $rules); $validator = Validator::make($data, $rules);
@ -251,6 +266,14 @@ class ImportService
} }
} }
$productMap = [];
$products = $this->productRepo->all();
foreach ($products as $product) {
if ($key = strtolower(trim($product->product_key))) {
$productMap[$key] = $product->id;
}
}
$countryMap = []; $countryMap = [];
$countryMap2 = []; $countryMap2 = [];
$countries = Cache::get('countries'); $countries = Cache::get('countries');
@ -269,6 +292,7 @@ class ImportService
ENTITY_CLIENT => $clientMap, ENTITY_CLIENT => $clientMap,
ENTITY_INVOICE => $invoiceMap, ENTITY_INVOICE => $invoiceMap,
ENTITY_INVOICE.'_'.ENTITY_CLIENT => $invoiceClientMap, ENTITY_INVOICE.'_'.ENTITY_CLIENT => $invoiceClientMap,
ENTITY_PRODUCT => $productMap,
'countries' => $countryMap, 'countries' => $countryMap,
'countries2' => $countryMap2, 'countries2' => $countryMap2,
'currencies' => $currencyMap, 'currencies' => $currencyMap,
@ -280,13 +304,9 @@ class ImportService
$data = []; $data = [];
foreach ($files as $entityType => $filename) { foreach ($files as $entityType => $filename) {
if ($entityType === ENTITY_CLIENT) { $class = "App\\Models\\" . ucwords($entityType);
$columns = Client::getImportColumns(); $columns = $class::getImportColumns();
$map = Client::getImportMap(); $map = $class::getImportMap();
} else {
$columns = Invoice::getImportColumns();
$map = Invoice::getImportMap();
}
// Lookup field translations // Lookup field translations
foreach ($columns as $key => $value) { foreach ($columns as $key => $value) {
@ -452,12 +472,8 @@ class ImportService
private function convertToObject($entityType, $data, $map) private function convertToObject($entityType, $data, $map)
{ {
$obj = new stdClass(); $obj = new stdClass();
$class = "App\\Models\\" . ucwords($entityType);
if ($entityType === ENTITY_CLIENT) { $columns = $class::getImportColumns();
$columns = Client::getImportColumns();
} else {
$columns = Invoice::getImportColumns();
}
foreach ($columns as $column) { foreach ($columns as $column) {
$obj->$column = false; $obj->$column = false;

View File

@ -94,7 +94,7 @@ class InvoiceService extends BaseService
{ {
$account = $quote->account; $account = $quote->account;
if (!$quote->is_quote || $quote->quote_invoice_id) { if (!$quote->isType(INVOICE_TYPE_QUOTE) || $quote->quote_invoice_id) {
return null; return null;
} }
@ -120,8 +120,10 @@ class InvoiceService extends BaseService
public function getDatatable($accountId, $clientPublicId = null, $entityType, $search) public function getDatatable($accountId, $clientPublicId = null, $entityType, $search)
{ {
$datatable = new InvoiceDatatable( ! $clientPublicId, $clientPublicId); $datatable = new InvoiceDatatable( ! $clientPublicId, $clientPublicId);
$datatable->entityType = $entityType;
$query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search) $query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search)
->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE ? true : false); ->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD);
if(!Utils::hasPermission('view_all')){ if(!Utils::hasPermission('view_all')){
$query->where('invoices.user_id', '=', Auth::user()->id); $query->where('invoices.user_id', '=', Auth::user()->id);

View File

@ -132,7 +132,7 @@ class PushService
*/ */
private function entitySentMessage($invoice) private function entitySentMessage($invoice)
{ {
if($invoice->is_quote) if($invoice->isType(INVOICE_TYPE_QUOTE))
return trans("texts.notification_quote_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); return trans("texts.notification_quote_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
else else
return trans("texts.notification_invoice_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); return trans("texts.notification_invoice_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
@ -163,7 +163,7 @@ class PushService
*/ */
private function entityViewedMessage($invoice) private function entityViewedMessage($invoice)
{ {
if($invoice->is_quote) if($invoice->isType(INVOICE_TYPE_QUOTE))
return trans("texts.notification_quote_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); return trans("texts.notification_quote_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
else else
return trans("texts.notification_invoice_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); return trans("texts.notification_invoice_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);

View File

@ -27,7 +27,7 @@
// Whether checkboxes should always be present in the POST data, // Whether checkboxes should always be present in the POST data,
// no matter if you checked them or not // no matter if you checked them or not
'push_checkboxes' => false, 'push_checkboxes' => true,
// The value a checkbox will have in the POST array if unchecked // The value a checkbox will have in the POST array if unchecked
'unchecked_value' => 0, 'unchecked_value' => 0,
@ -181,4 +181,4 @@
), ),
); );

View File

@ -14,9 +14,9 @@ class AddQuotes extends Migration {
{ {
Schema::table('invoices', function($table) Schema::table('invoices', function($table)
{ {
$table->boolean('is_quote')->default(0); $table->boolean('invoice_type_id')->default(0);
$table->unsignedInteger('quote_id')->nullable(); $table->unsignedInteger('quote_id')->nullable();
$table->unsignedInteger('quote_invoice_id')->nullable(); $table->unsignedInteger('quote_invoice_id')->nullable();
}); });
} }
@ -29,7 +29,7 @@ class AddQuotes extends Migration {
{ {
Schema::table('invoices', function($table) Schema::table('invoices', function($table)
{ {
$table->dropColumn('is_quote'); $table->dropColumn('invoice_type_id');
$table->dropColumn('quote_id'); $table->dropColumn('quote_id');
$table->dropColumn('quote_invoice_id'); $table->dropColumn('quote_invoice_id');
}); });

View File

@ -15,8 +15,8 @@ class AddDocuments extends Migration {
$table->unsignedInteger('logo_width'); $table->unsignedInteger('logo_width');
$table->unsignedInteger('logo_height'); $table->unsignedInteger('logo_height');
$table->unsignedInteger('logo_size'); $table->unsignedInteger('logo_size');
$table->boolean('invoice_embed_documents')->default(1); $table->boolean('invoice_embed_documents')->default(0);
$table->boolean('document_email_attachment')->default(1); $table->boolean('document_email_attachment')->default(0);
}); });
\DB::table('accounts')->update(array('logo' => '')); \DB::table('accounts')->update(array('logo' => ''));

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddInvoiceTypeSupport extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('invoices', 'is_quote')) {
DB::update('update invoices set is_quote = is_quote + 1');
Schema::table('invoices', function ($table) {
$table->renameColumn('is_quote', 'invoice_type_id');
});
}
Schema::table('accounts', function($table)
{
$table->boolean('enable_second_tax_rate')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if (Schema::hasColumn('invoices', 'invoice_type_id')) {
DB::update('update invoices set invoice_type_id = invoice_type_id - 1');
}
Schema::table('accounts', function($table)
{
$table->dropColumn('enable_second_tax_rate');
});
}
}

File diff suppressed because one or more lines are too long

85
public/css/built.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

81
public/css/style.css vendored
View File

@ -101,7 +101,7 @@ border-bottom: 1px solid #dfe0e1;
table.dataTable.no-footer { table.dataTable.no-footer {
border-bottom: none; border-bottom: none;
} }
.table-striped>tbody>tr:nth-child(odd)>tr, .table-striped>tbody>tr:nth-child(odd)>tr,
.table-striped>tbody>tr:nth-child(odd)>th { .table-striped>tbody>tr:nth-child(odd)>th {
background-color: #FDFDFD; background-color: #FDFDFD;
} }
@ -192,7 +192,7 @@ opacity: 1;
filter: alpha(opacity=100); filter: alpha(opacity=100);
} }
/*buttons*/ /*buttons*/
.btn { font-weight: bold; .btn { font-weight: bold;
border-radius: 3px; border-radius: 3px;
padding: 9px 12px; padding: 9px 12px;
} }
@ -258,8 +258,8 @@ border-color: #0b4d78;
} }
.form-actions .btn, .form-actions .btn,
.form-actions div.btn-group { .form-actions div.btn-group {
margin-left: 10px; margin-left: 10px;
} }
.form-actions .btn.btn-success:first-child { .form-actions .btn.btn-success:first-child {
@ -377,7 +377,7 @@ border: none;
border-radius: 0; border-radius: 0;
color: #fff; color: #fff;
background-color: #9b9b9b; background-color: #9b9b9b;
} }
.nav-tabs.nav-justified>li:first-child>a { .nav-tabs.nav-justified>li:first-child>a {
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
@ -406,7 +406,7 @@ font-weight: bold;
ul.dropdown-menu, ul.dropdown-menu,
.twitter-typeahead .tt-menu { .twitter-typeahead .tt-menu {
x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
box-shadow: 0 0 10px 2px rgba(0,0,0,.05); box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
} }
@ -466,7 +466,7 @@ background-clip: padding-box;
/*********************************************** /***********************************************
Dashboard Dashboard
************************************************/ ************************************************/
.in-bold { .in-bold {
@ -628,14 +628,14 @@ div.discount-group span {
.navbar-default .navbar-nav > li > a:focus { .navbar-default .navbar-nav > li > a:focus {
color: #ffffff; color: #ffffff;
} }
.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus { .navbar-default .navbar-nav > .active > a:focus {
color: #ffffff; color: #ffffff;
background-color: #3276b1; background-color: #3276b1;
} }
.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:hover,
.navbar-default .navbar-nav > .open > a:focus { .navbar-default .navbar-nav > .open > a:focus {
color: #ffffff; color: #ffffff;
background-color: #3276b1; background-color: #3276b1;
@ -649,8 +649,8 @@ div.discount-group span {
border-top-color: #ffffff; border-top-color: #ffffff;
border-bottom-color: #ffffff; border-bottom-color: #ffffff;
} }
.navbar-default .navbar-nav > .open > a .caret, .navbar-default .navbar-nav > .open > a .caret,
.navbar-default .navbar-nav > .open > a:hover .caret, .navbar-default .navbar-nav > .open > a:hover .caret,
.navbar-default .navbar-nav > .open > a:focus .caret { .navbar-default .navbar-nav > .open > a:focus .caret {
border-top-color: #ffffff; border-top-color: #ffffff;
border-bottom-color: #ffffff; border-bottom-color: #ffffff;
@ -689,7 +689,7 @@ div.fb_iframe_widget {
display: inline; display: inline;
} }
div.fb_iframe_widget > span { div.fb_iframe_widget > span {
vertical-align: top !important; vertical-align: top !important;
} }
.pro-label { .pro-label {
font-size:9px; font-size:9px;
@ -728,12 +728,12 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
.plans-table .glyphicon-remove {background-color: #da4830;} .plans-table .glyphicon-remove {background-color: #da4830;}
.plans-table .glyphicon-ok {background-color: #35c156;} .plans-table .glyphicon-ok {background-color: #35c156;}
.plans-table .glyphicon-star {border-radius: 0; background-color: #2e2b2b; .plans-table .glyphicon-star {border-radius: 0; background-color: #2e2b2b;
display: block; display: block;
width: 60px; width: 60px;
height: 30px; height: 30px;
position: absolute; position: absolute;
top: -5px; top: -5px;
right: -20px; right: -20px;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg); -moz-transform: rotate(45deg);
-o-transform: rotate(45deg); -o-transform: rotate(45deg);
@ -763,11 +763,11 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
.ellipsis { .ellipsis {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.entityArchived { .entityArchived {
color: #888 !important; color: #888 !important;
} }
.entityDeleted { .entityDeleted {
@ -775,12 +775,12 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
} }
/* Custom, iPhone Retina */ /* Custom, iPhone Retina */
@media only screen and (min-width : 320px) { @media only screen and (min-width : 320px) {
} }
/* Extra Small Devices, Phones */ /* Extra Small Devices, Phones */
@media only screen and (min-width : 480px) { @media only screen and (min-width : 480px) {
} }
@ -828,7 +828,7 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
@media (max-width: 992px) { @media (max-width: 992px) {
.hide-phone { .hide-phone {
display: none !important; display: none !important;
} }
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@ -846,8 +846,8 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
.plans-table .free .cell { padding-right: 0; } .plans-table .free .cell { padding-right: 0; }
.plans-table .free .cell:first-child {margin-right: 0;} .plans-table .free .cell:first-child {margin-right: 0;}
.plans-table .cell div:first-child {margin-bottom: 5px;} .plans-table .cell div:first-child {margin-bottom: 5px;}
.plans-table .cell .cta {margin-bottom: 0 !important;} .plans-table .cell .cta {margin-bottom: 0 !important;}
.plans-table .pro {margin-top: 40px;} .plans-table .pro {margin-top: 40px;}
} }
label[for=recommendedGateway_id2].radio{ label[for=recommendedGateway_id2].radio{
@ -976,14 +976,14 @@ button .glyphicon {
.pro-plan-modal a.button { .pro-plan-modal a.button {
font-family: 'roboto_slabregular', Georgia, Times, serif; font-family: 'roboto_slabregular', Georgia, Times, serif;
background: #f38c4f; background: #f38c4f;
background: -moz-linear-gradient(top, #f38c4f 0%, #db7134 100%); background: -moz-linear-gradient(top, #f38c4f 0%, #db7134 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f38c4f), color-stop(100%,#db7134)); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f38c4f), color-stop(100%,#db7134));
background: -webkit-linear-gradient(top, #f38c4f 0%,#db7134 100%); background: -webkit-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: -o-linear-gradient(top, #f38c4f 0%,#db7134 100%); background: -o-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: -ms-linear-gradient(top, #f38c4f 0%,#db7134 100%); background: -ms-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: linear-gradient(to bottom, #f38c4f 0%,#db7134 100%); background: linear-gradient(to bottom, #f38c4f 0%,#db7134 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f38c4f', endColorstr='#db7134',GradientType=0 ); filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f38c4f', endColorstr='#db7134',GradientType=0 );
text-shadow: 1px 1px 1px rgba(0, 0, 0, .25); text-shadow: 1px 1px 1px rgba(0, 0, 0, .25);
width: 68%; width: 68%;
margin-top: 20px; margin-top: 20px;
@ -1046,7 +1046,7 @@ ul.user-accounts a:hover div.remove {
.invoice-contact .tooltip-inner { .invoice-contact .tooltip-inner {
text-align:left; text-align:left;
width: 350px; width: 350px;
} }
.smaller { .smaller {
@ -1103,10 +1103,3 @@ div.panel-body div.panel-body {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.dropzone .fallback-doc{
display:none;
}
.dropzone.dz-browser-not-supported .fallback-doc{
display:block;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1312,6 +1312,15 @@ $LANG = array(
'security' => 'Security', 'security' => 'Security',
'see_whats_new' => 'See what\'s new in v:version', 'see_whats_new' => 'See what\'s new in v:version',
'wait_for_upload' => 'Please wait for the document upload to complete.', 'wait_for_upload' => 'Please wait for the document upload to complete.',
'upgrade_for_permissions' => 'Upgrade to our Enterprise plan to enable permissions.',
'enable_second_tax_rate' => 'Enable specifying a <b>second tax rate</b>',
'payment_file' => 'Payment File',
'expense_file' => 'Expense File',
'product_file' => 'Product File',
'import_products' => 'Import Products',
'products_will_create' => 'products will be created.',
'product_key' => 'Product',
'created_products' => 'Successfully created :count product(s)',
'view_dashboard' => 'View Dashboard', 'view_dashboard' => 'View Dashboard',
'client_session_expired' => 'Session Expired', 'client_session_expired' => 'Session Expired',

View File

@ -7,9 +7,9 @@
<script src="{{ asset('js/quill.min.js') }}" type="text/javascript"></script> <script src="{{ asset('js/quill.min.js') }}" type="text/javascript"></script>
@stop @stop
@section('content') @section('content')
@parent @parent
<style type="text/css"> <style type="text/css">
#logo { #logo {
@ -23,10 +23,11 @@
->autocomplete('on') ->autocomplete('on')
->rules([ ->rules([
'name' => 'required', 'name' => 'required',
'website' => 'url',
]) !!} ]) !!}
{{ Former::populate($account) }} {{ Former::populate($account) }}
@include('accounts.nav', ['selected' => ACCOUNT_COMPANY_DETAILS]) @include('accounts.nav', ['selected' => ACCOUNT_COMPANY_DETAILS])
<div class="row"> <div class="row">
@ -76,7 +77,7 @@
<h3 class="panel-title">{!! trans('texts.address') !!}</h3> <h3 class="panel-title">{!! trans('texts.address') !!}</h3>
</div> </div>
<div class="panel-body form-padding-right"> <div class="panel-body form-padding-right">
{!! Former::text('address1')->autocomplete('address-line1') !!} {!! Former::text('address1')->autocomplete('address-line1') !!}
{!! Former::text('address2')->autocomplete('address-line2') !!} {!! Former::text('address2')->autocomplete('address-line2') !!}
{!! Former::text('city')->autocomplete('address-level2') !!} {!! Former::text('city')->autocomplete('address-level2') !!}
@ -88,7 +89,7 @@
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.signature') !!}</h3> <h3 class="panel-title">{!! trans('texts.signature') !!}</h3>
@ -96,25 +97,25 @@
<div class="panel-body"> <div class="panel-body">
<div class="col-md-10 col-md-offset-1"> <div class="col-md-10 col-md-offset-1">
{!! Former::textarea('email_footer')->style('display:none')->raw() !!} {!! Former::textarea('email_footer')->style('display:none')->raw() !!}
<div id="signatureEditor" class="form-control" style="min-height:160px" onclick="focusEditor()"></div> <div id="signatureEditor" class="form-control" style="min-height:160px" onclick="focusEditor()"></div>
@include('partials/quill_toolbar', ['name' => 'signature']) @include('partials/quill_toolbar', ['name' => 'signature'])
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<center> <center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center> </center>
{!! Former::close() !!} {!! Former::close() !!}
{!! Form::open(['url' => 'remove_logo', 'class' => 'removeLogoForm']) !!} {!! Form::open(['url' => 'remove_logo', 'class' => 'removeLogoForm']) !!}
{!! Form::close() !!} {!! Form::close() !!}
@ -158,4 +159,4 @@
@section('onReady') @section('onReady')
$('#name').focus(); $('#name').focus();
@stop @stop

View File

@ -4,9 +4,7 @@
@parent @parent
<style type="text/css"> <style type="text/css">
.contact-file, .import-file {
.task-file,
.payment-file {
display: none; display: none;
} }
</style> </style>
@ -34,7 +32,7 @@
@foreach (\App\Services\ImportService::$entityTypes as $entityType) @foreach (\App\Services\ImportService::$entityTypes as $entityType)
{!! Former::file("{$entityType}_file") {!! Former::file("{$entityType}_file")
->addGroupClass("{$entityType}-file") !!} ->addGroupClass("import-file {$entityType}-file") !!}
@endforeach @endforeach
{!! Former::actions( Button::info(trans('texts.upload'))->submit()->large()->appendIcon(Icon::create('open'))) !!} {!! Former::actions( Button::info(trans('texts.upload'))->submit()->large()->appendIcon(Icon::create('open'))) !!}
@ -67,13 +65,17 @@
trans('texts.payments') => array('name' => ENTITY_PAYMENT, 'value' => 1), trans('texts.payments') => array('name' => ENTITY_PAYMENT, 'value' => 1),
])->check(ENTITY_CLIENT)->check(ENTITY_TASK)->check(ENTITY_INVOICE)->check(ENTITY_PAYMENT) !!} ])->check(ENTITY_CLIENT)->check(ENTITY_TASK)->check(ENTITY_INVOICE)->check(ENTITY_PAYMENT) !!}
{!! Former::actions( Button::primary(trans('texts.download'))->submit()->large()->appendIcon(Icon::create('download-alt'))) !!} {!! Former::actions( Button::primary(trans('texts.download'))->submit()->large()->appendIcon(Icon::create('download-alt'))) !!}
</div> </div>
</div> </div>
{!! Former::close() !!} {!! Former::close() !!}
<script type="text/javascript"> <script type="text/javascript">
$(function() {
setFileTypesVisible();
});
function setEntityTypesVisible() { function setEntityTypesVisible() {
var selector = '.entity-types input[type=checkbox]'; var selector = '.entity-types input[type=checkbox]';
if ($('#format').val() === 'JSON') { if ($('#format').val() === 'JSON') {
@ -103,4 +105,4 @@
</script> </script>
@stop @stop

View File

@ -7,18 +7,16 @@
{!! Former::open('/import_csv')->addClass('warn-on-exit') !!} {!! Former::open('/import_csv')->addClass('warn-on-exit') !!}
@if (isset($data[ENTITY_CLIENT])) @foreach (App\Services\ImportService::$entityTypes as $entityType)
@include('accounts.partials.map', $data[ENTITY_CLIENT]) @if (isset($data[$entityType]))
@endif @include('accounts.partials.map', $data[$entityType])
@endif
@endforeach
@if (isset($data[ENTITY_INVOICE])) {!! Former::actions(
@include('accounts.partials.map', $data[ENTITY_INVOICE])
@endif
{!! Former::actions(
Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/import_export'))->appendIcon(Icon::create('remove-circle')), Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/import_export'))->appendIcon(Icon::create('remove-circle')),
Button::success(trans('texts.import'))->submit()->large()->appendIcon(Icon::create('floppy-disk'))) !!} Button::success(trans('texts.import'))->submit()->large()->appendIcon(Icon::create('floppy-disk'))) !!}
{!! Former::close() !!} {!! Former::close() !!}
@stop @stop

View File

@ -1,6 +1,6 @@
@extends('header') @extends('header')
@section('content') @section('content')
@parent @parent
@include('accounts.nav', ['selected' => ACCOUNT_TAX_RATES]) @include('accounts.nav', ['selected' => ACCOUNT_TAX_RATES])
@ -10,12 +10,13 @@
{{ Former::populateField('invoice_taxes', intval($account->invoice_taxes)) }} {{ Former::populateField('invoice_taxes', intval($account->invoice_taxes)) }}
{{ Former::populateField('invoice_item_taxes', intval($account->invoice_item_taxes)) }} {{ Former::populateField('invoice_item_taxes', intval($account->invoice_item_taxes)) }}
{{ Former::populateField('show_item_taxes', intval($account->show_item_taxes)) }} {{ Former::populateField('show_item_taxes', intval($account->show_item_taxes)) }}
{{ Former::populateField('enable_second_tax_rate', intval($account->enable_second_tax_rate)) }}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.tax_settings') !!}</h3> <h3 class="panel-title">{!! trans('texts.tax_settings') !!}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{!! Former::checkbox('invoice_taxes') {!! Former::checkbox('invoice_taxes')
@ -30,6 +31,10 @@
->text(trans('texts.show_line_item_tax')) ->text(trans('texts.show_line_item_tax'))
->label('&nbsp;') !!} ->label('&nbsp;') !!}
{!! Former::checkbox('enable_second_tax_rate')
->text(trans('texts.enable_second_tax_rate'))
->label('&nbsp;') !!}
&nbsp; &nbsp;
{!! Former::select('default_tax_rate_id') {!! Former::select('default_tax_rate_id')
@ -51,22 +56,22 @@
@include('partials.bulk_form', ['entityType' => ENTITY_TAX_RATE]) @include('partials.bulk_form', ['entityType' => ENTITY_TAX_RATE])
{!! Datatable::table() {!! Datatable::table()
->addColumn( ->addColumn(
trans('texts.name'), trans('texts.name'),
trans('texts.rate'), trans('texts.rate'),
trans('texts.action')) trans('texts.action'))
->setUrl(url('api/tax_rates/')) ->setUrl(url('api/tax_rates/'))
->setOptions('sPaginationType', 'bootstrap') ->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false) ->setOptions('bFilter', false)
->setOptions('bAutoWidth', false) ->setOptions('bAutoWidth', false)
->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]]) ->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]])
->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]]) ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]])
->render('datatable') !!} ->render('datatable') !!}
<script> <script>
window.onDatatableReady = actionListHandler; window.onDatatableReady = actionListHandler;
</script> </script>
@stop @stop

View File

@ -150,7 +150,7 @@
</thead> </thead>
<tbody> <tbody>
@foreach ($upcoming as $invoice) @foreach ($upcoming as $invoice)
@if (!$invoice->is_quote) @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr> <tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td> <td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
@can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id]) @can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id])
@ -185,7 +185,7 @@
</thead> </thead>
<tbody> <tbody>
@foreach ($pastDue as $invoice) @foreach ($pastDue as $invoice)
@if (!$invoice->is_quote) @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr> <tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td> <td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
@can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id]) @can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id])
@ -224,7 +224,7 @@
</thead> </thead>
<tbody> <tbody>
@foreach ($upcoming as $invoice) @foreach ($upcoming as $invoice)
@if ($invoice->is_quote) @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr> <tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td> <td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
<td>{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}</td> <td>{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}</td>
@ -255,7 +255,7 @@
</thead> </thead>
<tbody> <tbody>
@foreach ($pastDue as $invoice) @foreach ($pastDue as $invoice)
@if ($invoice->is_quote) @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr> <tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td> <td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
<td>{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}</td> <td>{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}</td>

View File

@ -15,7 +15,10 @@
@section('content') @section('content')
{!! Former::open($url)->addClass('warn-on-exit main-form')->method($method) !!} {!! Former::open($url)
->addClass('warn-on-exit main-form')
->onsubmit('return onFormSubmit(event)')
->method($method) !!}
<div style="display:none"> <div style="display:none">
{!! Former::text('action') !!} {!! Former::text('action') !!}
</div> </div>
@ -111,15 +114,8 @@
<div class="col-md-12 col-sm-8"> <div class="col-md-12 col-sm-8">
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9"> <div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload" class="dropzone"> <div id="document-upload" class="dropzone">
<div class="fallback">
<input name="documents[]" type="file" multiple />
</div>
<div data-bind="foreach: documents"> <div data-bind="foreach: documents">
<div class="fallback-doc"> <input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
<a href="#" class="fallback-doc-remove" data-bind="click: $parent.removeDocument"><i class="fa fa-close"></i></a>
<span data-bind="text:name"></span>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -154,6 +150,15 @@
clientMap[client.public_id] = client; clientMap[client.public_id] = client;
} }
function onFormSubmit(event) {
if (window.countUploadingDocuments > 0) {
alert("{!! trans('texts.wait_for_upload') !!}");
return false;
}
return true;
}
function onClientChange() { function onClientChange() {
var clientId = $('select#client_id').val(); var clientId = $('select#client_id').val();
var client = clientMap[clientId]; var client = clientMap[clientId];
@ -225,21 +230,24 @@
// Initialize document upload // Initialize document upload
dropzone = new Dropzone('#document-upload', { dropzone = new Dropzone('#document-upload', {
url:{!! json_encode(url('document')) !!}, url:{!! json_encode(url('documents')) !!},
params:{ params:{
_token:"{{ Session::getToken() }}" _token:"{{ Session::getToken() }}"
}, },
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!}, acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
addRemoveLinks:true, addRemoveLinks:true,
dictRemoveFileConfirmation:"{{trans('texts.are_you_sure')}}",
@foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key) @foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key)
"dict{{strval($key)}}":"{{trans('texts.dropzone_'.Utils::toClassCase($key))}}", "dict{{strval($key)}}":"{{trans('texts.dropzone_'.Utils::toClassCase($key))}}",
@endforeach @endforeach
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}}, maxFilesize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
}); });
if(dropzone instanceof Dropzone){ if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded); dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved); dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded); dropzone.on("success",handleDocumentUploaded);
dropzone.on("canceled",handleDocumentCanceled);
dropzone.on("error",handleDocumentError);
for (var i=0; i<model.documents().length; i++) { for (var i=0; i<model.documents().length; i++) {
var document = model.documents()[i]; var document = model.documents()[i];
var mockFile = { var mockFile = {
@ -362,6 +370,7 @@
} }
} }
window.countUploadingDocuments = 0;
@if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS)) @if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS))
function handleDocumentAdded(file){ function handleDocumentAdded(file){
// open document when clicked // open document when clicked
@ -373,20 +382,36 @@
if(file.mock)return; if(file.mock)return;
file.index = model.documents().length; file.index = model.documents().length;
model.addDocument({name:file.name, size:file.size, type:file.type}); model.addDocument({name:file.name, size:file.size, type:file.type});
window.countUploadingDocuments++;
} }
function handleDocumentRemoved(file){ function handleDocumentRemoved(file){
model.removeDocument(file.public_id); model.removeDocument(file.public_id);
$.ajax({
url: '{{ '/documents/' }}' + file.public_id,
type: 'DELETE',
success: function(result) {
// Do something with the result
}
});
} }
function handleDocumentUploaded(file, response){ function handleDocumentUploaded(file, response){
file.public_id = response.document.public_id file.public_id = response.document.public_id
model.documents()[file.index].update(response.document); model.documents()[file.index].update(response.document);
window.countUploadingDocuments--;
if(response.document.preview_url){ if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url); dropzone.emit('thumbnail', file, response.document.preview_url);
} }
} }
function handleDocumentCanceled() {
window.countUploadingDocuments--;
}
function handleDocumentError() {
window.countUploadingDocuments--;
}
@endif @endif
</script> </script>

View File

@ -243,7 +243,7 @@
@endif @endif
<th style="min-width:120px" data-bind="text: costLabel">{{ $invoiceLabels['unit_cost'] }}</th> <th style="min-width:120px" data-bind="text: costLabel">{{ $invoiceLabels['unit_cost'] }}</th>
<th style="{{ $account->hide_quantity ? 'display:none' : 'min-width:120px' }}" data-bind="text: qtyLabel">{{ $invoiceLabels['quantity'] }}</th> <th style="{{ $account->hide_quantity ? 'display:none' : 'min-width:120px' }}" data-bind="text: qtyLabel">{{ $invoiceLabels['quantity'] }}</th>
<th style="min-width:180px;display:none;" data-bind="visible: $root.invoice_item_taxes.show">{{ trans('texts.tax') }}</th> <th style="min-width:{{ $account->enable_second_tax_rate ? 180 : 120 }}px;display:none;" data-bind="visible: $root.invoice_item_taxes.show">{{ trans('texts.tax') }}</th>
<th style="min-width:120px;">{{ trans('texts.line_total') }}</th> <th style="min-width:120px;">{{ trans('texts.line_total') }}</th>
<th style="min-width:32px;" class="hide-border"></th> <th style="min-width:32px;" class="hide-border"></th>
</tr> </tr>
@ -288,16 +288,18 @@
->addOption('', '') ->addOption('', '')
->options($taxRateOptions) ->options($taxRateOptions)
->data_bind('value: tax1') ->data_bind('value: tax1')
->addClass('tax-select') ->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->raw() !!} ->raw() !!}
<input type="text" data-bind="value: tax_name1, attr: {name: 'invoice_items[' + $index() + '][tax_name1]'}" style="display:none"> <input type="text" data-bind="value: tax_name1, attr: {name: 'invoice_items[' + $index() + '][tax_name1]'}" style="display:none">
<input type="text" data-bind="value: tax_rate1, attr: {name: 'invoice_items[' + $index() + '][tax_rate1]'}" style="display:none"> <input type="text" data-bind="value: tax_rate1, attr: {name: 'invoice_items[' + $index() + '][tax_rate1]'}" style="display:none">
{!! Former::select('') <div data-bind="visible: $root.invoice().account.enable_second_tax_rate == '1'">
->addOption('', '') {!! Former::select('')
->options($taxRateOptions) ->addOption('', '')
->data_bind('value: tax2') ->options($taxRateOptions)
->addClass('tax-select') ->data_bind('value: tax2')
->raw() !!} ->addClass('tax-select')
->raw() !!}
</div>
<input type="text" data-bind="value: tax_name2, attr: {name: 'invoice_items[' + $index() + '][tax_name2]'}" style="display:none"> <input type="text" data-bind="value: tax_name2, attr: {name: 'invoice_items[' + $index() + '][tax_name2]'}" style="display:none">
<input type="text" data-bind="value: tax_rate2, attr: {name: 'invoice_items[' + $index() + '][tax_rate2]'}" style="display:none"> <input type="text" data-bind="value: tax_rate2, attr: {name: 'invoice_items[' + $index() + '][tax_rate2]'}" style="display:none">
</td> </td>
@ -362,15 +364,8 @@
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9"> <div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload"> <div id="document-upload">
<div class="dropzone"> <div class="dropzone">
<div class="fallback">
<input name="documents[]" type="file" multiple />
</div>
<div data-bind="foreach: documents"> <div data-bind="foreach: documents">
<div class="fallback-doc"> <input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
<a href="#" class="fallback-doc-remove" data-bind="click: $parent.removeDocument"><i class="fa fa-close"></i></a>
<span data-bind="text:name"></span>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
</div>
</div> </div>
</div> </div>
@if ($invoice->hasExpenseDocuments()) @if ($invoice->hasExpenseDocuments())
@ -438,17 +433,19 @@
->id('taxRateSelect1') ->id('taxRateSelect1')
->addOption('', '') ->addOption('', '')
->options($taxRateOptions) ->options($taxRateOptions)
->addClass('tax-select') ->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->data_bind('value: tax1') ->data_bind('value: tax1')
->raw() !!} ->raw() !!}
<input type="text" name="tax_name1" data-bind="value: tax_name1" style="display:none"> <input type="text" name="tax_name1" data-bind="value: tax_name1" style="display:none">
<input type="text" name="tax_rate1" data-bind="value: tax_rate1" style="display:none"> <input type="text" name="tax_rate1" data-bind="value: tax_rate1" style="display:none">
<div data-bind="visible: $root.invoice().account.enable_second_tax_rate == '1'">
{!! Former::select('') {!! Former::select('')
->addOption('', '') ->addOption('', '')
->options($taxRateOptions) ->options($taxRateOptions)
->addClass('tax-select') ->addClass('tax-select')
->data_bind('value: tax2') ->data_bind('value: tax2')
->raw() !!} ->raw() !!}
</div>
<input type="text" name="tax_name2" data-bind="value: tax_name2" style="display:none"> <input type="text" name="tax_name2" data-bind="value: tax_name2" style="display:none">
<input type="text" name="tax_rate2" data-bind="value: tax_rate2" style="display:none"> <input type="text" name="tax_rate2" data-bind="value: tax_rate2" style="display:none">
</td> </td>
@ -848,7 +845,7 @@
model.invoice().has_tasks(true); model.invoice().has_tasks(true);
@endif @endif
if(model.invoice().expenses() && !model.invoice().public_id()){ if(model.invoice().expenses().length && !model.invoice().public_id()){
model.expense_currency_id({{ isset($expenseCurrencyId) ? $expenseCurrencyId : 0 }}); model.expense_currency_id({{ isset($expenseCurrencyId) ? $expenseCurrencyId : 0 }});
// move the blank invoice line item to the end // move the blank invoice line item to the end
@ -1010,21 +1007,24 @@
} }
window.dropzone = new Dropzone('#document-upload .dropzone', { window.dropzone = new Dropzone('#document-upload .dropzone', {
url:{!! json_encode(url('document')) !!}, url:{!! json_encode(url('documents')) !!},
params:{ params:{
_token:"{{ Session::getToken() }}" _token:"{{ Session::getToken() }}"
}, },
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!}, acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
addRemoveLinks:true, addRemoveLinks:true,
dictRemoveFileConfirmation:"{{trans('texts.are_you_sure')}}",
@foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key) @foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key)
"dict{{Utils::toClassCase($key)}}":"{{trans('texts.dropzone_'.$key)}}", "dict{{Utils::toClassCase($key)}}":"{{trans('texts.dropzone_'.$key)}}",
@endforeach @endforeach
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}}, maxFilesize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
}); });
if(dropzone instanceof Dropzone){ if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded); dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved); dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded); dropzone.on("success",handleDocumentUploaded);
dropzone.on("canceled",handleDocumentCanceled);
dropzone.on("error",handleDocumentError);
for (var i=0; i<model.invoice().documents().length; i++) { for (var i=0; i<model.invoice().documents().length; i++) {
var document = model.invoice().documents()[i]; var document = model.invoice().documents()[i];
var mockFile = { var mockFile = {
@ -1202,8 +1202,13 @@
if (confirm('{!! trans("texts.confirm_email_$entityType") !!}' + '\n\n' + getSendToEmails())) { if (confirm('{!! trans("texts.confirm_email_$entityType") !!}' + '\n\n' + getSendToEmails())) {
var accountLanguageId = parseInt({{ $account->language_id ?: '0' }}); var accountLanguageId = parseInt({{ $account->language_id ?: '0' }});
var clientLanguageId = parseInt(model.invoice().client().language_id()) || 0; var clientLanguageId = parseInt(model.invoice().client().language_id()) || 0;
var attachPDF = {{ $account->attachPDF() ? 'true' : 'false' }};
// if they aren't attaching the pdf no need to generate it
if ( ! attachPDF) {
submitAction('email');
// if the client's language is different then we can't use the browser version of the PDF // if the client's language is different then we can't use the browser version of the PDF
if (clientLanguageId && clientLanguageId != accountLanguageId) { } else if (clientLanguageId && clientLanguageId != accountLanguageId) {
submitAction('email'); submitAction('email');
} else { } else {
preparePdfData('email'); preparePdfData('email');
@ -1466,6 +1471,13 @@
function handleDocumentRemoved(file){ function handleDocumentRemoved(file){
model.invoice().removeDocument(file.public_id); model.invoice().removeDocument(file.public_id);
refreshPDF(true); refreshPDF(true);
$.ajax({
url: '{{ '/documents/' }}' + file.public_id,
type: 'DELETE',
success: function(result) {
// Do something with the result
}
});
} }
function handleDocumentUploaded(file, response){ function handleDocumentUploaded(file, response){
@ -1473,11 +1485,18 @@
model.invoice().documents()[file.index].update(response.document); model.invoice().documents()[file.index].update(response.document);
window.countUploadingDocuments--; window.countUploadingDocuments--;
refreshPDF(true); refreshPDF(true);
if(response.document.preview_url){ if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url); dropzone.emit('thumbnail', file, response.document.preview_url);
} }
} }
function handleDocumentCanceled() {
window.countUploadingDocuments--;
}
function handleDocumentError() {
window.countUploadingDocuments--;
}
@endif @endif
</script> </script>

View File

@ -4,15 +4,15 @@
@parent @parent
@include('money_script') @include('money_script')
@foreach ($invoice->client->account->getFontFolders() as $font) @foreach ($invoice->client->account->getFontFolders() as $font)
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script> <script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
@endforeach @endforeach
<script src="{{ asset('pdf.built.js') }}" type="text/javascript"></script> <script src="{{ asset('pdf.built.js') }}" type="text/javascript"></script>
<style type="text/css"> <style type="text/css">
body { body {
background-color: #f8f8f8; background-color: #f8f8f8;
} }
.dropdown-menu li a{ .dropdown-menu li a{
@ -99,7 +99,7 @@
@include('partials.checkout_com_payment') @include('partials.checkout_com_payment')
@else @else
<div class="pull-right" style="text-align:right"> <div class="pull-right" style="text-align:right">
@if ($invoice->is_quote) @if ($invoice->isQuote())
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}&nbsp;&nbsp; {!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}&nbsp;&nbsp;
@if ($showApprove) @if ($showApprove)
{!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!} {!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!}
@ -110,7 +110,7 @@
{!! DropdownButton::success(trans('texts.pay_now'))->withContents($paymentTypes)->large() !!} {!! DropdownButton::success(trans('texts.pay_now'))->withContents($paymentTypes)->large() !!}
@else @else
<a href='{!! $paymentURL !!}' class="btn btn-success btn-lg">{{ trans('texts.pay_now') }}</a> <a href='{!! $paymentURL !!}' class="btn btn-success btn-lg">{{ trans('texts.pay_now') }}</a>
@endif @endif
@else @else
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!} {!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
@if ($account->isNinjaAccount()) @if ($account->isNinjaAccount())
@ -141,7 +141,7 @@
</ul> </ul>
</div> </div>
@endif @endif
@if ($account->hasFeature(FEATURE_DOCUMENTS) && $account->invoice_embed_documents) @if ($account->hasFeature(FEATURE_DOCUMENTS) && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document) @foreach ($invoice->documents as $document)
@if($document->isPDFEmbeddable()) @if($document->isPDFEmbeddable())
@ -164,7 +164,7 @@
remove_created_by:{{ $invoice->client->account->hasFeature(FEATURE_REMOVE_CREATED_BY) ? 'true' : 'false' }}, remove_created_by:{{ $invoice->client->account->hasFeature(FEATURE_REMOVE_CREATED_BY) ? 'true' : 'false' }},
invoice_settings:{{ $invoice->client->account->hasFeature(FEATURE_INVOICE_SETTINGS) ? 'true' : 'false' }} invoice_settings:{{ $invoice->client->account->hasFeature(FEATURE_INVOICE_SETTINGS) ? 'true' : 'false' }}
}; };
invoice.is_quote = {{ $invoice->is_quote ? 'true' : 'false' }}; invoice.is_quote = {{ $invoice->isQuote() ? 'true' : 'false' }};
invoice.contact = {!! $contact->toJson() !!}; invoice.contact = {!! $contact->toJson() !!};
function getPDFString(cb) { function getPDFString(cb) {
@ -181,16 +181,16 @@
doc.getDataUrl(function(pdfString) { doc.getDataUrl(function(pdfString) {
document.write(pdfString); document.write(pdfString);
document.close(); document.close();
if (window.hasOwnProperty('pjsc_meta')) { if (window.hasOwnProperty('pjsc_meta')) {
window['pjsc_meta'].remainingTasks--; window['pjsc_meta'].remainingTasks--;
} }
}); });
@else @else
refreshPDF(); refreshPDF();
@endif @endif
}); });
function onDownloadClick() { function onDownloadClick() {
var doc = generatePDF(invoice, invoice.invoice_design.javascript, true); var doc = generatePDF(invoice, invoice.invoice_design.javascript, true);
var fileName = invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice; var fileName = invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice;

View File

@ -1,6 +1,6 @@
@extends('header') @extends('header')
@section('content') @section('content')
@parent @parent
@include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT]) @include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT])
@ -31,13 +31,20 @@
</div> </div>
</div> </div>
@if (Utils::hasFeature(FEATURE_USER_PERMISSIONS))
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.permissions') !!}</h3> <h3 class="panel-title">{!! trans('texts.permissions') !!}</h3>
</div> </div>
<div class="panel-body form-padding-right"> <div class="panel-body form-padding-right">
@if ( ! Utils::hasFeature(FEATURE_USER_PERMISSIONS))
<div class="alert alert-warning">{{ trans('texts.upgrade_for_permissions') }}</div>
<script>
$(function() {
$('input[type=checkbox]').prop('disabled', true);
})
</script>
@endif
{!! Former::checkbox('is_admin') {!! Former::checkbox('is_admin')
->label('&nbsp;') ->label('&nbsp;')
@ -61,12 +68,11 @@
->id('permissions_edit_all') ->id('permissions_edit_all')
->text(trans('texts.user_edit_all')) ->text(trans('texts.user_edit_all'))
->help(trans('texts.edit_all_help')) !!} ->help(trans('texts.edit_all_help')) !!}
</div>
</div>
@endif
{!! Former::actions( </div>
</div>
{!! Former::actions(
Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(), Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(),
Button::success(trans($user && $user->confirmed ? 'texts.save' : 'texts.send_invite'))->submit()->large()->appendIcon(Icon::create($user && $user->confirmed ? 'floppy-disk' : 'send')) Button::success(trans($user && $user->confirmed ? 'texts.save' : 'texts.send_invite'))->submit()->large()->appendIcon(Icon::create($user && $user->confirmed ? 'floppy-disk' : 'send'))
)!!} )!!}
@ -88,4 +94,4 @@
if(!viewChecked)$('#permissions_edit_all').prop('checked',false) if(!viewChecked)$('#permissions_edit_all').prop('checked',false)
} }
fixCheckboxes(); fixCheckboxes();
@stop @stop