Merge remote-tracking branch 'upstream/develop' into wepay-integration

This commit is contained in:
Joshua Dwire 2016-06-01 16:46:03 -04:00
commit 49d47bb9e3
62 changed files with 948 additions and 625 deletions

View File

@ -5,6 +5,7 @@ sudo: true
php:
- 5.5.9
# - 5.6
# - 5.6
# - 7.0
# - hhvm
@ -27,6 +28,7 @@ before_install:
# set GitHub token and update composer
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
- composer self-update && composer -V
# - export USE_ZEND_ALLOC=0
install:
# install Composer dependencies
@ -66,6 +68,7 @@ before_script:
script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance ClientCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance ExpenseCest.php
@ -76,7 +79,6 @@ script:
- php ./vendor/codeception/codeception/codecept run acceptance OnlinePaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
#- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
#- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php
@ -96,4 +98,4 @@ after_script:
notifications:
email:
on_success: never
on_failure: change
on_failure: change

View File

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

View File

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

View File

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

View File

@ -262,6 +262,12 @@ class AppController extends BaseController
if (!Utils::isNinjaProd()) {
try {
set_time_limit(60 * 5);
Artisan::call('clear-compiled');
Artisan::call('cache:clear');
Artisan::call('debugbar:clear');
Artisan::call('route:clear');
Artisan::call('view:clear');
Artisan::call('config:clear');
Artisan::call('optimize', array('--force' => true));
Cache::flush();
Session::flush();

View File

@ -136,7 +136,7 @@ class ClientController extends BaseController
'credit' => $client->getTotalCredit(),
'title' => trans('texts.view_client'),
'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0,
'hasQuotes' => Invoice::scope()->where('is_quote', '=', true)->whereClientId($client->id)->count() > 0,
'hasQuotes' => Invoice::scope()->invoiceType(INVOICE_TYPE_QUOTE)->whereClientId($client->id)->count() > 0,
'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0,
'gatewayLink' => $client->getGatewayLink($accountGateway),
'gateway' => $accountGateway

View File

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

View File

@ -9,7 +9,7 @@ class DashboardApiController extends BaseAPIController
{
public function index()
{
$view_all = !Auth::user()->hasPermission('view_all');
$view_all = Auth::user()->hasPermission('view_all');
$user_id = Auth::user()->id;
// total_income, billed_clients, invoice_sent and active_clients
@ -24,7 +24,7 @@ class DashboardApiController extends BaseAPIController
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false)
->where('invoices.is_quote', '=', false);
->where('invoices.invoice_type_id', '=', false);
if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){
@ -62,7 +62,7 @@ class DashboardApiController extends BaseAPIController
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_quote', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false);
if(!$view_all){
@ -106,7 +106,7 @@ class DashboardApiController extends BaseAPIController
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
}
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote'])
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->orderBy('invoices.due_date', 'asc')
->take(50)
->get();
@ -131,7 +131,7 @@ class DashboardApiController extends BaseAPIController
}
$upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote'])
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->get();
$payments = DB::table('payments')
@ -157,13 +157,12 @@ class DashboardApiController extends BaseAPIController
$hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) {
foreach ($data as $invoice) {
if ($invoice->is_quote) {
if ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE) {
$hasQuotes = true;
}
}
}
$data = [
'id' => 1,
'paidToDate' => $paidToDate[0]->value ? $paidToDate[0]->value : 0,

View File

@ -26,7 +26,7 @@ class DashboardController extends BaseController
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_recurring', '=', false)
->where('invoices.is_quote', '=', false);
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD);
if(!$view_all){
$metrics = $metrics->where(function($query) use($user_id){
@ -64,7 +64,7 @@ class DashboardController extends BaseController
->where('accounts.id', '=', Auth::user()->account_id)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->where('invoices.is_quote', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_recurring', '=', false);
if(!$view_all){
@ -121,7 +121,7 @@ class DashboardController extends BaseController
$pastDue = $pastDue->where('invoices.user_id', '=', $user_id);
}
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote'])
$pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->orderBy('invoices.due_date', 'asc')
->take(50)
->get();
@ -147,7 +147,7 @@ class DashboardController extends BaseController
}
$upcoming = $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote'])
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->get();
$payments = DB::table('payments')
@ -173,7 +173,7 @@ class DashboardController extends BaseController
$hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) {
foreach ($data as $invoice) {
if ($invoice->is_quote) {
if ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE) {
$hasQuotes = true;
}
}

View File

@ -2,13 +2,21 @@
use App\Models\Document;
use App\Ninja\Repositories\DocumentRepository;
use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
class DocumentAPIController extends BaseAPIController
{
protected $documentRepo;
public function __construct()
protected $entityType = ENTITY_DOCUMENT;
public function __construct(DocumentRepository $documentRepo)
{
parent::__construct();
$this->documentRepo = $documentRepo;
}
public function index()
@ -16,16 +24,19 @@ class DocumentAPIController extends BaseAPIController
//stub
}
public function show($publicId)
public function show(DocumentRequest $request)
{
$document = Document::scope($publicId)->firstOrFail();
$document = $request->entity();
return DocumentController::getDownloadResponse($document);
}
public function store()
public function store(CreateDocumentRequest $request)
{
//stub
$document = $this->documentRepo->upload($request->all());
return $this->itemResponse($document);
}
public function update()

View File

@ -14,6 +14,7 @@ use App\Ninja\Repositories\DocumentRepository;
use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\UpdateDocumentRequest;
class DocumentController extends BaseController
{
@ -26,20 +27,20 @@ class DocumentController extends BaseController
$this->documentRepo = $documentRepo;
}
public function get(DocumentRequest $request)
{
return static::getDownloadResponse($request->entity());
}
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'],
@ -54,59 +55,55 @@ class DocumentController extends BaseController
$response = Response::make($document->getRaw(), 200);
$response->header('content-type', Document::$types[$document->type]['mime']);
}
return $response;
}
public function getPreview(DocumentRequest $request)
{
$document = $request->entity();
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(DocumentRequest $request, $publicId, $name)
{
$document = $request->entity();
if(substr($name, -3)=='.js'){
$name = substr($name, 0, -3);
}
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(CreateDocumentRequest $request)
{
if (!Utils::hasFeature(FEATURE_DOCUMENTS)) {
return;
}
$result = $this->documentRepo->upload(Input::all()['file'], $doc_array);
$result = $this->documentRepo->upload($request->all(), $doc_array);
if(is_string($result)){
return Response::json([
'error' => $result,
@ -120,4 +117,11 @@ class DocumentController extends BaseController
], 200);
}
}
public function delete(UpdateDocumentRequest $request)
{
$request->entity()->delete();
return RESULT_SUCCESS;
}
}

View File

@ -134,23 +134,23 @@ class ExportController extends BaseController
if ($request->input(ENTITY_INVOICE)) {
$data['invoices'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status')
->withArchived()
->where('is_quote', '=', false)
->where('is_recurring', '=', false)
->get();
$data['quotes'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_QUOTE)
->with('user', 'client.contacts', 'invoice_status')
->withArchived()
->where('is_quote', '=', true)
->where('is_recurring', '=', false)
->get();
$data['recurringInvoices'] = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status', 'frequency')
->withArchived()
->where('is_quote', '=', false)
->where('is_recurring', '=', true)
->get();
}

View File

@ -134,6 +134,7 @@ class InvoiceApiController extends BaseAPIController
'city',
'state',
'postal_code',
'country_id',
'private_notes',
'currency_code',
] as $field) {
@ -182,7 +183,7 @@ class InvoiceApiController extends BaseAPIController
$invoice = Invoice::scope($invoice->public_id)
->with('client', 'invoice_items', 'invitations')
->first();
return $this->itemResponse($invoice);
}
@ -269,7 +270,7 @@ class InvoiceApiController extends BaseAPIController
$item[$key] = $val;
}
}
return $item;
}
@ -308,7 +309,7 @@ class InvoiceApiController extends BaseAPIController
public function update(UpdateInvoiceAPIRequest $request, $publicId)
{
if ($request->action == ACTION_CONVERT) {
$quote = $request->entity();
$quote = $request->entity();
$invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id);
return $this->itemResponse($invoice);
} elseif ($request->action) {
@ -322,7 +323,7 @@ class InvoiceApiController extends BaseAPIController
$invoice = Invoice::scope($publicId)
->with('client', 'invoice_items', 'invitations')
->firstOrFail();
return $this->itemResponse($invoice);
}
@ -351,10 +352,23 @@ class InvoiceApiController extends BaseAPIController
public function destroy(UpdateInvoiceAPIRequest $request)
{
$invoice = $request->entity();
$this->invoiceRepo->delete($invoice);
return $this->itemResponse($invoice);
}
public function download(InvoiceRequest $request)
{
$invoice = $request->entity();
$pdfString = $invoice->getPDFString();
header('Content-Type: application/pdf');
header('Content-Length: ' . strlen($pdfString));
header('Content-disposition: attachment; filename="' . $invoice->getFileName() . '"');
header('Cache-Control: public, must-revalidate, max-age=0');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
return $pdfString;
}
}

View File

@ -574,9 +574,9 @@ class InvoiceController extends BaseController
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS),
];
$invoice->is_quote = intval($invoice->is_quote);
$invoice->invoice_type_id = intval($invoice->invoice_type_id);
$activityTypeId = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE;
$activityTypeId = $invoice->isType(INVOICE_TYPE_QUOTE) ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE;
$activities = Activity::scope(false, $invoice->account_id)
->where('activity_type_id', '=', $activityTypeId)
->where('invoice_id', '=', $invoice->id)
@ -596,7 +596,7 @@ class InvoiceController extends BaseController
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS),
];
$backup->is_quote = isset($backup->is_quote) && intval($backup->is_quote);
$backup->invoice_type_id = isset($backup->invoice_type_id) && intval($backup->invoice_type_id) == INVOICE_TYPE_QUOTE;
$backup->account = $invoice->account->toArray();
$versionsJson[$activity->id] = $backup;

View File

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

View File

