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:
- 5.5.9
# - 5.6
# - 5.6
# - 7.0
# - hhvm
@ -27,6 +28,7 @@ before_install:
# set GitHub token and update composer
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
- composer self-update && composer -V
# - export USE_ZEND_ALLOC=0
install:
# install Composer dependencies
@ -66,6 +68,7 @@ before_script:
script:
- 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 TaxRatesCest.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 ExpenseCest.php
@ -76,7 +79,6 @@ script:
- 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 TaskCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
#- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
#- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php

View File

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

View File

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

View File

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

View File

@ -262,6 +262,12 @@ class AppController extends BaseController
if (!Utils::isNinjaProd()) {
try {
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));
Cache::flush();
Session::flush();

View File

@ -136,7 +136,7 @@ class ClientController extends BaseController
'credit' => $client->getTotalCredit(),
'title' => trans('texts.view_client'),
'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,
'gatewayLink' => $client->getGatewayLink($accountGateway),
'gateway' => $accountGateway

View File

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

View File

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

View File

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

View File

@ -2,13 +2,21 @@
use App\Models\Document;
use App\Ninja\Repositories\DocumentRepository;
use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
class DocumentAPIController extends BaseAPIController
{
protected $documentRepo;
public function __construct()
protected $entityType = ENTITY_DOCUMENT;
public function __construct(DocumentRepository $documentRepo)
{
parent::__construct();
$this->documentRepo = $documentRepo;
}
public function index()
@ -16,16 +24,19 @@ class DocumentAPIController extends BaseAPIController
//stub
}
public function show($publicId)
public function show(DocumentRequest $request)
{
$document = Document::scope($publicId)->firstOrFail();
$document = $request->entity();
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()

View File

@ -14,6 +14,7 @@ use App\Ninja\Repositories\DocumentRepository;
use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\UpdateDocumentRequest;
class DocumentController extends BaseController
{
@ -101,11 +102,7 @@ class DocumentController extends BaseController
public function postUpload(CreateDocumentRequest $request)
{
if (!Utils::hasFeature(FEATURE_DOCUMENTS)) {
return;
}
$result = $this->documentRepo->upload(Input::all()['file'], $doc_array);
$result = $this->documentRepo->upload($request->all(), $doc_array);
if(is_string($result)){
return Response::json([
@ -120,4 +117,11 @@ class DocumentController extends BaseController
], 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)) {
$data['invoices'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status')
->withArchived()
->where('is_quote', '=', false)
->where('is_recurring', '=', false)
->get();
$data['quotes'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_QUOTE)
->with('user', 'client.contacts', 'invoice_status')
->withArchived()
->where('is_quote', '=', true)
->where('is_recurring', '=', false)
->get();
$data['recurringInvoices'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status', 'frequency')
->withArchived()
->where('is_quote', '=', false)
->where('is_recurring', '=', true)
->get();
}

View File

@ -134,6 +134,7 @@ class InvoiceApiController extends BaseAPIController
'city',
'state',
'postal_code',
'country_id',
'private_notes',
'currency_code',
] as $field) {
@ -357,4 +358,17 @@ class InvoiceApiController extends BaseAPIController
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),
'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)
->where('activity_type_id', '=', $activityTypeId)
->where('invoice_id', '=', $invoice->id)
@ -596,7 +596,7 @@ class InvoiceController extends BaseController
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
'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();
$versionsJson[$activity->id] = $backup;

View File

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

View File

@ -130,7 +130,7 @@ class QuoteController extends BaseController
'taxRateOptions' => $options,
'defaultTax' => $defaultTax,
'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(),
'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'),

View File

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

View File

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

View File

@ -1,7 +1,15 @@
<?php namespace App\Http\Requests;
use App\Models\Invoice;
use App\Models\Expense;
class CreateDocumentRequest extends DocumentRequest
{
protected $autoload = [
ENTITY_INVOICE,
ENTITY_EXPENSE,
];
/**
* Determine if the user is authorized to make this request.
*
@ -9,6 +17,18 @@ class CreateDocumentRequest extends DocumentRequest
*/
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);
}
@ -20,7 +40,8 @@ class CreateDocumentRequest extends DocumentRequest
public function rules()
{
return [
//'file' => 'mimes:jpg'
];
}
}

View File

