mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-08 20:22:42 +01:00
Add support for invoice attachments
This commit is contained in:
parent
5640c74f35
commit
88808d44bf
@ -95,6 +95,7 @@ module.exports = function(grunt) {
|
|||||||
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.no.min.js',
|
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.no.min.js',
|
||||||
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.es.min.js',
|
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.es.min.js',
|
||||||
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.sv.min.js',
|
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.sv.min.js',
|
||||||
|
'public/vendor/dropzone/dist/min/dropzone.min.js',
|
||||||
'public/vendor/typeahead.js/dist/typeahead.jquery.min.js',
|
'public/vendor/typeahead.js/dist/typeahead.jquery.min.js',
|
||||||
'public/vendor/accounting/accounting.min.js',
|
'public/vendor/accounting/accounting.min.js',
|
||||||
'public/vendor/spectrum/spectrum.js',
|
'public/vendor/spectrum/spectrum.js',
|
||||||
@ -137,6 +138,7 @@ module.exports = function(grunt) {
|
|||||||
'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css',
|
'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css',
|
||||||
'public/vendor/font-awesome/css/font-awesome.min.css',
|
'public/vendor/font-awesome/css/font-awesome.min.css',
|
||||||
'public/vendor/bootstrap-datepicker/dist/css/bootstrap-datepicker3.css',
|
'public/vendor/bootstrap-datepicker/dist/css/bootstrap-datepicker3.css',
|
||||||
|
'public/vendor/dropzone/dist/min/dropzone.min.css',
|
||||||
'public/vendor/spectrum/spectrum.css',
|
'public/vendor/spectrum/spectrum.css',
|
||||||
'public/css/bootstrap-combobox.css',
|
'public/css/bootstrap-combobox.css',
|
||||||
'public/css/typeahead.js-bootstrap.css',
|
'public/css/typeahead.js-bootstrap.css',
|
||||||
|
60
app/Http/Controllers/DocumentController.php
Normal file
60
app/Http/Controllers/DocumentController.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Datatable;
|
||||||
|
use Input;
|
||||||
|
use Redirect;
|
||||||
|
use Session;
|
||||||
|
use URL;
|
||||||
|
use Utils;
|
||||||
|
use View;
|
||||||
|
use Validator;
|
||||||
|
use Response;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Ninja\Repositories\DocumentRepository;
|
||||||
|
|
||||||
|
class DocumentController extends BaseController
|
||||||
|
{
|
||||||
|
protected $documentRepo;
|
||||||
|
protected $model = 'App\Models\Document';
|
||||||
|
|
||||||
|
public function __construct(DocumentRepository $documentRepo)
|
||||||
|
{
|
||||||
|
// parent::__construct();
|
||||||
|
|
||||||
|
$this->documentRepo = $documentRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get($publicId)
|
||||||
|
{
|
||||||
|
$document = Document::scope($publicId)
|
||||||
|
->withTrashed()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
if(!$this->checkViewPermission($document, $response)){
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$public_url = $document->getPublicUrl();
|
||||||
|
if($public_url){
|
||||||
|
return redirect($public_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$response = Response::make($document->getRaw(), 200);
|
||||||
|
$response->header('content-type', $document->type);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postUpload()
|
||||||
|
{
|
||||||
|
if(!$this->checkCreatePermission($response)){
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = Input::all();
|
||||||
|
|
||||||
|
$response = $this->documentRepo->upload($document);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ use App\Models\Activity;
|
|||||||
use App\Ninja\Mailers\ContactMailer as Mailer;
|
use App\Ninja\Mailers\ContactMailer as Mailer;
|
||||||
use App\Ninja\Repositories\InvoiceRepository;
|
use App\Ninja\Repositories\InvoiceRepository;
|
||||||
use App\Ninja\Repositories\ClientRepository;
|
use App\Ninja\Repositories\ClientRepository;
|
||||||
|
use App\Ninja\Repositories\DocumentRepository;
|
||||||
use App\Services\InvoiceService;
|
use App\Services\InvoiceService;
|
||||||
use App\Services\RecurringInvoiceService;
|
use App\Services\RecurringInvoiceService;
|
||||||
use App\Http\Requests\SaveInvoiceWithClientRequest;
|
use App\Http\Requests\SaveInvoiceWithClientRequest;
|
||||||
@ -32,11 +33,12 @@ class InvoiceController extends BaseController
|
|||||||
protected $mailer;
|
protected $mailer;
|
||||||
protected $invoiceRepo;
|
protected $invoiceRepo;
|
||||||
protected $clientRepo;
|
protected $clientRepo;
|
||||||
|
protected $documentRepo;
|
||||||
protected $invoiceService;
|
protected $invoiceService;
|
||||||
protected $recurringInvoiceService;
|
protected $recurringInvoiceService;
|
||||||
protected $model = 'App\Models\Invoice';
|
protected $model = 'App\Models\Invoice';
|
||||||
|
|
||||||
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, RecurringInvoiceService $recurringInvoiceService)
|
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, DocumentRepository $documentRepo, RecurringInvoiceService $recurringInvoiceService)
|
||||||
{
|
{
|
||||||
// parent::__construct();
|
// parent::__construct();
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ class InvoiceController extends BaseController
|
|||||||
{
|
{
|
||||||
$account = Auth::user()->account;
|
$account = Auth::user()->account;
|
||||||
$invoice = Invoice::scope($publicId)
|
$invoice = Invoice::scope($publicId)
|
||||||
->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'payments')
|
->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'documents', 'payments')
|
||||||
->withTrashed()
|
->withTrashed()
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
|
@ -132,6 +132,9 @@ Route::group(['middleware' => 'auth:user'], function() {
|
|||||||
Route::post('invoices/bulk', 'InvoiceController@bulk');
|
Route::post('invoices/bulk', 'InvoiceController@bulk');
|
||||||
Route::post('recurring_invoices/bulk', 'InvoiceController@bulk');
|
Route::post('recurring_invoices/bulk', 'InvoiceController@bulk');
|
||||||
|
|
||||||
|
Route::get('document/{public_id}/{filename?}', 'DocumentController@get');
|
||||||
|
Route::post('document', 'DocumentController@postUpload');
|
||||||
|
|
||||||
Route::get('quotes/create/{client_id?}', 'QuoteController@create');
|
Route::get('quotes/create/{client_id?}', 'QuoteController@create');
|
||||||
Route::get('quotes/{public_id}/clone', 'InvoiceController@cloneInvoice');
|
Route::get('quotes/{public_id}/clone', 'InvoiceController@cloneInvoice');
|
||||||
Route::get('quotes/{public_id}/edit', 'InvoiceController@edit');
|
Route::get('quotes/{public_id}/edit', 'InvoiceController@edit');
|
||||||
@ -425,6 +428,7 @@ if (!defined('CONTACT_EMAIL')) {
|
|||||||
define('MAX_IFRAME_URL_LENGTH', 250);
|
define('MAX_IFRAME_URL_LENGTH', 250);
|
||||||
define('MAX_LOGO_FILE_SIZE', 200); // KB
|
define('MAX_LOGO_FILE_SIZE', 200); // KB
|
||||||
define('MAX_FAILED_LOGINS', 10);
|
define('MAX_FAILED_LOGINS', 10);
|
||||||
|
define('DEFAULT_MAX_DOCUMENT_SIZE', 10000);// KB
|
||||||
define('DEFAULT_FONT_SIZE', 9);
|
define('DEFAULT_FONT_SIZE', 9);
|
||||||
define('DEFAULT_HEADER_FONT', 1);// Roboto
|
define('DEFAULT_HEADER_FONT', 1);// Roboto
|
||||||
define('DEFAULT_BODY_FONT', 1);// Roboto
|
define('DEFAULT_BODY_FONT', 1);// Roboto
|
||||||
|
102
app/Models/Document.php
Normal file
102
app/Models/Document.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class Document extends EntityModel
|
||||||
|
{
|
||||||
|
public static $extensions = array(
|
||||||
|
'png' => 'image/png',
|
||||||
|
'jpg' => 'image/jpeg',
|
||||||
|
'jpeg' => 'image/jpeg',
|
||||||
|
'pdf' => 'application/pdf',
|
||||||
|
'gif' => 'image/gif'
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $types = array(
|
||||||
|
'image/png' => array(
|
||||||
|
'extension' => 'png',
|
||||||
|
'image' => true,
|
||||||
|
),
|
||||||
|
'image/jpeg' => array(
|
||||||
|
'extension' => 'jpeg',
|
||||||
|
'image' => true,
|
||||||
|
),
|
||||||
|
'image/gif' => array(
|
||||||
|
'extension' => 'gif',
|
||||||
|
'image' => true,
|
||||||
|
),
|
||||||
|
'application/pdf' => array(
|
||||||
|
'extension' => 'pdf',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expenses
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $dates = ['deleted_at'];
|
||||||
|
|
||||||
|
public function fill(array $attributes)
|
||||||
|
{
|
||||||
|
parent::fill($attributes);
|
||||||
|
|
||||||
|
if(empty($this->attributes['disk'])){
|
||||||
|
$this->attributes['disk'] = env('LOGO_DISK', 'documents');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function account()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\Account');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\User');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function expense()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\Expense')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\Invoice')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisk(){
|
||||||
|
return Storage::disk(!empty($this->disk)?$this->disk:env('LOGO_DISK', 'documents'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDiskAttribute($value)
|
||||||
|
{
|
||||||
|
$this->attributes['disk'] = $value?$value:env('LOGO_DISK', 'documents');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPublicUrl(){
|
||||||
|
$disk = $this->getDisk();
|
||||||
|
$adapter = $disk->getAdapter();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRaw(){
|
||||||
|
$disk = $this->getDisk();
|
||||||
|
|
||||||
|
return $disk->get($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrl(){
|
||||||
|
return url('document/'.$this->public_id.'/'.$this->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray()
|
||||||
|
{
|
||||||
|
$array = parent::toArray();
|
||||||
|
$array['url'] = $this->getUrl();
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
}
|
@ -175,6 +175,11 @@ class Invoice extends EntityModel implements BalanceAffecting
|
|||||||
return $this->hasMany('App\Models\InvoiceItem')->orderBy('id');
|
return $this->hasMany('App\Models\InvoiceItem')->orderBy('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function documents()
|
||||||
|
{
|
||||||
|
return $this->hasMany('App\Models\Document')->orderBy('id');
|
||||||
|
}
|
||||||
|
|
||||||
public function invoice_status()
|
public function invoice_status()
|
||||||
{
|
{
|
||||||
return $this->belongsTo('App\Models\InvoiceStatus');
|
return $this->belongsTo('App\Models\InvoiceStatus');
|
||||||
@ -385,6 +390,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
|||||||
'amount',
|
'amount',
|
||||||
'balance',
|
'balance',
|
||||||
'invoice_items',
|
'invoice_items',
|
||||||
|
'documents',
|
||||||
'client',
|
'client',
|
||||||
'tax_name',
|
'tax_name',
|
||||||
'tax_rate',
|
'tax_rate',
|
||||||
|
128
app/Ninja/Repositories/DocumentRepository.php
Normal file
128
app/Ninja/Repositories/DocumentRepository.php
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<?php namespace app\Ninja\Repositories;
|
||||||
|
|
||||||
|
use DB;
|
||||||
|
use Utils;
|
||||||
|
use Response;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Ninja\Repositories\BaseRepository;
|
||||||
|
use Intervention\Image\Facades\Image;
|
||||||
|
use Session;
|
||||||
|
|
||||||
|
class DocumentRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
// Expenses
|
||||||
|
public function getClassName()
|
||||||
|
{
|
||||||
|
return 'App\Models\Document';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all()
|
||||||
|
{
|
||||||
|
return Document::scope()
|
||||||
|
->with('user')
|
||||||
|
->withTrashed()
|
||||||
|
->where('is_deleted', '=', false)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find()
|
||||||
|
{
|
||||||
|
$accountid = \Auth::user()->account_id;
|
||||||
|
$query = DB::table('clients')
|
||||||
|
->join('accounts', 'accounts.id', '=', 'clients.account_id')
|
||||||
|
->leftjoin('clients', 'clients.id', '=', 'clients.client_id')
|
||||||
|
/*->leftJoin('expenses', 'expenses.id', '=', 'clients.expense_id')
|
||||||
|
->leftJoin('invoices', 'invoices.id', '=', 'clients.invoice_id')*/
|
||||||
|
->where('documents.account_id', '=', $accountid)
|
||||||
|
/*->where('vendors.deleted_at', '=', null)
|
||||||
|
->where('clients.deleted_at', '=', null)*/
|
||||||
|
->select(
|
||||||
|
'documents.account_id',
|
||||||
|
'documents.path',
|
||||||
|
'documents.deleted_at',
|
||||||
|
'documents.size',
|
||||||
|
'documents.width',
|
||||||
|
'documents.height',
|
||||||
|
'documents.id',
|
||||||
|
'documents.is_deleted',
|
||||||
|
'documents.public_id',
|
||||||
|
'documents.invoice_id',
|
||||||
|
'documents.expense_id',
|
||||||
|
'documents.user_id',
|
||||||
|
'invoices.public_id as invoice_public_id',
|
||||||
|
'invoices.user_id as invoice_user_id',
|
||||||
|
'expenses.public_id as expense_public_id',
|
||||||
|
'expenses.user_id as expense_user_id'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload($input)
|
||||||
|
{
|
||||||
|
$uploaded = $input['file'];
|
||||||
|
|
||||||
|
$extension = strtolower($uploaded->extension());
|
||||||
|
if(empty(Document::$extensions[$extension])){
|
||||||
|
return Response::json([
|
||||||
|
'error' => 'Unsupported extension',
|
||||||
|
'code' => 400
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documentType = Document::$extensions[$extension];
|
||||||
|
$filePath = $uploaded->path();
|
||||||
|
$fileContents = null;
|
||||||
|
$name = $uploaded->getClientOriginalName();
|
||||||
|
|
||||||
|
if(filesize($filePath)/1000 > env('MAX_DOCUMENT_SIZE', DEFAULT_MAX_DOCUMENT_SIZE)){
|
||||||
|
return Response::json([
|
||||||
|
'error' => 'File too large',
|
||||||
|
'code' => 400
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($documentType == 'image/gif'){
|
||||||
|
// Convert gif to png
|
||||||
|
$img = Image::make($filePath);
|
||||||
|
|
||||||
|
$fileContents = (string) $img->encode('png');
|
||||||
|
$documentType = 'image/png';
|
||||||
|
$name = pathinfo($name)['filename'].'.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
$documentTypeData = Document::$types[$documentType];
|
||||||
|
|
||||||
|
|
||||||
|
$hash = $fileContents?sha1($fileContents):sha1_file($filePath);
|
||||||
|
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentTypeData['extension'];
|
||||||
|
|
||||||
|
$document = Document::createNew();
|
||||||
|
$disk = $document->getDisk();
|
||||||
|
if(!$disk->exists($filename)){// Have we already stored the same file
|
||||||
|
$disk->put($filename, $fileContents?$fileContents:file_get_contents($filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->path = $filename;
|
||||||
|
$document->type = $documentType;
|
||||||
|
$document->size = $fileContents?strlen($fileContents):filesize($filePath);
|
||||||
|
$document->name = substr($name, -255);
|
||||||
|
|
||||||
|
if(!empty($documentTypeData['image'])){
|
||||||
|
$imageSize = getimagesize($filePath);
|
||||||
|
if($imageSize){
|
||||||
|
$document->width = $imageSize[0];
|
||||||
|
$document->height = $imageSize[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->save();
|
||||||
|
|
||||||
|
|
||||||
|
return Response::json([
|
||||||
|
'error' => false,
|
||||||
|
'document' => $document,
|
||||||
|
'code' => 200
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ use App\Models\InvoiceItem;
|
|||||||
use App\Models\Invitation;
|
use App\Models\Invitation;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
|
use App\Models\Document;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Services\PaymentService;
|
use App\Services\PaymentService;
|
||||||
use App\Ninja\Repositories\BaseRepository;
|
use App\Ninja\Repositories\BaseRepository;
|
||||||
@ -398,6 +399,23 @@ class InvoiceRepository extends BaseRepository
|
|||||||
$invoice->invoice_items()->forceDelete();
|
$invoice->invoice_items()->forceDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($data['documents'] as $document_id){
|
||||||
|
$document = Document::scope($document_id)->first();
|
||||||
|
if($document && !$checkSubPermissions || $document->canEdit()){
|
||||||
|
$document->invoice_id = $invoice->id;
|
||||||
|
$document->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($invoice->documents as $document){
|
||||||
|
if(!in_array($document->id, $data['documents'])){
|
||||||
|
// Removed
|
||||||
|
if(!$checkSubPermissions || $document->canEdit()){
|
||||||
|
$document->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($data['invoice_items'] as $item) {
|
foreach ($data['invoice_items'] as $item) {
|
||||||
$item = (array) $item;
|
$item = (array) $item;
|
||||||
if (empty($item['cost']) && empty($item['product_key']) && empty($item['notes']) && empty($item['custom_value1']) && empty($item['custom_value2'])) {
|
if (empty($item['cost']) && empty($item['product_key']) && empty($item['notes']) && empty($item['custom_value1']) && empty($item['custom_value2'])) {
|
||||||
|
@ -26,7 +26,8 @@
|
|||||||
"quill": "~0.20.0",
|
"quill": "~0.20.0",
|
||||||
"datetimepicker": "~2.4.5",
|
"datetimepicker": "~2.4.5",
|
||||||
"stacktrace-js": "~1.0.1",
|
"stacktrace-js": "~1.0.1",
|
||||||
"fuse.js": "~2.0.2"
|
"fuse.js": "~2.0.2",
|
||||||
|
"dropzone": "~4.3.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"jquery": "~1.11"
|
"jquery": "~1.11"
|
||||||
|
66
database/migrations/2016_03_22_168362_add_documents.php
Normal file
66
database/migrations/2016_03_22_168362_add_documents.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddDocuments extends Migration {
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
/*Schema::table('accounts', function($table) {
|
||||||
|
$table->string('logo')->nullable()->default(null);
|
||||||
|
$table->unsignedInteger('logo_width');
|
||||||
|
$table->unsignedInteger('logo_height');
|
||||||
|
$table->unsignedInteger('logo_size');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('accounts')->update(array('logo' => ''));*/
|
||||||
|
Schema::dropIfExists('documents');
|
||||||
|
Schema::create('documents', function($t)
|
||||||
|
{
|
||||||
|
$t->increments('id');
|
||||||
|
$t->unsignedInteger('public_id')->nullable();
|
||||||
|
$t->unsignedInteger('account_id');
|
||||||
|
$t->unsignedInteger('user_id');
|
||||||
|
$t->unsignedInteger('invoice_id')->nullable();
|
||||||
|
$t->unsignedInteger('expense_id')->nullable();
|
||||||
|
$t->string('path');
|
||||||
|
$t->string('name');
|
||||||
|
$t->string('type');
|
||||||
|
$t->string('disk');
|
||||||
|
$t->unsignedInteger('size');
|
||||||
|
$t->unsignedInteger('width')->nullable();
|
||||||
|
$t->unsignedInteger('height')->nullable();
|
||||||
|
|
||||||
|
$t->timestamps();
|
||||||
|
$t->softDeletes();
|
||||||
|
|
||||||
|
$t->foreign('account_id')->references('id')->on('accounts');
|
||||||
|
$t->foreign('user_id')->references('id')->on('users');
|
||||||
|
$t->foreign('invoice_id')->references('id')->on('invoices');
|
||||||
|
$t->foreign('expense_id')->references('id')->on('expenses');
|
||||||
|
|
||||||
|
|
||||||
|
$t->unique( array('account_id','public_id') );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('accounts', function($table) {
|
||||||
|
$table->dropColumn('logo');
|
||||||
|
$table->dropColumn('logo_width');
|
||||||
|
$table->dropColumn('logo_height');
|
||||||
|
$table->dropColumn('logo_size');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('documents');
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use DB;
|
|
||||||
|
|
||||||
class AddDocuments extends Migration {
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
/*Schema::table('accounts', function($table) {
|
|
||||||
$table->string('logo')->nullable()->default(null);
|
|
||||||
$table->unsignedInteger('logo_width');
|
|
||||||
$table->unsignedInteger('logo_height');
|
|
||||||
$table->unsignedInteger('logo_size');
|
|
||||||
});*/
|
|
||||||
|
|
||||||
DB::table('accounts')->update(array('logo' => ''));
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::table('accounts', function($table) {
|
|
||||||
$table->dropColumn('logo');
|
|
||||||
$table->dropColumn('logo_width');
|
|
||||||
$table->dropColumn('logo_height');
|
|
||||||
$table->dropColumn('logo_size');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
25
public/css/built.css
vendored
25
public/css/built.css
vendored
File diff suppressed because one or more lines are too long
23
public/css/style.css
vendored
23
public/css/style.css
vendored
@ -1062,3 +1062,26 @@ td.right {
|
|||||||
div.panel-body div.panel-body {
|
div.panel-body div.panel-body {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Attached Documents */
|
||||||
|
.dropzone {
|
||||||
|
border:1px solid #ebe7e7;
|
||||||
|
background:#f9f9f9 !important;
|
||||||
|
border-radius:3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone .dz-preview.dz-image-preview{
|
||||||
|
background:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone .dz-preview .dz-image{
|
||||||
|
width:119px;
|
||||||
|
height:119px;
|
||||||
|
border-radius:5px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone .dz-preview.dz-image-preview .dz-image img{
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
@ -1097,6 +1097,10 @@ $LANG = array(
|
|||||||
'november' => 'November',
|
'november' => 'November',
|
||||||
'december' => 'December',
|
'december' => 'December',
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
'invoice_documents' => 'Attached Documents',
|
||||||
|
'document_upload_message' => 'Drop files here or click to upload.'
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return $LANG;
|
return $LANG;
|
||||||
|
@ -270,6 +270,7 @@
|
|||||||
<li role="presentation" class="active"><a href="#notes" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.note_to_client') }}</a></li>
|
<li role="presentation" class="active"><a href="#notes" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.note_to_client') }}</a></li>
|
||||||
<li role="presentation"><a href="#terms" aria-controls="terms" role="tab" data-toggle="tab">{{ trans("texts.{$entityType}_terms") }}</a></li>
|
<li role="presentation"><a href="#terms" aria-controls="terms" role="tab" data-toggle="tab">{{ trans("texts.{$entityType}_terms") }}</a></li>
|
||||||
<li role="presentation"><a href="#footer" aria-controls="footer" role="tab" data-toggle="tab">{{ trans("texts.{$entityType}_footer") }}</a></li>
|
<li role="presentation"><a href="#footer" aria-controls="footer" role="tab" data-toggle="tab">{{ trans("texts.{$entityType}_footer") }}</a></li>
|
||||||
|
<li role="presentation"><a href="#attached-documents" aria-controls="attached-documents" role="tab" data-toggle="tab">{{ trans("texts.{$entityType}_documents") }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@ -301,6 +302,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>') !!}
|
</div>') !!}
|
||||||
</div>
|
</div>
|
||||||
|
<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="file" type="file" multiple />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="foreach: documents">
|
||||||
|
<input type="hidden" name="documents[]" data-bind="value: public_id">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -675,10 +686,12 @@
|
|||||||
@include('invoices.knockout')
|
@include('invoices.knockout')
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
Dropzone.autoDiscover = false;
|
||||||
|
|
||||||
var products = {!! $products !!};
|
var products = {!! $products !!};
|
||||||
var clients = {!! $clients !!};
|
var clients = {!! $clients !!};
|
||||||
var account = {!! Auth::user()->account !!};
|
var account = {!! Auth::user()->account !!};
|
||||||
|
var dropzone;
|
||||||
|
|
||||||
var clientMap = {};
|
var clientMap = {};
|
||||||
var $clientSelect = $('select#client');
|
var $clientSelect = $('select#client');
|
||||||
@ -905,6 +918,43 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
applyComboboxListeners();
|
applyComboboxListeners();
|
||||||
|
|
||||||
|
// Initialize document upload
|
||||||
|
dropzone = new Dropzone('#document-upload', {
|
||||||
|
url:{!! json_encode(url('document')) !!},
|
||||||
|
params:{
|
||||||
|
_token:"{{ Session::getToken() }}"
|
||||||
|
},
|
||||||
|
acceptedFiles:{!! json_encode(implode(',',array_keys(\App\Models\Document::$types))) !!},
|
||||||
|
addRemoveLinks:true,
|
||||||
|
maxFileSize:{{floatval(env('MAX_DOCUMENT_SIZE', DEFAULT_MAX_DOCUMENT_SIZE)/1000)}},
|
||||||
|
dictDefaultMessage:{!! json_encode(trans('texts.document_upload_message')) !!}
|
||||||
|
});
|
||||||
|
dropzone.on("addedfile",handleDocumentAdded);
|
||||||
|
dropzone.on("removedfile",handleDocumentRemoved);
|
||||||
|
dropzone.on("success",handleDocumentUploaded);
|
||||||
|
|
||||||
|
for (var i=0; i<model.invoice().documents().length; i++) {
|
||||||
|
var document = model.invoice().documents()[i];
|
||||||
|
var mockFile = {
|
||||||
|
name:document.name(),
|
||||||
|
size:document.size(),
|
||||||
|
type:document.type(),
|
||||||
|
public_id:document.public_id(),
|
||||||
|
status:Dropzone.SUCCESS,
|
||||||
|
accepted:true,
|
||||||
|
url:document.url(),
|
||||||
|
mock:true,
|
||||||
|
index:i
|
||||||
|
};
|
||||||
|
|
||||||
|
dropzone.emit('addedfile', mockFile);
|
||||||
|
dropzone.emit('complete', mockFile);
|
||||||
|
if(document.type().match(/image.*/)){
|
||||||
|
dropzone.emit('thumbnail', mockFile, document.url());
|
||||||
|
}
|
||||||
|
dropzone.files.push(mockFile);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onFrequencyChange(){
|
function onFrequencyChange(){
|
||||||
@ -1263,6 +1313,21 @@
|
|||||||
model.invoice().invoice_number(number);
|
model.invoice().invoice_number(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDocumentAdded(file){
|
||||||
|
if(file.mock)return;
|
||||||
|
file.index = model.invoice().documents().length;
|
||||||
|
model.invoice().addDocument({name:file.name, size:file.size, type:file.type});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDocumentRemoved(file){
|
||||||
|
model.invoice().removeDocument(file.public_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDocumentUploaded(file, response){
|
||||||
|
file.public_id = response.document.public_id
|
||||||
|
model.invoice().documents()[file.index].update(response.document);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@stop
|
@stop
|
||||||
|
@ -226,6 +226,7 @@ function InvoiceModel(data) {
|
|||||||
self.auto_bill = ko.observable();
|
self.auto_bill = ko.observable();
|
||||||
self.invoice_status_id = ko.observable(0);
|
self.invoice_status_id = ko.observable(0);
|
||||||
self.invoice_items = ko.observableArray();
|
self.invoice_items = ko.observableArray();
|
||||||
|
self.documents = ko.observableArray();
|
||||||
self.amount = ko.observable(0);
|
self.amount = ko.observable(0);
|
||||||
self.balance = ko.observable(0);
|
self.balance = ko.observable(0);
|
||||||
self.invoice_design_id = ko.observable(1);
|
self.invoice_design_id = ko.observable(1);
|
||||||
@ -251,6 +252,11 @@ function InvoiceModel(data) {
|
|||||||
return new ItemModel(options.data);
|
return new ItemModel(options.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'documents': {
|
||||||
|
create: function(options) {
|
||||||
|
return new DocumentModel(options.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
'tax': {
|
'tax': {
|
||||||
create: function(options) {
|
create: function(options) {
|
||||||
return new TaxRateModel(options.data);
|
return new TaxRateModel(options.data);
|
||||||
@ -268,6 +274,18 @@ function InvoiceModel(data) {
|
|||||||
return itemModel;
|
return itemModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.addDocument = function() {
|
||||||
|
var documentModel = new DocumentModel();
|
||||||
|
self.documents.push(documentModel);
|
||||||
|
return documentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.removeDocument = function(public_id) {
|
||||||
|
self.documents.remove(function(document) {
|
||||||
|
return document.public_id() == public_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
ko.mapping.fromJS(data, self.mapping, self);
|
ko.mapping.fromJS(data, self.mapping, self);
|
||||||
} else {
|
} else {
|
||||||
@ -811,6 +829,23 @@ function ItemModel(data) {
|
|||||||
this.onSelect = function() {}
|
this.onSelect = function() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DocumentModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.public_id = ko.observable(0);
|
||||||
|
self.size = ko.observable(0);
|
||||||
|
self.name = ko.observable('');
|
||||||
|
self.type = ko.observable('');
|
||||||
|
self.url = ko.observable('');
|
||||||
|
|
||||||
|
self.update = function(data){
|
||||||
|
ko.mapping.fromJS(data, {}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
self.update(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom binding for product key typeahead */
|
/* Custom binding for product key typeahead */
|
||||||
ko.bindingHandlers.typeahead = {
|
ko.bindingHandlers.typeahead = {
|
||||||
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||||
|
Loading…
Reference in New Issue
Block a user