@ -113,16 +113,16 @@ class QuoteController extends BaseController
$rates = TaxRate::scope()->orderBy('name')->get();
$options = [];
$defaultTax = false;
foreach ($rates as $rate) {
$options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%';
$options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%';
// load default invoice tax
if ($rate->id == $account->default_tax_rate_id) {
$defaultTax = $rate;
}
}
}
return [
'entityType' => ENTITY_QUOTE,
'account' => Auth::user()->account,
@ -130,7 +130,7 @@ class QuoteController extends BaseController
'taxRateOptions' => $options,
'defaultTax' => $defaultTax,
'countries' => Cache::get('countries'),
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(),
'clients' => Client::scope()->viewable()->with('contacts', 'country')->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->orderBy('name')->get(),
'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'),

View File

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

View File

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

View File

@ -1,7 +1,15 @@
<?php namespace App\Http\Requests;
use App\Models\Invoice;
use App\Models\Expense;
class CreateDocumentRequest extends DocumentRequest
{
protected $autoload = [
ENTITY_INVOICE,
ENTITY_EXPENSE,
];
/**
* Determine if the user is authorized to make this request.
*
@ -9,6 +17,18 @@ class CreateDocumentRequest extends DocumentRequest
*/
public function authorize()
{
if ( ! $this->user()->hasFeature(FEATURE_DOCUMENTS)) {
return false;
}
if ($this->invoice && $this->user()->cannot('edit', $this->invoice)) {
return false;
}
if ($this->expense && $this->user()->cannot('edit', $this->expense)) {
return false;
}
return $this->user()->can('create', ENTITY_DOCUMENT);
}
@ -20,7 +40,8 @@ class CreateDocumentRequest extends DocumentRequest
public function rules()
{
return [
//'file' => 'mimes:jpg'
];
}
}

View File

@ -2,8 +2,50 @@
use Illuminate\Foundation\Http\FormRequest;
// https://laracasts.com/discuss/channels/general-discussion/laravel-5-modify-input-before-validation/replies/34366
abstract class Request extends FormRequest {
//
// populate in subclass to auto load record
protected $autoload = [];
/**
* Validate the input.
*
* @param \Illuminate\Validation\Factory $factory
* @return \Illuminate\Validation\Validator
*/
public function validator($factory)
{
return $factory->make(
$this->sanitizeInput(), $this->container->call([$this, 'rules']), $this->messages()
);
}
/**
* Sanitize the input.
*
* @return array
*/
protected function sanitizeInput()
{
if (method_exists($this, 'sanitize')) {
$input = $this->container->call([$this, 'sanitize']);
} else {
$input = $this->all();
}
// autoload referenced entities
foreach ($this->autoload as $entityType) {
if ($id = $this->input("{$entityType}_public_id") ?: $this->input("{$entityType}_id")) {
$class = "App\\Models\\" . ucwords($entityType);
$entity = $class::scope($id)->firstOrFail();
$input[$entityType] = $entity;
$input[$entityType . '_id'] = $entity->id;
}
}
$this->replace($input);
return $this->all();
}
}

View File

@ -0,0 +1,26 @@
<?php namespace App\Http\Requests;
class UpdateDocumentRequest extends DocumentRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
];
}
}

View File

@ -157,7 +157,8 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('documents/{documents}/{filename?}', 'DocumentController@get');
Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS');
Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview');
Route::post('document', 'DocumentController@postUpload');
Route::post('documents', 'DocumentController@postUpload');
Route::delete('documents/{documents}', 'DocumentController@delete');
Route::get('quotes/create/{client_id?}', 'QuoteController@create');
Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice');
@ -271,6 +272,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function()
//Route::get('quotes', 'QuoteApiController@index');
//Route::resource('quotes', 'QuoteApiController');
Route::get('invoices', 'InvoiceApiController@index');
Route::get('download/{invoice_id}', 'InvoiceApiController@download');
Route::resource('invoices', 'InvoiceApiController');
Route::get('payments', 'PaymentApiController@index');
Route::resource('payments', 'PaymentApiController');
@ -360,6 +362,9 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_BANK_ACCOUNT', 'bank_account');
define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
define('PERSON_VENDOR_CONTACT','vendorcontact');

View File