@ -2,8 +2,50 @@
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 {
//
// 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/js/{documents}/{filename}', 'DocumentController@getVFSJS');
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/{invoices}/clone', 'InvoiceController@cloneInvoice');
@ -271,6 +272,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function()
//Route::get('quotes', 'QuoteApiController@index');
//Route::resource('quotes', 'QuoteApiController');
Route::get('invoices', 'InvoiceApiController@index');
Route::get('download/{invoice_id}', 'InvoiceApiController@download');
Route::resource('invoices', 'InvoiceApiController');
Route::get('payments', 'PaymentApiController@index');
Route::resource('payments', 'PaymentApiController');
@ -360,6 +362,9 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_BANK_ACCOUNT', 'bank_account');
define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
define('PERSON_VENDOR_CONTACT','vendorcontact');

View File

@ -56,6 +56,11 @@ class Account extends Eloquent
'currency_id',
'language_id',
'military_time',
'invoice_taxes',
'invoice_item_taxes',
'show_item_taxes',
'default_tax_rate_id',
'enable_second_tax_rate',
];
public static $basicSettings = [
@ -529,7 +534,7 @@ class Account extends Eloquent
$invoice = Invoice::createNew();
$invoice->is_recurring = false;
$invoice->is_quote = false;
$invoice->invoice_type_id = INVOICE_TYPE_STANDARD;
$invoice->invoice_date = Utils::today();
$invoice->start_date = Utils::today();
$invoice->invoice_design_id = $this->invoice_design_id;
@ -540,7 +545,7 @@ class Account extends Eloquent
$invoice->is_recurring = true;
} else {
if ($entityType == ENTITY_QUOTE) {
$invoice->is_quote = true;
$invoice->invoice_type_id = INVOICE_TYPE_QUOTE;
}
if ($this->hasClientNumberPattern($invoice) && !$clientId) {
@ -558,34 +563,34 @@ class Account extends Eloquent
return $invoice;
}
public function getNumberPrefix($isQuote)
public function getNumberPrefix($invoice_type_id)
{
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) {
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)) {
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)
{
$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');
}
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) {
return false;
@ -595,7 +600,7 @@ class Account extends Eloquent
$replace = [date('Y')];
$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}')) {
$search[] = '{$userId}';
@ -638,9 +643,9 @@ class Account extends Eloquent
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)
@ -651,12 +656,11 @@ class Account extends Eloquent
public function getNextInvoiceNumber($invoice)
{
if ($this->hasNumberPattern($invoice->is_quote)) {
return $this->getNumberPattern($invoice);
}
$counter = $this->getCounter($invoice->is_quote);
$prefix = $this->getNumberPrefix($invoice->is_quote);
if ($this->hasNumberPattern($invoice->invoice_type_id)) {
$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
@ -669,7 +673,7 @@ class Account extends Eloquent
// update the invoice counter to be caught up
if ($counterOffset > 1) {
if ($invoice->is_quote && !$this->share_counter) {
if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) {
$this->quote_number_counter += $counterOffset - 1;
} else {
$this->invoice_number_counter += $counterOffset - 1;
@ -677,24 +681,27 @@ class Account extends Eloquent
$this->save();
}
}
if ($invoice->recurring_invoice_id) {
$number = $this->recurring_invoice_number_prefix . $number;
}
return $number;
}
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;
} else {
$default = $this->invoice_number_counter;
$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();
}
@ -1107,7 +1114,7 @@ class Account extends Eloquent
'invoice_items',
'created_at',
'is_recurring',
'is_quote',
'invoice_type_id',
]);
foreach ($invoice->invoice_items as $invoiceItem) {
@ -1317,7 +1324,7 @@ class Account extends Eloquent
return Utils::isEmpty($entity->$field) ? false : true;
}
public function attatchPDF()
public function attachPDF()
{
return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment;
}

View File

