1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 12:12:48 +01:00

Display documents in invoice PDF

This commit is contained in:
Joshua Dwire 2016-03-23 18:40:42 -04:00
parent d810ba7f6b
commit 942f543bbb
20 changed files with 222 additions and 33 deletions

View File

@ -171,7 +171,7 @@ module.exports = function(grunt) {
'public/js/pdf_viewer.js',
'public/js/compatibility.js',
'public/js/pdfmake.min.js',
'public/js/vfs_fonts.js',
'public/js/vfs.js',
],
dest: 'public/pdf.built.js',
nonull: true

File diff suppressed because one or more lines are too long

View File

@ -70,6 +70,31 @@ class DocumentController extends BaseController
return $response;
}
public function getVFSJS($publicId, $name){
$document = Document::scope($publicId)
->firstOrFail();
if(substr($name, -3)=='.js'){
$name = substr($name, 0, -3);
}
if(!$this->checkViewPermission($document, $response)){
return $response;
}
if(substr($document->type, 0, 6) != 'image/'){
return Response::view('error', array('error'=>'Image does not exist!'), 404);
}
$content = $document->preview?$document->getRawPreview():$document->getRaw();
$content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")';
$response = Response::make($content, 200);
$response->header('content-type', 'text/javascript');
$response->header('cache-control', 'max-age=31536000');
return $response;
}
public function postUpload()
{
if (!Utils::isPro()) {

View File

@ -536,7 +536,7 @@ class InvoiceController extends BaseController
public function invoiceHistory($publicId)
{
$invoice = Invoice::withTrashed()->scope($publicId)->firstOrFail();
$invoice->load('user', 'invoice_items', 'account.country', 'client.contacts', 'client.country');
$invoice->load('user', 'invoice_items', 'documents', 'account.country', 'client.contacts', 'client.country');
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
$invoice->is_pro = Auth::user()->isPro();

View File

@ -7,10 +7,12 @@ use URL;
use Input;
use Utils;
use Request;
use Response;
use Session;
use Datatable;
use App\Models\Gateway;
use App\Models\Invitation;
use App\Models\Document;
use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\ActivityRepository;
@ -22,8 +24,9 @@ class PublicClientController extends BaseController
{
private $invoiceRepo;
private $paymentRepo;
private $documentRepo;
public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, PaymentService $paymentService)
public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, PaymentService $paymentService)
{
$this->invoiceRepo = $invoiceRepo;
$this->paymentRepo = $paymentRepo;
@ -372,5 +375,44 @@ class PublicClientController extends BaseController
return $invitation;
}
public function getDocumentVFSJS($publicId, $name){
if (!$invitation = $this->getInvitation()) {
return $this->returnError();
}
$clientId = $invitation->invoice->client_id;
$document = Document::scope($publicId, $invitation->account_id)->first();
if(!$document || substr($document->type, 0, 6) != 'image/'){
return Response::view('error', array('error'=>'Image does not exist!'), 404);
}
$authorized = false;
if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){
$authorized = true;
} else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){
$authorized = true;
}
if(!$authorized){
return Response::view('error', array('error'=>'Not authorized'), 403);
}
if(substr($name, -3)=='.js'){
$name = substr($name, 0, -3);
}
$content = $document->preview?$document->getRawPreview():$document->getRaw();
$content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")';
$response = Response::make($content, 200);
$response->header('content-type', 'text/javascript');
$response->header('cache-control', 'max-age=31536000');
return $response;
}
}

View File