@ -18,7 +18,7 @@ class Account extends Eloquent
{
use PresentableTrait;
use SoftDeletes;
public static $plan_prices = array(
PLAN_PRO => array(
PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY,
@ -56,6 +56,11 @@ class Account extends Eloquent
'currency_id',
'language_id',
'military_time',
'invoice_taxes',
'invoice_item_taxes',
'show_item_taxes',
'default_tax_rate_id',
'enable_second_tax_rate',
];
public static $basicSettings = [
@ -406,7 +411,7 @@ class Account extends Eloquent
return $gateway;
}
}
return false;
}
@ -426,27 +431,27 @@ class Account extends Eloquent
if($this->logo == ''){
$this->calculateLogoDetails();
}
return !empty($this->logo);
}
public function getLogoDisk(){
return Storage::disk(env('LOGO_FILESYSTEM', 'logos'));
}
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_height = imagesy($image);
$this->logo_size = $disk->size($this->logo);
} else {
$this->logo = null;
@ -458,33 +463,33 @@ class Account extends Eloquent
if(!$this->hasLogo()){
return null;
}
$disk = $this->getLogoDisk();
return $disk->get($this->logo);
}
public function getLogoURL($cachebuster = false)
{
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 ($cachebuster) {
$logo_url .= '?no_cache='.time();
}
if($count == 1){
return str_replace(DIRECTORY_SEPARATOR, '/', $logo_url);
}
}
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk());
}
@ -529,18 +534,18 @@ class Account extends Eloquent
$invoice = Invoice::createNew();
$invoice->is_recurring = false;
$invoice->is_quote = false;
$invoice->invoice_type_id = INVOICE_TYPE_STANDARD;
$invoice->invoice_date = Utils::today();
$invoice->start_date = Utils::today();
$invoice->invoice_design_id = $this->invoice_design_id;
$invoice->client_id = $clientId;
if ($entityType === ENTITY_RECURRING_INVOICE) {
$invoice->invoice_number = microtime(true);
$invoice->is_recurring = true;
} else {
if ($entityType == ENTITY_QUOTE) {
$invoice->is_quote = true;
$invoice->invoice_type_id = INVOICE_TYPE_QUOTE;
}
if ($this->hasClientNumberPattern($invoice) && !$clientId) {
@ -549,7 +554,7 @@ class Account extends Eloquent
$invoice->invoice_number = $this->getNextInvoiceNumber($invoice);
}
}
if (!$clientId) {
$invoice->client = Client::createNew();
$invoice->client->public_id = 0;
@ -558,34 +563,34 @@ class Account extends Eloquent
return $invoice;
}
public function getNumberPrefix($isQuote)
public function getNumberPrefix($invoice_type_id)
{
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) {
return '';
}
return ($isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: '';
return ($invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: '';
}
public function hasNumberPattern($isQuote)
public function hasNumberPattern($invoice_type_id)
{
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) {
return false;
}
return $isQuote ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false);
return $invoice_type_id == INVOICE_TYPE_QUOTE ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false);
}
public function hasClientNumberPattern($invoice)
{
$pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern;
$pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern;
return strstr($pattern, '$custom');
}
public function getNumberPattern($invoice)
{
$pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern;
$pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern;
if (!$pattern) {
return false;
@ -595,7 +600,7 @@ class Account extends Eloquent
$replace = [date('Y')];
$search[] = '{$counter}';
$replace[] = str_pad($this->getCounter($invoice->is_quote), $this->invoice_number_padding, '0', STR_PAD_LEFT);
$replace[] = str_pad($this->getCounter($invoice->invoice_type_id), $this->invoice_number_padding, '0', STR_PAD_LEFT);
if (strstr($pattern, '{$userId}')) {
$search[] = '{$userId}';
@ -638,9 +643,9 @@ class Account extends Eloquent
return str_replace($search, $replace, $pattern);
}
public function getCounter($isQuote)
public function getCounter($invoice_type_id)
{
return $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter;
return $invoice_type_id == INVOICE_TYPE_QUOTE && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter;
}
public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE)
@ -651,31 +656,35 @@ class Account extends Eloquent
public function getNextInvoiceNumber($invoice)
{
if ($this->hasNumberPattern($invoice->is_quote)) {
return $this->getNumberPattern($invoice);
if ($this->hasNumberPattern($invoice->invoice_type_id)) {
$number = $this->getNumberPattern($invoice);
} else {
$counter = $this->getCounter($invoice->invoice_type_id);
$prefix = $this->getNumberPrefix($invoice->invoice_type_id);
$counterOffset = 0;
// confirm the invoice number isn't already taken
do {
$number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT);
$check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first();
$counter++;
$counterOffset++;
} while ($check);
// update the invoice counter to be caught up
if ($counterOffset > 1) {
if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) {
$this->quote_number_counter += $counterOffset - 1;
} else {
$this->invoice_number_counter += $counterOffset - 1;
}
$this->save();
}
}
$counter = $this->getCounter($invoice->is_quote);
$prefix = $this->getNumberPrefix($invoice->is_quote);
$counterOffset = 0;
// confirm the invoice number isn't already taken
do {
$number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT);
$check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first();
$counter++;
$counterOffset++;
} while ($check);
// update the invoice counter to be caught up
if ($counterOffset > 1) {
if ($invoice->is_quote && !$this->share_counter) {
$this->quote_number_counter += $counterOffset - 1;
} else {
$this->invoice_number_counter += $counterOffset - 1;
}
$this->save();
if ($invoice->recurring_invoice_id) {
$number = $this->recurring_invoice_number_prefix . $number;
}
return $number;
@ -683,19 +692,17 @@ class Account extends Eloquent
public function incrementCounter($invoice)
{
if ($invoice->is_quote && !$this->share_counter) {
// if they didn't use the counter don't increment it
if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice)) {
return;
}
if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) {
$this->quote_number_counter += 1;
} else {
$default = $this->invoice_number_counter;
$actual = Utils::parseInt($invoice->invoice_number);
if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $default != $actual) {
$this->invoice_number_counter = $actual + 1;
} else {
$this->invoice_number_counter += 1;
}
$this->invoice_number_counter += 1;
}
$this->save();
}
@ -718,7 +725,7 @@ class Account extends Eloquent
$query->where('updated_at', '>=', $updatedAt);
}
}]);
}
}
}
public function loadLocalizationSettings($client = false)
@ -731,8 +738,8 @@ class Account extends Eloquent
Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT);
Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT);
$currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY;
$locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE);
$currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY;
$locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE);
Session::put(SESSION_CURRENCY, $currencyId);
Session::put(SESSION_LOCALE, $locale);
@ -815,7 +822,7 @@ class Account extends Eloquent
if ( ! Utils::isNinja()) {
return;
}
$this->company->trial_plan = $plan;
$this->company->trial_started = date_create()->format('Y-m-d');
$this->company->save();
@ -826,18 +833,18 @@ class Account extends Eloquent
if (Utils::isNinjaDev()) {
return true;
}
$planDetails = $this->getPlanDetails();
$selfHost = !Utils::isNinjaProd();
if (!$selfHost && function_exists('ninja_account_features')) {
$result = ninja_account_features($this, $feature);
if ($result != null) {
return $result;
}
}
switch ($feature) {
// Pro
case FEATURE_CUSTOMIZE_INVOICE_DESIGN:
@ -854,7 +861,7 @@ class Account extends Eloquent
case FEATURE_CLIENT_PORTAL_PASSWORD:
case FEATURE_CUSTOM_URL:
return $selfHost || !empty($planDetails);
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case FEATURE_MORE_CLIENTS:
return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false)));
@ -867,26 +874,26 @@ class Account extends Eloquent
// Fallthrough
case FEATURE_CLIENT_PORTAL_CSS:
return !empty($planDetails);// A plan is required even for self-hosted users
// Enterprise; No Trial allowed; grandfathered for old pro users
case FEATURE_USERS:// Grandfathered for old Pro users
if($planDetails && $planDetails['trial']) {
// Do they have a non-trial plan?
$planDetails = $this->getPlanDetails(false, false);
}
return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE));
// Enterprise; No Trial allowed
case FEATURE_DOCUMENTS:
case FEATURE_USER_PERMISSIONS:
return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial'];
default:
return false;
}
}
public function isPro(&$plan_details = null)
{
if (!Utils::isNinjaProd()) {
@ -898,7 +905,7 @@ class Account extends Eloquent
}
$plan_details = $this->getPlanDetails();
return !empty($plan_details);
}
@ -913,36 +920,36 @@ class Account extends Eloquent
}
$plan_details = $this->getPlanDetails();
return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE;
}
public function getPlanDetails($include_inactive = false, $include_trial = true)
{
if (!$this->company) {
return null;
}
$plan = $this->company->plan;
$trial_plan = $this->company->trial_plan;
if(!$plan && (!$trial_plan || !$include_trial)) {
return null;
}
}
$trial_active = false;
if ($trial_plan && $include_trial) {
$trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started);
$trial_expires = clone $trial_started;
$trial_expires->modify('+2 weeks');
if ($trial_expires >= date_create()) {
$trial_active = true;
}
}
$plan_active = false;
if ($plan) {
if ($plan) {
if ($this->company->plan_expires == null) {
$plan_active = true;
$plan_expires = false;
@ -953,11 +960,11 @@ class Account extends Eloquent
}
}
}
if (!$include_inactive && !$plan_active && !$trial_active) {
return null;
}
// Should we show plan details or trial details?
if (($plan && !$trial_plan) || !$include_trial) {
$use_plan = true;
@ -984,7 +991,7 @@ class Account extends Eloquent
$use_plan = $plan_expires >= $trial_expires;
}
}
if ($use_plan) {
return array(
'trial' => false,
@ -1011,7 +1018,7 @@ class Account extends Eloquent
if (!Utils::isNinjaProd()) {
return false;
}
$plan_details = $this->getPlanDetails();
return $plan_details && $plan_details['trial'];
@ -1026,7 +1033,7 @@ class Account extends Eloquent
return array(PLAN_PRO, PLAN_ENTERPRISE);
}
}
if ($this->company->trial_plan == PLAN_PRO) {
if ($plan) {
return $plan != PLAN_PRO;
@ -1034,28 +1041,28 @@ class Account extends Eloquent
return array(PLAN_ENTERPRISE);
}
}
return false;
}
public function getCountTrialDaysLeft()
{
$planDetails = $this->getPlanDetails(true);
if(!$planDetails || !$planDetails['trial']) {
return 0;
}
$today = new DateTime('now');
$interval = $today->diff($planDetails['expires']);
return $interval ? $interval->d : 0;
}
public function getRenewalDate()
{
$planDetails = $this->getPlanDetails();
if ($planDetails) {
$date = $planDetails['expires'];
$date = max($date, date_create());
@ -1107,7 +1114,7 @@ class Account extends Eloquent
'invoice_items',
'created_at',
'is_recurring',
'is_quote',
'invoice_type_id',
]);
foreach ($invoice->invoice_items as $invoiceItem) {
@ -1185,7 +1192,7 @@ class Account extends Eloquent
$field = "email_template_{$entityType}";
$template = $this->$field;
}
if (!$template) {
$template = $this->getDefaultEmailTemplate($entityType, $message);
}
@ -1275,7 +1282,7 @@ class Account extends Eloquent
{
$url = SITE_URL;
$iframe_url = $this->iframe_url;
if ($iframe_url) {
return "{$iframe_url}/?";
} else if ($this->subdomain) {
@ -1310,18 +1317,18 @@ class Account extends Eloquent
if (!$entity) {
return false;
}
// convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1'
$field = str_replace(['invoice_', 'label'], ['', 'value'], $field);
return Utils::isEmpty($entity->$field) ? false : true;
}
public function attatchPDF()
public function attachPDF()
{
return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment;
}
public function getEmailDesignId()
{
return $this->hasFeature(FEATURE_CUSTOM_EMAILS) ? $this->email_design_id : EMAIL_DESIGN_PLAIN;
@ -1329,11 +1336,11 @@ class Account extends Eloquent
public function clientViewCSS(){
$css = '';
if ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) {
$bodyFont = $this->getBodyFontCss();
$headerFont = $this->getHeaderFontCss();
$css = 'body{'.$bodyFont.'}';
if ($headerFont != $bodyFont) {
$css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}';
@ -1343,17 +1350,17 @@ class Account extends Eloquent
// For self-hosted users, a white-label license is required for custom CSS
$css .= $this->client_view_css;
}
return $css;
}
public function getFontsUrl($protocol = ''){
$bodyFont = $this->getHeaderFontId();
$headerFont = $this->getBodyFontId();
$bodyFontSettings = Utils::getFromCache($bodyFont, 'fonts');
$google_fonts = array($bodyFontSettings['google_font']);
if($headerFont != $bodyFont){
$headerFontSettings = Utils::getFromCache($headerFont, 'fonts');
$google_fonts[] = $headerFontSettings['google_font'];
@ -1361,7 +1368,7 @@ class Account extends Eloquent
return ($protocol?$protocol.':':'').'//fonts.googleapis.com/css?family='.implode('|',$google_fonts);
}
public function getHeaderFontId() {
return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT;
}
@ -1373,47 +1380,47 @@ class Account extends Eloquent
public function getHeaderFontName(){
return Utils::getFromCache($this->getHeaderFontId(), 'fonts')['name'];
}
public function getBodyFontName(){
return Utils::getFromCache($this->getBodyFontId(), 'fonts')['name'];
}
public function getHeaderFontCss($include_weight = true){
$font_data = Utils::getFromCache($this->getHeaderFontId(), 'fonts');
$css = 'font-family:'.$font_data['css_stack'].';';
if($include_weight){
$css .= 'font-weight:'.$font_data['css_weight'].';';
}
return $css;
}
public function getBodyFontCss($include_weight = true){
$font_data = Utils::getFromCache($this->getBodyFontId(), 'fonts');
$css = 'font-family:'.$font_data['css_stack'].';';
if($include_weight){
$css .= 'font-weight:'.$font_data['css_weight'].';';
}
return $css;
}
public function getFonts(){
return array_unique(array($this->getHeaderFontId(), $this->getBodyFontId()));
}
public function getFontsData(){
$data = array();
foreach($this->getFonts() as $font){
$data[] = Utils::getFromCache($font, 'fonts');
}
return $data;
}
public function getFontFolders(){
return array_map(function($item){return $item['folder'];}, $this->getFontsData());
}

View File

@ -6,20 +6,25 @@ use Auth;
class Document extends EntityModel
{
protected $fillable = [
'invoice_id',
'expense_id',
];
public static $extraExtensions = array(
'jpg' => 'jpeg',
'tif' => 'tiff',
);
public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts
'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain',
'application/zip', 'application/msword',
'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel',
'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml',
'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint',
);
public static $types = array(
'png' => array(
'mime' => 'image/png',
@ -70,18 +75,18 @@ class Document extends EntityModel
'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
),
);
public function fill(array $attributes)
{
parent::fill($attributes);
if(empty($this->attributes['disk'])){
$this->attributes['disk'] = env('DOCUMENT_FILESYSTEM', 'documents');
}
return $this;
}
public function account()
{
return $this->belongsTo('App\Models\Account');
@ -101,7 +106,7 @@ class Document extends EntityModel
{
return $this->belongsTo('App\Models\Invoice')->withTrashed();
}
public function getDisk(){
return Storage::disk(!empty($this->disk)?$this->disk:env('DOCUMENT_FILESYSTEM', 'documents'));
}
@ -110,19 +115,19 @@ class Document extends EntityModel
{
$this->attributes['disk'] = $value?$value:env('DOCUMENT_FILESYSTEM', 'documents');
}
public function getDirectUrl(){
return static::getDirectFileUrl($this->path, $this->getDisk());
}
public function getDirectPreviewUrl(){
return $this->preview?static::getDirectFileUrl($this->preview, $this->getDisk(), true):null;
}
public static function getDirectFileUrl($path, $disk, $prioritizeSpeed = false){
$adapter = $disk->getAdapter();
$fullPath = $adapter->applyPathPrefix($path);
if($adapter instanceof \League\Flysystem\AwsS3v3\AwsS3Adapter) {
$client = $adapter->getClient();
$command = $client->getCommand('GetObject', [
@ -136,12 +141,12 @@ class Document extends EntityModel
$secret = env('RACKSPACE_TEMP_URL_SECRET');
if($secret){
$object = $adapter->getContainer()->getObject($fullPath);
if(env('RACKSPACE_TEMP_URL_SECRET_SET')){
// Go ahead and set the secret too
$object->getService()->getAccount()->setTempUrlSecret($secret);
}
}
$url = $object->getUrl();
$expiry = strtotime('+10 minutes');
$urlPath = urldecode($url->getPath());
@ -150,64 +155,64 @@ class Document extends EntityModel
return sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry);
}
}
return null;
}
public function getRaw(){
$disk = $this->getDisk();
return $disk->get($this->path);
}
public function getStream(){
$disk = $this->getDisk();
return $disk->readStream($this->path);
}
public function getRawPreview(){
$disk = $this->getDisk();
return $disk->get($this->preview);
}
public function getUrl(){
return url('documents/'.$this->public_id.'/'.$this->name);
}
public function getClientUrl($invitation){
return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name);
}
public function isPDFEmbeddable(){
return $this->type == 'jpeg' || $this->type == 'png' || $this->preview;
}
public function getVFSJSUrl(){
if(!$this->isPDFEmbeddable())return null;
return url('documents/js/'.$this->public_id.'/'.$this->name.'.js');
}
public function getClientVFSJSUrl(){
if(!$this->isPDFEmbeddable())return null;
return url('client/documents/js/'.$this->public_id.'/'.$this->name.'.js');
}
public function getPreviewUrl(){
return $this->preview?url('documents/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null;
}
public function toArray()
{
$array = parent::toArray();
if(empty($this->visible) || in_array('url', $this->visible))$array['url'] = $this->getUrl();
if(empty($this->visible) || in_array('preview_url', $this->visible))$array['preview_url'] = $this->getPreviewUrl();
return $array;
}
public function cloneDocument(){
$document = Document::createNew($this);
$document->path = $this->path;
@ -219,7 +224,7 @@ class Document extends EntityModel
$document->size = $this->size;
$document->width = $this->width;
$document->height = $this->height;
return $document;
}
}
@ -230,11 +235,11 @@ Document::deleted(function ($document) {
->where('documents.path', '=', $document->path)
->where('documents.disk', '=', $document->disk)
->count();
if(!$same_path_count){
$document->getDisk()->delete($document->path);
}
if($document->preview){
$same_preview_count = DB::table('documents')
->where('documents.account_id', '=', $document->account_id)
@ -245,5 +250,5 @@ Document::deleted(function ($document) {
$document->getDisk()->delete($document->preview);
}
}
});
});

View File

@ -30,7 +30,7 @@ class EntityModel extends Eloquent
} else {
$lastEntity = $className::scope(false, $entity->account_id);
}
$lastEntity = $lastEntity->orderBy('public_id', 'DESC')
->first();
@ -86,6 +86,15 @@ class EntityModel extends Eloquent
return $query;
}
public function scopeViewable($query)
{
if (Auth::check() && ! Auth::user()->hasPermission('view_all')) {
$query->where($this->getEntityType(). 's.user_id', '=', Auth::user()->id);
}
return $query;
}
public function scopeWithArchived($query)
{
return $query->withTrashed()->where('is_deleted', '=', false);
@ -110,7 +119,7 @@ class EntityModel extends Eloquent
{
return 'App\\Ninja\\Transformers\\' . ucwords(Utils::toCamelCase($entityType)) . 'Transformer';
}
public function setNullValues()
{
foreach ($this->fillable as $field) {

View File

@ -29,9 +29,9 @@ class Invoice extends EntityModel implements BalanceAffecting
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
'tax_rate2',
];
protected $casts = [
'is_recurring' => 'boolean',
'has_tasks' => 'boolean',
@ -96,7 +96,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function affectsBalance()
{
return !$this->is_quote && !$this->is_recurring;
return $this->isType(INVOICE_TYPE_STANDARD) && !$this->is_recurring;
}
public function getAdjustment()
@ -139,7 +139,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getAmountPaid($calculate = false)
{
if ($this->is_quote || $this->is_recurring) {
if ($this->isType(INVOICE_TYPE_QUOTE) || $this->is_recurring) {
return 0;
}
@ -230,10 +230,23 @@ class Invoice extends EntityModel implements BalanceAffecting
public function scopeInvoices($query)
{
return $query->where('is_quote', '=', false)
return $query->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false);
}
public function scopeInvoiceType($query, $typeId)
{
return $query->where('invoice_type_id', '=', $typeId);
}
public function isType($typeId) {
return $this->invoice_type_id == $typeId;
}
public function isQuote() {
return $this->isType(INVOICE_TYPE_QUOTE);
}
public function markInvitationsSent($notify = false)
{
foreach ($this->invitations as $invitation) {
@ -256,7 +269,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return;
}
if ($this->is_quote) {
if ($this->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteInvitationWasEmailed($invitation));
} else {
event(new InvoiceInvitationWasEmailed($invitation));
@ -292,7 +305,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function markApproved()
{
if ($this->is_quote) {
if ($this->isType(INVOICE_TYPE_QUOTE)) {
$this->invoice_status_id = INVOICE_STATUS_APPROVED;
$this->save();
}
@ -341,7 +354,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getEntityType()
{
return $this->is_quote ? ENTITY_QUOTE : ENTITY_INVOICE;
return $this->isType(INVOICE_TYPE_QUOTE) ? ENTITY_QUOTE : ENTITY_INVOICE;
}
public function isSent()
@ -416,7 +429,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'invoice_design_id',
'invoice_fonts',
'features',
'is_quote',
'invoice_type_id',
'custom_value1',
'custom_value2',
'custom_taxes1',
@ -515,12 +528,12 @@ class Invoice extends EntityModel implements BalanceAffecting
'name',
]);
}
foreach ($this->expenses as $expense) {
$expense->setVisible([
'documents',
]);
foreach ($expense->documents as $document) {
$document->setVisible([
'public_id',
@ -579,12 +592,12 @@ class Invoice extends EntityModel implements BalanceAffecting
return $schedule[1]->getStart();
}
public function getDueDate($invoice_date = null){
if(!$this->is_recurring) {
return $this->due_date ? $this->due_date : null;
}
else{
else{
$now = time();
if($invoice_date) {
// If $invoice_date is specified, all calculations are based on that date
@ -598,7 +611,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$now = $invoice_date->getTimestamp();
}
}
if($this->due_date && $this->due_date != '0000-00-00'){
// This is a recurring invoice; we're using a custom format here.
// The year is always 1998; January is 1st, 2nd, last day of the month.
@ -607,7 +620,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$monthVal = (int)date('n', $dueDateVal);
$dayVal = (int)date('j', $dueDateVal);
$dueDate = false;
if($monthVal == 1) {// January; day of month
$currentDay = (int)date('j', $now);
$lastDayOfMonth = (int)date('t', $now);
@ -634,7 +647,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if($dueDay > $lastDayOfMonth){
// No later than the end of the month
$dueDay = $lastDayOfMonth;
}
}
}
$dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear);
@ -663,7 +676,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return date('Y-m-d', strtotime('+'.$days.' day', $now));
}
}
// Couldn't calculate one
return null;
}
@ -681,11 +694,11 @@ class Invoice extends EntityModel implements BalanceAffecting
$dateStart = $date->getStart();
$date = $this->account->formatDate($dateStart);
$dueDate = $this->getDueDate($dateStart);
if($dueDate) {
$date .= ' <small>(' . trans('texts.due') . ' ' . $this->account->formatDate($dueDate) . ')</small>';
}
$dates[] = $date;
}
@ -799,16 +812,16 @@ class Invoice extends EntityModel implements BalanceAffecting
$invitation = $this->invitations[0];
$link = $invitation->getLink('view', true);
$key = env('PHANTOMJS_CLOUD_KEY');
if (Utils::isNinjaDev()) {
$link = env('TEST_LINK');
}
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$pdfString = file_get_contents($url);
$pdfString = strip_tags($pdfString);
if ( ! $pdfString || strlen($pdfString) < 200) {
Utils::logError("PhantomJSCloud - failed to create pdf: {$pdfString}");
return false;
@ -861,14 +874,14 @@ class Invoice extends EntityModel implements BalanceAffecting
return $total;
}
// if $calculatePaid is true we'll loop through each payment to
// if $calculatePaid is true we'll loop through each payment to
// determine the sum, otherwise we'll use the cached paid_to_date amount
public function getTaxes($calculatePaid = false)
{
$taxes = [];
$taxable = $this->getTaxable();
$paidAmount = $this->getAmountPaid($calculatePaid);
if ($this->tax_name1) {
$invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2);
$invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
@ -883,7 +896,7 @@ class Invoice extends EntityModel implements BalanceAffecting
foreach ($this->invoice_items as $invoiceItem) {
$itemTaxAmount = $this->getItemTaxable($invoiceItem, $taxable);
if ($invoiceItem->tax_name1) {
$itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2);
$itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
@ -896,20 +909,20 @@ class Invoice extends EntityModel implements BalanceAffecting
$this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount);
}
}
return $taxes;
}
private function calculateTax(&$taxes, $name, $rate, $amount, $paid)
{
private function calculateTax(&$taxes, $name, $rate, $amount, $paid)
{
if ( ! $amount) {
return;
}
}
$amount = round($amount, 2);
$paid = round($paid, 2);
$key = $rate . ' ' . $name;
if ( ! isset($taxes[$key])) {
$taxes[$key] = [
'name' => $name,
@ -920,14 +933,14 @@ class Invoice extends EntityModel implements BalanceAffecting
}
$taxes[$key]['amount'] += $amount;
$taxes[$key]['paid'] += $paid;
$taxes[$key]['paid'] += $paid;
}
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;
@ -957,7 +970,7 @@ Invoice::creating(function ($invoice) {
});
Invoice::created(function ($invoice) {
if ($invoice->is_quote) {
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasCreated($invoice));
} else {
event(new InvoiceWasCreated($invoice));
@ -965,7 +978,7 @@ Invoice::created(function ($invoice) {
});
Invoice::updating(function ($invoice) {
if ($invoice->is_quote) {
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasUpdated($invoice));
} else {
event(new InvoiceWasUpdated($invoice));

View File

@ -16,6 +16,24 @@ class Product extends EntityModel
'default_tax_rate_id',
];
public static function getImportColumns()
{
return [
'product_key',
'notes',
'cost',
];
}
public static function getImportMap()
{
return [
'product|item' => 'product_key',
'notes|description|details' => 'notes',
'cost|amount|price' => 'cost',
];
}
public function getEntityType()
{
return ENTITY_PRODUCT;

View File

@ -15,21 +15,38 @@ class BaseTransformer extends TransformerAbstract
protected function hasClient($name)
{
$name = strtolower($name);
$name = trim(strtolower($name));
return isset($this->maps[ENTITY_CLIENT][$name]);
}
protected function hasProduct($key)
{
$key = trim(strtolower($key));
return isset($this->maps[ENTITY_PRODUCT][$key]);
}
protected function getString($data, $field)
{
return (isset($data->$field) && $data->$field) ? $data->$field : '';
}
protected function getNumber($data, $field)
{
return (isset($data->$field) && $data->$field) ? $data->$field : 0;
}
protected function getClientId($name)
{
$name = strtolower($name);
return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null;
}
protected function getProductId($name)
{
$name = strtolower($name);
return isset($this->maps[ENTITY_PRODUCT][$name]) ? $this->maps[ENTITY_PRODUCT][$name] : null;
}
protected function getCountryId($name)
{
$name = strtolower($name);
@ -53,7 +70,7 @@ class BaseTransformer extends TransformerAbstract
if ( ! $date instanceof DateTime) {
$date = DateTime::createFromFormat($format, $date);
}
return $date ? $date->format('Y-m-d') : null;
}
@ -87,11 +104,11 @@ class BaseTransformer extends TransformerAbstract
return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null;
}
protected function getVendorId($name)
{
$name = strtolower($name);
return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null;
}
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Ninja\Import\CSV;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
class ProductTransformer extends BaseTransformer
{
public function transform($data)
{
if (empty($data->product_key) || $this->hasProduct($data->product_key)) {
return false;
}
return new Item($data, function ($data) {
return [
'product_key' => $this->getString($data, 'product_key'),
'notes' => $this->getString($data, 'notes'),
'cost' => $this->getNumber($data, 'cost'),
];
});
}
}

View File

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

View File

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

View File

@ -57,43 +57,49 @@ class DocumentRepository extends BaseRepository
return $query;
}
public function upload($uploaded, &$doc_array=null)
public function upload($data, &$doc_array=null)
{
$uploaded = $data['file'];
$extension = strtolower($uploaded->getClientOriginalExtension());
if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){
$documentType = Document::$extraExtensions[$extension];
$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';
}
// don't allow a document to be linked to both an invoice and an expense
if (array_get($data, 'invoice_id') && array_get($data, 'expense_id')) {
unset($data['expense_id']);
}
$hash = sha1_file($filePath);
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType;
$document = Document::createNew();
$document->fill($data);
$disk = $document->getDisk();
if(!$disk->exists($filename)){// Have we already stored the same file
$stream = fopen($filePath, 'r');
$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;
@ -105,32 +111,32 @@ class DocumentRepository extends BaseRepository
// Needs to be converted
$makePreview = true;
} else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){
$makePreview = true;
$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;
@ -141,9 +147,9 @@ class DocumentRepository extends BaseRepository
$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);
@ -153,23 +159,23 @@ class DocumentRepository extends BaseRepository
}
}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;
@ -177,10 +183,10 @@ class DocumentRepository extends BaseRepository
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')
@ -192,7 +198,7 @@ class DocumentRepository extends BaseRepository
->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)
//->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT)
->select(
'invitations.invitation_key',
'invoices.invoice_number',
@ -205,22 +211,22 @@ class DocumentRepository extends BaseRepository
$table = \Datatable::query($query)
->addColumn('invoice_number', function ($model) {
return link_to(
'/view/'.$model->invitation_key,
'/view/'.$model->invitation_key,
$model->invoice_number
)->toHtml();
)->toHtml();
})
->addColumn('name', function ($model) {
return link_to(
'/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name,
'/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name,
$model->name,
['target'=>'_blank']
)->toHtml();
)->toHtml();
})
->addColumn('document_date', function ($model) {
return Utils::fromSqlDate($model->created_at);
return Utils::dateToString($model->created_at);
})
->addColumn('document_size', function ($model) {
return Form::human_filesize($model->size);
return Form::human_filesize($model->size);
});
return $table->make();

View File

@ -148,33 +148,17 @@ class ExpenseRepository extends BaseRepository
// 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 && Auth::user()->can('edit', $document)){
$document->invoice_id = null;
$document->expense_id = $expense->id;
$document->save();
}
}
if(!empty($input['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){
// Fallback upload
$doc_errors = array();
foreach($input['documents'] as $upload){
$result = $this->documentRepo->upload($upload);
if(is_string($result)){
$doc_errors[] = $result;
}
else{
$result->expense_id = $expense->id;
$result->save();
$document_ids[] = $result->public_id;
// check document completed upload before user submitted form
if ($document_id) {
$document = Document::scope($document_id)->first();
if($document && Auth::user()->can('edit', $document)){
$document->invoice_id = null;
$document->expense_id = $expense->id;
$document->save();
}
}
if(!empty($doc_errors)){
Session::flash('error', implode('<br>',array_map('htmlentities',$doc_errors)));
}
}
// prevent loading all of the documents if we don't have to
if ( ! $expense->wasRecentlyCreated) {
foreach ($expense->documents as $document){

View File

@ -32,9 +32,9 @@ class InvoiceRepository extends BaseRepository
public function all()
{
return Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->with('user', 'client.contacts', 'invoice_status')
->withTrashed()
->where('is_quote', '=', false)
->where('is_recurring', '=', false)
->get();
}
@ -106,7 +106,7 @@ class InvoiceRepository extends BaseRepository
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', $accountId)
->where('invoices.is_quote', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true)
->where('contacts.is_primary', '=', true)
@ -156,7 +156,7 @@ class InvoiceRepository extends BaseRepository
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null)
->where('invoices.is_quote', '=', false)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true)
@ -203,14 +203,14 @@ class InvoiceRepository extends BaseRepository
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null)
->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE)
->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD)
->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('contacts.deleted_at', '=', null)
->where('contacts.is_primary', '=', true)
->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)
//->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT)
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -287,7 +287,7 @@ class InvoiceRepository extends BaseRepository
$account->invoice_footer = trim($data['invoice_footer']);
}
$account->save();
}
}
if (isset($data['invoice_number']) && !$invoice->is_recurring) {
$invoice->invoice_number = trim($data['invoice_number']);
@ -329,7 +329,7 @@ class InvoiceRepository extends BaseRepository
if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) {
$invoice->auto_bill = AUTO_BILL_OFF;
}
if (isset($data['recurring_due_date'])) {
$invoice->due_date = $data['recurring_due_date'];
} elseif (isset($data['due_date'])) {
@ -351,7 +351,7 @@ class InvoiceRepository extends BaseRepository
} else {
$invoice->terms = '';
}
$invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : '');
$invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null;
@ -370,8 +370,8 @@ class InvoiceRepository extends BaseRepository
// provide backwards compatability
if (isset($data['tax_name']) && isset($data['tax_rate'])) {
$data['tax_name1'] = $data['tax_name'];
$data['tax_rate1'] = $data['tax_rate'];
$data['tax_name1'] = $data['tax_name'];
$data['tax_rate1'] = $data['tax_rate'];
}
$total = 0;
@ -405,11 +405,11 @@ class InvoiceRepository extends BaseRepository
}
if (isset($item['tax_rate1']) && Utils::parseFloat($item['tax_rate1']) > 0) {
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']);
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']);
$itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2);
}
if (isset($item['tax_rate2']) && Utils::parseFloat($item['tax_rate2']) > 0) {
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']);
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']);
$itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2);
}
}
@ -453,7 +453,7 @@ class InvoiceRepository extends BaseRepository
$taxAmount1 = round($total * $invoice->tax_rate1 / 100, 2);
$taxAmount2 = round($total * $invoice->tax_rate2 / 100, 2);
$total = round($total + $taxAmount1 + $taxAmount2, 2);
$total = round($total + $taxAmount1 + $taxAmount2, 2);
$total += $itemTax;
// custom fields not charged taxes
@ -476,43 +476,24 @@ class InvoiceRepository extends BaseRepository
if ($publicId) {
$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 && Auth::user()->can('edit', $document)){
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']) && Auth::user()->can('create', ENTITY_DOCUMENT)){
// Fallback upload
$doc_errors = array();
foreach($data['documents'] as $upload){
$result = $this->documentRepo->upload($upload);
if(is_string($result)){
$doc_errors[] = $result;
}
else{
$result->invoice_id = $invoice->id;
$result->save();
$document_ids[] = $result->public_id;
}
}
if(!empty($doc_errors)){
Session::flash('error', implode('<br>',array_map('htmlentities',$doc_errors)));
}
}
foreach ($invoice->documents as $document){
if(!in_array($document->public_id, $document_ids)){
// Removed
@ -586,12 +567,12 @@ class InvoiceRepository extends BaseRepository
// provide backwards compatability
if (isset($item['tax_name']) && isset($item['tax_rate'])) {
$item['tax_name1'] = $item['tax_name'];
$item['tax_rate1'] = $item['tax_rate'];
$item['tax_name1'] = $item['tax_name'];
$item['tax_rate1'] = $item['tax_rate'];
}
$invoiceItem->fill($item);
$invoice->invoice_items()->save($invoiceItem);
}
@ -642,7 +623,7 @@ class InvoiceRepository extends BaseRepository
'tax_name2',
'tax_rate2',
'amount',
'is_quote',
'invoice_type_id',
'custom_value1',
'custom_value2',
'custom_taxes1',
@ -654,7 +635,7 @@ class InvoiceRepository extends BaseRepository
}
if ($quotePublicId) {
$clone->is_quote = false;
$clone->invoice_type_id = INVOICE_TYPE_STANDARD;
$clone->quote_id = $quotePublicId;
}
@ -675,9 +656,9 @@ class InvoiceRepository extends BaseRepository
'cost',
'qty',
'tax_name1',
'tax_rate1',
'tax_rate1',
'tax_name2',
'tax_rate2',
'tax_rate2',
] as $field) {
$cloneItem->$field = $item->$field;
}
@ -686,7 +667,7 @@ class InvoiceRepository extends BaseRepository
}
foreach ($invoice->documents as $document) {
$cloneDocument = $document->cloneDocument();
$cloneDocument = $document->cloneDocument();
$invoice->documents()->save($cloneDocument);
}
@ -731,8 +712,8 @@ class InvoiceRepository extends BaseRepository
public function findOpenInvoices($clientId)
{
return Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
->whereClientId($clientId)
->whereIsQuote(false)
->whereIsRecurring(false)
->whereDeletedAt(null)
->whereHasTasks(true)
@ -760,7 +741,7 @@ class InvoiceRepository extends BaseRepository
$invoice = Invoice::createNew($recurInvoice);
$invoice->client_id = $recurInvoice->client_id;
$invoice->recurring_invoice_id = $recurInvoice->id;
$invoice->invoice_number = $recurInvoice->account->recurring_invoice_number_prefix . $recurInvoice->account->getNextInvoiceNumber($recurInvoice);
$invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber($invoice);
$invoice->amount = $recurInvoice->amount;
$invoice->balance = $recurInvoice->amount;
$invoice->invoice_date = date_create()->format('Y-m-d');
@ -839,9 +820,9 @@ class InvoiceRepository extends BaseRepository
}
$sql = implode(' OR ', $dates);
$invoices = Invoice::whereAccountId($account->id)
$invoices = Invoice::invoiceType(INVOICE_TYPE_STANDARD)
->whereAccountId($account->id)
->where('balance', '>', 0)
->where('is_quote', '=', false)
->where('is_recurring', '=', false)
->whereRaw('('.$sql.')')
->get();

