diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index a709bd7a4689..1845615cad0f 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -34,9 +34,7 @@ class DocumentAPIController extends BaseAPIController public function store(CreateDocumentRequest $request) { - Log::info($request); - - $document = $this->documentRepo->upload($request->file); + $document = $this->documentRepo->upload($request->all()); return $this->itemResponse($document); } diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index f0871a2408f2..6f6b0bd0889d 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -102,7 +102,7 @@ class DocumentController extends BaseController public function postUpload(CreateDocumentRequest $request) { - $result = $this->documentRepo->upload($request->file, $doc_array); + $result = $this->documentRepo->upload($request->all(), $doc_array); if(is_string($result)){ return Response::json([ diff --git a/app/Http/Requests/CreateDocumentRequest.php b/app/Http/Requests/CreateDocumentRequest.php index dca98a3ec3ed..5037ae3beb68 100644 --- a/app/Http/Requests/CreateDocumentRequest.php +++ b/app/Http/Requests/CreateDocumentRequest.php @@ -1,7 +1,15 @@ user()->can('create', ENTITY_DOCUMENT) && $this->user()->hasFeature(FEATURE_DOCUMENTS); + 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); } /** @@ -23,4 +43,5 @@ class CreateDocumentRequest extends DocumentRequest //'file' => 'mimes:jpg' ]; } + } diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 4516ab2bb566..c704e409f373 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -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(); + } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 5dd8a12c1851..61526c9095d4 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -652,30 +652,34 @@ class Account extends Eloquent public function getNextInvoiceNumber($invoice) { 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->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(); + if ($invoice->recurring_invoice_id) { + $number = $this->recurring_invoice_number_prefix . $number; } return $number; @@ -683,17 +687,15 @@ class Account extends Eloquent public function incrementCounter($invoice) { + // 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->invoice_number_counter += 1; } $this->save(); diff --git a/app/Models/Document.php b/app/Models/Document.php index 6d9c24857143..cc455fed0d79 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -6,20 +6,25 @@ use Auth; class Document extends EntityModel { + protected $fillable = [ + 'invoice_id', + 'expense_id', + ]; + public static $extraExtensions = array( 'jpg' => 'jpeg', 'tif' => 'tiff', ); - + 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', '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.spreadsheetml.sheet','application/postscript', 'image/svg+xml', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint', ); - + public static $types = array( 'png' => array( 'mime' => 'image/png', @@ -70,18 +75,18 @@ class Document extends EntityModel 'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', ), ); - + public function fill(array $attributes) { parent::fill($attributes); - + if(empty($this->attributes['disk'])){ $this->attributes['disk'] = env('DOCUMENT_FILESYSTEM', 'documents'); } - + return $this; } - + public function account() { return $this->belongsTo('App\Models\Account'); @@ -101,7 +106,7 @@ class Document extends EntityModel { return $this->belongsTo('App\Models\Invoice')->withTrashed(); } - + public function getDisk(){ 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'); } - + public function getDirectUrl(){ return static::getDirectFileUrl($this->path, $this->getDisk()); } - + public function getDirectPreviewUrl(){ return $this->preview?static::getDirectFileUrl($this->preview, $this->getDisk(), true):null; } - + public static function getDirectFileUrl($path, $disk, $prioritizeSpeed = false){ $adapter = $disk->getAdapter(); $fullPath = $adapter->applyPathPrefix($path); - + if($adapter instanceof \League\Flysystem\AwsS3v3\AwsS3Adapter) { $client = $adapter->getClient(); $command = $client->getCommand('GetObject', [ @@ -136,12 +141,12 @@ class Document extends EntityModel $secret = env('RACKSPACE_TEMP_URL_SECRET'); if($secret){ $object = $adapter->getContainer()->getObject($fullPath); - + if(env('RACKSPACE_TEMP_URL_SECRET_SET')){ // Go ahead and set the secret too $object->getService()->getAccount()->setTempUrlSecret($secret); - } - + } + $url = $object->getUrl(); $expiry = strtotime('+10 minutes'); $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 null; } - + public function getRaw(){ $disk = $this->getDisk(); - + return $disk->get($this->path); } - + public function getStream(){ $disk = $this->getDisk(); - + return $disk->readStream($this->path); } - + public function getRawPreview(){ $disk = $this->getDisk(); - + return $disk->get($this->preview); } - + public function getUrl(){ return url('documents/'.$this->public_id.'/'.$this->name); } - + public function getClientUrl($invitation){ return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name); } - + public function isPDFEmbeddable(){ return $this->type == 'jpeg' || $this->type == 'png' || $this->preview; } - + public function getVFSJSUrl(){ if(!$this->isPDFEmbeddable())return null; return url('documents/js/'.$this->public_id.'/'.$this->name.'.js'); } - + public function getClientVFSJSUrl(){ if(!$this->isPDFEmbeddable())return null; return url('client/documents/js/'.$this->public_id.'/'.$this->name.'.js'); } - + public function getPreviewUrl(){ return $this->preview?url('documents/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null; } - + public function toArray() { $array = parent::toArray(); - + 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(); - + return $array; } - + public function cloneDocument(){ $document = Document::createNew($this); $document->path = $this->path; @@ -219,7 +224,7 @@ class Document extends EntityModel $document->size = $this->size; $document->width = $this->width; $document->height = $this->height; - + return $document; } } @@ -230,11 +235,11 @@ Document::deleted(function ($document) { ->where('documents.path', '=', $document->path) ->where('documents.disk', '=', $document->disk) ->count(); - + if(!$same_path_count){ $document->getDisk()->delete($document->path); } - + if($document->preview){ $same_preview_count = DB::table('documents') ->where('documents.account_id', '=', $document->account_id) @@ -245,5 +250,5 @@ Document::deleted(function ($document) { $document->getDisk()->delete($document->preview); } } - -}); \ No newline at end of file + +}); diff --git a/app/Models/Product.php b/app/Models/Product.php index 05944c9fff94..548d7b384521 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -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; diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php index 8e17bfeec36f..a06f420ca8c6 100644 --- a/app/Ninja/Import/BaseTransformer.php +++ b/app/Ninja/Import/BaseTransformer.php @@ -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); @@ -53,7 +70,7 @@ class BaseTransformer extends TransformerAbstract if ( ! $date instanceof DateTime) { $date = DateTime::createFromFormat($format, $date); } - + 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; } - + protected function getVendorId($name) { $name = strtolower($name); return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; } - -} \ No newline at end of file + +} diff --git a/app/Ninja/Import/CSV/ProductTransformer.php b/app/Ninja/Import/CSV/ProductTransformer.php new file mode 100644 index 000000000000..248d3ed09cba --- /dev/null +++ b/app/Ninja/Import/CSV/ProductTransformer.php @@ -0,0 +1,22 @@ +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'), + ]; + }); + } +} diff --git a/app/Ninja/Repositories/DocumentRepository.php b/app/Ninja/Repositories/DocumentRepository.php index a2278b1843ae..0724144a7f09 100644 --- a/app/Ninja/Repositories/DocumentRepository.php +++ b/app/Ninja/Repositories/DocumentRepository.php @@ -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'); diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index cb687430ebc5..c15e261848d0 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -741,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'); diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php index eb0e7383e9e5..968f9ca44398 100644 --- a/app/Ninja/Repositories/ProductRepository.php +++ b/app/Ninja/Repositories/ProductRepository.php @@ -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') @@ -30,11 +37,11 @@ class ProductRepository extends BaseRepository 'products.deleted_at' ); } - + public function save($data, $product = null) { $publicId = isset($data['public_id']) ? $data['public_id'] : false; - + if ($product) { // do nothing } elseif ($publicId) { @@ -50,4 +57,4 @@ class ProductRepository extends BaseRepository return $product; } -} \ No newline at end of file +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e7587e48b11a..79e0f5dc032d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -29,8 +29,8 @@ class AppServiceProvider extends ServiceProvider { else{ $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 = '') { @@ -58,11 +58,11 @@ class AppServiceProvider extends ServiceProvider { $str = '