@ -47,6 +47,7 @@ Route::group(['middleware' => 'auth:client'], function() {
Route::get('client/invoices', 'PublicClientController@invoiceIndex');
Route::get('client/payments', 'PublicClientController@paymentIndex');
Route::get('client/dashboard', 'PublicClientController@dashboard');
Route::get('client/document/js/{public_id}/{filename}', 'PublicClientController@getDocumentVFSJS');
});
Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable'));
@ -133,6 +134,7 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::post('recurring_invoices/bulk', 'InvoiceController@bulk');
Route::get('document/{public_id}/{filename?}', 'DocumentController@get');
Route::get('document/js/{public_id}/{filename}', 'DocumentController@getVFSJS');
Route::get('document/preview/{public_id}/{filename?}', 'DocumentController@getPreview');
Route::post('document', 'DocumentController@postUpload');
@ -430,7 +432,7 @@ if (!defined('CONTACT_EMAIL')) {
define('MAX_LOGO_FILE_SIZE', 200); // KB
define('MAX_FAILED_LOGINS', 10);
define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB
define('DOCUMENT_PREVIEW_SIZE', env('DOCUMENT_PREVIEW_SIZE', 150));// pixels
define('DOCUMENT_PREVIEW_SIZE', env('DOCUMENT_PREVIEW_SIZE', 300));// pixels
define('DEFAULT_FONT_SIZE', 9);
define('DEFAULT_HEADER_FONT', 1);// Roboto
define('DEFAULT_BODY_FONT', 1);// Roboto

View File

@ -132,6 +132,14 @@ class Document extends EntityModel
return url('document/'.$this->public_id.'/'.$this->name);
}
public function getVFSJSUrl(){
return url('document/js/'.$this->public_id.'/'.$this->name.'.js');
}
public function getClientVFSJSUrl(){
return url('client/document/js/'.$this->public_id.'/'.$this->name.'.js');
}
public function getPreviewUrl(){
return $this->preview?url('document/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null;
}

View File

@ -94,12 +94,14 @@ class DocumentRepository extends BaseRepository
if(in_array($documentType, array('image/jpeg','image/png','image/gif','image/bmp','image/tiff'))){
$makePreview = false;
$imageSize = getimagesize($filePath);
$width = $imageSize[0];
$height = $imageSize[1];
$imgManagerConfig = array();
if(in_array($documentType, array('image/gif','image/bmp','image/tiff'))){
// Needs to be converted
$makePreview = true;
} else {
if($imageSize[0] > DOCUMENT_PREVIEW_SIZE || $imageSize[1] > DOCUMENT_PREVIEW_SIZE){
if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){
$makePreview = true;
}
}
@ -126,14 +128,30 @@ class DocumentRepository extends BaseRepository
$imgManager = new ImageManager($imgManagerConfig);
$img = $imgManager->make($filePath);
$img->fit(DOCUMENT_PREVIEW_SIZE, DOCUMENT_PREVIEW_SIZE, function ($constraint) {
$constraint->upsize();
});
if($width <= DOCUMENT_PREVIEW_SIZE && $height <= DOCUMENT_PREVIEW_SIZE){
$previewWidth = $width;
$previewHeight = $height;
} else if($width > $height) {
$previewWidth = DOCUMENT_PREVIEW_SIZE;
$previewHeight = $height * DOCUMENT_PREVIEW_SIZE / $width;
} else {
$previewHeight = DOCUMENT_PREVIEW_SIZE;
$previewWidth = $width * DOCUMENT_PREVIEW_SIZE / $height;
}
$img->resize($previewWidth, $previewHeight);
$previewContent = (string) $img->encode($previewType);
$disk->put($document->preview, $previewContent);
$base64 = base64_encode($previewContent);
}
}
else{
$base64 = base64_encode($disk->get($document->preview));
}
}else{
$base64 = base64_encode(file_get_contents($filePath));
}
}
$document->path = $filename;
@ -147,11 +165,16 @@ class DocumentRepository extends BaseRepository
}
$document->save();
$doc_array = $document->toArray();
if(!empty($base64)){
$mime = !empty($previewType)?Document::$extensions[$previewType]:$documentType;
$doc_array['base64'] = 'data:'.$mime.';base64,'.$base64;
}
return Response::json([
'error' => false,
'document' => $document,
'document' => $doc_array,
'code' => 200
], 200);
}

View File