View File

@ -11,6 +11,13 @@ class ProductRepository extends BaseRepository
return 'App\Models\Product';
}
public function all()
{
return Product::scope()
->withTrashed()
->get();
}
public function find($accountId)
{
return DB::table('products')
@ -30,11 +37,11 @@ class ProductRepository extends BaseRepository
'products.deleted_at'
);
}
public function save($data, $product = null)
{
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
if ($product) {
// do nothing
} elseif ($publicId) {
@ -50,4 +57,4 @@ class ProductRepository extends BaseRepository
return $product;
}
}
}

View File

@ -14,6 +14,7 @@ class DocumentTransformer extends EntityTransformer
'type' => $document->type,
'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null,
'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null,
'updated_at' => $this->getTimestamp($document->updated_at),
]);
}
}
}

View File

@ -34,7 +34,7 @@ class InvoiceTransformer extends EntityTransformer
public function __construct($account = null, $serializer = null, $client = null)
{
parent::__construct($account, $serializer);
$this->client = $client;
}
@ -93,7 +93,7 @@ class InvoiceTransformer extends EntityTransformer
'terms' => $invoice->terms,
'public_notes' => $invoice->public_notes,
'is_deleted' => (bool) $invoice->is_deleted,
'is_quote' => (bool) $invoice->is_quote,
'invoice_type_id' => (int) $invoice->invoice_type_id,
'is_recurring' => (bool) $invoice->is_recurring,
'frequency_id' => (int) $invoice->frequency_id,
'start_date' => $invoice->start_date,
@ -119,6 +119,7 @@ class InvoiceTransformer extends EntityTransformer
'quote_invoice_id' => (int) $invoice->quote_invoice_id,
'custom_text_value1' => $invoice->custom_text_value1,
'custom_text_value2' => $invoice->custom_text_value2,
'is_quote' => (bool) $invoice->isType(INVOICE_TYPE_QUOTE), // Temp to support mobile app
]);
}
}

