mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-06 23:54:36 -04:00
Merge branch 'develop' of github.com:hillelcoren/invoice-ninja into develop
Conflicts: resources/views/invoices/knockout.blade.php
This commit is contained in:
commit
d2d22fbb50
24
.env.example
24
.env.example
@ -44,3 +44,27 @@ API_SECRET=password
|
||||
#GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google
|
||||
|
||||
#GOOGLE_MAPS_API_KEY=
|
||||
|
||||
#S3_KEY=
|
||||
#S3_SECRET=
|
||||
#S3_REGION=
|
||||
#S3_BUCKET=
|
||||
|
||||
#RACKSPACE_USERNAME=
|
||||
#RACKSPACE_KEY=
|
||||
#RACKSPACE_CONTAINER=
|
||||
#RACKSPACE_REGION=
|
||||
|
||||
#RACKSPACE_TEMP_URL_SECRET=
|
||||
|
||||
# If this is set to anything, the URL secret will be set the next
|
||||
# time a file is downloaded through the client portal.
|
||||
# Only set this temporarily, as it slows things down.
|
||||
#RACKSPACE_TEMP_URL_SECRET_SET=
|
||||
|
||||
#DOCUMENT_FILESYSTEM=
|
||||
|
||||
#MAX_DOCUMENT_SIZE # KB
|
||||
#MAX_EMAIL_DOCUMENTS_SIZE # Total KB
|
||||
#MAX_ZIP_DOCUMENTS_SIZE # Total KB (uncompressed)
|
||||
#DOCUMENT_PREVIEW_SIZE # Pixels
|
@ -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.es.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/accounting/accounting.min.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/font-awesome/css/font-awesome.min.css',
|
||||
'public/vendor/bootstrap-datepicker/dist/css/bootstrap-datepicker3.css',
|
||||
'public/vendor/dropzone/dist/min/dropzone.min.css',
|
||||
'public/vendor/spectrum/spectrum.css',
|
||||
'public/css/bootstrap-combobox.css',
|
||||
'public/css/typeahead.js-bootstrap.css',
|
||||
@ -169,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
|
||||
|
41
app/Console/Commands/RemoveOrphanedDocuments.php
Normal file
41
app/Console/Commands/RemoveOrphanedDocuments.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php namespace App\Console\Commands;
|
||||
|
||||
use DateTime;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RemoveOrphanedDocuments extends Command
|
||||
{
|
||||
protected $name = 'ninja:remove-orphaned-documents';
|
||||
protected $description = 'Removes old documents not associated with an expense or invoice';
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' Running RemoveOrphanedDocuments...');
|
||||
|
||||
$documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', array(new DateTime('-1 hour')))
|
||||
->get();
|
||||
|
||||
$this->info(count($documents).' orphaned document(s) found');
|
||||
|
||||
foreach ($documents as $document) {
|
||||
$document->delete();
|
||||
}
|
||||
|
||||
$this->info('Done');
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
{
|
||||
return array(
|
||||
//array('example', InputArgument::REQUIRED, 'An example argument.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
return array(
|
||||
//array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null),
|
||||
);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected $commands = [
|
||||
'App\Console\Commands\SendRecurringInvoices',
|
||||
'App\Console\Commands\RemoveOrphanedDocuments',
|
||||
'App\Console\Commands\ResetData',
|
||||
'App\Console\Commands\CheckData',
|
||||
'App\Console\Commands\SendRenewalInvoices',
|
||||
|
File diff suppressed because one or more lines are too long
140
app/Http/Controllers/DocumentController.php
Normal file
140
app/Http/Controllers/DocumentController.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?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)
|
||||
->firstOrFail();
|
||||
|
||||
if(!$this->checkViewPermission($document, $response)){
|
||||
return $response;
|
||||
}
|
||||
|
||||
return static::getDownloadResponse($document);
|
||||
}
|
||||
|
||||
public static function getDownloadResponse($document){
|
||||
$direct_url = $document->getDirectUrl();
|
||||
if($direct_url){
|
||||
return redirect($direct_url);
|
||||
}
|
||||
|
||||
$stream = $document->getStream();
|
||||
|
||||
if($stream){
|
||||
$headers = [
|
||||
'Content-Type' => Document::$types[$document->type]['mime'],
|
||||
'Content-Length' => $document->size,
|
||||
];
|
||||
|
||||
$response = Response::stream(function() use ($stream) {
|
||||
fpassthru($stream);
|
||||
}, 200, $headers);
|
||||
}
|
||||
else{
|
||||
$response = Response::make($document->getRaw(), 200);
|
||||
$response->header('content-type', Document::$types[$document->type]['mime']);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getPreview($publicId)
|
||||
{
|
||||
$document = Document::scope($publicId)
|
||||
->firstOrFail();
|
||||
|
||||
if(!$this->checkViewPermission($document, $response)){
|
||||
return $response;
|
||||
}
|
||||
|
||||
if(empty($document->preview)){
|
||||
return Response::view('error', array('error'=>'Preview does not exist!'), 404);
|
||||
}
|
||||
|
||||
$direct_url = $document->getDirectPreviewUrl();
|
||||
if($direct_url){
|
||||
return redirect($direct_url);
|
||||
}
|
||||
|
||||
$previewType = pathinfo($document->preview, PATHINFO_EXTENSION);
|
||||
$response = Response::make($document->getRawPreview(), 200);
|
||||
$response->header('content-type', Document::$types[$previewType]['mime']);
|
||||
|
||||
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(!$document->isPDFEmbeddable()){
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$this->checkCreatePermission($response)){
|
||||
return $response;
|
||||
}
|
||||
|
||||
$result = $this->documentRepo->upload(Input::all()['file'], $doc_array);
|
||||
|
||||
if(is_string($result)){
|
||||
return Response::json([
|
||||
'error' => $result,
|
||||
'code' => 400
|
||||
], 400);
|
||||
} else {
|
||||
return Response::json([
|
||||
'error' => false,
|
||||
'document' => $doc_array,
|
||||
'code' => 200
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
}
|
@ -99,7 +99,7 @@ class ExpenseController extends BaseController
|
||||
|
||||
public function edit($publicId)
|
||||
{
|
||||
$expense = Expense::scope($publicId)->firstOrFail();
|
||||
$expense = Expense::scope($publicId)->with('documents')->firstOrFail();
|
||||
|
||||
if(!$this->checkEditPermission($expense, $response)){
|
||||
return $response;
|
||||
@ -163,7 +163,14 @@ class ExpenseController extends BaseController
|
||||
*/
|
||||
public function update(UpdateExpenseRequest $request)
|
||||
{
|
||||
$expense = $this->expenseService->save($request->input());
|
||||
$data = $request->input();
|
||||
$data['documents'] = $request->file('documents');
|
||||
|
||||
if(!$this->checkUpdatePermission($data, $response)){
|
||||
return $response;
|
||||
}
|
||||
|
||||
$expense = $this->expenseService->save($data, true);
|
||||
|
||||
Session::flash('message', trans('texts.updated_expense'));
|
||||
|
||||
@ -177,7 +184,14 @@ class ExpenseController extends BaseController
|
||||
|
||||
public function store(CreateExpenseRequest $request)
|
||||
{
|
||||
$expense = $this->expenseService->save($request->input());
|
||||
$data = $request->input();
|
||||
$data['documents'] = $request->file('documents');
|
||||
|
||||
if(!$this->checkUpdatePermission($data, $response)){
|
||||
return $response;
|
||||
}
|
||||
|
||||
$expense = $this->expenseService->save($data);
|
||||
|
||||
Session::flash('message', trans('texts.created_expense'));
|
||||
|
||||
@ -195,7 +209,6 @@ class ExpenseController extends BaseController
|
||||
$expenses = Expense::scope($ids)->with('client')->get();
|
||||
$clientPublicId = null;
|
||||
$currencyId = null;
|
||||
$data = [];
|
||||
|
||||
// Validate that either all expenses do not have a client or if there is a client, it is the same client
|
||||
foreach ($expenses as $expense)
|
||||
@ -220,19 +233,11 @@ class ExpenseController extends BaseController
|
||||
Session::flash('error', trans('texts.expense_error_invoiced'));
|
||||
return Redirect::to('expenses');
|
||||
}
|
||||
|
||||
$account = Auth::user()->account;
|
||||
$data[] = [
|
||||
'publicId' => $expense->public_id,
|
||||
'description' => $expense->public_notes,
|
||||
'qty' => 1,
|
||||
'cost' => $expense->present()->converted_amount,
|
||||
];
|
||||
}
|
||||
|
||||
return Redirect::to("invoices/create/{$clientPublicId}")
|
||||
->with('expenseCurrencyId', $currencyId)
|
||||
->with('expenses', $data);
|
||||
->with('expenses', $ids);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -17,12 +17,14 @@ use App\Models\Invoice;
|
||||
use App\Models\Client;
|
||||
use App\Models\Account;
|
||||
use App\Models\Product;
|
||||
use App\Models\Expense;
|
||||
use App\Models\TaxRate;
|
||||
use App\Models\InvoiceDesign;
|
||||
use App\Models\Activity;
|
||||
use App\Ninja\Mailers\ContactMailer as Mailer;
|
||||
use App\Ninja\Repositories\InvoiceRepository;
|
||||
use App\Ninja\Repositories\ClientRepository;
|
||||
use App\Ninja\Repositories\DocumentRepository;
|
||||
use App\Services\InvoiceService;
|
||||
use App\Services\RecurringInvoiceService;
|
||||
use App\Http\Requests\SaveInvoiceWithClientRequest;
|
||||
@ -32,11 +34,12 @@ class InvoiceController extends BaseController
|
||||
protected $mailer;
|
||||
protected $invoiceRepo;
|
||||
protected $clientRepo;
|
||||
protected $documentRepo;
|
||||
protected $invoiceService;
|
||||
protected $recurringInvoiceService;
|
||||
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();
|
||||
|
||||
@ -89,7 +92,7 @@ class InvoiceController extends BaseController
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$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', 'expenses', 'expenses.documents', 'payments')
|
||||
->withTrashed()
|
||||
->firstOrFail();
|
||||
|
||||
@ -240,6 +243,11 @@ class InvoiceController extends BaseController
|
||||
$invoice = $account->createInvoice($entityType, $clientId);
|
||||
$invoice->public_id = 0;
|
||||
|
||||
if(Session::get('expenses')){
|
||||
$invoice->expenses = Expense::scope(Session::get('expenses'))->with('documents')->get();
|
||||
}
|
||||
|
||||
|
||||
$clients = Client::scope()->with('contacts', 'country')->orderBy('name');
|
||||
if(!Auth::user()->hasPermission('view_all')){
|
||||
$clients = $clients->where('clients.user_id', '=', Auth::user()->id);
|
||||
@ -356,7 +364,6 @@ class InvoiceController extends BaseController
|
||||
'recurringDueDateHelp' => $recurringDueDateHelp,
|
||||
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
|
||||
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
|
||||
'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null,
|
||||
'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null,
|
||||
];
|
||||
|
||||
@ -370,6 +377,7 @@ class InvoiceController extends BaseController
|
||||
public function store(SaveInvoiceWithClientRequest $request)
|
||||
{
|
||||
$data = $request->input();
|
||||
$data['documents'] = $request->file('documents');
|
||||
|
||||
if(!$this->checkUpdatePermission($data, $response)){
|
||||
return $response;
|
||||
@ -410,6 +418,7 @@ class InvoiceController extends BaseController
|
||||
public function update(SaveInvoiceWithClientRequest $request)
|
||||
{
|
||||
$data = $request->input();
|
||||
$data['documents'] = $request->file('documents');
|
||||
|
||||
if(!$this->checkUpdatePermission($data, $response)){
|
||||
return $response;
|
||||
@ -540,7 +549,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', 'expenses', 'expenses.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();
|
||||
|
@ -7,27 +7,33 @@ 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;
|
||||
use App\Ninja\Repositories\DocumentRepository;
|
||||
use App\Events\InvoiceInvitationWasViewed;
|
||||
use App\Events\QuoteInvitationWasViewed;
|
||||
use App\Services\PaymentService;
|
||||
use Barracuda\ArchiveStream\ZipArchive;
|
||||
|
||||
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, DocumentRepository $documentRepo, PaymentService $paymentService)
|
||||
{
|
||||
$this->invoiceRepo = $invoiceRepo;
|
||||
$this->paymentRepo = $paymentRepo;
|
||||
$this->activityRepo = $activityRepo;
|
||||
$this->documentRepo = $documentRepo;
|
||||
$this->paymentService = $paymentService;
|
||||
}
|
||||
|
||||
@ -119,6 +125,7 @@ class PublicClientController extends BaseController
|
||||
'hideLogo' => $account->isWhiteLabel(),
|
||||
'hideHeader' => $account->isNinjaAccount(),
|
||||
'hideDashboard' => !$account->enable_client_portal,
|
||||
'showDocuments' => $account->isPro(),
|
||||
'clientViewCSS' => $account->clientViewCSS(),
|
||||
'clientFontUrl' => $account->getFontsUrl(),
|
||||
'invoice' => $invoice->hidePrivateFields(),
|
||||
@ -133,6 +140,15 @@ class PublicClientController extends BaseController
|
||||
'phantomjs' => Input::has('phantomjs'),
|
||||
);
|
||||
|
||||
if($account->isPro() && $this->canCreateZip()){
|
||||
$zipDocs = $this->getInvoiceZipDocuments($invoice, $size);
|
||||
|
||||
if(count($zipDocs) > 1){
|
||||
$data['documentsZipURL'] = URL::to("client/documents/{$invitation->invitation_key}");
|
||||
$data['documentsZipSize'] = $size;
|
||||
}
|
||||
}
|
||||
|
||||
return View::make('invoices.view', $data);
|
||||
}
|
||||
|
||||
@ -252,6 +268,7 @@ class PublicClientController extends BaseController
|
||||
'color' => $color,
|
||||
'hideLogo' => $account->isWhiteLabel(),
|
||||
'hideDashboard' => !$account->enable_client_portal,
|
||||
'showDocuments' => $account->isPro(),
|
||||
'clientViewCSS' => $account->clientViewCSS(),
|
||||
'clientFontUrl' => $account->getFontsUrl(),
|
||||
'title' => trans('texts.invoices'),
|
||||
@ -284,6 +301,7 @@ class PublicClientController extends BaseController
|
||||
'color' => $color,
|
||||
'hideLogo' => $account->isWhiteLabel(),
|
||||
'hideDashboard' => !$account->enable_client_portal,
|
||||
'showDocuments' => $account->isPro(),
|
||||
'clientViewCSS' => $account->clientViewCSS(),
|
||||
'clientFontUrl' => $account->getFontsUrl(),
|
||||
'entityType' => ENTITY_PAYMENT,
|
||||
@ -322,6 +340,7 @@ class PublicClientController extends BaseController
|
||||
'color' => $color,
|
||||
'hideLogo' => $account->isWhiteLabel(),
|
||||
'hideDashboard' => !$account->enable_client_portal,
|
||||
'showDocuments' => $account->isPro(),
|
||||
'clientViewCSS' => $account->clientViewCSS(),
|
||||
'clientFontUrl' => $account->getFontsUrl(),
|
||||
'title' => trans('texts.quotes'),
|
||||
@ -342,6 +361,39 @@ class PublicClientController extends BaseController
|
||||
return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, Input::get('sSearch'));
|
||||
}
|
||||
|
||||
public function documentIndex()
|
||||
{
|
||||
if (!$invitation = $this->getInvitation()) {
|
||||
return $this->returnError();
|
||||
}
|
||||
$account = $invitation->account;
|
||||
$color = $account->primary_color ? $account->primary_color : '#0b4d78';
|
||||
|
||||
$data = [
|
||||
'color' => $color,
|
||||
'hideLogo' => $account->isWhiteLabel(),
|
||||
'hideDashboard' => !$account->enable_client_portal,
|
||||
'showDocuments' => $account->isPro(),
|
||||
'clientViewCSS' => $account->clientViewCSS(),
|
||||
'clientFontUrl' => $account->getFontsUrl(),
|
||||
'title' => trans('texts.documents'),
|
||||
'entityType' => ENTITY_DOCUMENT,
|
||||
'columns' => Utils::trans(['invoice_number', 'name', 'document_date', 'document_size']),
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
}
|
||||
|
||||
|
||||
public function documentDatatable()
|
||||
{
|
||||
if (!$invitation = $this->getInvitation()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->documentRepo->getClientDatatable($invitation->contact_id, ENTITY_DOCUMENT, Input::get('sSearch'));
|
||||
}
|
||||
|
||||
private function returnError($error = false)
|
||||
{
|
||||
return response()->view('error', [
|
||||
@ -373,4 +425,147 @@ 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->isPDFEmbeddable()){
|
||||
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;
|
||||
}
|
||||
|
||||
protected function canCreateZip(){
|
||||
return function_exists('gmp_init');
|
||||
}
|
||||
|
||||
protected function getInvoiceZipDocuments($invoice, &$size=0){
|
||||
$documents = $invoice->documents;
|
||||
|
||||
foreach($invoice->expenses as $expense){
|
||||
$documents = $documents->merge($expense->documents);
|
||||
}
|
||||
|
||||
$documents = $documents->sortBy('size');
|
||||
|
||||
$size = 0;
|
||||
$maxSize = MAX_ZIP_DOCUMENTS_SIZE * 1000;
|
||||
$toZip = array();
|
||||
foreach($documents as $document){
|
||||
if($size + $document->size > $maxSize)break;
|
||||
|
||||
if(!empty($toZip[$document->name])){
|
||||
// This name is taken
|
||||
if($toZip[$document->name]->hash != $document->hash){
|
||||
// 2 different files with the same name
|
||||
$nameInfo = pathinfo($document->name);
|
||||
|
||||
for($i = 1;; $i++){
|
||||
$name = $nameInfo['filename'].' ('.$i.').'.$nameInfo['extension'];
|
||||
|
||||
if(empty($toZip[$name])){
|
||||
$toZip[$name] = $document;
|
||||
$size += $document->size;
|
||||
break;
|
||||
} else if ($toZip[$name]->hash == $document->hash){
|
||||
// We're not adding this after all
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else{
|
||||
$toZip[$document->name] = $document;
|
||||
$size += $document->size;
|
||||
}
|
||||
}
|
||||
|
||||
return $toZip;
|
||||
}
|
||||
|
||||
public function getInvoiceDocumentsZip($invitationKey){
|
||||
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
||||
return $this->returnError();
|
||||
}
|
||||
|
||||
Session::put('invitation_key', $invitationKey); // track current invitation
|
||||
|
||||
$invoice = $invitation->invoice;
|
||||
|
||||
$toZip = $this->getInvoiceZipDocuments($invoice);
|
||||
|
||||
if(!count($toZip)){
|
||||
return Response::view('error', array('error'=>'No documents small enough'), 404);
|
||||
}
|
||||
|
||||
$zip = new ZipArchive($invitation->account->name.' Invoice '.$invoice->invoice_number.'.zip');
|
||||
return Response::stream(function() use ($toZip, $zip) {
|
||||
foreach($toZip as $name=>$document){
|
||||
$fileStream = $document->getStream();
|
||||
if($fileStream){
|
||||
$zip->init_file_stream_transfer($name, $document->size, array('time'=>$document->created_at->timestamp));
|
||||
while ($buffer = fread($fileStream, 256000))$zip->stream_file_part($buffer);
|
||||
fclose($fileStream);
|
||||
$zip->complete_file_stream();
|
||||
}
|
||||
else{
|
||||
$zip->add_file($name, $document->getRaw());
|
||||
}
|
||||
}
|
||||
$zip->finish();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
public function getDocument($invitationKey, $publicId){
|
||||
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
||||
return $this->returnError();
|
||||
}
|
||||
|
||||
Session::put('invitation_key', $invitationKey); // track current invitation
|
||||
|
||||
$clientId = $invitation->invoice->client_id;
|
||||
$document = Document::scope($publicId, $invitation->account_id)->firstOrFail();
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
return DocumentController::getDownloadResponse($document);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class Authenticate {
|
||||
|
||||
// Does this account require portal passwords?
|
||||
$account = Account::whereId($account_id)->first();
|
||||
if(!$account->enable_portal_password || !$account->isPro()){
|
||||
if($account && (!$account->enable_portal_password || !$account->isPro())){
|
||||
$authenticated = true;
|
||||
}
|
||||
|
||||
|
@ -45,14 +45,20 @@ Route::group(['middleware' => 'auth:client'], function() {
|
||||
Route::get('complete', 'PaymentController@offsite_payment');
|
||||
Route::get('client/quotes', 'PublicClientController@quoteIndex');
|
||||
Route::get('client/invoices', 'PublicClientController@invoiceIndex');
|
||||
Route::get('client/documents', 'PublicClientController@documentIndex');
|
||||
Route::get('client/payments', 'PublicClientController@paymentIndex');
|
||||
Route::get('client/dashboard', 'PublicClientController@dashboard');
|
||||
Route::get('client/document/js/{public_id}/{filename}', 'PublicClientController@getDocumentVFSJS');
|
||||
Route::get('client/document/{invitation_key}/{public_id}/{filename?}', 'PublicClientController@getDocument');
|
||||
Route::get('client/documents/{invitation_key}/{filename?}', 'PublicClientController@getInvoiceDocumentsZip');
|
||||
|
||||
Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable'));
|
||||
Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable'));
|
||||
Route::get('api/client.documents', array('as'=>'api.client.documents', 'uses'=>'PublicClientController@documentDatatable'));
|
||||
Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable'));
|
||||
Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable'));
|
||||
});
|
||||
|
||||
Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable'));
|
||||
Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable'));
|
||||
Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable'));
|
||||
Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable'));
|
||||
|
||||
Route::get('license', 'PaymentController@show_license_payment');
|
||||
Route::post('license', 'PaymentController@do_license_payment');
|
||||
@ -132,6 +138,11 @@ Route::group(['middleware' => 'auth:user'], function() {
|
||||
Route::post('invoices/bulk', 'InvoiceController@bulk');
|
||||
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');
|
||||
|
||||
Route::get('quotes/create/{client_id?}', 'QuoteController@create');
|
||||
Route::get('quotes/{public_id}/clone', 'InvoiceController@cloneInvoice');
|
||||
Route::get('quotes/{public_id}/edit', 'InvoiceController@edit');
|
||||
@ -310,6 +321,7 @@ if (!defined('CONTACT_EMAIL')) {
|
||||
define('ENTITY_CLIENT', 'client');
|
||||
define('ENTITY_CONTACT', 'contact');
|
||||
define('ENTITY_INVOICE', 'invoice');
|
||||
define('ENTITY_DOCUMENT', 'document');
|
||||
define('ENTITY_INVOICE_ITEMS', 'invoice_items');
|
||||
define('ENTITY_INVITATION', 'invitation');
|
||||
define('ENTITY_RECURRING_INVOICE', 'recurring_invoice');
|
||||
@ -425,6 +437,10 @@ if (!defined('CONTACT_EMAIL')) {
|
||||
define('MAX_IFRAME_URL_LENGTH', 250);
|
||||
define('MAX_LOGO_FILE_SIZE', 200); // KB
|
||||
define('MAX_FAILED_LOGINS', 10);
|
||||
define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB
|
||||
define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000));// Total KB
|
||||
define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000));// Total KB (uncompressed)
|
||||
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
|
||||
|
@ -6,9 +6,11 @@ use Session;
|
||||
use DateTime;
|
||||
use Event;
|
||||
use Cache;
|
||||
use Document;
|
||||
use App;
|
||||
use File;
|
||||
use App\Events\UserSettingsChanged;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
@ -384,26 +386,64 @@ class Account extends Eloquent
|
||||
|
||||
public function hasLogo()
|
||||
{
|
||||
return file_exists($this->getLogoFullPath());
|
||||
if($this->logo == ''){
|
||||
$this->calculateLogoDetails();
|
||||
}
|
||||
|
||||
public function getLogoPath()
|
||||
{
|
||||
$fileName = 'logo/' . $this->account_key;
|
||||
|
||||
return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg';
|
||||
return !empty($this->logo);
|
||||
}
|
||||
|
||||
public function getLogoFullPath()
|
||||
{
|
||||
$fileName = public_path() . '/logo/' . $this->account_key;
|
||||
public function getLogoDisk(){
|
||||
return Storage::disk(env('LOGO_FILESYSTEM', 'logos'));
|
||||
}
|
||||
|
||||
return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg';
|
||||
protected function calculateLogoDetails(){
|
||||
$disk = $this->getLogoDisk();
|
||||
|
||||
if($disk->exists($this->account_key.'.png')){
|
||||
$this->logo = $this->account_key.'.png';
|
||||
} else if($disk->exists($this->account_key.'.jpg')) {
|
||||
$this->logo = $this->account_key.'.jpg';
|
||||
}
|
||||
|
||||
if(!empty($this->logo)){
|
||||
$image = imagecreatefromstring($disk->get($this->logo));
|
||||
$this->logo_width = imagesx($image);
|
||||
$this->logo_height = imagesy($image);
|
||||
$this->logo_size = $disk->size($this->logo);
|
||||
} else {
|
||||
$this->logo = null;
|
||||
}
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function getLogoRaw(){
|
||||
if(!$this->hasLogo()){
|
||||
return null;
|
||||
}
|
||||
|
||||
$disk = $this->getLogoDisk();
|
||||
return $disk->get($this->logo);
|
||||
}
|
||||
|
||||
public function getLogoURL()
|
||||
{
|
||||
return SITE_URL . '/' . $this->getLogoPath();
|
||||
if(!$this->hasLogo()){
|
||||
return null;
|
||||
}
|
||||
|
||||
$disk = $this->getLogoDisk();
|
||||
$adapter = $disk->getAdapter();
|
||||
|
||||
if($adapter instanceof \League\Flysystem\Adapter\Local) {
|
||||
// Stored locally
|
||||
$logo_url = str_replace(public_path(), url('/'), $adapter->applyPathPrefix($this->logo), $count);
|
||||
if($count == 1){
|
||||
return str_replace(DIRECTORY_SEPARATOR, '/', $logo_url);
|
||||
}
|
||||
}
|
||||
|
||||
Document::getDirectFileUrl($this->logo, $this->getDisk());
|
||||
}
|
||||
|
||||
public function getToken($name)
|
||||
@ -419,24 +459,20 @@ class Account extends Eloquent
|
||||
|
||||
public function getLogoWidth()
|
||||
{
|
||||
$path = $this->getLogoFullPath();
|
||||
if (!file_exists($path)) {
|
||||
return 0;
|
||||
if(!$this->hasLogo()){
|
||||
return null;
|
||||
}
|
||||
list($width, $height) = getimagesize($path);
|
||||
|
||||
return $width;
|
||||
return $this->logo_width;
|
||||
}
|
||||
|
||||
public function getLogoHeight()
|
||||
{
|
||||
$path = $this->getLogoFullPath();
|
||||
if (!file_exists($path)) {
|
||||
return 0;
|
||||
if(!$this->hasLogo()){
|
||||
return null;
|
||||
}
|
||||
list($width, $height) = getimagesize($path);
|
||||
|
||||
return $height;
|
||||
return $this->logo_height;
|
||||
}
|
||||
|
||||
public function createInvoice($entityType = ENTITY_INVOICE, $clientId = null)
|
||||
@ -815,12 +851,11 @@ class Account extends Eloquent
|
||||
|
||||
public function getLogoSize()
|
||||
{
|
||||
if (!$this->hasLogo()) {
|
||||
return 0;
|
||||
if(!$this->hasLogo()){
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $this->getLogoFullPath();
|
||||
return round(File::size($filename) / 1000);
|
||||
return round($this->logo_size / 1000);
|
||||
}
|
||||
|
||||
public function isLogoTooLarge()
|
||||
|
263
app/Models/Document.php
Normal file
263
app/Models/Document.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use DB;
|
||||
use Auth;
|
||||
|
||||
class Document extends EntityModel
|
||||
{
|
||||
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/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',
|
||||
),
|
||||
'ai' => array(
|
||||
'mime' => 'application/postscript',
|
||||
),
|
||||
'svg' => array(
|
||||
'mime' => 'image/svg+xml',
|
||||
),
|
||||
'jpeg' => array(
|
||||
'mime' => 'image/jpeg',
|
||||
),
|
||||
'tiff' => array(
|
||||
'mime' => 'image/tiff',
|
||||
),
|
||||
'pdf' => array(
|
||||
'mime' => 'application/pdf',
|
||||
),
|
||||
'gif' => array(
|
||||
'mime' => 'image/gif',
|
||||
),
|
||||
'psd' => array(
|
||||
'mime' => 'image/vnd.adobe.photoshop',
|
||||
),
|
||||
'txt' => array(
|
||||
'mime' => 'text/plain',
|
||||
),
|
||||
'zip' => array(
|
||||
'mime' => 'application/zip',
|
||||
),
|
||||
'doc' => array(
|
||||
'mime' => 'application/msword',
|
||||
),
|
||||
'xls' => array(
|
||||
'mime' => 'application/vnd.ms-excel',
|
||||
),
|
||||
'ppt' => array(
|
||||
'mime' => 'application/vnd.ms-powerpoint',
|
||||
),
|
||||
'xlsx' => array(
|
||||
'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
),
|
||||
'docx' => array(
|
||||
'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
),
|
||||
'pptx' => array(
|
||||
'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');
|
||||
}
|
||||
|
||||
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('DOCUMENT_FILESYSTEM', 'documents'));
|
||||
}
|
||||
|
||||
public function setDiskAttribute($value)
|
||||
{
|
||||
$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', [
|
||||
'Bucket' => $adapter->getBucket(),
|
||||
'Key' => $fullPath
|
||||
]);
|
||||
|
||||
return (string) $client->createPresignedRequest($command, '+10 minutes')->getUri();
|
||||
} else if (!$prioritizeSpeed // Rackspace temp URLs are slow, so we don't use them for previews
|
||||
&& $adapter instanceof \League\Flysystem\Rackspace\RackspaceAdapter) {
|
||||
$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());
|
||||
$body = sprintf("%s\n%d\n%s", 'GET', $expiry, $urlPath);
|
||||
$hash = hash_hmac('sha1', $body, $secret);
|
||||
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('document/'.$this->public_id.'/'.$this->name);
|
||||
}
|
||||
|
||||
public function getClientUrl($invitation){
|
||||
return url('client/document/'.$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('document/js/'.$this->public_id.'/'.$this->name.'.js');
|
||||
}
|
||||
|
||||
public function getClientVFSJSUrl(){
|
||||
if(!$this->isPDFEmbeddable())return null;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
$document->preview = $this->preview;
|
||||
$document->name = $this->name;
|
||||
$document->type = $this->type;
|
||||
$document->disk = $this->disk;
|
||||
$document->hash = $this->hash;
|
||||
$document->size = $this->size;
|
||||
$document->width = $this->width;
|
||||
$document->height = $this->height;
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
public static function canCreate(){
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function canViewItem($document){
|
||||
if(Auth::user()->hasPermission('view_all'))return true;
|
||||
if($document->expense){
|
||||
if($document->expense->invoice)return $document->expense->invoice->canView();
|
||||
return $document->expense->canView();
|
||||
}
|
||||
if($document->invoice)return $document->invoice->canView();
|
||||
return Auth::user()->id == $item->user_id;
|
||||
}
|
||||
}
|
||||
|
||||
Document::deleted(function ($document) {
|
||||
$same_path_count = DB::table('documents')
|
||||
->where('documents.account_id', '=', $document->account_id)
|
||||
->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)
|
||||
->where('documents.preview', '=', $document->preview)
|
||||
->where('documents.disk', '=', $document->disk)
|
||||
->count();
|
||||
if(!$same_preview_count){
|
||||
$document->getDisk()->delete($document->preview);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
@ -24,9 +24,14 @@ class EntityModel extends Eloquent
|
||||
Utils::fatalError();
|
||||
}
|
||||
|
||||
if(method_exists($className, 'withTrashed')){
|
||||
$lastEntity = $className::withTrashed()
|
||||
->scope(false, $entity->account_id)
|
||||
->orderBy('public_id', 'DESC')
|
||||
->scope(false, $entity->account_id);
|
||||
} else {
|
||||
$lastEntity = $className::scope(false, $entity->account_id);
|
||||
}
|
||||
|
||||
$lastEntity = $lastEntity->orderBy('public_id', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($lastEntity) {
|
||||
|
@ -53,6 +53,11 @@ class Expense extends EntityModel
|
||||
return $this->belongsTo('App\Models\Invoice')->withTrashed();
|
||||
}
|
||||
|
||||
public function documents()
|
||||
{
|
||||
return $this->hasMany('App\Models\Document')->orderBy('id');
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if($this->expense_number)
|
||||
@ -80,6 +85,20 @@ class Expense extends EntityModel
|
||||
{
|
||||
return $this->invoice_currency_id != $this->expense_currency_id;
|
||||
}
|
||||
|
||||
public function convertedAmount()
|
||||
{
|
||||
return round($this->amount * $this->exchange_rate, 2);
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$array = parent::toArray();
|
||||
|
||||
if(empty($this->visible) || in_array('converted_amount', $this->visible))$array['previewconverted_amount_url'] = $this->convertedAmount();
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
Expense::creating(function ($expense) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use Utils;
|
||||
use DateTime;
|
||||
use URL;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
use App\Models\BalanceAffecting;
|
||||
@ -175,6 +176,11 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
return $this->hasMany('App\Models\InvoiceItem')->orderBy('id');
|
||||
}
|
||||
|
||||
public function documents()
|
||||
{
|
||||
return $this->hasMany('App\Models\Document')->orderBy('id');
|
||||
}
|
||||
|
||||
public function invoice_status()
|
||||
{
|
||||
return $this->belongsTo('App\Models\InvoiceStatus');
|
||||
@ -385,6 +391,8 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
'amount',
|
||||
'balance',
|
||||
'invoice_items',
|
||||
'documents',
|
||||
'expenses',
|
||||
'client',
|
||||
'tax_name',
|
||||
'tax_rate',
|
||||
@ -457,6 +465,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
'custom_invoice_text_label2',
|
||||
'custom_invoice_item_label1',
|
||||
'custom_invoice_item_label2',
|
||||
'invoice_embed_documents'
|
||||
]);
|
||||
|
||||
foreach ($this->invoice_items as $invoiceItem) {
|
||||
@ -481,6 +490,26 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($this->documents as $document) {
|
||||
$document->setVisible([
|
||||
'public_id',
|
||||
'name',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($this->expenses as $expense) {
|
||||
$expense->setVisible([
|
||||
'documents',
|
||||
]);
|
||||
|
||||
foreach ($expense->documents as $document) {
|
||||
$document->setVisible([
|
||||
'public_id',
|
||||
'name',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -861,6 +890,18 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
return $taxes;
|
||||
}
|
||||
|
||||
public function hasDocuments(){
|
||||
if(count($this->documents))return true;
|
||||
return $this->hasExpenseDocuments();
|
||||
}
|
||||
|
||||
public function hasExpenseDocuments(){
|
||||
foreach($this->expenses as $expense){
|
||||
if(count($expense->documents))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Invoice::creating(function ($invoice) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace App\Ninja\Mailers;
|
||||
|
||||
use Form;
|
||||
use HTML;
|
||||
use Utils;
|
||||
use Event;
|
||||
use URL;
|
||||
@ -28,6 +29,7 @@ class ContactMailer extends Mailer
|
||||
'invoice',
|
||||
'quote',
|
||||
'password',
|
||||
'documents',
|
||||
'viewLink',
|
||||
'viewButton',
|
||||
'paymentLink',
|
||||
@ -60,8 +62,31 @@ class ContactMailer extends Mailer
|
||||
$pdfString = $invoice->getPDFString();
|
||||
}
|
||||
|
||||
$documentStrings = array();
|
||||
if ($account->document_email_attachment && $invoice->hasDocuments()) {
|
||||
$documents = $invoice->documents;
|
||||
|
||||
foreach($invoice->expenses as $expense){
|
||||
$documents = $documents->merge($expense->documents);
|
||||
}
|
||||
|
||||
$documents = $documents->sortBy('size');
|
||||
|
||||
$size = 0;
|
||||
$maxSize = MAX_EMAIL_DOCUMENTS_SIZE * 1000;
|
||||
foreach($documents as $document){
|
||||
$size += $document->size;
|
||||
if($size > $maxSize)break;
|
||||
|
||||
$documentStrings[] = array(
|
||||
'name' => $document->name,
|
||||
'data' => $document->getRaw(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($invoice->invitations as $invitation) {
|
||||
$response = $this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString);
|
||||
$response = $this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString, $documentStrings);
|
||||
if ($response === true) {
|
||||
$sent = true;
|
||||
}
|
||||
@ -80,7 +105,7 @@ class ContactMailer extends Mailer
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString)
|
||||
private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString, $documentStrings)
|
||||
{
|
||||
$client = $invoice->client;
|
||||
$account = $invoice->account;
|
||||
@ -127,6 +152,7 @@ class ContactMailer extends Mailer
|
||||
'account' => $account,
|
||||
'client' => $client,
|
||||
'invoice' => $invoice,
|
||||
'documents' => $documentStrings,
|
||||
];
|
||||
|
||||
if ($account->attatchPDF()) {
|
||||
@ -263,6 +289,20 @@ class ContactMailer extends Mailer
|
||||
$invitation = $data['invitation'];
|
||||
$invoice = $invitation->invoice;
|
||||
$passwordHTML = isset($data['password'])?'<p>'.trans('texts.password').': '.$data['password'].'<p>':false;
|
||||
$documentsHTML = '';
|
||||
|
||||
if($account->isPro() && $invoice->hasDocuments()){
|
||||
$documentsHTML .= trans('texts.email_documents_header').'<ul>';
|
||||
foreach($invoice->documents as $document){
|
||||
$documentsHTML .= '<li><a href="'.HTML::entities($document->getClientUrl($invitation)).'">'.HTML::entities($document->name).'</a></li>';
|
||||
}
|
||||
foreach($invoice->expenses as $expense){
|
||||
foreach($expense->documents as $document){
|
||||
$documentsHTML .= '<li><a href="'.HTML::entities($document->getClientUrl($invitation)).'">'.HTML::entities($document->name).'</a></li>';
|
||||
}
|
||||
}
|
||||
$documentsHTML .= '</ul>';
|
||||
}
|
||||
|
||||
$variables = [
|
||||
'$footer' => $account->getEmailFooter(),
|
||||
@ -285,6 +325,7 @@ class ContactMailer extends Mailer
|
||||
'$customClient2' => $account->custom_client_label2,
|
||||
'$customInvoice1' => $account->custom_invoice_text_label1,
|
||||
'$customInvoice2' => $account->custom_invoice_text_label2,
|
||||
'$documents' => $documentsHTML,
|
||||
];
|
||||
|
||||
// Add variables for available payment types
|
||||
|
@ -44,6 +44,13 @@ class Mailer
|
||||
if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) {
|
||||
$message->attachData($data['pdfString'], $data['pdfFileName']);
|
||||
}
|
||||
|
||||
// Attach documents to the email
|
||||
if(!empty($data['documents'])){
|
||||
foreach($data['documents'] as $document){
|
||||
$message->attachData($document['data'], $document['name']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $this->handleSuccess($response, $data);
|
||||
|
@ -16,14 +16,9 @@ class ExpensePresenter extends Presenter {
|
||||
return Utils::fromSqlDate($this->entity->expense_date);
|
||||
}
|
||||
|
||||
public function converted_amount()
|
||||
{
|
||||
return round($this->entity->amount * $this->entity->exchange_rate, 2);
|
||||
}
|
||||
|
||||
public function invoiced_amount()
|
||||
{
|
||||
return $this->entity->invoice_id ? $this->converted_amount() : 0;
|
||||
return $this->entity->invoice_id ? $this->entity->convertedAmount() : 0;
|
||||
}
|
||||
|
||||
public function link()
|
||||
|
@ -475,7 +475,7 @@ class AccountRepository
|
||||
$item->account_id = $user->account->id;
|
||||
$item->account_name = $user->account->getDisplayName();
|
||||
$item->pro_plan_paid = $user->account->pro_plan_paid;
|
||||
$item->logo_path = $user->account->hasLogo() ? $user->account->getLogoPath() : null;
|
||||
$item->logo_url = $user->account->hasLogo() ? $user->account->getLogoUrl() : null;
|
||||
$data[] = $item;
|
||||
}
|
||||
|
||||
|
231
app/Ninja/Repositories/DocumentRepository.php
Normal file
231
app/Ninja/Repositories/DocumentRepository.php
Normal file
@ -0,0 +1,231 @@
|
||||
<?php namespace app\Ninja\Repositories;
|
||||
|
||||
use DB;
|
||||
use Utils;
|
||||
use Response;
|
||||
use App\Models\Document;
|
||||
use App\Ninja\Repositories\BaseRepository;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Session;
|
||||
use Form;
|
||||
|
||||
class DocumentRepository extends BaseRepository
|
||||
{
|
||||
// Expenses
|
||||
public function getClassName()
|
||||
{
|
||||
return 'App\Models\Document';
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
return Document::scope()
|
||||
->with('user')
|
||||
->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($uploaded, &$doc_array=null)
|
||||
{
|
||||
$extension = strtolower($uploaded->getClientOriginalExtension());
|
||||
if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){
|
||||
$documentType = Document::$extraExtensions[$extension];
|
||||
}
|
||||
else{
|
||||
$documentType = $extension;
|
||||
}
|
||||
|
||||
if(empty(Document::$types[$documentType])){
|
||||
return 'Unsupported file type';
|
||||
}
|
||||
|
||||
$documentTypeData = Document::$types[$documentType];
|
||||
|
||||
$filePath = $uploaded->path();
|
||||
$name = $uploaded->getClientOriginalName();
|
||||
$size = filesize($filePath);
|
||||
|
||||
if($size/1000 > MAX_DOCUMENT_SIZE){
|
||||
return 'File too large';
|
||||
}
|
||||
|
||||
|
||||
|
||||
$hash = sha1_file($filePath);
|
||||
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType;
|
||||
|
||||
$document = Document::createNew();
|
||||
$disk = $document->getDisk();
|
||||
if(!$disk->exists($filename)){// Have we already stored the same file
|
||||
$stream = fopen($filePath, 'r');
|
||||
$disk->getDriver()->putStream($filename, $stream, ['mimetype'=>$documentTypeData['mime']]);
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
// This is an image; check if we need to create a preview
|
||||
if(in_array($documentType, array('jpeg','png','gif','bmp','tiff','psd'))){
|
||||
$makePreview = false;
|
||||
$imageSize = getimagesize($filePath);
|
||||
$width = $imageSize[0];
|
||||
$height = $imageSize[1];
|
||||
$imgManagerConfig = array();
|
||||
if(in_array($documentType, array('gif','bmp','tiff','psd'))){
|
||||
// Needs to be converted
|
||||
$makePreview = true;
|
||||
} else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){
|
||||
$makePreview = true;
|
||||
}
|
||||
|
||||
if(in_array($documentType,array('bmp','tiff','psd'))){
|
||||
if(!class_exists('Imagick')){
|
||||
// Cant't read this
|
||||
$makePreview = false;
|
||||
} else {
|
||||
$imgManagerConfig['driver'] = 'imagick';
|
||||
}
|
||||
}
|
||||
|
||||
if($makePreview){
|
||||
$previewType = 'jpeg';
|
||||
if(in_array($documentType, array('png','gif','tiff','psd'))){
|
||||
// Has transparency
|
||||
$previewType = 'png';
|
||||
}
|
||||
|
||||
$document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType.'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType;
|
||||
if(!$disk->exists($document->preview)){
|
||||
// We haven't created a preview yet
|
||||
$imgManager = new ImageManager($imgManagerConfig);
|
||||
|
||||
$img = $imgManager->make($filePath);
|
||||
|
||||
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;
|
||||
$document->type = $documentType;
|
||||
$document->size = $size;
|
||||
$document->hash = $hash;
|
||||
$document->name = substr($name, -255);
|
||||
|
||||
if(!empty($imageSize)){
|
||||
$document->width = $imageSize[0];
|
||||
$document->height = $imageSize[1];
|
||||
}
|
||||
|
||||
$document->save();
|
||||
$doc_array = $document->toArray();
|
||||
|
||||
if(!empty($base64)){
|
||||
$mime = Document::$types[!empty($previewType)?$previewType:$documentType]['mime'];
|
||||
$doc_array['base64'] = 'data:'.$mime.';base64,'.$base64;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
public function getClientDatatable($contactId, $entityType, $search)
|
||||
{
|
||||
|
||||
$query = DB::table('invitations')
|
||||
->join('accounts', 'accounts.id', '=', 'invitations.account_id')
|
||||
->join('invoices', 'invoices.id', '=', 'invitations.invoice_id')
|
||||
->join('expenses', 'expenses.invoice_id', '=', 'invitations.invoice_id')
|
||||
->join('documents', function($join){
|
||||
$join->on('documents.invoice_id', '=', 'invitations.invoice_id')->orOn('documents.expense_id', '=', 'expenses.id');
|
||||
})
|
||||
->join('clients', 'clients.id', '=', 'invoices.client_id')
|
||||
->where('invitations.contact_id', '=', $contactId)
|
||||
->where('invitations.deleted_at', '=', null)
|
||||
->where('invoices.is_deleted', '=', false)
|
||||
->where('clients.deleted_at', '=', null)
|
||||
->where('invoices.is_recurring', '=', false)
|
||||
// This needs to be a setting to also hide the activity on the dashboard page
|
||||
//->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT)
|
||||
->select(
|
||||
'invitations.invitation_key',
|
||||
'invoices.invoice_number',
|
||||
'documents.name',
|
||||
'documents.public_id',
|
||||
'documents.created_at',
|
||||
'documents.size'
|
||||
);
|
||||
|
||||
$table = \Datatable::query($query)
|
||||
->addColumn('invoice_number', function ($model) {
|
||||
return link_to(
|
||||
'/view/'.$model->invitation_key,
|
||||
$model->invoice_number
|
||||
)->toHtml();
|
||||
})
|
||||
->addColumn('name', function ($model) {
|
||||
return link_to(
|
||||
'/client/document/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name,
|
||||
$model->name,
|
||||
['target'=>'_blank']
|
||||
)->toHtml();
|
||||
})
|
||||
->addColumn('document_date', function ($model) {
|
||||
return Utils::fromSqlDate($model->created_at);
|
||||
})
|
||||
->addColumn('document_size', function ($model) {
|
||||
return Form::human_filesize($model->size);
|
||||
});
|
||||
|
||||
return $table->make();
|
||||
}
|
||||
}
|
@ -4,17 +4,25 @@ use DB;
|
||||
use Utils;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\Document;
|
||||
use App\Ninja\Repositories\BaseRepository;
|
||||
use Session;
|
||||
|
||||
class ExpenseRepository extends BaseRepository
|
||||
{
|
||||
protected $documentRepo;
|
||||
|
||||
// Expenses
|
||||
public function getClassName()
|
||||
{
|
||||
return 'App\Models\Expense';
|
||||
}
|
||||
|
||||
public function __construct(DocumentRepository $documentRepo)
|
||||
{
|
||||
$this->documentRepo = $documentRepo;
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
return Expense::scope()
|
||||
@ -113,7 +121,7 @@ class ExpenseRepository extends BaseRepository
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function save($input)
|
||||
public function save($input, $checkSubPermissions=false)
|
||||
{
|
||||
$publicId = isset($input['public_id']) ? $input['public_id'] : false;
|
||||
|
||||
@ -147,6 +155,43 @@ class ExpenseRepository extends BaseRepository
|
||||
|
||||
$expense->save();
|
||||
|
||||
// Documents
|
||||
$document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();;
|
||||
foreach ($document_ids as $document_id){
|
||||
$document = Document::scope($document_id)->first();
|
||||
if($document && !$checkSubPermissions || $document->canEdit()){
|
||||
$document->invoice_id = null;
|
||||
$document->expense_id = $expense->id;
|
||||
$document->save();
|
||||
}
|
||||
}
|
||||
|
||||
if(!empty($input['documents']) && Document::canCreate()){
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($expense->documents as $document){
|
||||
if(!in_array($document->public_id, $document_ids)){
|
||||
// Not checking permissions; deleting a document is just editing the invoice
|
||||
$document->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return $expense;
|
||||
}
|
||||
|
||||
|
@ -2,24 +2,29 @@
|
||||
|
||||
use DB;
|
||||
use Utils;
|
||||
use Session;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\Invitation;
|
||||
use App\Models\Product;
|
||||
use App\Models\Task;
|
||||
use App\Models\Document;
|
||||
use App\Models\Expense;
|
||||
use App\Services\PaymentService;
|
||||
use App\Ninja\Repositories\BaseRepository;
|
||||
|
||||
class InvoiceRepository extends BaseRepository
|
||||
{
|
||||
protected $documentRepo;
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return 'App\Models\Invoice';
|
||||
}
|
||||
|
||||
public function __construct(PaymentService $paymentService)
|
||||
public function __construct(PaymentService $paymentService, DocumentRepository $documentRepo)
|
||||
{
|
||||
$this->documentRepo = $documentRepo;
|
||||
$this->paymentService = $paymentService;
|
||||
}
|
||||
|
||||
@ -398,6 +403,53 @@ class InvoiceRepository extends BaseRepository
|
||||
$invoice->invoice_items()->forceDelete();
|
||||
}
|
||||
|
||||
$document_ids = !empty($data['document_ids'])?array_map('intval', $data['document_ids']):array();;
|
||||
foreach ($document_ids as $document_id){
|
||||
$document = Document::scope($document_id)->first();
|
||||
if($document && !$checkSubPermissions || $document->canEdit()){
|
||||
|
||||
if($document->invoice_id && $document->invoice_id != $invoice->id){
|
||||
// From a clone
|
||||
$document = $document->cloneDocument();
|
||||
$document_ids[] = $document->public_id;// Don't remove this document
|
||||
}
|
||||
|
||||
$document->invoice_id = $invoice->id;
|
||||
$document->expense_id = null;
|
||||
$document->save();
|
||||
}
|
||||
}
|
||||
|
||||
if(!empty($data['documents']) && Document::canCreate()){
|
||||
// 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
|
||||
// Not checking permissions; deleting a document is just editing the invoice
|
||||
if($document->invoice_id == $invoice->id){
|
||||
// Make sure the document isn't on a clone
|
||||
$document->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data['invoice_items'] as $item) {
|
||||
$item = (array) $item;
|
||||
if (empty($item['cost']) && empty($item['product_key']) && empty($item['notes']) && empty($item['custom_value1']) && empty($item['custom_value2'])) {
|
||||
@ -553,6 +605,11 @@ class InvoiceRepository extends BaseRepository
|
||||
$clone->invoice_items()->save($cloneItem);
|
||||
}
|
||||
|
||||
foreach ($invoice->documents as $document) {
|
||||
$cloneDocument = $document->cloneDocument();
|
||||
$invoice->documents()->save($cloneDocument);
|
||||
}
|
||||
|
||||
foreach ($invoice->invitations as $invitation) {
|
||||
$cloneInvitation = Invitation::createNew($invoice);
|
||||
$cloneInvitation->contact_id = $invitation->contact_id;
|
||||
@ -581,7 +638,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) {
|
||||
@ -657,6 +714,11 @@ class InvoiceRepository extends BaseRepository
|
||||
$invoice->invoice_items()->save($item);
|
||||
}
|
||||
|
||||
foreach ($recurInvoice->documents as $recurDocument) {
|
||||
$document = $recurDocument->cloneDocument();
|
||||
$invoice->documents()->save($document);
|
||||
}
|
||||
|
||||
foreach ($recurInvoice->invitations as $recurInvitation) {
|
||||
$invitation = Invitation::createNew($recurInvitation);
|
||||
$invitation->contact_id = $recurInvitation->contact_id;
|
||||
|
@ -22,8 +22,15 @@ class AppServiceProvider extends ServiceProvider {
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
Form::macro('image_data', function($imagePath) {
|
||||
return 'data:image/jpeg;base64,' . base64_encode(file_get_contents($imagePath));
|
||||
Form::macro('image_data', function($image, $contents = false) {
|
||||
if(!$contents){
|
||||
$contents = file_get_contents($image);
|
||||
}
|
||||
else{
|
||||
$contents = $image;
|
||||
}
|
||||
|
||||
return 'data:image/jpeg;base64,' . base64_encode($contents);
|
||||
});
|
||||
|
||||
Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') {
|
||||
@ -152,6 +159,13 @@ class AppServiceProvider extends ServiceProvider {
|
||||
return $str . '</ol>';
|
||||
});
|
||||
|
||||
Form::macro('human_filesize', function($bytes, $decimals = 1) {
|
||||
$size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
if($factor == 0)$decimals=0;// There aren't fractional bytes
|
||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
|
||||
});
|
||||
|
||||
Validator::extend('positive', function($attribute, $value, $parameters) {
|
||||
return Utils::parseFloat($value) >= 0;
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ class ExpenseService extends BaseService
|
||||
return $this->expenseRepo;
|
||||
}
|
||||
|
||||
public function save($data)
|
||||
public function save($data, $checkSubPermissions=false)
|
||||
{
|
||||
if (isset($data['client_id']) && $data['client_id']) {
|
||||
$data['client_id'] = Client::getPrivateId($data['client_id']);
|
||||
@ -38,7 +38,7 @@ class ExpenseService extends BaseService
|
||||
$data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']);
|
||||
}
|
||||
|
||||
return $this->expenseRepo->save($data);
|
||||
return $this->expenseRepo->save($data, $checkSubPermissions);
|
||||
}
|
||||
|
||||
public function getDatatable($search)
|
||||
|
@ -26,7 +26,8 @@
|
||||
"quill": "~0.20.0",
|
||||
"datetimepicker": "~2.4.5",
|
||||
"stacktrace-js": "~1.0.1",
|
||||
"fuse.js": "~2.0.2"
|
||||
"fuse.js": "~2.0.2",
|
||||
"dropzone": "~4.3.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"jquery": "~1.11"
|
||||
|
@ -66,7 +66,10 @@
|
||||
"maatwebsite/excel": "~2.0",
|
||||
"ezyang/htmlpurifier": "~v4.7",
|
||||
"cerdic/css-tidy": "~v1.5",
|
||||
"asgrim/ofxparser": "^1.1"
|
||||
"asgrim/ofxparser": "^1.1",
|
||||
"league/flysystem-aws-s3-v3": "~1.0",
|
||||
"league/flysystem-rackspace": "~1.0",
|
||||
"barracudanetworks/archivestream-php": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0",
|
||||
|
359
composer.lock
generated
359
composer.lock
generated
@ -4,8 +4,8 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "e5e8524886bd38794a15e406acc3745a",
|
||||
"content-hash": "6b3f343959ba3f330c425325574dfe28",
|
||||
"hash": "5b6b0afc0a26fa2b1dc48241fd665c1e",
|
||||
"content-hash": "33f39a8d05247b96374ed20e95499936",
|
||||
"packages": [
|
||||
{
|
||||
"name": "agmscode/omnipay-agms",
|
||||
@ -321,6 +321,126 @@
|
||||
],
|
||||
"time": "2015-12-11 11:08:57"
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.17.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "f8c0cc9357e10896a5c57104f2c79d1b727d97d0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f8c0cc9357e10896a5c57104f2c79d1b727d97d0",
|
||||
"reference": "f8c0cc9357e10896a5c57104f2c79d1b727d97d0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "~5.3|~6.0.1|~6.1",
|
||||
"guzzlehttp/promises": "~1.0",
|
||||
"guzzlehttp/psr7": "~1.0",
|
||||
"mtdowling/jmespath.php": "~2.2",
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"andrewsville/php-token-reflection": "^1.4",
|
||||
"aws/aws-php-sns-message-validator": "~1.0",
|
||||
"behat/behat": "~3.0",
|
||||
"doctrine/cache": "~1.4",
|
||||
"ext-dom": "*",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-spl": "*",
|
||||
"nette/neon": "^2.3",
|
||||
"phpunit/phpunit": "~4.0|~5.0",
|
||||
"psr/cache": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
|
||||
"doctrine/cache": "To use the DoctrineCacheAdapter",
|
||||
"ext-curl": "To send requests using cURL",
|
||||
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Aws\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Amazon Web Services",
|
||||
"homepage": "http://aws.amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
|
||||
"homepage": "http://aws.amazon.com/sdkforphp",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"cloud",
|
||||
"dynamodb",
|
||||
"ec2",
|
||||
"glacier",
|
||||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"time": "2016-03-22 19:19:22"
|
||||
},
|
||||
{
|
||||
"name": "barracudanetworks/archivestream-php",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barracudanetworks/ArchiveStream-php.git",
|
||||
"reference": "9a81c7de7f0cd5ea2150fc3dc00f1c43178362b6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barracudanetworks/ArchiveStream-php/zipball/9a81c7de7f0cd5ea2150fc3dc00f1c43178362b6",
|
||||
"reference": "9a81c7de7f0cd5ea2150fc3dc00f1c43178362b6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-gmp": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=5.1.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barracuda\\ArchiveStream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A library for dynamically streaming dynamic tar or zip files without the need to have the complete file stored on the server.",
|
||||
"homepage": "https://github.com/barracudanetworks/ArchiveStream-php",
|
||||
"keywords": [
|
||||
"archive",
|
||||
"php",
|
||||
"stream",
|
||||
"tar",
|
||||
"zip"
|
||||
],
|
||||
"time": "2016-01-07 06:02:26"
|
||||
},
|
||||
{
|
||||
"name": "barryvdh/laravel-debugbar",
|
||||
"version": "v2.2.0",
|
||||
@ -579,7 +699,7 @@
|
||||
"laravel"
|
||||
],
|
||||
"abandoned": "OpenSkill/Datatable",
|
||||
"time": "2015-04-29 07:00:36"
|
||||
"time": "2015-11-23 21:33:41"
|
||||
},
|
||||
{
|
||||
"name": "classpreloader/classpreloader",
|
||||
@ -3056,6 +3176,100 @@
|
||||
],
|
||||
"time": "2016-03-14 21:54:11"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-aws-s3-v3",
|
||||
"version": "1.0.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
|
||||
"reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/595e24678bf78f8107ebc9355d8376ae0eb712c6",
|
||||
"reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-sdk-php": "^3.0.0",
|
||||
"league/flysystem": "~1.0",
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"henrikbjorn/phpspec-code-coverage": "~1.0.1",
|
||||
"phpspec/phpspec": "^2.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\AwsS3v3\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frenky.net"
|
||||
}
|
||||
],
|
||||
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
|
||||
"time": "2015-11-19 08:44:16"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-rackspace",
|
||||
"version": "1.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-rackspace.git",
|
||||
"reference": "ba877e837f5dce60e78a0555de37eb9bfc7dd6b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-rackspace/zipball/ba877e837f5dce60e78a0555de37eb9bfc7dd6b9",
|
||||
"reference": "ba877e837f5dce60e78a0555de37eb9bfc7dd6b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/flysystem": "~1.0",
|
||||
"php": ">=5.4.0",
|
||||
"rackspace/php-opencloud": "~1.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\Rackspace\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frenky.net"
|
||||
}
|
||||
],
|
||||
"description": "Flysystem adapter for Rackspace",
|
||||
"time": "2016-03-11 12:13:42"
|
||||
},
|
||||
{
|
||||
"name": "league/fractal",
|
||||
"version": "0.13.0",
|
||||
@ -3585,6 +3799,33 @@
|
||||
],
|
||||
"time": "2015-07-14 19:53:54"
|
||||
},
|
||||
{
|
||||
"name": "mikemccabe/json-patch-php",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mikemccabe/json-patch-php.git",
|
||||
"reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mikemccabe/json-patch-php/zipball/b3af30a6aec7f6467c773cd49b2d974a70f7c0d4",
|
||||
"reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"mikemccabe\\JsonPatch\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0"
|
||||
],
|
||||
"description": "Produce and apply json-patch objects",
|
||||
"time": "2015-01-05 21:19:54"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "1.18.1",
|
||||
@ -3707,6 +3948,61 @@
|
||||
],
|
||||
"time": "2016-01-26 21:23:30"
|
||||
},
|
||||
{
|
||||
"name": "mtdowling/jmespath.php",
|
||||
"version": "2.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jmespath/jmespath.php.git",
|
||||
"reference": "192f93e43c2c97acde7694993ab171b3de284093"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/192f93e43c2c97acde7694993ab171b3de284093",
|
||||
"reference": "192f93e43c2c97acde7694993ab171b3de284093",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/jp.php"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"JmesPath\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/JmesPath.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Declaratively specify how to extract elements from a JSON document",
|
||||
"keywords": [
|
||||
"json",
|
||||
"jsonpath"
|
||||
],
|
||||
"time": "2016-01-05 18:25:05"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "1.21.0",
|
||||
@ -5906,6 +6202,63 @@
|
||||
],
|
||||
"time": "2016-03-09 05:03:14"
|
||||
},
|
||||
{
|
||||
"name": "rackspace/php-opencloud",
|
||||
"version": "v1.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rackspace/php-opencloud.git",
|
||||
"reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/d6b71feed7f9e7a4b52e0240a79f06473ba69c8c",
|
||||
"reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"guzzle/guzzle": "~3.8",
|
||||
"mikemccabe/json-patch-php": "~0.1",
|
||||
"php": ">=5.4",
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"apigen/apigen": "~4.0",
|
||||
"fabpot/php-cs-fixer": "1.0.*@dev",
|
||||
"jakub-onderka/php-parallel-lint": "0.*",
|
||||
"phpspec/prophecy": "~1.4",
|
||||
"phpunit/phpunit": "4.3.*",
|
||||
"satooshi/php-coveralls": "0.6.*@dev"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"OpenCloud": [
|
||||
"lib/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jamie Hannaford",
|
||||
"email": "jamie.hannaford@rackspace.com",
|
||||
"homepage": "https://github.com/jamiehannaford"
|
||||
}
|
||||
],
|
||||
"description": "PHP SDK for Rackspace/OpenStack APIs",
|
||||
"keywords": [
|
||||
"Openstack",
|
||||
"nova",
|
||||
"opencloud",
|
||||
"rackspace",
|
||||
"swift"
|
||||
],
|
||||
"time": "2016-01-29 10:34:57"
|
||||
},
|
||||
{
|
||||
"name": "samvaughton/omnipay-barclays-epdq",
|
||||
"version": "2.2.0",
|
||||
|
@ -48,22 +48,32 @@ return [
|
||||
'root' => storage_path().'/app',
|
||||
],
|
||||
|
||||
'logos' => [
|
||||
'driver' => 'local',
|
||||
'root' => env('LOGO_PATH', public_path().'/logo'),
|
||||
],
|
||||
|
||||
'documents' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path().'/documents',
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => 'your-key',
|
||||
'secret' => 'your-secret',
|
||||
'region' => 'your-region',
|
||||
'bucket' => 'your-bucket',
|
||||
'key' => env('S3_KEY', ''),
|
||||
'secret' => env('S3_SECRET', ''),
|
||||
'region' => env('S3_REGION', 'us-east-1'),
|
||||
'bucket' => env('S3_BUCKET', ''),
|
||||
],
|
||||
|
||||
'rackspace' => [
|
||||
'driver' => 'rackspace',
|
||||
'username' => 'your-username',
|
||||
'key' => 'your-key',
|
||||
'container' => 'your-container',
|
||||
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
|
||||
'region' => 'IAD',
|
||||
'url_type' => 'publicURL'
|
||||
'username' => env('RACKSPACE_USERNAME', ''),
|
||||
'key' => env('RACKSPACE_KEY', ''),
|
||||
'container' => env('RACKSPACE_CONTAINER', ''),
|
||||
'endpoint' => env('RACKSPACE_ENDPOINT', 'https://identity.api.rackspacecloud.com/v2.0/'),
|
||||
'region' => env('RACKSPACE_REGION', 'IAD'),
|
||||
'url_type' => env('RACKSPACE_URL_TYPE', 'publicURL')
|
||||
],
|
||||
|
||||
],
|
||||
|
70
database/migrations/2016_03_22_168362_add_documents.php
Normal file
70
database/migrations/2016_03_22_168362_add_documents.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?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');
|
||||
$table->boolean('invoice_embed_documents')->default(1);
|
||||
$table->boolean('document_email_attachment')->default(1);
|
||||
});
|
||||
|
||||
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('preview');
|
||||
$t->string('name');
|
||||
$t->string('type');
|
||||
$t->string('disk');
|
||||
$t->string('hash', 40);
|
||||
$t->unsignedInteger('size');
|
||||
$t->unsignedInteger('width')->nullable();
|
||||
$t->unsignedInteger('height')->nullable();
|
||||
|
||||
$t->timestamps();
|
||||
|
||||
$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');
|
||||
$table->dropColumn('invoice_embed_documents');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('documents');
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
41
public/css/built.css
vendored
41
public/css/built.css
vendored
File diff suppressed because one or more lines are too long
39
public/css/style.css
vendored
39
public/css/style.css
vendored
@ -1062,3 +1062,42 @@ td.right {
|
||||
div.panel-body div.panel-body {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
/* Attached Documents */
|
||||
#document-upload {
|
||||
border:1px solid #ebe7e7;
|
||||
background:#f9f9f9 !important;
|
||||
border-radius:3px;
|
||||
padding:20px;
|
||||
}
|
||||
|
||||
.invoice-table #document-upload{
|
||||
max-width:560px;
|
||||
}
|
||||
|
||||
#document-upload .dropzone{
|
||||
background:none;
|
||||
border:none;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-image-preview{
|
||||
background:none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-image{
|
||||
border-radius:5px!important;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-image-preview .dz-image img{
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone .fallback-doc{
|
||||
display:none;
|
||||
}
|
||||
.dropzone.dz-browser-not-supported .fallback-doc{
|
||||
display:block;
|
||||
}
|
@ -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,39 @@ NINJA.invoiceLines = function(invoice) {
|
||||
return NINJA.prepareDataTable(grid, 'invoiceItems');
|
||||
}
|
||||
|
||||
NINJA.invoiceDocuments = function(invoice) {
|
||||
if(!invoice.account.invoice_embed_documents)return[];
|
||||
var stack = [];
|
||||
var stackItem = null;
|
||||
|
||||
var j = 0;
|
||||
for (var i = 0; i < invoice.documents.length; i++)addDoc(invoice.documents[i]);
|
||||
|
||||
if(invoice.expenses){
|
||||
for (var i = 0; i < invoice.expenses.length; i++) {
|
||||
var expense = invoice.expenses[i];
|
||||
for (var i = 0; i < expense.documents.length; i++)addDoc(expense.documents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function addDoc(document){
|
||||
var path = document.base64;
|
||||
|
||||
if(!path)path = 'docs/'+document.public_id+'/'+document.name;
|
||||
if(path && (window.pdfMake.vfs[path] || document.base64)){
|
||||
// Only embed if we actually have an image for it
|
||||
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.length?{stack:stack}:[];
|
||||
}
|
||||
|
||||
NINJA.subtotals = function(invoice, hideBalance)
|
||||
{
|
||||
if (!invoice) {
|
||||
|
@ -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');
|
||||
}
|
@ -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');
|
||||
}
|
@ -1097,6 +1097,32 @@ $LANG = array(
|
||||
'november' => 'November',
|
||||
'december' => 'December',
|
||||
|
||||
// Documents
|
||||
'documents_header' => 'Documents:',
|
||||
'email_documents_header' => 'Documents:',
|
||||
'email_documents_example_1' => 'Widgets Receipt.pdf',
|
||||
'email_documents_example_2' => 'Final Deliverable.zip',
|
||||
'invoice_documents' => 'Attached Documents',
|
||||
'expense_documents' => 'Attached Documents',
|
||||
'invoice_embed_documents' => 'Embed Documents',
|
||||
'invoice_embed_documents_help' => 'Include attached images in the invoice.',
|
||||
'document_email_attachment' => 'Attach Documents',
|
||||
'download_documents' => 'Download Documents (:size)',
|
||||
'documents_from_expenses' => 'From Expenses:',
|
||||
'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage
|
||||
'DefaultMessage' => 'Drop files or click to upload',
|
||||
'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.',
|
||||
'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.',
|
||||
'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.',
|
||||
'InvalidFileType' => 'You can\'t upload files of this type.',
|
||||
'ResponseError' => 'Server responded with {{statusCode}} code.',
|
||||
'CancelUpload' => 'Cancel upload',
|
||||
'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?',
|
||||
'RemoveFile' => 'Remove file',
|
||||
),
|
||||
'documents' => 'Documents',
|
||||
'document_date' => 'Document Date',
|
||||
'document_size' => 'Size',
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
@ -51,8 +51,8 @@
|
||||
<div class="form-group">
|
||||
<div class="col-lg-4 col-sm-4"></div>
|
||||
<div class="col-lg-8 col-sm-8">
|
||||
<a href="/{{ $account->getLogoPath().'?no_cache='.time() }}" target="_blank">
|
||||
{!! HTML::image($account->getLogoPath().'?no_cache='.time(), 'Logo', ['max-width' => 200]) !!}
|
||||
<a href="{{ $account->getLogoUrl().'?no_cache='.time() }}" target="_blank">
|
||||
{!! HTML::image($account->getLogoUrl().'?no_cache='.time(), 'Logo', ['max-width' => 200]) !!}
|
||||
</a>
|
||||
<a href="#" onclick="deleteLogo()">{{ trans('texts.remove_logo') }}</a>
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@
|
||||
])->addClass('warn-on-exit') !!}
|
||||
{{ Former::populate($account) }}
|
||||
{{ Former::populateField('pdf_email_attachment', intval($account->pdf_email_attachment)) }}
|
||||
{{ Former::populateField('document_email_attachment', intval($account->document_email_attachment)) }}
|
||||
{{ Former::populateField('enable_email_markup', intval($account->enable_email_markup)) }}
|
||||
|
||||
<div class="panel panel-default">
|
||||
@ -27,6 +28,7 @@
|
||||
</div>
|
||||
<div class="panel-body form-padding-right">
|
||||
{!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
|
||||
{!! Former::checkbox('document_email_attachment')->text(trans('texts.enable')) !!}
|
||||
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -201,6 +201,13 @@
|
||||
|
||||
var keys = {!! json_encode(\App\Ninja\Mailers\ContactMailer::$variableFields) !!};
|
||||
var passwordHtml = "{!! $account->isPro() && $account->enable_portal_password && $account->send_portal_password?'<p>'.trans('texts.password').': 6h2NWNdw6<p>':'' !!}";
|
||||
|
||||
@if ($account->isPro())
|
||||
var documentsHtml = "{!! trans('texts.email_documents_header').'<ul><li><a>'.trans('texts.email_documents_example_1').'</a></li><li><a>'.trans('texts.email_documents_example_2').'</a></li></ul>' !!}";
|
||||
@else
|
||||
var documentsHtml = "";
|
||||
@endif
|
||||
|
||||
var vals = [
|
||||
{!! json_encode($emailFooter) !!},
|
||||
"{{ $account->getDisplayName() }}",
|
||||
@ -213,6 +220,7 @@
|
||||
"0001",
|
||||
"0001",
|
||||
passwordHtml,
|
||||
documentsHtml,
|
||||
"{{ URL::to('/view/...') }}$password",
|
||||
'{!! Form::flatButton('view_invoice', '#0b4d78') !!}$password',
|
||||
"{{ URL::to('/payment/...') }}$password",
|
||||
|
@ -3,7 +3,7 @@
|
||||
<a href="{{ $account->website }}" style="color: #19BB40; text-decoration: underline;">
|
||||
@endif
|
||||
|
||||
<img src="{{ $message->embed($account->getLogoFullPath()) }}" style="max-height:50px; max-width:140px; margin-left: 33px;" />
|
||||
<img src="{{ $message->embed($account->getLogoURL()) }}" style="max-height:50px; max-width:140px; margin-left: 33px;" />
|
||||
|
||||
@if ($account->website)
|
||||
</a>
|
||||
|
@ -105,6 +105,27 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@if ($account->isPro())
|
||||
<div clas="row">
|
||||
<div class="col-md-2 col-sm-4"><div class="control-label" style="margin-bottom:10px;">{{trans('texts.expense_documents')}}</div></div>
|
||||
<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>
|
||||
|
||||
@ -122,6 +143,7 @@
|
||||
{!! Former::close() !!}
|
||||
|
||||
<script type="text/javascript">
|
||||
Dropzone.autoDiscover = false;
|
||||
|
||||
var vendors = {!! $vendors !!};
|
||||
var clients = {!! $clients !!};
|
||||
@ -194,6 +216,56 @@
|
||||
@else
|
||||
$('#amount').focus();
|
||||
@endif
|
||||
|
||||
@if (Auth::user()->account->isPro())
|
||||
$('.main-form').submit(function(){
|
||||
if($('#document-upload .fallback input').val())$(this).attr('enctype', 'multipart/form-data')
|
||||
else $(this).removeAttr('enctype')
|
||||
})
|
||||
|
||||
// Initialize document upload
|
||||
dropzone = new Dropzone('#document-upload', {
|
||||
url:{!! json_encode(url('document')) !!},
|
||||
params:{
|
||||
_token:"{{ Session::getToken() }}"
|
||||
},
|
||||
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
|
||||
addRemoveLinks:true,
|
||||
@foreach(trans('texts.dropzone') as $key=>$text)
|
||||
"dict{{strval($key)}}":"{{strval($text)}}",
|
||||
@endforeach
|
||||
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
|
||||
});
|
||||
if(dropzone instanceof Dropzone){
|
||||
dropzone.on("addedfile",handleDocumentAdded);
|
||||
dropzone.on("removedfile",handleDocumentRemoved);
|
||||
dropzone.on("success",handleDocumentUploaded);
|
||||
for (var i=0; i<model.documents().length; i++) {
|
||||
var document = model.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.preview_url()||document.url(),
|
||||
mock:true,
|
||||
index:i
|
||||
};
|
||||
|
||||
dropzone.emit('addedfile', mockFile);
|
||||
dropzone.emit('complete', mockFile);
|
||||
if(document.preview_url()){
|
||||
dropzone.emit('thumbnail', mockFile, document.preview_url()||document.url());
|
||||
}
|
||||
else if(document.type()=='jpeg' || document.type()=='png' || document.type()=='svg'){
|
||||
dropzone.emit('thumbnail', mockFile, document.url());
|
||||
}
|
||||
dropzone.files.push(mockFile);
|
||||
}
|
||||
}
|
||||
@endif
|
||||
});
|
||||
|
||||
var ViewModel = function(data) {
|
||||
@ -201,13 +273,22 @@
|
||||
|
||||
self.expense_currency_id = ko.observable();
|
||||
self.invoice_currency_id = ko.observable();
|
||||
self.documents = ko.observableArray();
|
||||
self.amount = ko.observable();
|
||||
self.exchange_rate = ko.observable(1);
|
||||
self.should_be_invoiced = ko.observable();
|
||||
self.convert_currency = ko.observable(false);
|
||||
|
||||
self.mapping = {
|
||||
'documents': {
|
||||
create: function(options) {
|
||||
return new DocumentModel(options.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
ko.mapping.fromJS(data, {}, this);
|
||||
ko.mapping.fromJS(data, self.mapping, this);
|
||||
}
|
||||
|
||||
self.account_currency_id = ko.observable({{ $account->getCurrencyId() }});
|
||||
@ -250,8 +331,57 @@
|
||||
|| invoiceCurrencyId != self.account_currency_id()
|
||||
|| expenseCurrencyId != self.account_currency_id();
|
||||
})
|
||||
};
|
||||
|
||||
self.addDocument = function() {
|
||||
var documentModel = new DocumentModel();
|
||||
self.documents.push(documentModel);
|
||||
return documentModel;
|
||||
}
|
||||
|
||||
self.removeDocument = function(doc) {
|
||||
var public_id = doc.public_id?doc.public_id():doc;
|
||||
self.documents.remove(function(document) {
|
||||
return document.public_id() == public_id;
|
||||
});
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@if (Auth::user()->account->isPro())
|
||||
function handleDocumentAdded(file){
|
||||
if(file.mock)return;
|
||||
file.index = model.documents().length;
|
||||
model.addDocument({name:file.name, size:file.size, type:file.type});
|
||||
}
|
||||
|
||||
function handleDocumentRemoved(file){
|
||||
model.removeDocument(file.public_id);
|
||||
}
|
||||
|
||||
function handleDocumentUploaded(file, response){
|
||||
file.public_id = response.document.public_id
|
||||
model.documents()[file.index].update(response.document);
|
||||
|
||||
if(response.document.preview_url){
|
||||
dropzone.emit('thumbnail', file, response.document.preview_url);
|
||||
}
|
||||
}
|
||||
@endif
|
||||
</script>
|
||||
|
||||
@stop
|
@ -448,7 +448,7 @@
|
||||
'user_id' => $item->user_id,
|
||||
'account_name' => $item->account_name,
|
||||
'user_name' => $item->user_name,
|
||||
'logo_path' => isset($item->logo_path) ? $item->logo_path : "",
|
||||
'logo_url' => isset($item->logo_url) ? $item->logo_url : "",
|
||||
'selected' => true,
|
||||
])
|
||||
@endif
|
||||
@ -460,7 +460,7 @@
|
||||
'user_id' => $item->user_id,
|
||||
'account_name' => $item->account_name,
|
||||
'user_name' => $item->user_name,
|
||||
'logo_path' => isset($item->logo_path) ? $item->logo_path : "",
|
||||
'logo_url' => isset($item->logo_url) ? $item->logo_url : "",
|
||||
'selected' => false,
|
||||
])
|
||||
@endif
|
||||
@ -469,7 +469,7 @@
|
||||
@include('user_account', [
|
||||
'account_name' => Auth::user()->account->name ?: trans('texts.untitled'),
|
||||
'user_name' => Auth::user()->getDisplayName(),
|
||||
'logo_path' => Auth::user()->account->getLogoPath(),
|
||||
'logo_url' => Auth::user()->account->getLogoURL(),
|
||||
'selected' => true,
|
||||
])
|
||||
@endif
|
||||
|
@ -95,7 +95,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-3 logo">
|
||||
@if ($account->hasLogo())
|
||||
{!! HTML::image($account->getLogoPath()) !!}
|
||||
{!! HTML::image($account->getLogoURL()) !!}
|
||||
@else
|
||||
<h2>{{ $account->name}}</h2>
|
||||
@endif
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
{!! Former::open($url)
|
||||
->method($method)
|
||||
->addClass('warn-on-exit')
|
||||
->addClass('warn-on-exit main-form')
|
||||
->autocomplete('off')
|
||||
->onsubmit('return onFormSubmit(event)')
|
||||
->rules(array(
|
||||
@ -273,6 +273,9 @@
|
||||
<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 ($account->isPro())
|
||||
<li role="presentation"><a href="#attached-documents" aria-controls="attached-documents" role="tab" data-toggle="tab">{{ trans("texts.invoice_documents") }}</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@ -304,6 +307,32 @@
|
||||
</div>
|
||||
</div>') !!}
|
||||
</div>
|
||||
@if ($account->isPro())
|
||||
<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)
|
||||
@foreach($expense->documents as $document)
|
||||
<div>{{$document->name}}</div>
|
||||
@endforeach
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -681,10 +710,12 @@
|
||||
@include('invoices.knockout')
|
||||
|
||||
<script type="text/javascript">
|
||||
Dropzone.autoDiscover = false;
|
||||
|
||||
var products = {!! $products !!};
|
||||
var clients = {!! $clients !!};
|
||||
var account = {!! Auth::user()->account !!};
|
||||
var dropzone;
|
||||
|
||||
var clientMap = {};
|
||||
var $clientSelect = $('select#client');
|
||||
@ -757,24 +788,24 @@
|
||||
model.invoice().has_tasks(true);
|
||||
@endif
|
||||
|
||||
@if (isset($expenses) && $expenses)
|
||||
if(model.invoice().expenses() && !model.invoice().public_id()){
|
||||
model.expense_currency_id({{ $expenseCurrencyId }});
|
||||
|
||||
// move the blank invoice line item to the end
|
||||
var blank = model.invoice().invoice_items.pop();
|
||||
var expenses = {!! $expenses !!};
|
||||
var expenses = model.invoice().expenses();
|
||||
|
||||
for (var i=0; i<expenses.length; i++) {
|
||||
var expense = expenses[i];
|
||||
var item = model.invoice().addItem();
|
||||
item.notes(expense.description);
|
||||
item.qty(expense.qty);
|
||||
item.expense_public_id(expense.publicId);
|
||||
item.cost(expense.cost);
|
||||
item.notes(expense.public_notes());
|
||||
item.qty(1);
|
||||
item.expense_public_id(expense.public_id());
|
||||
item.cost(expense.converted_amount());
|
||||
}
|
||||
model.invoice().invoice_items.push(blank);
|
||||
model.invoice().has_expenses(true);
|
||||
@endif
|
||||
}
|
||||
|
||||
@endif
|
||||
|
||||
@ -899,6 +930,56 @@
|
||||
@endif
|
||||
|
||||
applyComboboxListeners();
|
||||
|
||||
@if (Auth::user()->account->isPro())
|
||||
$('.main-form').submit(function(){
|
||||
if($('#document-upload .dropzone .fallback input').val())$(this).attr('enctype', 'multipart/form-data')
|
||||
else $(this).removeAttr('enctype')
|
||||
})
|
||||
|
||||
// Initialize document upload
|
||||
dropzone = new Dropzone('#document-upload .dropzone', {
|
||||
url:{!! json_encode(url('document')) !!},
|
||||
params:{
|
||||
_token:"{{ Session::getToken() }}"
|
||||
},
|
||||
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
|
||||
addRemoveLinks:true,
|
||||
@foreach(trans('texts.dropzone') as $key=>$text)
|
||||
"dict{{strval($key)}}":"{{strval($text)}}",
|
||||
@endforeach
|
||||
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
|
||||
});
|
||||
if(dropzone instanceof Dropzone){
|
||||
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.preview_url()||document.url(),
|
||||
mock:true,
|
||||
index:i
|
||||
};
|
||||
|
||||
dropzone.emit('addedfile', mockFile);
|
||||
dropzone.emit('complete', mockFile);
|
||||
if(document.preview_url()){
|
||||
dropzone.emit('thumbnail', mockFile, document.preview_url()||document.url());
|
||||
}
|
||||
else if(document.type()=='jpeg' || document.type()=='png' || document.type()=='svg'){
|
||||
dropzone.emit('thumbnail', mockFile, document.url());
|
||||
}
|
||||
dropzone.files.push(mockFile);
|
||||
}
|
||||
}
|
||||
@endif
|
||||
});
|
||||
|
||||
function onFrequencyChange(){
|
||||
@ -943,7 +1024,9 @@
|
||||
}
|
||||
|
||||
function createInvoiceModel() {
|
||||
var invoice = ko.toJS(window.model).invoice;
|
||||
var model = ko.toJS(window.model);
|
||||
if(!model)return;
|
||||
var invoice = model.invoice;
|
||||
invoice.is_pro = {{ Auth::user()->isPro() ? 'true' : 'false' }};
|
||||
invoice.is_quote = {{ $entityType == ENTITY_QUOTE ? 'true' : 'false' }};
|
||||
invoice.contact = _.findWhere(invoice.client.contacts, {send_invoice: true});
|
||||
@ -965,7 +1048,7 @@
|
||||
@endif
|
||||
|
||||
@if ($account->hasLogo())
|
||||
invoice.image = "{{ Form::image_data($account->getLogoPath()) }}";
|
||||
invoice.image = "{{ Form::image_data($account->getLogoRaw(), true) }}";
|
||||
invoice.imageWidth = {{ $account->getLogoWidth() }};
|
||||
invoice.imageHeight = {{ $account->getLogoHeight() }};
|
||||
@endif
|
||||
@ -1257,6 +1340,43 @@
|
||||
model.invoice().invoice_number(number);
|
||||
}
|
||||
|
||||
@if ($account->isPro())
|
||||
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);
|
||||
refreshPDF(true);
|
||||
}
|
||||
|
||||
function handleDocumentUploaded(file, response){
|
||||
file.public_id = response.document.public_id
|
||||
model.invoice().documents()[file.index].update(response.document);
|
||||
refreshPDF(true);
|
||||
|
||||
if(response.document.preview_url){
|
||||
dropzone.emit('thumbnail', file, response.document.preview_url);
|
||||
}
|
||||
}
|
||||
@endif
|
||||
|
||||
</script>
|
||||
@if ($account->isPro() && $account->invoice_embed_documents)
|
||||
@foreach ($invoice->documents as $document)
|
||||
@if($document->isPDFEmbeddable())
|
||||
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
|
||||
@endif
|
||||
@endforeach
|
||||
@foreach ($invoice->expenses as $expense)
|
||||
@foreach ($expense->documents as $document)
|
||||
@if($document->isPDFEmbeddable())
|
||||
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@stop
|
||||
|
@ -57,4 +57,18 @@
|
||||
|
||||
@include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800])
|
||||
|
||||
@if (Utils::isPro() && $invoice->account->invoice_embed_documents)
|
||||
@foreach ($invoice->documents as $document)
|
||||
@if($document->isPDFEmbeddable())
|
||||
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
|
||||
@endif
|
||||
@endforeach
|
||||
@foreach ($invoice->expenses as $expense)
|
||||
@foreach ($expense->documents as $document)
|
||||
@if($document->isPDFEmbeddable())
|
||||
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
@endif
|
||||
@stop
|
@ -182,6 +182,8 @@ function InvoiceModel(data) {
|
||||
self.auto_bill = ko.observable();
|
||||
self.invoice_status_id = ko.observable(0);
|
||||
self.invoice_items = ko.observableArray();
|
||||
self.documents = ko.observableArray();
|
||||
self.expenses = ko.observableArray();
|
||||
self.amount = ko.observable(0);
|
||||
self.balance = ko.observable(0);
|
||||
self.invoice_design_id = ko.observable(1);
|
||||
@ -207,6 +209,16 @@ function InvoiceModel(data) {
|
||||
return new ItemModel(options.data);
|
||||
}
|
||||
},
|
||||
'documents': {
|
||||
create: function(options) {
|
||||
return new DocumentModel(options.data);
|
||||
}
|
||||
},
|
||||
'expenses': {
|
||||
create: function(options) {
|
||||
return new ExpenseModel(options.data);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.addItem = function() {
|
||||
@ -219,6 +231,19 @@ function InvoiceModel(data) {
|
||||
return itemModel;
|
||||
}
|
||||
|
||||
self.addDocument = function() {
|
||||
var documentModel = new DocumentModel();
|
||||
self.documents.push(documentModel);
|
||||
return documentModel;
|
||||
}
|
||||
|
||||
self.removeDocument = function(doc) {
|
||||
var public_id = doc.public_id?doc.public_id():doc;
|
||||
self.documents.remove(function(document) {
|
||||
return document.public_id() == public_id;
|
||||
});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
ko.mapping.fromJS(data, self.mapping, self);
|
||||
} else {
|
||||
@ -699,6 +724,45 @@ function ItemModel(data) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
var ExpenseModel = function(data) {
|
||||
var self = this;
|
||||
|
||||
self.mapping = {
|
||||
'documents': {
|
||||
create: function(options) {
|
||||
return new DocumentModel(options.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.description = ko.observable('');
|
||||
self.qty = ko.observable(0);
|
||||
self.public_id = ko.observable(0);
|
||||
self.amount = ko.observable();
|
||||
self.converted_amount = ko.observable();
|
||||
|
||||
if (data) {
|
||||
ko.mapping.fromJS(data, self.mapping, this);
|
||||
}
|
||||
};
|
||||
|
||||
/* Custom binding for product key typeahead */
|
||||
ko.bindingHandlers.typeahead = {
|
||||
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||
|
@ -75,7 +75,7 @@
|
||||
logoImages.imageLogoHeight3 = 81/2;
|
||||
|
||||
@if ($account->hasLogo())
|
||||
window.accountLogo = "{{ Form::image_data($account->getLogoPath()) }}";
|
||||
window.accountLogo = "{{ Form::image_data($account->getLogoRaw(), true) }}";
|
||||
if (window.invoice) {
|
||||
invoice.image = window.accountLogo;
|
||||
invoice.imageWidth = {{ $account->getLogoWidth() }};
|
||||
@ -123,7 +123,7 @@
|
||||
$('#theFrame').attr('src', string).show();
|
||||
} else {
|
||||
if (isRefreshing) {
|
||||
//needsRefresh = true;
|
||||
needsRefresh = true;
|
||||
return;
|
||||
}
|
||||
isRefreshing = true;
|
||||
|
@ -24,7 +24,6 @@
|
||||
@if ($checkoutComToken)
|
||||
@include('partials.checkout_com_payment')
|
||||
@else
|
||||
<p> </p>
|
||||
<div class="pull-right" style="text-align:right">
|
||||
@if ($invoice->is_quote)
|
||||
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||
@ -39,13 +38,47 @@
|
||||
<a href='{!! $paymentURL !!}' class="btn btn-success btn-lg">{{ trans('texts.pay_now') }}</a>
|
||||
@endif
|
||||
@else
|
||||
{!! Button::normal('Download PDF')->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||
@endif
|
||||
</div>
|
||||
<div class="pull-left">
|
||||
@if(!empty($documentsZipURL))
|
||||
{!! Button::normal(trans('texts.download_documents', array('size'=>Form::human_filesize($documentsZipSize))))->asLinkTo($documentsZipURL)->large() !!}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="clearfix"></div><p> </p>
|
||||
@if ($account->isPro() && $invoice->hasDocuments())
|
||||
<div class="invoice-documents">
|
||||
<h3>{{ trans('texts.documents_header') }}</h3>
|
||||
<ul>
|
||||
@foreach ($invoice->documents as $document)
|
||||
<li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}} ({{Form::human_filesize($document->size)}})</a></li>
|
||||
@endforeach
|
||||
@foreach ($invoice->expenses as $expense)
|
||||
@foreach ($expense->documents as $document)
|
||||
<li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}} ({{Form::human_filesize($document->size)}})</a></li>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($account->isPro() && $account->invoice_embed_documents)
|
||||
@foreach ($invoice->documents as $document)
|
||||
@if($document->isPDFEmbeddable())
|
||||
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" async></script>
|
||||
@endif
|
||||
@endforeach
|
||||
@foreach ($invoice->expenses as $expense)
|
||||
@foreach ($expense->documents as $document)
|
||||
@if($document->isPDFEmbeddable())
|
||||
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" async></script>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
@endif
|
||||
<script type="text/javascript">
|
||||
|
||||
window.invoice = {!! $invoice->toJson() !!};
|
||||
@ -91,5 +124,4 @@
|
||||
<p> </p>
|
||||
|
||||
</div>
|
||||
|
||||
@stop
|
||||
|
@ -87,6 +87,11 @@
|
||||
<li {{ Request::is('*client/invoices') ? 'class="active"' : '' }}>
|
||||
{!! link_to('/client/invoices', trans('texts.invoices') ) !!}
|
||||
</li>
|
||||
@if (!empty($showDocuments))
|
||||
<li {{ Request::is('*client/documents') ? 'class="active"' : '' }}>
|
||||
{!! link_to('/client/documents', trans('texts.documents') ) !!}
|
||||
</li>
|
||||
@endif
|
||||
<li {{ Request::is('*client/payments') ? 'class="active"' : '' }}>
|
||||
{!! link_to('/client/payments', trans('texts.payments') ) !!}
|
||||
</li>
|
||||
|
@ -9,9 +9,9 @@
|
||||
<a href="{{ URL::to("/settings/user_details") }}">
|
||||
@endif
|
||||
|
||||
@if (file_exists($logo_path))
|
||||
@if (!empty($logo_url))
|
||||
<div class="pull-left" style="height: 40px; margin-right: 16px;">
|
||||
<img style="width: 40px; margin-top:6px" src="{{ asset($logo_path) }}"/>
|
||||
<img style="width: 40px; margin-top:6px" src="{{ asset($logo_url) }}"/>
|
||||
</div>
|
||||
@else
|
||||
<div class="pull-left" style="width: 40px; min-height: 40px; margin-right: 16px"> </div>
|
||||
|
@ -24,8 +24,8 @@
|
||||
@foreach (Session::get(SESSION_USER_ACCOUNTS) as $account)
|
||||
<tr>
|
||||
<td>
|
||||
@if (isset($account->logo_path))
|
||||
{!! HTML::image($account->logo_path.'?no_cache='.time(), 'Logo', ['width' => 100]) !!}
|
||||
@if (isset($account->logo_url))
|
||||
{!! HTML::image($account->logo_url.'?no_cache='.time(), 'Logo', ['width' => 100]) !!}
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
|
@ -51,6 +51,12 @@
|
||||
"paddingBottom": "$amount:14"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stack": [
|
||||
"$invoiceDocuments"
|
||||
],
|
||||
"style": "invoiceDocuments"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
@ -254,6 +260,12 @@
|
||||
"help": {
|
||||
"fontSize": "$fontSizeSmaller",
|
||||
"color": "#737373"
|
||||
},
|
||||
"invoiceDocuments": {
|
||||
"margin": [47, 0, 47, 0]
|
||||
},
|
||||
"invoiceDocument": {
|
||||
"margin": [0, 10, 0, 10]
|
||||
}
|
||||
},
|
||||
"pageMargins": [0, 80, 0, 40]
|
||||
|
@ -69,6 +69,12 @@
|
||||
"paddingBottom": "$amount:14"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stack": [
|
||||
"$invoiceDocuments"
|
||||
],
|
||||
"style": "invoiceDocuments"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"$notesAndTerms",
|
||||
@ -200,6 +206,12 @@
|
||||
"help": {
|
||||
"fontSize": "$fontSizeSmaller",
|
||||
"color": "#737373"
|
||||
},
|
||||
"invoiceDocuments": {
|
||||
"margin": [7, 0, 7, 0]
|
||||
},
|
||||
"invoiceDocument": {
|
||||
"margin": [0, 10, 0, 10]
|
||||
}
|
||||
},
|
||||
"pageMargins": [40, 40, 40, 60]
|
||||
|
@ -85,7 +85,14 @@
|
||||
"margin": [0, 16, 8, 0]
|
||||
}
|
||||
]
|
||||
} ],
|
||||
},
|
||||
{
|
||||
"stack": [
|
||||
"$invoiceDocuments"
|
||||
],
|
||||
"style": "invoiceDocuments"
|
||||
}
|
||||
],
|
||||
"footer": [
|
||||
{
|
||||
"canvas": [
|
||||
@ -244,7 +251,13 @@
|
||||
"help": {
|
||||
"fontSize": "$fontSizeSmaller",
|
||||
"color": "#737373"
|
||||
},
|
||||
"invoiceDocuments": {
|
||||
"margin": [7, 0, 7, 0]
|
||||
},
|
||||
"invoiceDocument": {
|
||||
"margin": [0, 10, 0, 10]
|
||||
}
|
||||
},
|
||||
"pageMargins": [40, 120, 40, 50]
|
||||
}
|
||||
}
|
@ -57,6 +57,12 @@
|
||||
"paddingBottom": "$amount:8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stack": [
|
||||
"$invoiceDocuments"
|
||||
],
|
||||
"style": "invoiceDocuments"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"$notesAndTerms",
|
||||
@ -166,6 +172,12 @@
|
||||
"help": {
|
||||
"fontSize": "$fontSizeSmaller",
|
||||
"color": "#737373"
|
||||
},
|
||||
"invoiceDocuments": {
|
||||
"margin": [7, 0, 7, 0]
|
||||
},
|
||||
"invoiceDocument": {
|
||||
"margin": [0, 10, 0, 10]
|
||||
}
|
||||
},
|
||||
"pageMargins": [40, 40, 40, 60]
|
||||
|
Loading…
x
Reference in New Issue
Block a user