@ -600,7 +600,7 @@ class InvoiceRepository extends BaseRepository
return false;
}
$invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country');
$invoice->load('user', 'invoice_items', 'documents', 'invoice_design', 'account.country', 'client.contacts', 'client.country');
$client = $invoice->client;
if (!$client || $client->is_deleted) {

View File

@ -10,14 +10,15 @@ class AddDocuments extends Migration {
*/
public function up()
{
/*Schema::table('accounts', function($table) {
Schema::table('accounts', function($table) {
$table->string('logo')->nullable()->default(null);
$table->unsignedInteger('logo_width');
$table->unsignedInteger('logo_height');
$table->unsignedInteger('logo_size');
$table->boolean('invoice_embed_documents')->default(1);
});
DB::table('accounts')->update(array('logo' => ''));*/
DB::table('accounts')->update(array('logo' => ''));
Schema::dropIfExists('documents');
Schema::create('documents', function($t)
{
@ -59,6 +60,7 @@ class AddDocuments extends Migration {
$table->dropColumn('logo_width');
$table->dropColumn('logo_height');
$table->dropColumn('logo_size');
$table->dropColumn('invoice_embed_documents');
});
Schema::dropIfExists('documents');

View File

@ -31101,11 +31101,12 @@ function GetPdfMake(invoice, javascript, callback) {
function addFont(font){
if(window.ninjaFontVfs[font.folder]){
folder = 'fonts/'+font.folder;
pdfMake.fonts[font.name] = {
normal: font.folder+'/'+font.normal,
italics: font.folder+'/'+font.italics,
bold: font.folder+'/'+font.bold,
bolditalics: font.folder+'/'+font.bolditalics
normal: folder+'/'+font.normal,
italics: folder+'/'+font.italics,
bold: folder+'/'+font.bold,
bolditalics: folder+'/'+font.bolditalics
}
}
}
@ -31136,6 +31137,7 @@ NINJA.decodeJavascript = function(invoice, javascript)
'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16,
'invoiceLineItems': NINJA.invoiceLines(invoice),
'invoiceLineItemColumns': NINJA.invoiceColumns(invoice),
'invoiceDocuments' : NINJA.invoiceDocuments(invoice),
'quantityWidth': NINJA.quantityWidth(invoice),
'taxWidth': NINJA.taxWidth(invoice),
'clientDetails': NINJA.clientDetails(invoice),
@ -31393,6 +31395,31 @@ NINJA.invoiceLines = function(invoice) {
return NINJA.prepareDataTable(grid, 'invoiceItems');
}
NINJA.invoiceDocuments = function(invoice) {
if(!invoice.documents || !invoice.account.invoice_embed_documents)return[];
var stack = [];
var stackItem = null;
var j = 0;
for (var i = 0; i < invoice.documents.length; i++) {
var document = invoice.documents[i];
var path = document.base64;
if(!path && (document.preview_url || document.type == 'image/png' || document.type == 'image/jpeg')){
path = 'docs/'+document.public_id+'/'+document.name;
}
if(path && (window.pdfMake.vfs[path] || document.base64)){
if(j%3==0){
stackItem = {columns:[]};
stack.push(stackItem);
}
stackItem.columns.push({stack:[{image:path,style:'invoiceDocument',fit:[150,150]}], width:175})
j++;
}
}
return {stack:stack};
}
NINJA.subtotals = function(invoice, hideBalance)
{
if (!invoice) {

View File

@ -109,11 +109,12 @@ function GetPdfMake(invoice, javascript, callback) {
function addFont(font){
if(window.ninjaFontVfs[font.folder]){
folder = 'fonts/'+font.folder;
pdfMake.fonts[font.name] = {
normal: font.folder+'/'+font.normal,
italics: font.folder+'/'+font.italics,
bold: font.folder+'/'+font.bold,
bolditalics: font.folder+'/'+font.bolditalics
normal: folder+'/'+font.normal,
italics: folder+'/'+font.italics,
bold: folder+'/'+font.bold,
bolditalics: folder+'/'+font.bolditalics
}
}
}
@ -144,6 +145,7 @@ NINJA.decodeJavascript = function(invoice, javascript)
'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16,
'invoiceLineItems': NINJA.invoiceLines(invoice),
'invoiceLineItemColumns': NINJA.invoiceColumns(invoice),
'invoiceDocuments' : NINJA.invoiceDocuments(invoice),
'quantityWidth': NINJA.quantityWidth(invoice),
'taxWidth': NINJA.taxWidth(invoice),
'clientDetails': NINJA.clientDetails(invoice),
@ -401,6 +403,31 @@ NINJA.invoiceLines = function(invoice) {
return NINJA.prepareDataTable(grid, 'invoiceItems');
}
NINJA.invoiceDocuments = function(invoice) {
if(!invoice.documents || !invoice.account.invoice_embed_documents)return[];
var stack = [];
var stackItem = null;
var j = 0;
for (var i = 0; i < invoice.documents.length; i++) {
var document = invoice.documents[i];
var path = document.base64;
if(!path && (document.preview_url || document.type == 'image/png' || document.type == 'image/jpeg')){
path = 'docs/'+document.public_id+'/'+document.name;
}
if(path && (window.pdfMake.vfs[path] || document.base64)){
if(j%3==0){
stackItem = {columns:[]};
stack.push(stackItem);
}
stackItem.columns.push({stack:[{image:path,style:'invoiceDocument',fit:[150,150]}], width:175})
j++;
}
}
return {stack:stack};
}
NINJA.subtotals = function(invoice, hideBalance)
{
if (!invoice) {

View File

@ -3,7 +3,12 @@ if(window.ninjaFontVfs)ninjaLoadFontVfs();
function ninjaLoadFontVfs(){
jQuery.each(window.ninjaFontVfs, function(font, files){
jQuery.each(files, function(filename, file){
window.pdfMake.vfs[font+'/'+filename] = file;
window.pdfMake.vfs['fonts/'+font+'/'+filename] = file;
});
})
}
function ninjaAddVFSDoc(name,content){
window.pdfMake.vfs['docs/'+name] = content;
if(window.refreshPDF)refreshPDF(true);
jQuery(document).trigger('ninjaVFSDocAdded');
}

View File

@ -7926,7 +7926,12 @@ if(window.ninjaFontVfs)ninjaLoadFontVfs();
function ninjaLoadFontVfs(){
jQuery.each(window.ninjaFontVfs, function(font, files){
jQuery.each(files, function(filename, file){
window.pdfMake.vfs[font+'/'+filename] = file;
window.pdfMake.vfs['fonts/'+font+'/'+filename] = file;
});
})
}
function ninjaAddVFSDoc(name,content){
window.pdfMake.vfs['docs/'+name] = content;
if(window.refreshPDF)refreshPDF(true);
jQuery(document).trigger('ninjaVFSDocAdded');
}

View File

@ -1099,8 +1099,9 @@ $LANG = array(
// Documents
'invoice_documents' => 'Attached Documents',
'document_upload_message' => 'Drop files here or click to upload.'
'document_upload_message' => 'Drop files here or click to upload.',
'invoice_embed_documents' => 'Embed Documents',
'invoice_embed_documents_help' => 'Include attached images in the invoice.',
);
return $LANG;

View File

@ -48,6 +48,7 @@
function getPDFString(cb) {
invoice.is_pro = {!! Auth::user()->isPro() ? 'true' : 'false' !!};
invoice.account.hide_quantity = $('#hide_quantity').is(":checked");
invoice.account.invoice_embed_documents = $('#invoice_embed_documents').is(":checked");
invoice.account.hide_paid_to_date = $('#hide_paid_to_date').is(":checked");
invoice.invoice_design_id = $('#invoice_design_id').val();
@ -207,6 +208,7 @@
{!! Former::checkbox('hide_quantity')->text(trans('texts.hide_quantity_help')) !!}
{!! Former::checkbox('hide_paid_to_date')->text(trans('texts.hide_paid_to_date_help')) !!}
{!! Former::checkbox('invoice_embed_documents')->text(trans('texts.invoice_embed_documents_help')) !!}
</div>
</div>

View File

@ -270,7 +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"><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>
@if (Auth::user()->account->isPro())
@if ($account->isPro())
<li role="presentation"><a href="#attached-documents" aria-controls="attached-documents" role="tab" data-toggle="tab">{{ trans("texts.{$entityType}_documents") }}</a></li>
@endif
</ul>
@ -304,7 +304,7 @@
</div>
</div>') !!}
</div>
@if (Auth::user()->account->isPro())
@if ($account->isPro())
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload" class="dropzone">
<div class="fallback">
@ -1319,7 +1319,7 @@
model.invoice().invoice_number(number);
}
@if (Auth::user()->account->isPro())
@if ($account->isPro())
function handleDocumentAdded(file){
if(file.mock)return;
file.index = model.invoice().documents().length;
@ -1328,14 +1328,21 @@
function handleDocumentRemoved(file){
model.invoice().removeDocument(file.public_id);
refreshPDF(true);
}
function handleDocumentUploaded(file, response){
file.public_id = response.document.public_id
model.invoice().documents()[file.index].update(response.document);
refreshPDF(true);
}
@endif
</script>
@if ($account->isPro() && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endforeach
@endif
@stop

View File

@ -56,5 +56,10 @@
<br/>&nbsp;<br/>
@include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800])
@if (Auth::user()->account->isPro() && Auth::user()->account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endforeach
@endif
@stop

View File

@ -123,7 +123,7 @@
$('#theFrame').attr('src', string).show();
} else {
if (isRefreshing) {
//needsRefresh = true;
needsRefresh = true;
return;
}
isRefreshing = true;

View File

@ -45,7 +45,11 @@
@endif
<div class="clearfix"></div><p>&nbsp;</p>
@if ($account->isPro() && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" {{ Input::has('phantomjs')?'':'async' }}></script>
@endforeach
@endif
<script type="text/javascript">
window.invoice = {!! $invoice->toJson() !!};
@ -90,6 +94,5 @@
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
</div>
@stop