View File

@ -29,8 +29,8 @@ class AppServiceProvider extends ServiceProvider {
else{
$contents = $image;
}
return 'data:image/jpeg;base64,' . base64_encode($contents);
return 'data:image/jpeg;base64,' . base64_encode($contents);
});
Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') {
@ -58,11 +58,11 @@ class AppServiceProvider extends ServiceProvider {
$str = '<li class="dropdown '.$class.'">
<a href="'.URL::to($types).'" class="dropdown-toggle">'.trans("texts.$types").'</a>';
$items = [];
if($user->can('create', $type))$items[] = '<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>';
if ($type == ENTITY_INVOICE) {
if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('recurring_invoices').'">'.trans("texts.recurring_invoices").'</a></li>';
@ -81,7 +81,7 @@ class AppServiceProvider extends ServiceProvider {
$items[] = '<li><a href="'.URL::to('vendors').'">'.trans("texts.vendors").'</a></li>';
if($user->can('create', ENTITY_VENDOR))$items[] = '<li><a href="'.URL::to('vendors/create').'">'.trans("texts.new_vendor").'</a></li>';
}
if(!empty($items)){
$str.= '<ul class="dropdown-menu" id="menu1">'.implode($items).'</ul>';
}
@ -157,14 +157,14 @@ 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;
});