@ -6,6 +6,11 @@ use Auth;
class Document extends EntityModel
{
protected $fillable = [
'invoice_id',
'expense_id',
];
public static $extraExtensions = array(
'jpg' => 'jpeg',
'tif' => 'tiff',

View File

@ -86,6 +86,15 @@ class EntityModel extends Eloquent
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)
{
return $query->withTrashed()->where('is_deleted', '=', false);

View File

@ -96,7 +96,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function affectsBalance()
{
return !$this->is_quote && !$this->is_recurring;
return $this->isType(INVOICE_TYPE_STANDARD) && !$this->is_recurring;
}
public function getAdjustment()
@ -139,7 +139,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getAmountPaid($calculate = false)
{
if ($this->is_quote || $this->is_recurring) {
if ($this->isType(INVOICE_TYPE_QUOTE) || $this->is_recurring) {
return 0;
}
@ -230,10 +230,23 @@ class Invoice extends EntityModel implements BalanceAffecting
public function scopeInvoices($query)
{
return $query->where('is_quote', '=', false)
return $query->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->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)
{
foreach ($this->invitations as $invitation) {
@ -256,7 +269,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return;
}
if ($this->is_quote) {
if ($this->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteInvitationWasEmailed($invitation));
} else {
event(new InvoiceInvitationWasEmailed($invitation));
@ -292,7 +305,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function markApproved()
{
if ($this->is_quote) {
if ($this->isType(INVOICE_TYPE_QUOTE)) {
$this->invoice_status_id = INVOICE_STATUS_APPROVED;
$this->save();
}
@ -341,7 +354,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getEntityType()
{
return $this->is_quote ? ENTITY_QUOTE : ENTITY_INVOICE;
return $this->isType(INVOICE_TYPE_QUOTE) ? ENTITY_QUOTE : ENTITY_INVOICE;
}
public function isSent()
@ -416,7 +429,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'invoice_design_id',
'invoice_fonts',
'features',
'is_quote',
'invoice_type_id',
'custom_value1',
'custom_value2',
'custom_taxes1',
@ -957,7 +970,7 @@ Invoice::creating(function ($invoice) {
});
Invoice::created(function ($invoice) {
if ($invoice->is_quote) {
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasCreated($invoice));
} else {
event(new InvoiceWasCreated($invoice));
@ -965,7 +978,7 @@ Invoice::created(function ($invoice) {
});
Invoice::updating(function ($invoice) {
if ($invoice->is_quote) {
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasUpdated($invoice));
} else {
event(new InvoiceWasUpdated($invoice));

View File

@ -16,6 +16,24 @@ class Product extends EntityModel
'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()
{
return ENTITY_PRODUCT;

View File

@ -15,21 +15,38 @@ class BaseTransformer extends TransformerAbstract
protected function hasClient($name)
{
$name = strtolower($name);
$name = trim(strtolower($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)
{
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)
{
$name = strtolower($name);
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)
{
$name = strtolower($name);

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

View File

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

View File

@ -57,8 +57,9 @@ class DocumentRepository extends BaseRepository
return $query;
}
public function upload($uploaded, &$doc_array=null)
public function upload($data, &$doc_array=null)
{
$uploaded = $data['file'];
$extension = strtolower($uploaded->getClientOriginalExtension());
if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){
$documentType = Document::$extraExtensions[$extension];
@ -81,12 +82,17 @@ class DocumentRepository extends BaseRepository
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);
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType;
$document = Document::createNew();
$document->fill($data);
$disk = $document->getDisk();
if(!$disk->exists($filename)){// Have we already stored the same file
$stream = fopen($filePath, 'r');
@ -217,7 +223,7 @@ class DocumentRepository extends BaseRepository
)->toHtml();
})
->addColumn('document_date', function ($model) {
return Utils::fromSqlDate($model->created_at);
return Utils::dateToString($model->created_at);
})
->addColumn('document_size', function ($model) {
return Form::human_filesize($model->size);

View File

@ -148,6 +148,8 @@ class ExpenseRepository extends BaseRepository
// Documents
$document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();;
foreach ($document_ids as $document_id){
// check document completed upload before user submitted form
if ($document_id) {
$document = Document::scope($document_id)->first();
if($document && Auth::user()->can('edit', $document)){
$document->invoice_id = null;
@ -155,24 +157,6 @@ class ExpenseRepository extends BaseRepository
$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

View File

@ -32,9 +32,9 @@ class InvoiceRepository extends BaseRepository
public function all()
{
return Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status')
->withTrashed()
->where('is_quote', '=', false)
->where('is_recurring', '=', false)
->get();
}
@ -106,7 +106,7 @@ class InvoiceRepository extends BaseRepository
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', $accountId)
->where('invoices.is_quote', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true)
->where('contacts.is_primary', '=', true)
@ -156,7 +156,7 @@ class InvoiceRepository extends BaseRepository
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null)
->where('invoices.is_quote', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true)
@ -203,7 +203,7 @@ class InvoiceRepository extends BaseRepository
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invitations.contact_id', '=', $contactId)
->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('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
@ -494,25 +494,6 @@ class InvoiceRepository extends BaseRepository
}
}
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){
if(!in_array($document->public_id, $document_ids)){
// Removed
@ -642,7 +623,7 @@ class InvoiceRepository extends BaseRepository
'tax_name2',
'tax_rate2',
'amount',
'is_quote',
'invoice_type_id',
'custom_value1',
'custom_value2',
'custom_taxes1',
@ -654,7 +635,7 @@ class InvoiceRepository extends BaseRepository
}
if ($quotePublicId) {
$clone->is_quote = false;
$clone->invoice_type_id = INVOICE_TYPE_STANDARD;
$clone->quote_id = $quotePublicId;
}
@ -731,8 +712,8 @@ class InvoiceRepository extends BaseRepository
public function findOpenInvoices($clientId)
{
return Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->whereClientId($clientId)
->whereIsQuote(false)
->whereIsRecurring(false)
->whereDeletedAt(null)
->whereHasTasks(true)
@ -760,7 +741,7 @@ class InvoiceRepository extends BaseRepository
$invoice = Invoice::createNew($recurInvoice);
$invoice->client_id = $recurInvoice->client_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->balance = $recurInvoice->amount;
$invoice->invoice_date = date_create()->format('Y-m-d');
@ -839,9 +820,9 @@ class InvoiceRepository extends BaseRepository
}
$sql = implode(' OR ', $dates);
$invoices = Invoice::whereAccountId($account->id)
$invoices = Invoice::invoiceType(INVOICE_TYPE_STANDARD)
->whereAccountId($account->id)
->where('balance', '>', 0)
->where('is_quote', '=', false)
->where('is_recurring', '=', false)
->whereRaw('('.$sql.')')
->get();

View File

@ -11,6 +11,13 @@ class ProductRepository extends BaseRepository
return 'App\Models\Product';
}
public function all()
{
return Product::scope()
->withTrashed()
->get();
}
public function find($accountId)
{
return DB::table('products')

View File

@ -14,6 +14,7 @@ class DocumentTransformer extends EntityTransformer
'type' => $document->type,
'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,
'updated_at' => $this->getTimestamp($document->updated_at),
]);
}
}

View File

@ -93,7 +93,7 @@ class InvoiceTransformer extends EntityTransformer
'terms' => $invoice->terms,
'public_notes' => $invoice->public_notes,
'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,
'frequency_id' => (int) $invoice->frequency_id,
'start_date' => $invoice->start_date,
@ -119,6 +119,7 @@ class InvoiceTransformer extends EntityTransformer
'quote_invoice_id' => (int) $invoice->quote_invoice_id,
'custom_text_value1' => $invoice->custom_text_value1,
'custom_text_value2' => $invoice->custom_text_value2,
'is_quote' => (bool) $invoice->isType(INVOICE_TYPE_QUOTE), // Temp to support mobile app
]);
}
}

View File

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

View File

@ -94,7 +94,7 @@ class InvoiceService extends BaseService
{
$account = $quote->account;
if (!$quote->is_quote || $quote->quote_invoice_id) {
if (!$quote->isType(INVOICE_TYPE_QUOTE) || $quote->quote_invoice_id) {
return null;
}
@ -120,8 +120,10 @@ class InvoiceService extends BaseService
public function getDatatable($accountId, $clientPublicId = null, $entityType, $search)
{
$datatable = new InvoiceDatatable( ! $clientPublicId, $clientPublicId);
$datatable->entityType = $entityType;
$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')){
$query->where('invoices.user_id', '=', Auth::user()->id);

View File

@ -132,7 +132,7 @@ class PushService
*/
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]);
else
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)
{
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]);
else
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,
// 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
'unchecked_value' => 0,

View File

@ -14,7 +14,7 @@ class AddQuotes extends Migration {
{
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_invoice_id')->nullable();
});
@ -29,7 +29,7 @@ class AddQuotes extends Migration {
{
Schema::table('invoices', function($table)
{
$table->dropColumn('is_quote');
$table->dropColumn('invoice_type_id');
$table->dropColumn('quote_id');
$table->dropColumn('quote_invoice_id');
});

View File

@ -15,8 +15,8 @@ class AddDocuments extends Migration {
$table->unsignedInteger('logo_width');
$table->unsignedInteger('logo_height');
$table->unsignedInteger('logo_size');
$table->boolean('invoice_embed_documents')->default(1);
$table->boolean('document_email_attachment')->default(1);
$table->boolean('invoice_embed_documents')->default(0);
$table->boolean('document_email_attachment')->default(0);
});
\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

11
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

View File

@ -1103,10 +1103,3 @@ div.panel-body div.panel-body {
width: 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',
'see_whats_new' => 'See what\'s new in v:version',
'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',
'client_session_expired' => 'Session Expired',

View File

@ -23,6 +23,7 @@
->autocomplete('on')
->rules([
'name' => 'required',
'website' => 'url',
]) !!}
{{ Former::populate($account) }}

View File

@ -4,9 +4,7 @@
@parent
<style type="text/css">
.contact-file,
.task-file,
.payment-file {
.import-file {
display: none;
}
</style>
@ -34,7 +32,7 @@
@foreach (\App\Services\ImportService::$entityTypes as $entityType)
{!! Former::file("{$entityType}_file")
->addGroupClass("{$entityType}-file") !!}
->addGroupClass("import-file {$entityType}-file") !!}
@endforeach
{!! Former::actions( Button::info(trans('texts.upload'))->submit()->large()->appendIcon(Icon::create('open'))) !!}
@ -74,6 +72,10 @@
<script type="text/javascript">
$(function() {
setFileTypesVisible();
});
function setEntityTypesVisible() {
var selector = '.entity-types input[type=checkbox]';
if ($('#format').val() === 'JSON') {

View File

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

View File

@ -10,6 +10,7 @@
{{ Former::populateField('invoice_taxes', intval($account->invoice_taxes)) }}
{{ Former::populateField('invoice_item_taxes', intval($account->invoice_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">
@ -30,6 +31,10 @@
->text(trans('texts.show_line_item_tax'))
->label('&nbsp;') !!}
{!! Former::checkbox('enable_second_tax_rate')
->text(trans('texts.enable_second_tax_rate'))
->label('&nbsp;') !!}
&nbsp;
{!! Former::select('default_tax_rate_id')

View File

@ -150,7 +150,7 @@
</thead>
<tbody>
@foreach ($upcoming as $invoice)
@if (!$invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
@can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id])
@ -185,7 +185,7 @@
</thead>
<tbody>
@foreach ($pastDue as $invoice)
@if (!$invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
@can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id])
@ -224,7 +224,7 @@
</thead>
<tbody>
@foreach ($upcoming as $invoice)
@if ($invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<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>
@ -255,7 +255,7 @@
</thead>
<tbody>
@foreach ($pastDue as $invoice)
@if ($invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<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>

View File

@ -15,7 +15,10 @@
@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">
{!! Former::text('action') !!}
</div>
@ -111,20 +114,13 @@
<div class="col-md-12 col-sm-8">
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload" class="dropzone">
<div class="fallback">
<input name="documents[]" type="file" multiple />
</div>
<div data-bind="foreach: documents">
<div class="fallback-doc">
<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>
@endif
</div>
</div>
@ -154,6 +150,15 @@
clientMap[client.public_id] = client;
}
function onFormSubmit(event) {
if (window.countUploadingDocuments > 0) {
alert("{!! trans('texts.wait_for_upload') !!}");
return false;
}
return true;
}
function onClientChange() {
var clientId = $('select#client_id').val();
var client = clientMap[clientId];
@ -225,21 +230,24 @@
// Initialize document upload
dropzone = new Dropzone('#document-upload', {
url:{!! json_encode(url('document')) !!},
url:{!! json_encode(url('documents')) !!},
params:{
_token:"{{ Session::getToken() }}"
},
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
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)
"dict{{strval($key)}}":"{{trans('texts.dropzone_'.Utils::toClassCase($key))}}",
@endforeach
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
maxFilesize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
});
if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded);
dropzone.on("canceled",handleDocumentCanceled);
dropzone.on("error",handleDocumentError);
for (var i=0; i<model.documents().length; i++) {
var document = model.documents()[i];
var mockFile = {
@ -362,6 +370,7 @@
}
}
window.countUploadingDocuments = 0;
@if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS))
function handleDocumentAdded(file){
// open document when clicked
@ -373,20 +382,36 @@
if(file.mock)return;
file.index = model.documents().length;
model.addDocument({name:file.name, size:file.size, type:file.type});
window.countUploadingDocuments++;
}
function handleDocumentRemoved(file){
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){
file.public_id = response.document.public_id
model.documents()[file.index].update(response.document);
window.countUploadingDocuments--;
if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url);
}
}
function handleDocumentCanceled() {
window.countUploadingDocuments--;
}
function handleDocumentError() {
window.countUploadingDocuments--;
}
@endif
</script>

View File

@ -243,7 +243,7 @@
@endif
<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="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:32px;" class="hide-border"></th>
</tr>
@ -288,16 +288,18 @@
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax1')
->addClass('tax-select')
->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->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_rate1, attr: {name: 'invoice_items[' + $index() + '][tax_rate1]'}" style="display:none">
<div data-bind="visible: $root.invoice().account.enable_second_tax_rate == '1'">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax2')
->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_rate2, attr: {name: 'invoice_items[' + $index() + '][tax_rate2]'}" style="display:none">
</td>
@ -362,17 +364,10 @@
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload">
<div class="dropzone">
<div class="fallback">
<input name="documents[]" type="file" multiple />
</div>
<div data-bind="foreach: documents">
<div class="fallback-doc">
<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>
@if ($invoice->hasExpenseDocuments())
<h4>{{trans('texts.documents_from_expenses')}}</h4>
@foreach($invoice->expenses as $expense)
@ -438,17 +433,19 @@
->id('taxRateSelect1')
->addOption('', '')
->options($taxRateOptions)
->addClass('tax-select')
->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->data_bind('value: tax1')
->raw() !!}
<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">
<div data-bind="visible: $root.invoice().account.enable_second_tax_rate == '1'">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->addClass('tax-select')
->data_bind('value: tax2')
->raw() !!}
</div>
<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">
</td>
@ -848,7 +845,7 @@
model.invoice().has_tasks(true);
@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 }});
// move the blank invoice line item to the end
@ -1010,21 +1007,24 @@
}
window.dropzone = new Dropzone('#document-upload .dropzone', {
url:{!! json_encode(url('document')) !!},
url:{!! json_encode(url('documents')) !!},
params:{
_token:"{{ Session::getToken() }}"
},
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
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)
"dict{{Utils::toClassCase($key)}}":"{{trans('texts.dropzone_'.$key)}}",
@endforeach
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
maxFilesize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
});
if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded);
dropzone.on("canceled",handleDocumentCanceled);
dropzone.on("error",handleDocumentError);
for (var i=0; i<model.invoice().documents().length; i++) {
var document = model.invoice().documents()[i];
var mockFile = {
@ -1202,8 +1202,13 @@
if (confirm('{!! trans("texts.confirm_email_$entityType") !!}' + '\n\n' + getSendToEmails())) {
var accountLanguageId = parseInt({{ $account->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 (clientLanguageId && clientLanguageId != accountLanguageId) {
} else if (clientLanguageId && clientLanguageId != accountLanguageId) {
submitAction('email');
} else {
preparePdfData('email');
@ -1466,6 +1471,13 @@
function handleDocumentRemoved(file){
model.invoice().removeDocument(file.public_id);
refreshPDF(true);
$.ajax({
url: '{{ '/documents/' }}' + file.public_id,
type: 'DELETE',
success: function(result) {
// Do something with the result
}
});
}
function handleDocumentUploaded(file, response){
@ -1473,11 +1485,18 @@
model.invoice().documents()[file.index].update(response.document);
window.countUploadingDocuments--;
refreshPDF(true);
if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url);
}
}
function handleDocumentCanceled() {
window.countUploadingDocuments--;
}
function handleDocumentError() {
window.countUploadingDocuments--;
}
@endif
</script>

View File

@ -99,7 +99,7 @@
@include('partials.checkout_com_payment')
@else
<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;
@if ($showApprove)
{!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!}
@ -164,7 +164,7 @@
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.is_quote = {{ $invoice->is_quote ? 'true' : 'false' }};
invoice.is_quote = {{ $invoice->isQuote() ? 'true' : 'false' }};
invoice.contact = {!! $contact->toJson() !!};
function getPDFString(cb) {

View File

@ -31,13 +31,20 @@
</div>
</div>
@if (Utils::hasFeature(FEATURE_USER_PERMISSIONS))
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.permissions') !!}</h3>
</div>
<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')
->label('&nbsp;')
@ -64,7 +71,6 @@
</div>
</div>
@endif
{!! Former::actions(
Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(),