View File

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

View File

@ -94,7 +94,7 @@ class InvoiceService extends BaseService
{
$account = $quote->account;
if (!$quote->is_quote || $quote->quote_invoice_id) {
if (!$quote->isType(INVOICE_TYPE_QUOTE) || $quote->quote_invoice_id) {
return null;
}
@ -120,8 +120,10 @@ class InvoiceService extends BaseService
public function getDatatable($accountId, $clientPublicId = null, $entityType, $search)
{
$datatable = new InvoiceDatatable( ! $clientPublicId, $clientPublicId);
$datatable->entityType = $entityType;
$query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search)
->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE ? true : false);
->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD);
if(!Utils::hasPermission('view_all')){
$query->where('invoices.user_id', '=', Auth::user()->id);

View File

@ -132,7 +132,7 @@ class PushService
*/
private function entitySentMessage($invoice)
{
if($invoice->is_quote)
if($invoice->isType(INVOICE_TYPE_QUOTE))
return trans("texts.notification_quote_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
else
return trans("texts.notification_invoice_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
@ -163,7 +163,7 @@ class PushService
*/
private function entityViewedMessage($invoice)
{
if($invoice->is_quote)
if($invoice->isType(INVOICE_TYPE_QUOTE))
return trans("texts.notification_quote_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
else
return trans("texts.notification_invoice_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);

View File

@ -27,7 +27,7 @@
// Whether checkboxes should always be present in the POST data,
// no matter if you checked them or not
'push_checkboxes' => false,
'push_checkboxes' => true,
// The value a checkbox will have in the POST array if unchecked
'unchecked_value' => 0,
@ -181,4 +181,4 @@
),
);
);

View File

@ -14,9 +14,9 @@ class AddQuotes extends Migration {
{
Schema::table('invoices', function($table)
{
$table->boolean('is_quote')->default(0);
$table->boolean('invoice_type_id')->default(0);
$table->unsignedInteger('quote_id')->nullable();
$table->unsignedInteger('quote_invoice_id')->nullable();
$table->unsignedInteger('quote_invoice_id')->nullable();
});
}
@ -29,7 +29,7 @@ class AddQuotes extends Migration {
{
Schema::table('invoices', function($table)
{
$table->dropColumn('is_quote');
$table->dropColumn('invoice_type_id');
$table->dropColumn('quote_id');
$table->dropColumn('quote_invoice_id');
});

View File

@ -15,8 +15,8 @@ class AddDocuments extends Migration {
$table->unsignedInteger('logo_width');
$table->unsignedInteger('logo_height');
$table->unsignedInteger('logo_size');
$table->boolean('invoice_embed_documents')->default(1);
$table->boolean('document_email_attachment')->default(1);
$table->boolean('invoice_embed_documents')->default(0);
$table->boolean('document_email_attachment')->default(0);
});
\DB::table('accounts')->update(array('logo' => ''));

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddInvoiceTypeSupport extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasColumn('invoices', 'is_quote')) {
DB::update('update invoices set is_quote = is_quote + 1');
Schema::table('invoices', function ($table) {
$table->renameColumn('is_quote', 'invoice_type_id');
});
}
Schema::table('accounts', function($table)
{
$table->boolean('enable_second_tax_rate')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if (Schema::hasColumn('invoices', 'invoice_type_id')) {
DB::update('update invoices set invoice_type_id = invoice_type_id - 1');
}
Schema::table('accounts', function($table)
{
$table->dropColumn('enable_second_tax_rate');
});
}
}

File diff suppressed because one or more lines are too long

85
public/css/built.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

81
public/css/style.css vendored
View File

@ -101,7 +101,7 @@ border-bottom: 1px solid #dfe0e1;
table.dataTable.no-footer {
border-bottom: none;
}
.table-striped>tbody>tr:nth-child(odd)>tr,
.table-striped>tbody>tr:nth-child(odd)>tr,
.table-striped>tbody>tr:nth-child(odd)>th {
background-color: #FDFDFD;
}
@ -192,7 +192,7 @@ opacity: 1;
filter: alpha(opacity=100);
}
/*buttons*/
.btn { font-weight: bold;
.btn { font-weight: bold;
border-radius: 3px;
padding: 9px 12px;
}
@ -258,8 +258,8 @@ border-color: #0b4d78;
}
.form-actions .btn,
.form-actions div.btn-group {
margin-left: 10px;
.form-actions div.btn-group {
margin-left: 10px;
}
.form-actions .btn.btn-success:first-child {
@ -377,7 +377,7 @@ border: none;
border-radius: 0;
color: #fff;
background-color: #9b9b9b;
}
.nav-tabs.nav-justified>li:first-child>a {
border-radius: 3px 0 0 3px;
@ -406,7 +406,7 @@ font-weight: bold;
ul.dropdown-menu,
.twitter-typeahead .tt-menu {
x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
}
@ -466,7 +466,7 @@ background-clip: padding-box;
/***********************************************
Dashboard
Dashboard
************************************************/
.in-bold {
@ -628,14 +628,14 @@ div.discount-group span {
.navbar-default .navbar-nav > li > a:focus {
color: #ffffff;
}
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
color: #ffffff;
background-color: #3276b1;
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .open > a:hover,
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .open > a:hover,
.navbar-default .navbar-nav > .open > a:focus {
color: #ffffff;
background-color: #3276b1;
@ -649,8 +649,8 @@ div.discount-group span {
border-top-color: #ffffff;
border-bottom-color: #ffffff;
}
.navbar-default .navbar-nav > .open > a .caret,
.navbar-default .navbar-nav > .open > a:hover .caret,
.navbar-default .navbar-nav > .open > a .caret,
.navbar-default .navbar-nav > .open > a:hover .caret,
.navbar-default .navbar-nav > .open > a:focus .caret {
border-top-color: #ffffff;
border-bottom-color: #ffffff;
@ -689,7 +689,7 @@ div.fb_iframe_widget {
display: inline;
}
div.fb_iframe_widget > span {
vertical-align: top !important;
vertical-align: top !important;
}
.pro-label {
font-size:9px;
@ -728,12 +728,12 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
.plans-table .glyphicon-remove {background-color: #da4830;}
.plans-table .glyphicon-ok {background-color: #35c156;}
.plans-table .glyphicon-star {border-radius: 0; background-color: #2e2b2b;
display: block;
width: 60px;
height: 30px;
position: absolute;
top: -5px;
right: -20px;
display: block;
width: 60px;
height: 30px;
position: absolute;
top: -5px;
right: -20px;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
@ -763,11 +763,11 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-overflow: ellipsis;
}
.entityArchived {
color: #888 !important;
color: #888 !important;
}
.entityDeleted {
@ -775,12 +775,12 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
}
/* Custom, iPhone Retina */
/* Custom, iPhone Retina */
@media only screen and (min-width : 320px) {
}
/* Extra Small Devices, Phones */
/* Extra Small Devices, Phones */
@media only screen and (min-width : 480px) {
}
@ -828,7 +828,7 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
@media (max-width: 992px) {
.hide-phone {
display: none !important;
}
}
}
@media (max-width: 767px) {
@ -846,8 +846,8 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2);
.plans-table .free .cell { padding-right: 0; }
.plans-table .free .cell:first-child {margin-right: 0;}
.plans-table .cell div:first-child {margin-bottom: 5px;}
.plans-table .cell .cta {margin-bottom: 0 !important;}
.plans-table .pro {margin-top: 40px;}
.plans-table .cell .cta {margin-bottom: 0 !important;}
.plans-table .pro {margin-top: 40px;}
}
label[for=recommendedGateway_id2].radio{
@ -976,14 +976,14 @@ button .glyphicon {
.pro-plan-modal a.button {
font-family: 'roboto_slabregular', Georgia, Times, serif;
background: #f38c4f;
background: -moz-linear-gradient(top, #f38c4f 0%, #db7134 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f38c4f), color-stop(100%,#db7134));
background: -webkit-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: -o-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: -ms-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: linear-gradient(to bottom, #f38c4f 0%,#db7134 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f38c4f', endColorstr='#db7134',GradientType=0 );
background: #f38c4f;
background: -moz-linear-gradient(top, #f38c4f 0%, #db7134 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f38c4f), color-stop(100%,#db7134));
background: -webkit-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: -o-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: -ms-linear-gradient(top, #f38c4f 0%,#db7134 100%);
background: linear-gradient(to bottom, #f38c4f 0%,#db7134 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f38c4f', endColorstr='#db7134',GradientType=0 );
text-shadow: 1px 1px 1px rgba(0, 0, 0, .25);
width: 68%;
margin-top: 20px;
@ -1046,7 +1046,7 @@ ul.user-accounts a:hover div.remove {
.invoice-contact .tooltip-inner {
text-align:left;
width: 350px;
width: 350px;
}
.smaller {
@ -1103,10 +1103,3 @@ div.panel-body div.panel-body {
width: 100%;
height: 100%;
}
.dropzone .fallback-doc{
display:none;
}
.dropzone.dz-browser-not-supported .fallback-doc{
display:block;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1312,6 +1312,15 @@ $LANG = array(
'security' => 'Security',
'see_whats_new' => 'See what\'s new in v:version',
'wait_for_upload' => 'Please wait for the document upload to complete.',
'upgrade_for_permissions' => 'Upgrade to our Enterprise plan to enable permissions.',
'enable_second_tax_rate' => 'Enable specifying a <b>second tax rate</b>',
'payment_file' => 'Payment File',
'expense_file' => 'Expense File',
'product_file' => 'Product File',
'import_products' => 'Import Products',
'products_will_create' => 'products will be created.',
'product_key' => 'Product',
'created_products' => 'Successfully created :count product(s)',
'view_dashboard' => 'View Dashboard',
'client_session_expired' => 'Session Expired',

View File

@ -7,9 +7,9 @@
<script src="{{ asset('js/quill.min.js') }}" type="text/javascript"></script>
@stop
@section('content')
@section('content')
@parent
<style type="text/css">
#logo {
@ -23,10 +23,11 @@
->autocomplete('on')
->rules([
'name' => 'required',
'website' => 'url',
]) !!}
{{ Former::populate($account) }}
@include('accounts.nav', ['selected' => ACCOUNT_COMPANY_DETAILS])
<div class="row">
@ -76,7 +77,7 @@
<h3 class="panel-title">{!! trans('texts.address') !!}</h3>
</div>
<div class="panel-body form-padding-right">
{!! Former::text('address1')->autocomplete('address-line1') !!}
{!! Former::text('address2')->autocomplete('address-line2') !!}
{!! Former::text('city')->autocomplete('address-level2') !!}
@ -88,7 +89,7 @@
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.signature') !!}</h3>
@ -96,25 +97,25 @@
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! Former::textarea('email_footer')->style('display:none')->raw() !!}
{!! Former::textarea('email_footer')->style('display:none')->raw() !!}
<div id="signatureEditor" class="form-control" style="min-height:160px" onclick="focusEditor()"></div>
@include('partials/quill_toolbar', ['name' => 'signature'])
</div>
</div>
</div>
</div>
</div>
<center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center>
{!! Former::close() !!}
{!! Form::open(['url' => 'remove_logo', 'class' => 'removeLogoForm']) !!}
{!! Form::open(['url' => 'remove_logo', 'class' => 'removeLogoForm']) !!}
{!! Form::close() !!}
@ -158,4 +159,4 @@
@section('onReady')
$('#name').focus();
@stop
@stop

View File

@ -4,9 +4,7 @@
@parent
<style type="text/css">
.contact-file,
.task-file,
.payment-file {
.import-file {
display: none;
}
</style>
@ -34,7 +32,7 @@
@foreach (\App\Services\ImportService::$entityTypes as $entityType)
{!! Former::file("{$entityType}_file")
->addGroupClass("{$entityType}-file") !!}
->addGroupClass("import-file {$entityType}-file") !!}
@endforeach
{!! Former::actions( Button::info(trans('texts.upload'))->submit()->large()->appendIcon(Icon::create('open'))) !!}
@ -67,13 +65,17 @@
trans('texts.payments') => array('name' => ENTITY_PAYMENT, 'value' => 1),
])->check(ENTITY_CLIENT)->check(ENTITY_TASK)->check(ENTITY_INVOICE)->check(ENTITY_PAYMENT) !!}
{!! Former::actions( Button::primary(trans('texts.download'))->submit()->large()->appendIcon(Icon::create('download-alt'))) !!}
{!! Former::actions( Button::primary(trans('texts.download'))->submit()->large()->appendIcon(Icon::create('download-alt'))) !!}
</div>
</div>
{!! Former::close() !!}
<script type="text/javascript">
$(function() {
setFileTypesVisible();
});
function setEntityTypesVisible() {
var selector = '.entity-types input[type=checkbox]';
if ($('#format').val() === 'JSON') {
@ -103,4 +105,4 @@
</script>
@stop
@stop

View File

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

View File

@ -1,6 +1,6 @@
@extends('header')
@section('content')
@section('content')
@parent
@include('accounts.nav', ['selected' => ACCOUNT_TAX_RATES])
@ -10,12 +10,13 @@
{{ Former::populateField('invoice_taxes', intval($account->invoice_taxes)) }}
{{ Former::populateField('invoice_item_taxes', intval($account->invoice_item_taxes)) }}
{{ Former::populateField('show_item_taxes', intval($account->show_item_taxes)) }}
{{ Former::populateField('enable_second_tax_rate', intval($account->enable_second_tax_rate)) }}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.tax_settings') !!}</h3>
</div>
</div>
<div class="panel-body">
{!! Former::checkbox('invoice_taxes')
@ -30,6 +31,10 @@
->text(trans('texts.show_line_item_tax'))
->label('&nbsp;') !!}
{!! Former::checkbox('enable_second_tax_rate')
->text(trans('texts.enable_second_tax_rate'))
->label('&nbsp;') !!}
&nbsp;
{!! Former::select('default_tax_rate_id')
@ -51,22 +56,22 @@
@include('partials.bulk_form', ['entityType' => ENTITY_TAX_RATE])
{!! Datatable::table()
{!! Datatable::table()
->addColumn(
trans('texts.name'),
trans('texts.rate'),
trans('texts.action'))
->setUrl(url('api/tax_rates/'))
->setUrl(url('api/tax_rates/'))
->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false)
->setOptions('bAutoWidth', false)
->setOptions('bFilter', false)
->setOptions('bAutoWidth', false)
->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]])
->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]])
->render('datatable') !!}
<script>
window.onDatatableReady = actionListHandler;
</script>
</script>
@stop
@stop

View File

@ -150,7 +150,7 @@
</thead>
<tbody>
@foreach ($upcoming as $invoice)
@if (!$invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
@can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id])
@ -185,7 +185,7 @@
</thead>
<tbody>
@foreach ($pastDue as $invoice)
@if (!$invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
@can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id])
@ -224,7 +224,7 @@
</thead>
<tbody>
@foreach ($upcoming as $invoice)
@if ($invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
<td>{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}</td>
@ -255,7 +255,7 @@
</thead>
<tbody>
@foreach ($pastDue as $invoice)
@if ($invoice->is_quote)
@if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD)
<tr>
<td>{!! \App\Models\Invoice::calcLink($invoice) !!}</td>
<td>{!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!}</td>

View File

@ -15,7 +15,10 @@
@section('content')
{!! Former::open($url)->addClass('warn-on-exit main-form')->method($method) !!}
{!! Former::open($url)
->addClass('warn-on-exit main-form')
->onsubmit('return onFormSubmit(event)')
->method($method) !!}
<div style="display:none">
{!! Former::text('action') !!}
</div>
@ -111,15 +114,8 @@
<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>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
</div>
</div>
</div>
@ -154,6 +150,15 @@
clientMap[client.public_id] = client;
}
function onFormSubmit(event) {
if (window.countUploadingDocuments > 0) {
alert("{!! trans('texts.wait_for_upload') !!}");
return false;
}
return true;
}
function onClientChange() {
var clientId = $('select#client_id').val();
var client = clientMap[clientId];
@ -225,21 +230,24 @@
// Initialize document upload
dropzone = new Dropzone('#document-upload', {
url:{!! json_encode(url('document')) !!},
url:{!! json_encode(url('documents')) !!},
params:{
_token:"{{ Session::getToken() }}"
},
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
addRemoveLinks:true,
dictRemoveFileConfirmation:"{{trans('texts.are_you_sure')}}",
@foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key)
"dict{{strval($key)}}":"{{trans('texts.dropzone_'.Utils::toClassCase($key))}}",
@endforeach
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
maxFilesize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
});
if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded);
dropzone.on("canceled",handleDocumentCanceled);
dropzone.on("error",handleDocumentError);
for (var i=0; i<model.documents().length; i++) {
var document = model.documents()[i];
var mockFile = {
@ -362,6 +370,7 @@
}
}
window.countUploadingDocuments = 0;
@if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS))
function handleDocumentAdded(file){
// open document when clicked
@ -373,20 +382,36 @@
if(file.mock)return;
file.index = model.documents().length;
model.addDocument({name:file.name, size:file.size, type:file.type});
window.countUploadingDocuments++;
}
function handleDocumentRemoved(file){
model.removeDocument(file.public_id);
$.ajax({
url: '{{ '/documents/' }}' + file.public_id,
type: 'DELETE',
success: function(result) {
// Do something with the result
}
});
}
function handleDocumentUploaded(file, response){
file.public_id = response.document.public_id
model.documents()[file.index].update(response.document);
window.countUploadingDocuments--;
if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url);
}
}
function handleDocumentCanceled() {
window.countUploadingDocuments--;
}
function handleDocumentError() {
window.countUploadingDocuments--;
}
@endif
</script>

View File

@ -243,7 +243,7 @@
@endif
<th style="min-width:120px" data-bind="text: costLabel">{{ $invoiceLabels['unit_cost'] }}</th>
<th style="{{ $account->hide_quantity ? 'display:none' : 'min-width:120px' }}" data-bind="text: qtyLabel">{{ $invoiceLabels['quantity'] }}</th>
<th style="min-width:180px;display:none;" data-bind="visible: $root.invoice_item_taxes.show">{{ trans('texts.tax') }}</th>
<th style="min-width:{{ $account->enable_second_tax_rate ? 180 : 120 }}px;display:none;" data-bind="visible: $root.invoice_item_taxes.show">{{ trans('texts.tax') }}</th>
<th style="min-width:120px;">{{ trans('texts.line_total') }}</th>
<th style="min-width:32px;" class="hide-border"></th>
</tr>
@ -288,16 +288,18 @@
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax1')
->addClass('tax-select')
->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->raw() !!}
<input type="text" data-bind="value: tax_name1, attr: {name: 'invoice_items[' + $index() + '][tax_name1]'}" style="display:none">
<input type="text" data-bind="value: tax_rate1, attr: {name: 'invoice_items[' + $index() + '][tax_rate1]'}" style="display:none">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax2')
->addClass('tax-select')
->raw() !!}
<div data-bind="visible: $root.invoice().account.enable_second_tax_rate == '1'">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax2')
->addClass('tax-select')
->raw() !!}
</div>
<input type="text" data-bind="value: tax_name2, attr: {name: 'invoice_items[' + $index() + '][tax_name2]'}" style="display:none">
<input type="text" data-bind="value: tax_rate2, attr: {name: 'invoice_items[' + $index() + '][tax_rate2]'}" style="display:none">
</td>
@ -362,15 +364,8 @@
<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>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
</div>
</div>
@if ($invoice->hasExpenseDocuments())
@ -438,17 +433,19 @@
->id('taxRateSelect1')
->addOption('', '')
->options($taxRateOptions)
->addClass('tax-select')
->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->data_bind('value: tax1')
->raw() !!}
<input type="text" name="tax_name1" data-bind="value: tax_name1" style="display:none">
<input type="text" name="tax_rate1" data-bind="value: tax_rate1" style="display:none">
<div data-bind="visible: $root.invoice().account.enable_second_tax_rate == '1'">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->addClass('tax-select')
->data_bind('value: tax2')
->raw() !!}
</div>
<input type="text" name="tax_name2" data-bind="value: tax_name2" style="display:none">
<input type="text" name="tax_rate2" data-bind="value: tax_rate2" style="display:none">
</td>
@ -848,7 +845,7 @@
model.invoice().has_tasks(true);
@endif
if(model.invoice().expenses() && !model.invoice().public_id()){
if(model.invoice().expenses().length && !model.invoice().public_id()){
model.expense_currency_id({{ isset($expenseCurrencyId) ? $expenseCurrencyId : 0 }});
// move the blank invoice line item to the end
@ -1010,21 +1007,24 @@
}
window.dropzone = new Dropzone('#document-upload .dropzone', {
url:{!! json_encode(url('document')) !!},
url:{!! json_encode(url('documents')) !!},
params:{
_token:"{{ Session::getToken() }}"
},
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
addRemoveLinks:true,
dictRemoveFileConfirmation:"{{trans('texts.are_you_sure')}}",
@foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key)
"dict{{Utils::toClassCase($key)}}":"{{trans('texts.dropzone_'.$key)}}",
@endforeach
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
maxFilesize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
});
if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded);
dropzone.on("canceled",handleDocumentCanceled);
dropzone.on("error",handleDocumentError);
for (var i=0; i<model.invoice().documents().length; i++) {
var document = model.invoice().documents()[i];
var mockFile = {
@ -1202,8 +1202,13 @@
if (confirm('{!! trans("texts.confirm_email_$entityType") !!}' + '\n\n' + getSendToEmails())) {
var accountLanguageId = parseInt({{ $account->language_id ?: '0' }});
var clientLanguageId = parseInt(model.invoice().client().language_id()) || 0;
var attachPDF = {{ $account->attachPDF() ? 'true' : 'false' }};
// if they aren't attaching the pdf no need to generate it
if ( ! attachPDF) {
submitAction('email');
// if the client's language is different then we can't use the browser version of the PDF
if (clientLanguageId && clientLanguageId != accountLanguageId) {
} else if (clientLanguageId && clientLanguageId != accountLanguageId) {
submitAction('email');
} else {
preparePdfData('email');
@ -1466,6 +1471,13 @@
function handleDocumentRemoved(file){
model.invoice().removeDocument(file.public_id);
refreshPDF(true);
$.ajax({
url: '{{ '/documents/' }}' + file.public_id,
type: 'DELETE',
success: function(result) {
// Do something with the result
}
});
}
function handleDocumentUploaded(file, response){
@ -1473,11 +1485,18 @@
model.invoice().documents()[file.index].update(response.document);
window.countUploadingDocuments--;
refreshPDF(true);
if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url);
}
}
function handleDocumentCanceled() {
window.countUploadingDocuments--;
}
function handleDocumentError() {
window.countUploadingDocuments--;
}
@endif
</script>

View File

@ -4,15 +4,15 @@
@parent
@include('money_script')
@foreach ($invoice->client->account->getFontFolders() as $font)
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
@endforeach
<script src="{{ asset('pdf.built.js') }}" type="text/javascript"></script>
<style type="text/css">
body {
background-color: #f8f8f8;
background-color: #f8f8f8;
}
.dropdown-menu li a{
@ -99,7 +99,7 @@
@include('partials.checkout_com_payment')
@else
<div class="pull-right" style="text-align:right">
@if ($invoice->is_quote)
@if ($invoice->isQuote())
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}&nbsp;&nbsp;
@if ($showApprove)
{!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!}
@ -110,7 +110,7 @@
{!! DropdownButton::success(trans('texts.pay_now'))->withContents($paymentTypes)->large() !!}
@else
<a href='{!! $paymentURL !!}' class="btn btn-success btn-lg">{{ trans('texts.pay_now') }}</a>
@endif
@endif
@else
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
@if ($account->isNinjaAccount())
@ -141,7 +141,7 @@
</ul>
</div>
@endif
@if ($account->hasFeature(FEATURE_DOCUMENTS) && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
@if($document->isPDFEmbeddable())
@ -164,7 +164,7 @@
remove_created_by:{{ $invoice->client->account->hasFeature(FEATURE_REMOVE_CREATED_BY) ? 'true' : 'false' }},
invoice_settings:{{ $invoice->client->account->hasFeature(FEATURE_INVOICE_SETTINGS) ? 'true' : 'false' }}
};
invoice.is_quote = {{ $invoice->is_quote ? 'true' : 'false' }};
invoice.is_quote = {{ $invoice->isQuote() ? 'true' : 'false' }};
invoice.contact = {!! $contact->toJson() !!};
function getPDFString(cb) {
@ -181,16 +181,16 @@
doc.getDataUrl(function(pdfString) {
document.write(pdfString);
document.close();
if (window.hasOwnProperty('pjsc_meta')) {
window['pjsc_meta'].remainingTasks--;
}
});
@else
@else
refreshPDF();
@endif
});
function onDownloadClick() {
var doc = generatePDF(invoice, invoice.invoice_design.javascript, true);
var fileName = invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice;

View File

@ -1,6 +1,6 @@
@extends('header')
@section('content')
@section('content')
@parent
@include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT])
@ -31,13 +31,20 @@
</div>
</div>
@if (Utils::hasFeature(FEATURE_USER_PERMISSIONS))
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.permissions') !!}</h3>
</div>
<div class="panel-body form-padding-right">
@if ( ! Utils::hasFeature(FEATURE_USER_PERMISSIONS))
<div class="alert alert-warning">{{ trans('texts.upgrade_for_permissions') }}</div>
<script>
$(function() {
$('input[type=checkbox]').prop('disabled', true);
})
</script>
@endif
{!! Former::checkbox('is_admin')
->label('&nbsp;')
@ -61,12 +68,11 @@
->id('permissions_edit_all')
->text(trans('texts.user_edit_all'))
->help(trans('texts.edit_all_help')) !!}
</div>
</div>
@endif
{!! Former::actions(
</div>
</div>
{!! Former::actions(
Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(),
Button::success(trans($user && $user->confirmed ? 'texts.save' : 'texts.send_invite'))->submit()->large()->appendIcon(Icon::create($user && $user->confirmed ? 'floppy-disk' : 'send'))
)!!}
@ -88,4 +94,4 @@
if(!viewChecked)$('#permissions_edit_all').prop('checked',false)
}
fixCheckboxes();
@stop
@stop