Merge branch 'release-4.2.0'

This commit is contained in:
Hillel Coren 2018-02-21 21:48:45 +02:00
commit 1633f30a38
324 changed files with 61326 additions and 3775 deletions

View File

@ -73,7 +73,7 @@ class ChargeRenewalInvoices extends Command
->orderBy('id') ->orderBy('id')
->get(); ->get();
$this->info(count($invoices).' invoices found'); $this->info($invoices->count() . ' invoices found');
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {

View File

@ -5,6 +5,7 @@ namespace App\Console\Commands;
use Carbon; use Carbon;
use App\Libraries\CurlUtils; use App\Libraries\CurlUtils;
use DB; use DB;
use App;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Mail; use Mail;
@ -81,6 +82,7 @@ class CheckData extends Command
} }
//$this->checkInvoices(); //$this->checkInvoices();
$this->checkTranslations();
$this->checkInvoiceBalances(); $this->checkInvoiceBalances();
$this->checkClientBalances(); $this->checkClientBalances();
$this->checkContacts(); $this->checkContacts();
@ -115,6 +117,40 @@ class CheckData extends Command
$this->log .= $str . "\n"; $this->log .= $str . "\n";
} }
private function checkTranslations()
{
$invalid = 0;
foreach (cache('languages') as $language) {
App::setLocale($language->locale);
foreach (trans('texts') as $text) {
if (strpos($text, '=') !== false) {
$invalid++;
$this->logMessage($language->locale . ' is invalid: ' . $text);
}
preg_match('/(.script)/', strtolower($text), $matches);
if (count($matches)) {
foreach ($matches as $match) {
if (in_array($match, ['escript', 'bscript', 'nscript'])) {
continue;
}
$invalid++;
$this->logMessage(sprintf('%s is invalid: %s', $language->locale, $text));
break;
}
}
}
}
if ($invalid > 0) {
$this->isValid = false;
}
App::setLocale('en');
$this->logMessage($invalid . ' invalid text strings');
}
private function checkDraftSentInvoices() private function checkDraftSentInvoices()
{ {
$invoices = Invoice::whereInvoiceStatusId(INVOICE_STATUS_SENT) $invoices = Invoice::whereInvoiceStatusId(INVOICE_STATUS_SENT)
@ -122,9 +158,9 @@ class CheckData extends Command
->withTrashed() ->withTrashed()
->get(); ->get();
$this->logMessage(count($invoices) . ' draft sent invoices'); $this->logMessage($invoices->count() . ' draft sent invoices');
if (count($invoices) > 0) { if ($invoices->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
@ -190,9 +226,9 @@ class CheckData extends Command
->havingRaw('count(users.id) > 1') ->havingRaw('count(users.id) > 1')
->get(['users.oauth_user_id']); ->get(['users.oauth_user_id']);
$this->logMessage(count($users) . ' users with duplicate oauth ids'); $this->logMessage($users->count() . ' users with duplicate oauth ids');
if (count($users) > 0) { if ($users->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
@ -308,9 +344,9 @@ class CheckData extends Command
->whereNull('contact_key') ->whereNull('contact_key')
->orderBy('id') ->orderBy('id')
->get(['id']); ->get(['id']);
$this->logMessage(count($contacts) . ' contacts without a contact_key'); $this->logMessage($contacts->count() . ' contacts without a contact_key');
if (count($contacts) > 0) { if ($contacts->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
@ -339,9 +375,9 @@ class CheckData extends Command
} }
$clients = $clients->get(['clients.id', 'clients.user_id', 'clients.account_id']); $clients = $clients->get(['clients.id', 'clients.user_id', 'clients.account_id']);
$this->logMessage(count($clients) . ' clients without any contacts'); $this->logMessage($clients->count() . ' clients without any contacts');
if (count($clients) > 0) { if ($clients->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
@ -374,9 +410,9 @@ class CheckData extends Command
} }
$clients = $clients->get(['clients.id', DB::raw('count(contacts.id)')]); $clients = $clients->get(['clients.id', DB::raw('count(contacts.id)')]);
$this->logMessage(count($clients) . ' clients without a single primary contact'); $this->logMessage($clients->count() . ' clients without a single primary contact');
if (count($clients) > 0) { if ($clients->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
} }
@ -423,9 +459,9 @@ class CheckData extends Command
->havingRaw('count(invitations.id) = 0') ->havingRaw('count(invitations.id) = 0')
->get(['invoices.id', 'invoices.user_id', 'invoices.account_id', 'invoices.client_id']); ->get(['invoices.id', 'invoices.user_id', 'invoices.account_id', 'invoices.client_id']);
$this->logMessage(count($invoices) . ' invoices without any invitations'); $this->logMessage($invoices->count() . ' invoices without any invitations');
if (count($invoices) > 0) { if ($invoices->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
@ -469,6 +505,10 @@ class CheckData extends Command
ENTITY_INVOICE, ENTITY_INVOICE,
ENTITY_CLIENT, ENTITY_CLIENT,
ENTITY_USER, ENTITY_USER,
ENTITY_TASK_STATUS,
],
'task_statuses' => [
ENTITY_USER,
], ],
'credits' => [ 'credits' => [
ENTITY_CLIENT, ENTITY_CLIENT,
@ -496,6 +536,25 @@ class CheckData extends Command
ENTITY_USER, ENTITY_USER,
ENTITY_CLIENT, ENTITY_CLIENT,
], ],
'proposals' => [
ENTITY_USER,
ENTITY_INVOICE,
ENTITY_PROPOSAL_TEMPLATE,
],
'proposal_categories' => [
ENTITY_USER,
],
'proposal_templates' => [
ENTITY_USER,
],
'proposal_snippets' => [
ENTITY_USER,
ENTITY_PROPOSAL_CATEGORY,
],
'proposal_invitations' => [
ENTITY_USER,
ENTITY_PROPOSAL,
],
]; ];
foreach ($tables as $table => $entityTypes) { foreach ($tables as $table => $entityTypes) {
@ -512,9 +571,9 @@ class CheckData extends Command
->where("{$table}.{$accountId}", '!=', DB::raw("{$tableName}.account_id")) ->where("{$table}.{$accountId}", '!=', DB::raw("{$tableName}.account_id"))
->get(["{$table}.id"]); ->get(["{$table}.id"]);
if (count($records)) { if ($records->count()) {
$this->isValid = false; $this->isValid = false;
$this->logMessage(count($records) . " {$table} records with incorrect {$entityType} account id"); $this->logMessage($records->count() . " {$table} records with incorrect {$entityType} account id");
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
foreach ($records as $record) { foreach ($records as $record) {
@ -549,9 +608,9 @@ class CheckData extends Command
->groupBy('clients.id') ->groupBy('clients.id')
->havingRaw('clients.paid_to_date != sum(coalesce(payments.amount - payments.refunded, 0)) and clients.paid_to_date != 999999999.9999') ->havingRaw('clients.paid_to_date != sum(coalesce(payments.amount - payments.refunded, 0)) and clients.paid_to_date != 999999999.9999')
->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(coalesce(payments.amount - payments.refunded, 0)) as amount')]); ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(coalesce(payments.amount - payments.refunded, 0)) as amount')]);
$this->logMessage(count($clients) . ' clients with incorrect paid to date'); $this->logMessage($clients->count() . ' clients with incorrect paid to date');
if (count($clients) > 0) { if ($clients->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
@ -580,9 +639,9 @@ class CheckData extends Command
->havingRaw('(invoices.amount - invoices.balance) != coalesce(sum(payments.amount - payments.refunded), 0)') ->havingRaw('(invoices.amount - invoices.balance) != coalesce(sum(payments.amount - payments.refunded), 0)')
->get(['invoices.id', 'invoices.amount', 'invoices.balance', DB::raw('coalesce(sum(payments.amount - payments.refunded), 0)')]); ->get(['invoices.id', 'invoices.amount', 'invoices.balance', DB::raw('coalesce(sum(payments.amount - payments.refunded), 0)')]);
$this->logMessage(count($invoices) . ' invoices with incorrect balances'); $this->logMessage($invoices->count() . ' invoices with incorrect balances');
if (count($invoices) > 0) { if ($invoices->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }
} }
@ -608,9 +667,9 @@ class CheckData extends Command
$clients = $clients->groupBy('clients.id', 'clients.balance') $clients = $clients->groupBy('clients.id', 'clients.balance')
->orderBy('accounts.company_id', 'DESC') ->orderBy('accounts.company_id', 'DESC')
->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]); ->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]);
$this->logMessage(count($clients) . ' clients with incorrect balance/activities'); $this->logMessage($clients->count() . ' clients with incorrect balance/activities');
if (count($clients) > 0) { if ($clients->count() > 0) {
$this->isValid = false; $this->isValid = false;
} }

View File

@ -360,6 +360,7 @@ class InitLookup extends Command
DB::statement('truncate lookup_users'); DB::statement('truncate lookup_users');
DB::statement('truncate lookup_contacts'); DB::statement('truncate lookup_contacts');
DB::statement('truncate lookup_invitations'); DB::statement('truncate lookup_invitations');
DB::statement('truncate lookup_proposal_invitations');
DB::statement('truncate lookup_account_tokens'); DB::statement('truncate lookup_account_tokens');
DB::statement('SET FOREIGN_KEY_CHECKS = 1'); DB::statement('SET FOREIGN_KEY_CHECKS = 1');
} }

View File

@ -32,7 +32,7 @@ class RemoveOrphanedDocuments extends Command
$documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', [new DateTime('-1 hour')]) $documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', [new DateTime('-1 hour')])
->get(); ->get();
$this->info(count($documents).' orphaned document(s) found'); $this->info($documents->count() . ' orphaned document(s) found');
foreach ($documents as $document) { foreach ($documents as $document) {
$document->delete(); $document->delete();

View File

@ -98,7 +98,7 @@ class SendRecurringInvoices extends Command
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today]) ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc') ->orderBy('id', 'asc')
->get(); ->get();
$this->info(count($invoices).' recurring invoice(s) found'); $this->info($invoices->count() . ' recurring invoice(s) found');
foreach ($invoices as $recurInvoice) { foreach ($invoices as $recurInvoice) {
$shouldSendToday = $recurInvoice->shouldSendToday(); $shouldSendToday = $recurInvoice->shouldSendToday();
@ -140,7 +140,7 @@ class SendRecurringInvoices extends Command
[$today->format('Y-m-d')]) [$today->format('Y-m-d')])
->orderBy('invoices.id', 'asc') ->orderBy('invoices.id', 'asc')
->get(); ->get();
$this->info(count($delayedAutoBillInvoices).' due recurring invoice instance(s) found'); $this->info($delayedAutoBillInvoices->count() . ' due recurring invoice instance(s) found');
/** @var Invoice $invoice */ /** @var Invoice $invoice */
foreach ($delayedAutoBillInvoices as $invoice) { foreach ($delayedAutoBillInvoices as $invoice) {
@ -165,7 +165,7 @@ class SendRecurringInvoices extends Command
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today]) ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc') ->orderBy('id', 'asc')
->get(); ->get();
$this->info(count($expenses).' recurring expenses(s) found'); $this->info($expenses->count() . ' recurring expenses(s) found');
foreach ($expenses as $expense) { foreach ($expenses as $expense) {
$shouldSendToday = $expense->shouldSendToday(); $shouldSendToday = $expense->shouldSendToday();

View File

@ -92,7 +92,7 @@ class SendReminders extends Command
private function chargeLateFees() private function chargeLateFees()
{ {
$accounts = $this->accountRepo->findWithFees(); $accounts = $this->accountRepo->findWithFees();
$this->info(count($accounts) . ' accounts found with fees'); $this->info($accounts->count() . ' accounts found with fees');
foreach ($accounts as $account) { foreach ($accounts as $account) {
if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
@ -100,7 +100,7 @@ class SendReminders extends Command
} }
$invoices = $this->invoiceRepo->findNeedingReminding($account, false); $invoices = $this->invoiceRepo->findNeedingReminding($account, false);
$this->info($account->name . ': ' . count($invoices) . ' invoices found'); $this->info($account->name . ': ' . $invoices->count() . ' invoices found');
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice, false)) { if ($reminder = $account->getInvoiceReminder($invoice, false)) {
@ -128,7 +128,7 @@ class SendReminders extends Command
// standard reminders // standard reminders
$invoices = $this->invoiceRepo->findNeedingReminding($account); $invoices = $this->invoiceRepo->findNeedingReminding($account);
$this->info($account->name . ': ' . count($invoices) . ' invoices found'); $this->info($account->name . ': ' . $invoices->count() . ' invoices found');
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice)) { if ($reminder = $account->getInvoiceReminder($invoice)) {
@ -142,7 +142,7 @@ class SendReminders extends Command
// endless reminders // endless reminders
$invoices = $this->invoiceRepo->findNeedingEndlessReminding($account); $invoices = $this->invoiceRepo->findNeedingEndlessReminding($account);
$this->info($account->name . ': ' . count($invoices) . ' endless invoices found'); $this->info($account->name . ': ' . $invoices->count() . ' endless invoices found');
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
if ($invoice->last_sent_date == date('Y-m-d')) { if ($invoice->last_sent_date == date('Y-m-d')) {
@ -159,7 +159,7 @@ class SendReminders extends Command
$scheduledReports = ScheduledReport::where('send_date', '<=', date('Y-m-d')) $scheduledReports = ScheduledReport::where('send_date', '<=', date('Y-m-d'))
->with('user', 'account.company') ->with('user', 'account.company')
->get(); ->get();
$this->info(count($scheduledReports) . ' scheduled reports'); $this->info($scheduledReports->count() . ' scheduled reports');
foreach ($scheduledReports as $scheduledReport) { foreach ($scheduledReports as $scheduledReport) {
$user = $scheduledReport->user; $user = $scheduledReport->user;

View File

@ -60,10 +60,10 @@ class SendRenewalInvoices extends Command
$companies = Company::whereRaw("datediff(plan_expires, curdate()) = 10 and (plan = 'pro' or plan = 'enterprise')") $companies = Company::whereRaw("datediff(plan_expires, curdate()) = 10 and (plan = 'pro' or plan = 'enterprise')")
->orderBy('id') ->orderBy('id')
->get(); ->get();
$this->info(count($companies).' companies found renewing in 10 days'); $this->info($companies->count() . ' companies found renewing in 10 days');
foreach ($companies as $company) { foreach ($companies as $company) {
if (! count($company->accounts)) { if (! $company->accounts->count()) {
continue; continue;
} }

View File

@ -28,10 +28,10 @@ class $CLASS$ extends AuthServiceProvider
* *
* @return void * @return void
*/ */
public function boot(GateContract $gate) public function boot()
{ {
parent::boot($gate); parent::boot();
$this->registerTranslations(); $this->registerTranslations();
$this->registerConfig(); $this->registerConfig();
$this->registerViews(); $this->registerViews();

View File

@ -53,17 +53,5 @@ class Kernel extends ConsoleKernel
->command('ninja:send-reminders --force') ->command('ninja:send-reminders --force')
->sendOutputTo($logFile) ->sendOutputTo($logFile)
->daily(); ->daily();
if (Utils::isNinja()) {
$schedule
->command('ninja:send-renewals --force')
->sendOutputTo($logFile)
->daily();
}
$schedule
->command('updater:check-for-update --prefixVersionWith=v')
->sendOutputTo($logFile)
->daily();
} }
} }

View File

@ -42,6 +42,11 @@ if (! defined('APP_NAME')) {
define('ENTITY_RECURRING_EXPENSE', 'recurring_expense'); define('ENTITY_RECURRING_EXPENSE', 'recurring_expense');
define('ENTITY_CUSTOMER', 'customer'); define('ENTITY_CUSTOMER', 'customer');
define('ENTITY_SUBSCRIPTION', 'subscription'); define('ENTITY_SUBSCRIPTION', 'subscription');
define('ENTITY_PROPOSAL', 'proposal');
define('ENTITY_PROPOSAL_TEMPLATE', 'proposal_template');
define('ENTITY_PROPOSAL_SNIPPET', 'proposal_snippet');
define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category');
define('ENTITY_PROPOSAL_INVITATION', 'proposal_invitation');
define('INVOICE_TYPE_STANDARD', 1); define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2); define('INVOICE_TYPE_QUOTE', 2);
@ -153,6 +158,7 @@ if (! defined('APP_NAME')) {
define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000)); // KB define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000)); // KB
define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000)); // Total KB define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000)); // Total KB
define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000)); // Total KB (uncompressed) define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000)); // Total KB (uncompressed)
define('MAX_EMAILS_SENT_PER_HOUR', 90);
define('DOCUMENT_PREVIEW_SIZE', env('DOCUMENT_PREVIEW_SIZE', 300)); // pixels define('DOCUMENT_PREVIEW_SIZE', env('DOCUMENT_PREVIEW_SIZE', 300)); // pixels
define('DEFAULT_FONT_SIZE', 9); define('DEFAULT_FONT_SIZE', 9);
define('DEFAULT_HEADER_FONT', 1); // Roboto define('DEFAULT_HEADER_FONT', 1); // Roboto
@ -290,6 +296,7 @@ if (! defined('APP_NAME')) {
define('GATEWAY_DWOLLA', 43); define('GATEWAY_DWOLLA', 43);
define('GATEWAY_CHECKOUT_COM', 47); define('GATEWAY_CHECKOUT_COM', 47);
define('GATEWAY_CYBERSOURCE', 49); define('GATEWAY_CYBERSOURCE', 49);
define('GATEWAY_PAYTRACE', 56);
define('GATEWAY_WEPAY', 60); define('GATEWAY_WEPAY', 60);
define('GATEWAY_BRAINTREE', 61); define('GATEWAY_BRAINTREE', 61);
define('GATEWAY_CUSTOM', 62); define('GATEWAY_CUSTOM', 62);
@ -331,7 +338,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest')); define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01'); define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '4.1.5' . env('NINJA_VERSION_SUFFIX')); define('NINJA_VERSION', '4.2.0' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@ -452,6 +459,7 @@ if (! defined('APP_NAME')) {
define('TEMPLATE_INVOICE', 'invoice'); define('TEMPLATE_INVOICE', 'invoice');
define('TEMPLATE_QUOTE', 'quote'); define('TEMPLATE_QUOTE', 'quote');
define('TEMPLATE_PROPOSAL', 'proposal');
define('TEMPLATE_PARTIAL', 'partial'); define('TEMPLATE_PARTIAL', 'partial');
define('TEMPLATE_PAYMENT', 'payment'); define('TEMPLATE_PAYMENT', 'payment');
define('TEMPLATE_REMINDER1', 'reminder1'); define('TEMPLATE_REMINDER1', 'reminder1');
@ -517,6 +525,9 @@ if (! defined('APP_NAME')) {
define('PLAN_TERM_MONTHLY', 'month'); define('PLAN_TERM_MONTHLY', 'month');
define('PLAN_TERM_YEARLY', 'year'); define('PLAN_TERM_YEARLY', 'year');
define('SUBSCRIPTION_FORMAT_JSON', 'JSON');
define('SUBSCRIPTION_FORMAT_UBL', 'UBL');
// Pro // Pro
define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design');
define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by');
@ -602,7 +613,6 @@ if (! defined('APP_NAME')) {
'dateFormats' => 'App\Models\DateFormat', 'dateFormats' => 'App\Models\DateFormat',
'datetimeFormats' => 'App\Models\DatetimeFormat', 'datetimeFormats' => 'App\Models\DatetimeFormat',
'languages' => 'App\Models\Language', 'languages' => 'App\Models\Language',
'paymentTerms' => 'App\Models\PaymentTerm',
'paymentTypes' => 'App\Models\PaymentType', 'paymentTypes' => 'App\Models\PaymentType',
'countries' => 'App\Models\Country', 'countries' => 'App\Models\Country',
'invoiceDesigns' => 'App\Models\InvoiceDesign', 'invoiceDesigns' => 'App\Models\InvoiceDesign',

View File

@ -0,0 +1,21 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
class SubdomainWasRemoved extends Event
{
use SerializesModels;
public $account;
/**
* Create a new event instance.
*
* @param $account
*/
public function __construct($account)
{
$this->account = $account;
}
}

View File

@ -2,7 +2,6 @@
namespace App\Exceptions; namespace App\Exceptions;
use Crawler;
use Exception; use Exception;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
@ -51,11 +50,12 @@ class Handler extends ExceptionHandler
return false; return false;
} }
if (! class_exists('Utils')) { // if these classes don't exist the install is broken, maybe due to permissions
if (! class_exists('Utils') || ! class_exists('Crawler')) {
return parent::report($e); return parent::report($e);
} }
if (Crawler::isCrawler()) { if (\Crawler::isCrawler()) {
return false; return false;
} }

View File

@ -184,12 +184,10 @@ class AccountApiController extends BaseAPIController
$devices = json_decode($account->devices, true); $devices = json_decode($account->devices, true);
foreach($devices as $key => $value) for($x=0; $x<count($devices); $x++)
{ {
if($request->token == $devices[$x]['token'])
if($request->token == $value['token']) unset($devices[$x]);
unset($devices[$key]);
} }
$account->devices = json_encode(array_values($devices)); $account->devices = json_encode(array_values($devices));

File diff suppressed because one or more lines are too long

View File

@ -152,7 +152,7 @@ class AccountGatewayController extends BaseController
'config' => false, 'config' => false,
'gateways' => $gateways, 'gateways' => $gateways,
'creditCardTypes' => $creditCards, 'creditCardTypes' => $creditCards,
'countGateways' => count($currentGateways), 'countGateways' => $currentGateways->count(),
]; ];
} }

View File

@ -175,14 +175,18 @@ class AppController extends BaseController
$_ENV['DB_PASSWORD'] = $db['type']['password']; $_ENV['DB_PASSWORD'] = $db['type']['password'];
if ($mail) { if ($mail) {
$_ENV['MAIL_DRIVER'] = $mail['driver']; $prefix = '';
$_ENV['MAIL_PORT'] = $mail['port']; if (($user = auth()->user()) && Account::count() > 1) {
$_ENV['MAIL_ENCRYPTION'] = $mail['encryption']; $prefix = $user->account_id . '_';
$_ENV['MAIL_HOST'] = $mail['host']; }
$_ENV['MAIL_USERNAME'] = $mail['username']; $_ENV[$prefix . 'MAIL_DRIVER'] = $mail['driver'];
$_ENV['MAIL_FROM_NAME'] = $mail['from']['name']; $_ENV[$prefix . 'MAIL_PORT'] = $mail['port'];
$_ENV['MAIL_FROM_ADDRESS'] = $mail['from']['address']; $_ENV[$prefix . 'MAIL_ENCRYPTION'] = $mail['encryption'];
$_ENV['MAIL_PASSWORD'] = $mail['password']; $_ENV[$prefix . 'MAIL_HOST'] = $mail['host'];
$_ENV[$prefix . 'MAIL_USERNAME'] = $mail['username'];
$_ENV[$prefix . 'MAIL_FROM_NAME'] = $mail['from']['name'];
$_ENV[$prefix . 'MAIL_FROM_ADDRESS'] = $mail['from']['address'];
$_ENV[$prefix . 'MAIL_PASSWORD'] = $mail['password'];
$_ENV['MAILGUN_DOMAIN'] = $mail['mailgun_domain']; $_ENV['MAILGUN_DOMAIN'] = $mail['mailgun_domain'];
$_ENV['MAILGUN_SECRET'] = $mail['mailgun_secret']; $_ENV['MAILGUN_SECRET'] = $mail['mailgun_secret'];
} }

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers\Auth;
use Event; use Event;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\PasswordReset;
use App\Events\UserLoggedIn; use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;

View File

@ -92,9 +92,11 @@ class BankAccountController extends BaseController
if ($publicId) { if ($publicId) {
$bankAccount = BankAccount::scope($publicId)->firstOrFail(); $bankAccount = BankAccount::scope($publicId)->firstOrFail();
if ($username != $bankAccount->username) { if ($username != $bankAccount->username) {
// TODO update username $bankAccount->setUsername($username);
$bankAccount->save();
} else {
$username = Crypt::decrypt($username);
} }
$username = Crypt::decrypt($username);
$bankId = $bankAccount->bank_id; $bankId = $bankAccount->bank_id;
} else { } else {
$bankAccount = new BankAccount; $bankAccount = new BankAccount;

View File

@ -99,15 +99,17 @@ class BaseAPIController extends Controller
$query->with($includes); $query->with($includes);
if ($updatedAt = intval(Input::get('updated_at'))) { if (Input::get('updated_at') > 0) {
$query->where('updated_at', '>=', date('Y-m-d H:i:s', $updatedAt)); $updatedAt = intval(Input::get('updated_at'));
$query->where('updated_at', '>=', date('Y-m-d H:i:s', $updatedAt));
} }
if ($clientPublicId = Input::get('client_id')) { if (Input::get('client_id') > 0) {
$filter = function ($query) use ($clientPublicId) { $clientPublicId = Input::get('client_id');
$filter = function ($query) use ($clientPublicId) {
$query->where('public_id', '=', $clientPublicId); $query->where('public_id', '=', $clientPublicId);
}; };
$query->whereHas('client', $filter); $query->whereHas('client', $filter);
} }
if (! Utils::hasPermission('view_all')) { if (! Utils::hasPermission('view_all')) {

View File

@ -15,6 +15,7 @@ class CalendarController extends BaseController
public function showCalendar() public function showCalendar()
{ {
$data = [ $data = [
'title' => trans('texts.calendar'),
'account' => auth()->user()->account, 'account' => auth()->user()->account,
]; ];

View File

@ -58,7 +58,7 @@ class LoginController extends Controller
public function showLoginForm() public function showLoginForm()
{ {
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST')); $subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
$hasAccountIndentifier = request()->account_key || ($subdomain && $subdomain != 'app'); $hasAccountIndentifier = request()->account_key || ($subdomain && ! in_array($subdomain, ['www', 'app']));
if (! session('contact_key')) { if (! session('contact_key')) {
if (Utils::isNinja()) { if (Utils::isNinja()) {

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers\ClientAuth;
use Password; use Password;
use Config; use Config;
use App\Models\PasswordReset;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -55,14 +54,8 @@ class ResetPasswordController extends Controller
public function showResetForm(Request $request, $token = null) public function showResetForm(Request $request, $token = null)
{ {
$passwordReset = PasswordReset::whereToken($token)->first();
if (! $passwordReset) {
return redirect('login')->withMessage(trans('texts.invalid_code'));
}
return view('clientauth.passwords.reset')->with( return view('clientauth.passwords.reset')->with(
['token' => $token, 'email' => $passwordReset->email] ['token' => $token]
); );
} }

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Events\InvoiceInvitationWasViewed; use App\Events\InvoiceInvitationWasViewed;
use App\Events\QuoteInvitationWasViewed; use App\Events\QuoteInvitationWasViewed;
use App\Models\Account;
use App\Models\Contact; use App\Models\Contact;
use App\Models\Document; use App\Models\Document;
use App\Models\Gateway; use App\Models\Gateway;
@ -55,7 +56,7 @@ class ClientPortalController extends BaseController
$this->taskRepo = $taskRepo; $this->taskRepo = $taskRepo;
} }
public function view($invitationKey) public function viewInvoice($invitationKey)
{ {
if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
return $this->returnError(); return $this->returnError();
@ -76,8 +77,6 @@ class ClientPortalController extends BaseController
]); ]);
} }
$account->loadLocalizationSettings($client);
if (! Input::has('phantomjs') && ! session('silent:' . $client->id) && ! Session::has($invitation->invitation_key) if (! Input::has('phantomjs') && ! session('silent:' . $client->id) && ! Session::has($invitation->invitation_key)
&& (! Auth::check() || Auth::user()->account_id != $invoice->account_id)) { && (! Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
if ($invoice->isType(INVOICE_TYPE_QUOTE)) { if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
@ -117,10 +116,10 @@ class ClientPortalController extends BaseController
// translate the country names // translate the country names
if ($invoice->client->country) { if ($invoice->client->country) {
$invoice->client->country->name = trans('texts.country_' . $invoice->client->country->name); $invoice->client->country->name = $invoice->client->country->getName();
} }
if ($invoice->account->country) { if ($invoice->account->country) {
$invoice->account->country->name = trans('texts.country_' . $invoice->account->country->name); $invoice->account->country->name = $invoice->account->country->getName();
} }
$data = []; $data = [];
@ -226,7 +225,7 @@ class ClientPortalController extends BaseController
return $pdfString; return $pdfString;
} }
public function sign($invitationKey) public function authorizeInvoice($invitationKey)
{ {
if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
return RESULT_FAILURE; return RESULT_FAILURE;
@ -262,13 +261,13 @@ class ClientPortalController extends BaseController
return redirect(request()->url()); return redirect(request()->url());
} }
$account->loadLocalizationSettings($client);
$color = $account->primary_color ? $account->primary_color : '#0b4d78'; $color = $account->primary_color ? $account->primary_color : '#0b4d78';
$customer = false; $customer = false;
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
} elseif (! $account->enable_client_portal_dashboard) { } elseif (! $account->enable_client_portal_dashboard) {
session()->reflash();
return redirect()->to('/client/invoices/'); return redirect()->to('/client/invoices/');
} }
@ -334,7 +333,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -368,7 +366,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -414,7 +411,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -499,7 +495,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -535,7 +530,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -571,7 +565,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $contact->client->show_tasks_in_portal) { if (! $contact->client->show_tasks_in_portal) {
return redirect()->to($account->enable_client_portal_dashboard ? '/client/dashboard' : '/client/payment_methods/'); return redirect()->to($account->enable_client_portal_dashboard ? '/client/dashboard' : '/client/payment_methods/');
@ -611,7 +604,6 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $account->enable_client_portal) { if (! $account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -962,4 +954,65 @@ class ClientPortalController extends BaseController
return Redirect::to('client/invoices/recurring'); return Redirect::to('client/invoices/recurring');
} }
public function showDetails()
{
if (! $contact = $this->getContact()) {
return $this->returnError();
}
$data = [
'contact' => $contact,
'client' => $contact->client,
'account' => $contact->account,
];
return view('invited.details', $data);
}
public function updateDetails(\Illuminate\Http\Request $request)
{
if (! $contact = $this->getContact()) {
return $this->returnError();
}
$client = $contact->client;
$account = $contact->account;
if (! $account->enable_client_portal) {
return $this->returnError();
}
$rules = [
'email' => 'required',
'address1' => 'required',
'city' => 'required',
'state' => 'required',
'postal_code' => 'required',
'country_id' => 'required',
];
if ($client->name) {
$rules['name'] = 'required';
} else {
$rules['first_name'] = 'required';
$rules['last_name'] = 'required';
}
if ($account->vat_number || $account->isNinjaAccount()) {
$rules['vat_number'] = 'required';
}
$this->validate($request, $rules);
$contact->fill(request()->all());
$contact->save();
$client->fill(request()->all());
$client->save();
event(new \App\Events\ClientWasUpdated($client));
return redirect($account->enable_client_portal_dashboard ? '/client/dashboard' : '/client/payment_methods')
->withMessage(trans('texts.updated_client_details'));
}
} }

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use mPDF;
use App\Models\Account;
use App\Models\Document;
use App\Models\Invitation;
use App\Ninja\Repositories\ProposalRepository;
class ClientPortalProposalController extends BaseController
{
private $invoiceRepo;
private $paymentRepo;
private $documentRepo;
private $propoosalRepo;
public function __construct(ProposalRepository $propoosalRepo)
{
$this->propoosalRepo = $propoosalRepo;
}
public function viewProposal($invitationKey)
{
if (! $invitation = $this->propoosalRepo->findInvitationByKey($invitationKey)) {
return $this->returnError(trans('texts.proposal_not_found'));
}
$account = $invitation->account;
$proposal = $invitation->proposal;
$invoiceInvitation = Invitation::whereContactId($invitation->contact_id)
->whereInvoiceId($proposal->invoice_id)
->firstOrFail();
$data = [
'proposal' => $proposal,
'account' => $account,
'invoiceInvitation' => $invoiceInvitation,
'proposalInvitation' => $invitation,
];
return view('invited.proposal', $data);
}
public function downloadProposal($invitationKey)
{
if (! $invitation = $this->propoosalRepo->findInvitationByKey($invitationKey)) {
return $this->returnError(trans('texts.proposal_not_found'));
}
$proposal = $invitation->proposal;
$mpdf = new mPDF();
$mpdf->WriteHTML($proposal->present()->htmlDocument);
$mpdf->Output($proposal->present()->filename, 'D');
}
public function getProposalImage($accountKey, $documentKey)
{
$account = Account::whereAccountKey($accountKey)
->firstOrFail();
$document = Document::whereAccountId($account->id)
->whereDocumentKey($documentKey)
->whereIsProposal(true)
->firstOrFail();
return DocumentController::getDownloadResponse($document);
}
}

View File

@ -43,12 +43,12 @@ class DashboardApiController extends BaseAPIController
$data = [ $data = [
'id' => 1, 'id' => 1,
'paidToDate' => count($paidToDate) && $paidToDate[0]->value ? $paidToDate[0]->value : 0, 'paidToDate' => $paidToDate->count() && $paidToDate[0]->value ? $paidToDate[0]->value : 0,
'paidToDateCurrency' => count($paidToDate) && $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : 0, 'paidToDateCurrency' => $paidToDate->count() && $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : 0,
'balances' => count($balances) && $balances[0]->value ? $balances[0]->value : 0, 'balances' => $balances->count() && $balances[0]->value ? $balances[0]->value : 0,
'balancesCurrency' => count($balances) && $balances[0]->currency_id ? $balances[0]->currency_id : 0, 'balancesCurrency' => $balances->count() && $balances[0]->currency_id ? $balances[0]->currency_id : 0,
'averageInvoice' => count($averageInvoice) && $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0, 'averageInvoice' => $averageInvoice->count() && $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0,
'averageInvoiceCurrency' => count($averageInvoice) && $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : 0, 'averageInvoiceCurrency' => $averageInvoice->count() && $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : 0,
'invoicesSent' => $metrics ? $metrics->invoices_sent : 0, 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0,
'activeClients' => $metrics ? $metrics->active_clients : 0, 'activeClients' => $metrics ? $metrics->active_clients : 0,
'activities' => $this->createCollection($activities, new ActivityTransformer(), ENTITY_ACTIVITY), 'activities' => $this->createCollection($activities, new ActivityTransformer(), ENTITY_ACTIVITY),

View File

@ -83,7 +83,7 @@ class DashboardController extends BaseController
'tasks' => $tasks, 'tasks' => $tasks,
'showBlueVinePromo' => $showBlueVinePromo, 'showBlueVinePromo' => $showBlueVinePromo,
'showWhiteLabelExpired' => $showWhiteLabelExpired, 'showWhiteLabelExpired' => $showWhiteLabelExpired,
'showExpenses' => count($expenses) && $account->isModuleEnabled(ENTITY_EXPENSE), 'showExpenses' => $expenses->count() && $account->isModuleEnabled(ENTITY_EXPENSE),
'headerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl', 'tr_TR']) ? 'in-large' : 'in-thin', 'headerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl', 'tr_TR']) ? 'in-large' : 'in-thin',
'footerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl', 'tr_TR']) ? '' : 'in-thin', 'footerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl', 'tr_TR']) ? '' : 'in-thin',
]; ];

View File

@ -105,11 +105,20 @@ class DocumentController extends BaseController
'code' => 400, 'code' => 400,
], 400); ], 400);
} else { } else {
return Response::json([ if ($request->grapesjs) {
'error' => false, $response = [
'document' => $doc_array, 'data' => [
'code' => 200, $result->getProposalUrl()
], 200); ]
];
} else {
$response = [
'error' => false,
'document' => $doc_array,
'code' => 200,
];
}
return Response::json($response, 200);
} }
} }

View File

@ -161,7 +161,7 @@ class HomeController extends BaseController
} }
$subject .= '] '; $subject .= '] ';
} else { } else {
$subject .= 'Self-Host | '; $subject .= 'Self-Host] | ';
} }
$subject .= date('M jS, g:ia'); $subject .= date('M jS, g:ia');
$message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com')) $message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com'))

View File

@ -68,6 +68,11 @@ class InvoiceApiController extends BaseAPIController
$invoices->whereInvoiceNumber($invoiceNumber); $invoices->whereInvoiceNumber($invoiceNumber);
} }
// Fllter by status
if ($statusId = Input::get('status_id')) {
$invoices->where('invoice_status_id', '>=', $statusId);
}
return $this->listResponse($invoices); return $this->listResponse($invoices);
} }

View File

@ -403,7 +403,11 @@ class InvoiceController extends BaseController
} }
if (! Auth::user()->confirmed) { if (! Auth::user()->confirmed) {
$errorMessage = trans(Auth::user()->registered ? 'texts.confirmation_required' : 'texts.registration_required'); if (Auth::user()->registered) {
$errorMessage = trans('texts.confirmation_required', ['link' => link_to('/resend_confirmation', trans('texts.click_here'))]);
} else {
$errorMessage = trans('texts.registration_required');
}
Session::flash('error', $errorMessage); Session::flash('error', $errorMessage);
return Redirect::to('invoices/'.$invoice->public_id.'/edit'); return Redirect::to('invoices/'.$invoice->public_id.'/edit');

View File

@ -294,6 +294,7 @@ class NinjaController extends BaseController
} }
$user = Auth::user(); $user = Auth::user();
$account = $user->account;
$url = NINJA_APP_URL . '/buy_now'; $url = NINJA_APP_URL . '/buy_now';
$contactKey = $user->primaryAccount()->account_key; $contactKey = $user->primaryAccount()->account_key;
@ -301,9 +302,17 @@ class NinjaController extends BaseController
'account_key' => NINJA_LICENSE_ACCOUNT_KEY, 'account_key' => NINJA_LICENSE_ACCOUNT_KEY,
'contact_key' => $contactKey, 'contact_key' => $contactKey,
'product_id' => PRODUCT_WHITE_LABEL, 'product_id' => PRODUCT_WHITE_LABEL,
'first_name' => Auth::user()->first_name, 'first_name' => $user->first_name,
'last_name' => Auth::user()->last_name, 'last_name' => $user->last_name,
'email' => Auth::user()->email, 'email' => $user->email,
'name' => $account->name,
'address1' => $account->address1,
'address2' => $account->address2,
'city' => $account->city,
'state' => $account->state,
'postal_code' => $account->postal_code,
'country_id' => $account->country_id,
'vat_number' => $account->vat_number,
'return_link' => true, 'return_link' => true,
]; ];

View File

@ -356,12 +356,14 @@ class OnlinePaymentController extends BaseController
return redirect()->to("{$failureUrl}/?error=" . $validator->errors()->first()); return redirect()->to("{$failureUrl}/?error=" . $validator->errors()->first());
} }
$data = [ $data = request()->all();
'currency_id' => $account->currency_id, $data['currency_id'] = $account->currency_id;
'contact' => Input::all(), $data['custom_value1'] = request()->custom_client1;
'custom_value1' => Input::get('custom_client1'), $data['custom_value2'] = request()->custom_client2;
'custom_value2' => Input::get('custom_client2'), $data['contact'] = request()->all();
]; $data['contact']['custom_value1'] = request()->custom_contact1;
$data['contact']['custom_value2'] = request()->custom_contact2;
if (request()->currency_code) { if (request()->currency_code) {
$data['currency_code'] = request()->currency_code; $data['currency_code'] = request()->currency_code;
} }
@ -425,20 +427,23 @@ class OnlinePaymentController extends BaseController
{ {
if (Utils::isNinja()) { if (Utils::isNinja()) {
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST')); $subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
if (! $subdomain || $subdomain == 'app') {
exit('Invalid subdomain');
}
$account = Account::whereSubdomain($subdomain)->first(); $account = Account::whereSubdomain($subdomain)->first();
} else { } else {
$account = Account::first(); $account = Account::first();
} }
if (! $account) { if (! $account) {
exit("Account not found"); exit('Account not found');
} }
$accountGateway = $account->account_gateways() $accountGateway = $account->account_gateways()
->whereGatewayId(GATEWAY_STRIPE)->first(); ->whereGatewayId(GATEWAY_STRIPE)->first();
if (! $account) { if (! $account) {
exit("Apple merchant id not set"); exit('Apple merchant id not set');
} }
echo $accountGateway->getConfigField('appleMerchantId'); echo $accountGateway->getConfigField('appleMerchantId');

View File

@ -89,16 +89,29 @@ class PaymentController extends BaseController
*/ */
public function create(PaymentRequest $request) public function create(PaymentRequest $request)
{ {
$user = auth()->user();
$account = $user->account;
$invoices = Invoice::scope() $invoices = Invoice::scope()
->invoices() ->invoices()
->where('invoices.invoice_status_id', '!=', INVOICE_STATUS_PAID) ->where('invoices.invoice_status_id', '!=', INVOICE_STATUS_PAID)
->with('client', 'invoice_status') ->with('client', 'invoice_status')
->orderBy('invoice_number')->get(); ->orderBy('invoice_number')->get();
$clientPublicId = Input::old('client') ? Input::old('client') : ($request->client_id ?: 0);
$invoicePublicId = Input::old('invoice') ? Input::old('invoice') : ($request->invoice_id ?: 0);
$totalCredit = false;
if ($clientPublicId && $client = Client::scope($clientPublicId)->first()) {
$totalCredit = $account->formatMoney($client->getTotalCredit(), $client);
} elseif ($invoicePublicId && $invoice = Invoice::scope($invoicePublicId)->first()) {
$totalCredit = $account->formatMoney($invoice->client->getTotalCredit(), $client);
}
$data = [ $data = [
'account' => Auth::user()->account, 'account' => Auth::user()->account,
'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0), 'clientPublicId' => $clientPublicId,
'invoicePublicId' => Input::old('invoice') ? Input::old('invoice') : ($request->invoice_id ?: 0), 'invoicePublicId' => $invoicePublicId,
'invoice' => null, 'invoice' => null,
'invoices' => $invoices, 'invoices' => $invoices,
'payment' => null, 'payment' => null,
@ -106,7 +119,9 @@ class PaymentController extends BaseController
'url' => 'payments', 'url' => 'payments',
'title' => trans('texts.new_payment'), 'title' => trans('texts.new_payment'),
'paymentTypeId' => Input::get('paymentTypeId'), 'paymentTypeId' => Input::get('paymentTypeId'),
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ]; 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
'totalCredit' => $totalCredit,
];
return View::make('payments.edit', $data); return View::make('payments.edit', $data);
} }

View File

@ -0,0 +1,160 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreatePaymentTermAPIRequest;
use App\Http\Requests\PaymentTermRequest;
use App\Http\Requests\UpdatePaymentTermRequest;
use App\Libraries\Utils;
use App\Models\PaymentTerm;
use App\Ninja\Repositories\PaymentTermRepository;
use Illuminate\Support\Facades\Input;
class PaymentTermApiController extends BaseAPIController
{
/**
* @var PaymentTermRepository
*/
protected $paymentTermRepo;
protected $entityType = ENTITY_PAYMENT_TERM;
/**
* PaymentTermApiController constructor.
*
* @param PaymentTermRepository $paymentTermRepo
*/
public function __construct(PaymentTermRepository $paymentTermRepo)
{
parent::__construct();
$this->paymentTermRepo = $paymentTermRepo;
}
/**
* @SWG\Get(
* path="/paymentTerms",
* summary="List payment terms",
* operationId="listPaymentTerms",
* tags={"payment terms"},
* @SWG\Response(
* response=200,
* description="A list of payment terms",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/PaymentTerms"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$paymentTerms = PaymentTerm::scope()
->orWhere('account_id',0)
->orderBy('num_days', 'asc');
return $this->listResponse($paymentTerms);
}
/**
* @SWG\Get(
* path="/paymentTerms/{payment_term_id}",
* summary="Retrieve a payment term",
* operationId="getPaymentTermId",
* tags={"payment term"},
* @SWG\Parameter(
* in="path",
* name="payment_term_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single payment term",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/PaymentTerms"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(PaymentTermRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/paymentTerms",
* summary="Create a payment Term",
* operationId="createPaymentTerm",
* tags={"payment term"},
* @SWG\Parameter(
* in="body",
* name="payment term",
* @SWG\Schema(ref="#/definitions/PaymentTerm")
* ),
* @SWG\Response(
* response=200,
* description="New payment Term",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/PaymentTerm"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function store(CreatePaymentTermAPIRequest $request)
{
$paymentTerm = PaymentTerm::createNew();
$paymentTerm->num_days = Utils::parseInt(Input::get('num_days'));
$paymentTerm->name = 'Net ' . $paymentTerm->num_days;
$paymentTerm->save();
return $this->itemResponse($paymentTerm);
}
/**
* @SWG\Delete(
* path="/paymentTerm/{num_days}",
* summary="Delete a payment term",
* operationId="deletePaymentTerm",
* tags={"payment term"},
* @SWG\Parameter(
* in="path",
* name="num_days",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted payment Term",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/PaymentTerm"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy($numDays)
{
$paymentTerm = PaymentTerm::where('num_days', $numDays)->first();
if(!$paymentTerm || $paymentTerm->account_id == 0)
return $this->errorResponse(['message'=>'Cannot delete a default or non existent Payment Term'], 400);
$this->paymentTermRepo->archive($paymentTerm);
return $this->itemResponse($paymentTerm);
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateProposalCategoryRequest;
use App\Http\Requests\ProposalCategoryRequest;
use App\Http\Requests\UpdateProposalCategoryRequest;
use App\Models\Invoice;
use App\Models\ProposalCategory;
use App\Ninja\Datatables\ProposalCategoryDatatable;
use App\Ninja\Repositories\ProposalCategoryRepository;
use App\Services\ProposalCategoryService;
use Auth;
use Input;
use Session;
use View;
class ProposalCategoryController extends BaseController
{
protected $proposalCategoryRepo;
protected $proposalCategoryService;
protected $entityType = ENTITY_PROPOSAL_CATEGORY;
public function __construct(ProposalCategoryRepository $proposalCategoryRepo, ProposalCategoryService $proposalCategoryService)
{
$this->proposalCategoryRepo = $proposalCategoryRepo;
$this->proposalCategoryService = $proposalCategoryService;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list_wrapper', [
'entityType' => ENTITY_PROPOSAL_CATEGORY,
'datatable' => new ProposalCategoryDatatable(),
'title' => trans('texts.proposal_categories'),
]);
}
public function getDatatable($expensePublicId = null)
{
$search = Input::get('sSearch');
$userId = Auth::user()->filterId();
return $this->proposalCategoryService->getDatatable($search, $userId);
}
public function create(ProposalCategoryRequest $request)
{
$data = [
'account' => auth()->user()->account,
'category' => null,
'method' => 'POST',
'url' => 'proposals/categories',
'title' => trans('texts.new_proposal_category'),
'quotes' => Invoice::scope()->with('client.contacts')->quotes()->orderBy('id')->get(),
'templates' => ProposalCategory::scope()->orderBy('name')->get(),
'quotePublicId' => $request->quote_id,
];
return View::make('proposals/categories.edit', $data);
}
public function show($publicId)
{
Session::reflash();
return redirect("proposals/categories/$publicId/edit");
}
public function edit(ProposalCategoryRequest $request)
{
$proposalCategory = $request->entity();
$data = [
'account' => auth()->user()->account,
'category' => $proposalCategory,
'method' => 'PUT',
'url' => 'proposals/categories/' . $proposalCategory->public_id,
'title' => trans('texts.edit_proposal_category'),
];
return View::make('proposals/categories.edit', $data);
}
public function store(CreateProposalCategoryRequest $request)
{
$proposalCategory = $this->proposalCategoryService->save($request->input());
Session::flash('message', trans('texts.created_proposal_category'));
return redirect()->to($proposalCategory->getRoute());
}
public function update(UpdateProposalCategoryRequest $request)
{
$proposalCategory = $this->proposalCategoryService->save($request->input(), $request->entity());
Session::flash('message', trans('texts.updated_proposal_category'));
$action = Input::get('action');
if (in_array($action, ['archive', 'delete', 'restore'])) {
return self::bulk();
}
return redirect()->to($proposalCategory->getRoute());
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->proposalCategoryService->bulk($ids, $action);
if ($count > 0) {
$field = $count == 1 ? "{$action}d_proposal_category" : "{$action}d_proposal_categories";
$message = trans("texts.$field", ['count' => $count]);
Session::flash('message', $message);
}
return redirect()->to('/proposals/categories');
}
}

View File

@ -0,0 +1,178 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateProposalRequest;
use App\Http\Requests\ProposalRequest;
use App\Http\Requests\UpdateProposalRequest;
use App\Jobs\SendInvoiceEmail;
use App\Models\Invoice;
use App\Models\Proposal;
use App\Models\ProposalTemplate;
use App\Ninja\Mailers\ContactMailer;
use App\Ninja\Datatables\ProposalDatatable;
use App\Ninja\Repositories\ProposalRepository;
use App\Services\ProposalService;
use Auth;
use Input;
use Session;
use View;
use mPDF;
class ProposalController extends BaseController
{
protected $proposalRepo;
protected $proposalService;
protected $contactMailer;
protected $entityType = ENTITY_PROPOSAL;
public function __construct(ProposalRepository $proposalRepo, ProposalService $proposalService, ContactMailer $contactMailer)
{
$this->proposalRepo = $proposalRepo;
$this->proposalService = $proposalService;
$this->contactMailer = $contactMailer;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list_wrapper', [
'entityType' => ENTITY_PROPOSAL,
'datatable' => new ProposalDatatable(),
'title' => trans('texts.proposals'),
]);
}
public function getDatatable($expensePublicId = null)
{
$search = Input::get('sSearch');
$userId = Auth::user()->filterId();
return $this->proposalService->getDatatable($search, $userId);
}
public function create(ProposalRequest $request)
{
$data = array_merge($this->getViewmodel(), [
'proposal' => null,
'method' => 'POST',
'url' => 'proposals',
'title' => trans('texts.new_proposal'),
'invoices' => Invoice::scope()->with('client.contacts', 'client.country')->unapprovedQuotes()->orderBy('id')->get(),
'invoicePublicId' => $request->invoice_id,
'templatePublicId' => $request->proposal_template_id,
]);
return View::make('proposals.edit', $data);
}
public function show($publicId)
{
Session::reflash();
return redirect("proposals/$publicId/edit");
}
public function edit(ProposalRequest $request)
{
$proposal = $request->entity();
$data = array_merge($this->getViewmodel(), [
'proposal' => $proposal,
'entity' => $proposal,
'method' => 'PUT',
'url' => 'proposals/' . $proposal->public_id,
'title' => trans('texts.edit_proposal'),
'invoices' => Invoice::scope()->with('client.contacts', 'client.country')->unapprovedQuotes($proposal->invoice_id)->orderBy('id')->get(),
'invoicePublicId' => $proposal->invoice ? $proposal->invoice->public_id : null,
'templatePublicId' => $proposal->proposal_template ? $proposal->proposal_template->public_id : null,
]);
return View::make('proposals.edit', $data);
}
private function getViewmodel()
{
$account = auth()->user()->account;
$templates = ProposalTemplate::whereAccountId($account->id)->orderBy('name')->get();
if (! $templates->count()) {
$templates = ProposalTemplate::whereNull('account_id')->orderBy('name')->get();
}
$data = [
'templates' => $templates,
'account' => $account,
];
return $data;
}
public function store(CreateProposalRequest $request)
{
$proposal = $this->proposalService->save($request->input());
$action = Input::get('action');
if ($action == 'email') {
$this->dispatch(new SendInvoiceEmail($proposal->invoice, auth()->user()->id, false, false, $proposal));
Session::flash('message', trans('texts.emailed_proposal'));
} else {
Session::flash('message', trans('texts.created_proposal'));
}
return redirect()->to($proposal->getRoute());
}
public function update(UpdateProposalRequest $request)
{
$proposal = $this->proposalService->save($request->input(), $request->entity());
$action = Input::get('action');
if (in_array($action, ['archive', 'delete', 'restore'])) {
return self::bulk();
}
if ($action == 'email') {
$this->dispatch(new SendInvoiceEmail($proposal->invoice, auth()->user()->id, false, false, $proposal));
Session::flash('message', trans('texts.emailed_proposal'));
} else {
Session::flash('message', trans('texts.updated_proposal'));
}
return redirect()->to($proposal->getRoute());
}
public function bulk()
{
$action = Input::get('bulk_action') ?: Input::get('action');
$ids = Input::get('bulk_public_id') ?: (Input::get('public_id') ?: Input::get('ids'));
$count = $this->proposalService->bulk($ids, $action);
if ($count > 0) {
$field = $count == 1 ? "{$action}d_proposal" : "{$action}d_proposals";
$message = trans("texts.$field", ['count' => $count]);
Session::flash('message', $message);
}
return redirect()->to('/proposals');
}
public function download(ProposalRequest $request)
{
$proposal = $request->entity();
$mpdf = new mPDF();
$mpdf->showImageErrors = true;
$mpdf->WriteHTML($proposal->present()->htmlDocument);
//$mpdf->Output();
$mpdf->Output($proposal->present()->filename, 'D');
}
}

View File

@ -0,0 +1,732 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateProposalSnippetRequest;
use App\Http\Requests\ProposalSnippetRequest;
use App\Http\Requests\UpdateProposalSnippetRequest;
use App\Models\Invoice;
use App\Models\ProposalSnippet;
use App\Models\ProposalCategory;
use App\Ninja\Datatables\ProposalSnippetDatatable;
use App\Ninja\Repositories\ProposalSnippetRepository;
use App\Services\ProposalSnippetService;
use Auth;
use Input;
use Session;
use View;
class ProposalSnippetController extends BaseController
{
protected $proposalSnippetRepo;
protected $proposalSnippetService;
protected $entityType = ENTITY_PROPOSAL_SNIPPET;
public function __construct(ProposalSnippetRepository $proposalSnippetRepo, ProposalSnippetService $proposalSnippetService)
{
$this->proposalSnippetRepo = $proposalSnippetRepo;
$this->proposalSnippetService = $proposalSnippetService;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list_wrapper', [
'entityType' => ENTITY_PROPOSAL_SNIPPET,
'datatable' => new ProposalSnippetDatatable(),
'title' => trans('texts.proposal_snippets'),
]);
}
public function getDatatable($expensePublicId = null)
{
$search = Input::get('sSearch');
$userId = Auth::user()->filterId();
return $this->proposalSnippetService->getDatatable($search, $userId);
}
public function create(ProposalSnippetRequest $request)
{
$data = [
'account' => auth()->user()->account,
'snippet' => null,
'method' => 'POST',
'url' => 'proposals/snippets',
'title' => trans('texts.new_proposal_snippet'),
'categories' => ProposalCategory::scope()->orderBy('name')->get(),
'categoryPublicId' => 0,
'icons' => $this->getIcons(),
];
return View::make('proposals/snippets/edit', $data);
}
public function show($publicId)
{
Session::reflash();
return redirect("proposals/snippets/$publicId/edit");
}
public function edit(ProposalSnippetRequest $request)
{
$proposalSnippet = $request->entity();
$data = [
'account' => auth()->user()->account,
'snippet' => $proposalSnippet,
'entity' => $proposalSnippet,
'method' => 'PUT',
'url' => 'proposals/snippets/' . $proposalSnippet->public_id,
'title' => trans('texts.edit_proposal_snippet'),
'categories' => ProposalCategory::scope()->orderBy('name')->get(),
'categoryPublicId' => $proposalSnippet->proposal_category ? $proposalSnippet->proposal_category->public_id : null,
'icons' => $this->getIcons(),
];
return View::make('proposals/snippets.edit', $data);
}
public function store(CreateProposalSnippetRequest $request)
{
$proposalSnippet = $this->proposalSnippetService->save($request->input());
Session::flash('message', trans('texts.created_proposal_snippet'));
return redirect()->to($proposalSnippet->getRoute());
}
public function update(UpdateProposalSnippetRequest $request)
{
$proposalSnippet = $this->proposalSnippetService->save($request->input(), $request->entity());
Session::flash('message', trans('texts.updated_proposal_snippet'));
$action = Input::get('action');
if (in_array($action, ['archive', 'delete', 'restore'])) {
return self::bulk();
}
return redirect()->to($proposalSnippet->getRoute());
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->proposalSnippetService->bulk($ids, $action);
if ($count > 0) {
$field = $count == 1 ? "{$action}d_proposal_snippet" : "{$action}d_proposal_snippets";
$message = trans("texts.$field", ['count' => $count]);
Session::flash('message', $message);
}
return redirect()->to('/proposals/snippets');
}
private function getIcons() {
$data = [];
$icons = [
['name'=>'glass','code'=>'f000'],
['name'=>'music','code'=>'f001'],
['name'=>'search','code'=>'f002'],
['name'=>'envelope-o','code'=>'f003'],
['name'=>'heart','code'=>'f004'],
['name'=>'star','code'=>'f005'],
['name'=>'star-o','code'=>'f006'],
['name'=>'user','code'=>'f007'],
['name'=>'film','code'=>'f008'],
['name'=>'th-large','code'=>'f009'],
['name'=>'th','code'=>'f00a'],
['name'=>'th-list','code'=>'f00b'],
['name'=>'check','code'=>'f00c'],
['name'=>'times','code'=>'f00d'],
['name'=>'search-plus','code'=>'f00e'],
['name'=>'search-minus','code'=>'f010'],
['name'=>'power-off','code'=>'f011'],
['name'=>'signal','code'=>'f012'],
['name'=>'cog','code'=>'f013'],
['name'=>'trash-o','code'=>'f014'],
['name'=>'home','code'=>'f015'],
['name'=>'file-o','code'=>'f016'],
['name'=>'clock-o','code'=>'f017'],
['name'=>'road','code'=>'f018'],
['name'=>'download','code'=>'f019'],
['name'=>'arrow-circle-o-down','code'=>'f01a'],
['name'=>'arrow-circle-o-up','code'=>'f01b'],
['name'=>'inbox','code'=>'f01c'],
['name'=>'play-circle-o','code'=>'f01d'],
['name'=>'repeat','code'=>'f01e'],
['name'=>'refresh','code'=>'f021'],
['name'=>'list-alt','code'=>'f022'],
['name'=>'lock','code'=>'f023'],
['name'=>'flag','code'=>'f024'],
['name'=>'headphones','code'=>'f025'],
['name'=>'volume-off','code'=>'f026'],
['name'=>'volume-down','code'=>'f027'],
['name'=>'volume-up','code'=>'f028'],
['name'=>'qrcode','code'=>'f029'],
['name'=>'barcode','code'=>'f02a'],
['name'=>'tag','code'=>'f02b'],
['name'=>'tags','code'=>'f02c'],
['name'=>'book','code'=>'f02d'],
['name'=>'bookmark','code'=>'f02e'],
['name'=>'print','code'=>'f02f'],
['name'=>'camera','code'=>'f030'],
['name'=>'font','code'=>'f031'],
['name'=>'bold','code'=>'f032'],
['name'=>'italic','code'=>'f033'],
['name'=>'text-height','code'=>'f034'],
['name'=>'text-width','code'=>'f035'],
['name'=>'align-left','code'=>'f036'],
['name'=>'align-center','code'=>'f037'],
['name'=>'align-right','code'=>'f038'],
['name'=>'align-justify','code'=>'f039'],
['name'=>'list','code'=>'f03a'],
['name'=>'outdent','code'=>'f03b'],
['name'=>'indent','code'=>'f03c'],
['name'=>'video-camera','code'=>'f03d'],
['name'=>'picture-o','code'=>'f03e'],
['name'=>'pencil','code'=>'f040'],
['name'=>'map-marker','code'=>'f041'],
['name'=>'adjust','code'=>'f042'],
['name'=>'tint','code'=>'f043'],
['name'=>'pencil-square-o','code'=>'f044'],
['name'=>'share-square-o','code'=>'f045'],
['name'=>'check-square-o','code'=>'f046'],
['name'=>'arrows','code'=>'f047'],
['name'=>'step-backward','code'=>'f048'],
['name'=>'fast-backward','code'=>'f049'],
['name'=>'backward','code'=>'f04a'],
['name'=>'play','code'=>'f04b'],
['name'=>'pause','code'=>'f04c'],
['name'=>'stop','code'=>'f04d'],
['name'=>'forward','code'=>'f04e'],
['name'=>'fast-forward','code'=>'f050'],
['name'=>'step-forward','code'=>'f051'],
['name'=>'eject','code'=>'f052'],
['name'=>'chevron-left','code'=>'f053'],
['name'=>'chevron-right','code'=>'f054'],
['name'=>'plus-circle','code'=>'f055'],
['name'=>'minus-circle','code'=>'f056'],
['name'=>'times-circle','code'=>'f057'],
['name'=>'check-circle','code'=>'f058'],
['name'=>'question-circle','code'=>'f059'],
['name'=>'info-circle','code'=>'f05a'],
['name'=>'crosshairs','code'=>'f05b'],
['name'=>'times-circle-o','code'=>'f05c'],
['name'=>'check-circle-o','code'=>'f05d'],
['name'=>'ban','code'=>'f05e'],
['name'=>'arrow-left','code'=>'f060'],
['name'=>'arrow-right','code'=>'f061'],
['name'=>'arrow-up','code'=>'f062'],
['name'=>'arrow-down','code'=>'f063'],
['name'=>'share','code'=>'f064'],
['name'=>'expand','code'=>'f065'],
['name'=>'compress','code'=>'f066'],
['name'=>'plus','code'=>'f067'],
['name'=>'minus','code'=>'f068'],
['name'=>'asterisk','code'=>'f069'],
['name'=>'exclamation-circle','code'=>'f06a'],
['name'=>'gift','code'=>'f06b'],
['name'=>'leaf','code'=>'f06c'],
['name'=>'fire','code'=>'f06d'],
['name'=>'eye','code'=>'f06e'],
['name'=>'eye-slash','code'=>'f070'],
['name'=>'exclamation-triangle','code'=>'f071'],
['name'=>'plane','code'=>'f072'],
['name'=>'calendar','code'=>'f073'],
['name'=>'random','code'=>'f074'],
['name'=>'comment','code'=>'f075'],
['name'=>'magnet','code'=>'f076'],
['name'=>'chevron-up','code'=>'f077'],
['name'=>'chevron-down','code'=>'f078'],
['name'=>'retweet','code'=>'f079'],
['name'=>'shopping-cart','code'=>'f07a'],
['name'=>'folder','code'=>'f07b'],
['name'=>'folder-open','code'=>'f07c'],
['name'=>'arrows-v','code'=>'f07d'],
['name'=>'arrows-h','code'=>'f07e'],
['name'=>'bar-chart','code'=>'f080'],
['name'=>'twitter-square','code'=>'f081'],
['name'=>'facebook-square','code'=>'f082'],
['name'=>'camera-retro','code'=>'f083'],
['name'=>'key','code'=>'f084'],
['name'=>'cogs','code'=>'f085'],
['name'=>'comments','code'=>'f086'],
['name'=>'thumbs-o-up','code'=>'f087'],
['name'=>'thumbs-o-down','code'=>'f088'],
['name'=>'star-half','code'=>'f089'],
['name'=>'heart-o','code'=>'f08a'],
['name'=>'sign-out','code'=>'f08b'],
['name'=>'linkedin-square','code'=>'f08c'],
['name'=>'thumb-tack','code'=>'f08d'],
['name'=>'external-link','code'=>'f08e'],
['name'=>'sign-in','code'=>'f090'],
['name'=>'trophy','code'=>'f091'],
['name'=>'github-square','code'=>'f092'],
['name'=>'upload','code'=>'f093'],
['name'=>'lemon-o','code'=>'f094'],
['name'=>'phone','code'=>'f095'],
['name'=>'square-o','code'=>'f096'],
['name'=>'bookmark-o','code'=>'f097'],
['name'=>'phone-square','code'=>'f098'],
['name'=>'twitter','code'=>'f099'],
['name'=>'facebook','code'=>'f09a'],
['name'=>'github','code'=>'f09b'],
['name'=>'unlock','code'=>'f09c'],
['name'=>'credit-card','code'=>'f09d'],
['name'=>'rss','code'=>'f09e'],
['name'=>'hdd-o','code'=>'f0a0'],
['name'=>'bullhorn','code'=>'f0a1'],
['name'=>'bell','code'=>'f0f3'],
['name'=>'certificate','code'=>'f0a3'],
['name'=>'hand-o-right','code'=>'f0a4'],
['name'=>'hand-o-left','code'=>'f0a5'],
['name'=>'hand-o-up','code'=>'f0a6'],
['name'=>'hand-o-down','code'=>'f0a7'],
['name'=>'arrow-circle-left','code'=>'f0a8'],
['name'=>'arrow-circle-right','code'=>'f0a9'],
['name'=>'arrow-circle-up','code'=>'f0aa'],
['name'=>'arrow-circle-down','code'=>'f0ab'],
['name'=>'globe','code'=>'f0ac'],
['name'=>'wrench','code'=>'f0ad'],
['name'=>'tasks','code'=>'f0ae'],
['name'=>'filter','code'=>'f0b0'],
['name'=>'briefcase','code'=>'f0b1'],
['name'=>'arrows-alt','code'=>'f0b2'],
['name'=>'users','code'=>'f0c0'],
['name'=>'link','code'=>'f0c1'],
['name'=>'cloud','code'=>'f0c2'],
['name'=>'flask','code'=>'f0c3'],
['name'=>'scissors','code'=>'f0c4'],
['name'=>'files-o','code'=>'f0c5'],
['name'=>'paperclip','code'=>'f0c6'],
['name'=>'floppy-o','code'=>'f0c7'],
['name'=>'square','code'=>'f0c8'],
['name'=>'bars','code'=>'f0c9'],
['name'=>'list-ul','code'=>'f0ca'],
['name'=>'list-ol','code'=>'f0cb'],
['name'=>'strikethrough','code'=>'f0cc'],
['name'=>'underline','code'=>'f0cd'],
['name'=>'table','code'=>'f0ce'],
['name'=>'magic','code'=>'f0d0'],
['name'=>'truck','code'=>'f0d1'],
['name'=>'pinterest','code'=>'f0d2'],
['name'=>'pinterest-square','code'=>'f0d3'],
['name'=>'google-plus-square','code'=>'f0d4'],
['name'=>'google-plus','code'=>'f0d5'],
['name'=>'money','code'=>'f0d6'],
['name'=>'caret-down','code'=>'f0d7'],
['name'=>'caret-up','code'=>'f0d8'],
['name'=>'caret-left','code'=>'f0d9'],
['name'=>'caret-right','code'=>'f0da'],
['name'=>'columns','code'=>'f0db'],
['name'=>'sort','code'=>'f0dc'],
['name'=>'sort-desc','code'=>'f0dd'],
['name'=>'sort-asc','code'=>'f0de'],
['name'=>'envelope','code'=>'f0e0'],
['name'=>'linkedin','code'=>'f0e1'],
['name'=>'undo','code'=>'f0e2'],
['name'=>'gavel','code'=>'f0e3'],
['name'=>'tachometer','code'=>'f0e4'],
['name'=>'comment-o','code'=>'f0e5'],
['name'=>'comments-o','code'=>'f0e6'],
['name'=>'bolt','code'=>'f0e7'],
['name'=>'sitemap','code'=>'f0e8'],
['name'=>'umbrella','code'=>'f0e9'],
['name'=>'clipboard','code'=>'f0ea'],
['name'=>'lightbulb-o','code'=>'f0eb'],
['name'=>'exchange','code'=>'f0ec'],
['name'=>'cloud-download','code'=>'f0ed'],
['name'=>'cloud-upload','code'=>'f0ee'],
['name'=>'user-md','code'=>'f0f0'],
['name'=>'stethoscope','code'=>'f0f1'],
['name'=>'suitcase','code'=>'f0f2'],
['name'=>'bell-o','code'=>'f0a2'],
['name'=>'coffee','code'=>'f0f4'],
['name'=>'cutlery','code'=>'f0f5'],
['name'=>'file-text-o','code'=>'f0f6'],
['name'=>'building-o','code'=>'f0f7'],
['name'=>'hospital-o','code'=>'f0f8'],
['name'=>'ambulance','code'=>'f0f9'],
['name'=>'medkit','code'=>'f0fa'],
['name'=>'fighter-jet','code'=>'f0fb'],
['name'=>'beer','code'=>'f0fc'],
['name'=>'h-square','code'=>'f0fd'],
['name'=>'plus-square','code'=>'f0fe'],
['name'=>'angle-double-left','code'=>'f100'],
['name'=>'angle-double-right','code'=>'f101'],
['name'=>'angle-double-up','code'=>'f102'],
['name'=>'angle-double-down','code'=>'f103'],
['name'=>'angle-left','code'=>'f104'],
['name'=>'angle-right','code'=>'f105'],
['name'=>'angle-up','code'=>'f106'],
['name'=>'angle-down','code'=>'f107'],
['name'=>'desktop','code'=>'f108'],
['name'=>'laptop','code'=>'f109'],
['name'=>'tablet','code'=>'f10a'],
['name'=>'mobile','code'=>'f10b'],
['name'=>'circle-o','code'=>'f10c'],
['name'=>'quote-left','code'=>'f10d'],
['name'=>'quote-right','code'=>'f10e'],
['name'=>'spinner','code'=>'f110'],
['name'=>'circle','code'=>'f111'],
['name'=>'reply','code'=>'f112'],
['name'=>'github-alt','code'=>'f113'],
['name'=>'folder-o','code'=>'f114'],
['name'=>'folder-open-o','code'=>'f115'],
['name'=>'smile-o','code'=>'f118'],
['name'=>'frown-o','code'=>'f119'],
['name'=>'meh-o','code'=>'f11a'],
['name'=>'gamepad','code'=>'f11b'],
['name'=>'keyboard-o','code'=>'f11c'],
['name'=>'flag-o','code'=>'f11d'],
['name'=>'flag-checkered','code'=>'f11e'],
['name'=>'terminal','code'=>'f120'],
['name'=>'code','code'=>'f121'],
['name'=>'reply-all','code'=>'f122'],
['name'=>'star-half-o','code'=>'f123'],
['name'=>'location-arrow','code'=>'f124'],
['name'=>'crop','code'=>'f125'],
['name'=>'code-fork','code'=>'f126'],
['name'=>'chain-broken','code'=>'f127'],
['name'=>'question','code'=>'f128'],
['name'=>'info','code'=>'f129'],
['name'=>'exclamation','code'=>'f12a'],
['name'=>'superscript','code'=>'f12b'],
['name'=>'subscript','code'=>'f12c'],
['name'=>'eraser','code'=>'f12d'],
['name'=>'puzzle-piece','code'=>'f12e'],
['name'=>'microphone','code'=>'f130'],
['name'=>'microphone-slash','code'=>'f131'],
['name'=>'shield','code'=>'f132'],
['name'=>'calendar-o','code'=>'f133'],
['name'=>'fire-extinguisher','code'=>'f134'],
['name'=>'rocket','code'=>'f135'],
['name'=>'maxcdn','code'=>'f136'],
['name'=>'chevron-circle-left','code'=>'f137'],
['name'=>'chevron-circle-right','code'=>'f138'],
['name'=>'chevron-circle-up','code'=>'f139'],
['name'=>'chevron-circle-down','code'=>'f13a'],
['name'=>'html5','code'=>'f13b'],
['name'=>'css3','code'=>'f13c'],
['name'=>'anchor','code'=>'f13d'],
['name'=>'unlock-alt','code'=>'f13e'],
['name'=>'bullseye','code'=>'f140'],
['name'=>'ellipsis-h','code'=>'f141'],
['name'=>'ellipsis-v','code'=>'f142'],
['name'=>'rss-square','code'=>'f143'],
['name'=>'play-circle','code'=>'f144'],
['name'=>'ticket','code'=>'f145'],
['name'=>'minus-square','code'=>'f146'],
['name'=>'minus-square-o','code'=>'f147'],
['name'=>'level-up','code'=>'f148'],
['name'=>'level-down','code'=>'f149'],
['name'=>'check-square','code'=>'f14a'],
['name'=>'pencil-square','code'=>'f14b'],
['name'=>'external-link-square','code'=>'f14c'],
['name'=>'share-square','code'=>'f14d'],
['name'=>'compass','code'=>'f14e'],
['name'=>'caret-square-o-down','code'=>'f150'],
['name'=>'caret-square-o-up','code'=>'f151'],
['name'=>'caret-square-o-right','code'=>'f152'],
['name'=>'eur','code'=>'f153'],
['name'=>'gbp','code'=>'f154'],
['name'=>'usd','code'=>'f155'],
['name'=>'inr','code'=>'f156'],
['name'=>'jpy','code'=>'f157'],
['name'=>'rub','code'=>'f158'],
['name'=>'krw','code'=>'f159'],
['name'=>'btc','code'=>'f15a'],
['name'=>'file','code'=>'f15b'],
['name'=>'file-text','code'=>'f15c'],
['name'=>'sort-alpha-asc','code'=>'f15d'],
['name'=>'sort-alpha-desc','code'=>'f15e'],
['name'=>'sort-amount-asc','code'=>'f160'],
['name'=>'sort-amount-desc','code'=>'f161'],
['name'=>'sort-numeric-asc','code'=>'f162'],
['name'=>'sort-numeric-desc','code'=>'f163'],
['name'=>'thumbs-up','code'=>'f164'],
['name'=>'thumbs-down','code'=>'f165'],
['name'=>'youtube-square','code'=>'f166'],
['name'=>'youtube','code'=>'f167'],
['name'=>'xing','code'=>'f168'],
['name'=>'xing-square','code'=>'f169'],
['name'=>'youtube-play','code'=>'f16a'],
['name'=>'dropbox','code'=>'f16b'],
['name'=>'stack-overflow','code'=>'f16c'],
['name'=>'instagram','code'=>'f16d'],
['name'=>'flickr','code'=>'f16e'],
['name'=>'adn','code'=>'f170'],
['name'=>'bitbucket','code'=>'f171'],
['name'=>'bitbucket-square','code'=>'f172'],
['name'=>'tumblr','code'=>'f173'],
['name'=>'tumblr-square','code'=>'f174'],
['name'=>'long-arrow-down','code'=>'f175'],
['name'=>'long-arrow-up','code'=>'f176'],
['name'=>'long-arrow-left','code'=>'f177'],
['name'=>'long-arrow-right','code'=>'f178'],
['name'=>'apple','code'=>'f179'],
['name'=>'windows','code'=>'f17a'],
['name'=>'android','code'=>'f17b'],
['name'=>'linux','code'=>'f17c'],
['name'=>'dribbble','code'=>'f17d'],
['name'=>'skype','code'=>'f17e'],
['name'=>'foursquare','code'=>'f180'],
['name'=>'trello','code'=>'f181'],
['name'=>'female','code'=>'f182'],
['name'=>'male','code'=>'f183'],
['name'=>'gratipay','code'=>'f184'],
['name'=>'sun-o','code'=>'f185'],
['name'=>'moon-o','code'=>'f186'],
['name'=>'archive','code'=>'f187'],
['name'=>'bug','code'=>'f188'],
['name'=>'vk','code'=>'f189'],
['name'=>'weibo','code'=>'f18a'],
['name'=>'renren','code'=>'f18b'],
['name'=>'pagelines','code'=>'f18c'],
['name'=>'stack-exchange','code'=>'f18d'],
['name'=>'arrow-circle-o-right','code'=>'f18e'],
['name'=>'arrow-circle-o-left','code'=>'f190'],
['name'=>'caret-square-o-left','code'=>'f191'],
['name'=>'dot-circle-o','code'=>'f192'],
['name'=>'wheelchair','code'=>'f193'],
['name'=>'vimeo-square','code'=>'f194'],
['name'=>'try','code'=>'f195'],
['name'=>'plus-square-o','code'=>'f196'],
['name'=>'space-shuttle','code'=>'f197'],
['name'=>'slack','code'=>'f198'],
['name'=>'envelope-square','code'=>'f199'],
['name'=>'wordpress','code'=>'f19a'],
['name'=>'openid','code'=>'f19b'],
['name'=>'university','code'=>'f19c'],
['name'=>'graduation-cap','code'=>'f19d'],
['name'=>'yahoo','code'=>'f19e'],
['name'=>'google','code'=>'f1a0'],
['name'=>'reddit','code'=>'f1a1'],
['name'=>'reddit-square','code'=>'f1a2'],
['name'=>'stumbleupon-circle','code'=>'f1a3'],
['name'=>'stumbleupon','code'=>'f1a4'],
['name'=>'delicious','code'=>'f1a5'],
['name'=>'digg','code'=>'f1a6'],
['name'=>'pied-piper','code'=>'f1a7'],
['name'=>'pied-piper-alt','code'=>'f1a8'],
['name'=>'drupal','code'=>'f1a9'],
['name'=>'joomla','code'=>'f1aa'],
['name'=>'language','code'=>'f1ab'],
['name'=>'fax','code'=>'f1ac'],
['name'=>'building','code'=>'f1ad'],
['name'=>'child','code'=>'f1ae'],
['name'=>'paw','code'=>'f1b0'],
['name'=>'spoon','code'=>'f1b1'],
['name'=>'cube','code'=>'f1b2'],
['name'=>'cubes','code'=>'f1b3'],
['name'=>'behance','code'=>'f1b4'],
['name'=>'behance-square','code'=>'f1b5'],
['name'=>'steam','code'=>'f1b6'],
['name'=>'steam-square','code'=>'f1b7'],
['name'=>'recycle','code'=>'f1b8'],
['name'=>'car','code'=>'f1b9'],
['name'=>'taxi','code'=>'f1ba'],
['name'=>'tree','code'=>'f1bb'],
['name'=>'spotify','code'=>'f1bc'],
['name'=>'deviantart','code'=>'f1bd'],
['name'=>'soundcloud','code'=>'f1be'],
['name'=>'database','code'=>'f1c0'],
['name'=>'file-pdf-o','code'=>'f1c1'],
['name'=>'file-word-o','code'=>'f1c2'],
['name'=>'file-excel-o','code'=>'f1c3'],
['name'=>'file-powerpoint-o','code'=>'f1c4'],
['name'=>'file-image-o','code'=>'f1c5'],
['name'=>'file-archive-o','code'=>'f1c6'],
['name'=>'file-audio-o','code'=>'f1c7'],
['name'=>'file-video-o','code'=>'f1c8'],
['name'=>'file-code-o','code'=>'f1c9'],
['name'=>'vine','code'=>'f1ca'],
['name'=>'codepen','code'=>'f1cb'],
['name'=>'jsfiddle','code'=>'f1cc'],
['name'=>'life-ring','code'=>'f1cd'],
['name'=>'circle-o-notch','code'=>'f1ce'],
['name'=>'rebel','code'=>'f1d0'],
['name'=>'empire','code'=>'f1d1'],
['name'=>'git-square','code'=>'f1d2'],
['name'=>'git','code'=>'f1d3'],
['name'=>'hacker-news','code'=>'f1d4'],
['name'=>'tencent-weibo','code'=>'f1d5'],
['name'=>'qq','code'=>'f1d6'],
['name'=>'weixin','code'=>'f1d7'],
['name'=>'paper-plane','code'=>'f1d8'],
['name'=>'paper-plane-o','code'=>'f1d9'],
['name'=>'history','code'=>'f1da'],
['name'=>'circle-thin','code'=>'f1db'],
['name'=>'header','code'=>'f1dc'],
['name'=>'paragraph','code'=>'f1dd'],
['name'=>'sliders','code'=>'f1de'],
['name'=>'share-alt','code'=>'f1e0'],
['name'=>'share-alt-square','code'=>'f1e1'],
['name'=>'bomb','code'=>'f1e2'],
['name'=>'futbol-o','code'=>'f1e3'],
['name'=>'tty','code'=>'f1e4'],
['name'=>'binoculars','code'=>'f1e5'],
['name'=>'plug','code'=>'f1e6'],
['name'=>'slideshare','code'=>'f1e7'],
['name'=>'twitch','code'=>'f1e8'],
['name'=>'yelp','code'=>'f1e9'],
['name'=>'newspaper-o','code'=>'f1ea'],
['name'=>'wifi','code'=>'f1eb'],
['name'=>'calculator','code'=>'f1ec'],
['name'=>'paypal','code'=>'f1ed'],
['name'=>'google-wallet','code'=>'f1ee'],
['name'=>'cc-visa','code'=>'f1f0'],
['name'=>'cc-mastercard','code'=>'f1f1'],
['name'=>'cc-discover','code'=>'f1f2'],
['name'=>'cc-amex','code'=>'f1f3'],
['name'=>'cc-paypal','code'=>'f1f4'],
['name'=>'cc-stripe','code'=>'f1f5'],
['name'=>'bell-slash','code'=>'f1f6'],
['name'=>'bell-slash-o','code'=>'f1f7'],
['name'=>'trash','code'=>'f1f8'],
['name'=>'copyright','code'=>'f1f9'],
['name'=>'at','code'=>'f1fa'],
['name'=>'eyedropper','code'=>'f1fb'],
['name'=>'paint-brush','code'=>'f1fc'],
['name'=>'birthday-cake','code'=>'f1fd'],
['name'=>'area-chart','code'=>'f1fe'],
['name'=>'pie-chart','code'=>'f200'],
['name'=>'line-chart','code'=>'f201'],
['name'=>'lastfm','code'=>'f202'],
['name'=>'lastfm-square','code'=>'f203'],
['name'=>'toggle-off','code'=>'f204'],
['name'=>'toggle-on','code'=>'f205'],
['name'=>'bicycle','code'=>'f206'],
['name'=>'bus','code'=>'f207'],
['name'=>'ioxhost','code'=>'f208'],
['name'=>'angellist','code'=>'f209'],
['name'=>'cc','code'=>'f20a'],
['name'=>'ils','code'=>'f20b'],
['name'=>'meanpath','code'=>'f20c'],
['name'=>'buysellads','code'=>'f20d'],
['name'=>'connectdevelop','code'=>'f20e'],
['name'=>'dashcube','code'=>'f210'],
['name'=>'forumbee','code'=>'f211'],
['name'=>'leanpub','code'=>'f212'],
['name'=>'sellsy','code'=>'f213'],
['name'=>'shirtsinbulk','code'=>'f214'],
['name'=>'simplybuilt','code'=>'f215'],
['name'=>'skyatlas','code'=>'f216'],
['name'=>'cart-plus','code'=>'f217'],
['name'=>'cart-arrow-down','code'=>'f218'],
['name'=>'diamond','code'=>'f219'],
['name'=>'ship','code'=>'f21a'],
['name'=>'user-secret','code'=>'f21b'],
['name'=>'motorcycle','code'=>'f21c'],
['name'=>'street-view','code'=>'f21d'],
['name'=>'heartbeat','code'=>'f21e'],
['name'=>'venus','code'=>'f221'],
['name'=>'mars','code'=>'f222'],
['name'=>'mercury','code'=>'f223'],
['name'=>'transgender','code'=>'f224'],
['name'=>'transgender-alt','code'=>'f225'],
['name'=>'venus-double','code'=>'f226'],
['name'=>'mars-double','code'=>'f227'],
['name'=>'venus-mars','code'=>'f228'],
['name'=>'mars-stroke','code'=>'f229'],
['name'=>'mars-stroke-v','code'=>'f22a'],
['name'=>'mars-stroke-h','code'=>'f22b'],
['name'=>'neuter','code'=>'f22c'],
['name'=>'genderless','code'=>'f22d'],
['name'=>'facebook-official','code'=>'f230'],
['name'=>'pinterest-p','code'=>'f231'],
['name'=>'whatsapp','code'=>'f232'],
['name'=>'server','code'=>'f233'],
['name'=>'user-plus','code'=>'f234'],
['name'=>'user-times','code'=>'f235'],
['name'=>'bed','code'=>'f236'],
['name'=>'viacoin','code'=>'f237'],
['name'=>'train','code'=>'f238'],
['name'=>'subway','code'=>'f239'],
['name'=>'medium','code'=>'f23a'],
['name'=>'y-combinator','code'=>'f23b'],
['name'=>'optin-monster','code'=>'f23c'],
['name'=>'opencart','code'=>'f23d'],
['name'=>'expeditedssl','code'=>'f23e'],
['name'=>'battery-full','code'=>'f240'],
['name'=>'battery-three-quarters','code'=>'f241'],
['name'=>'battery-half','code'=>'f242'],
['name'=>'battery-quarter','code'=>'f243'],
['name'=>'battery-empty','code'=>'f244'],
['name'=>'mouse-pointer','code'=>'f245'],
['name'=>'i-cursor','code'=>'f246'],
['name'=>'object-group','code'=>'f247'],
['name'=>'object-ungroup','code'=>'f248'],
['name'=>'sticky-note','code'=>'f249'],
['name'=>'sticky-note-o','code'=>'f24a'],
['name'=>'cc-jcb','code'=>'f24b'],
['name'=>'cc-diners-club','code'=>'f24c'],
['name'=>'clone','code'=>'f24d'],
['name'=>'balance-scale','code'=>'f24e'],
['name'=>'hourglass-o','code'=>'f250'],
['name'=>'hourglass-start','code'=>'f251'],
['name'=>'hourglass-half','code'=>'f252'],
['name'=>'hourglass-end','code'=>'f253'],
['name'=>'hourglass','code'=>'f254'],
['name'=>'hand-rock-o','code'=>'f255'],
['name'=>'hand-paper-o','code'=>'f256'],
['name'=>'hand-scissors-o','code'=>'f257'],
['name'=>'hand-lizard-o','code'=>'f258'],
['name'=>'hand-spock-o','code'=>'f259'],
['name'=>'hand-pointer-o','code'=>'f25a'],
['name'=>'hand-peace-o','code'=>'f25b'],
['name'=>'trademark','code'=>'f25c'],
['name'=>'registered','code'=>'f25d'],
['name'=>'creative-commons','code'=>'f25e'],
['name'=>'gg','code'=>'f260'],
['name'=>'gg-circle','code'=>'f261'],
['name'=>'tripadvisor','code'=>'f262'],
['name'=>'odnoklassniki','code'=>'f263'],
['name'=>'odnoklassniki-square','code'=>'f264'],
['name'=>'get-pocket','code'=>'f265'],
['name'=>'wikipedia-w','code'=>'f266'],
['name'=>'safari','code'=>'f267'],
['name'=>'chrome','code'=>'f268'],
['name'=>'firefox','code'=>'f269'],
['name'=>'opera','code'=>'f26a'],
['name'=>'internet-explorer','code'=>'f26b'],
['name'=>'television','code'=>'f26c'],
['name'=>'contao','code'=>'f26d'],
['name'=>'500px','code'=>'f26e'],
['name'=>'amazon','code'=>'f270'],
['name'=>'calendar-plus-o','code'=>'f271'],
['name'=>'calendar-minus-o','code'=>'f272'],
['name'=>'calendar-times-o','code'=>'f273'],
['name'=>'calendar-check-o','code'=>'f274'],
['name'=>'industry','code'=>'f275'],
['name'=>'map-pin','code'=>'f276'],
['name'=>'map-signs','code'=>'f277'],
['name'=>'map-o','code'=>'f278'],
['name'=>'map','code'=>'f279'],
['name'=>'commenting','code'=>'f27a'],
['name'=>'commenting-o','code'=>'f27b'],
['name'=>'houzz','code'=>'f27c'],
['name'=>'vimeo','code'=>'f27d'],
['name'=>'black-tie','code'=>'f27e'],
['name'=>'fonticons','code'=>'f280'],
];
foreach ($icons as $icon) {
$data[$icon['name']] = '&#x' . $icon['code'] . ' ' . ucwords(str_replace('-', ' ', $icon['name']));
}
ksort($data);
return $data;
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateProposalTemplateRequest;
use App\Http\Requests\ProposalTemplateRequest;
use App\Http\Requests\UpdateProposalTemplateRequest;
use App\Models\Invoice;
use App\Models\ProposalTemplate;
use App\Ninja\Datatables\ProposalTemplateDatatable;
use App\Ninja\Repositories\ProposalTemplateRepository;
use App\Services\ProposalTemplateService;
use Auth;
use Input;
use Session;
use View;
class ProposalTemplateController extends BaseController
{
protected $proposalTemplateRepo;
protected $proposalTemplateService;
protected $entityType = ENTITY_PROPOSAL_TEMPLATE;
public function __construct(ProposalTemplateRepository $proposalTemplateRepo, ProposalTemplateService $proposalTemplateService)
{
$this->proposalTemplateRepo = $proposalTemplateRepo;
$this->proposalTemplateService = $proposalTemplateService;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list_wrapper', [
'entityType' => ENTITY_PROPOSAL_TEMPLATE,
'datatable' => new ProposalTemplateDatatable(),
'title' => trans('texts.proposal_templates'),
]);
}
public function getDatatable($expensePublicId = null)
{
$search = Input::get('sSearch');
$userId = Auth::user()->filterId();
return $this->proposalTemplateService->getDatatable($search, $userId);
}
public function create(ProposalTemplateRequest $request)
{
$data = array_merge($this->getViewmodel(), [
'template' => null,
'method' => 'POST',
'url' => 'proposals/templates',
'title' => trans('texts.new_proposal_template'),
]);
return View::make('proposals/templates/edit', $data);
}
private function getViewmodel()
{
$customTemplates = ProposalTemplate::scope()->orderBy('name')->get();
$defaultTemplates = ProposalTemplate::whereNull('account_id')->orderBy('public_id')->get();
$options = [];
$customLabel = trans('texts.custom');
$defaultLabel = trans('texts.default');
foreach ($customTemplates as $template) {
if (! isset($options[$customLabel])) {
$options[$customLabel] = [];
}
$options[trans('texts.custom')][$template->public_id] = $template->name;
}
foreach ($defaultTemplates as $template) {
if (! isset($options[$defaultLabel])) {
$options[$defaultLabel] = [];
}
$options[trans('texts.default')][$template->public_id] = $template->name;
}
$data = [
'account' => auth()->user()->account,
'customTemplates' => $customTemplates,
'defaultTemplates' => $defaultTemplates,
'templateOptions' => $options,
];
return $data;
}
public function show($publicId)
{
Session::reflash();
return redirect("proposals/templates/$publicId/edit");
}
public function edit(ProposalTemplateRequest $request, $publicId = false, $clone = false)
{
$template = $request->entity();
if ($clone) {
$template->id = null;
$template->public_id = null;
$template->name = '';
$template->private_notes = '';
$method = 'POST';
$url = 'proposals/templates';
} else {
$method = 'PUT';
$url = 'proposals/templates/' . $template->public_id;
}
$data = array_merge($this->getViewmodel(), [
'template' => $template,
'entity' => $clone ? false : $template,
'method' => $method,
'url' => $url,
'title' => trans('texts.edit_proposal_template'),
]);
return View::make('proposals/templates/edit', $data);
}
public function cloneProposal(ProposalTemplateRequest $request, $publicId)
{
return self::edit($request, $publicId, true);
}
public function store(CreateProposalTemplateRequest $request)
{
$proposalTemplate = $this->proposalTemplateService->save($request->input());
Session::flash('message', trans('texts.created_proposal_template'));
return redirect()->to($proposalTemplate->getRoute());
}
public function update(UpdateProposalTemplateRequest $request)
{
$proposalTemplate = $this->proposalTemplateService->save($request->input(), $request->entity());
Session::flash('message', trans('texts.updated_proposal_template'));
$action = Input::get('action');
if (in_array($action, ['archive', 'delete', 'restore'])) {
return self::bulk();
}
return redirect()->to($proposalTemplate->getRoute());
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->proposalTemplateService->bulk($ids, $action);
if ($count > 0) {
$field = $count == 1 ? "{$action}d_proposal_template" : "{$action}d_proposal_templates";
$message = trans("texts.$field", ['count' => $count]);
Session::flash('message', $message);
}
return redirect()->to('/proposals/templates');
}
}

View File

@ -97,7 +97,7 @@ class QuoteController extends BaseController
return [ return [
'entityType' => ENTITY_QUOTE, 'entityType' => ENTITY_QUOTE,
'account' => $account, 'account' => Auth::user()->account->load('country'),
'products' => Product::scope()->orderBy('product_key')->get(), 'products' => Product::scope()->orderBy('product_key')->get(),
'taxRateOptions' => $account->present()->taxRateOptions, 'taxRateOptions' => $account->present()->taxRateOptions,
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(), 'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(),
@ -148,6 +148,11 @@ class QuoteController extends BaseController
{ {
$invitation = Invitation::with('invoice.invoice_items', 'invoice.invitations')->where('invitation_key', '=', $invitationKey)->firstOrFail(); $invitation = Invitation::with('invoice.invoice_items', 'invoice.invitations')->where('invitation_key', '=', $invitationKey)->firstOrFail();
$invoice = $invitation->invoice; $invoice = $invitation->invoice;
$account = $invoice->account;
if ($account->requiresAuthorization($invoice) && ! session('authorized:' . $invitation->invitation_key)) {
return redirect()->to('view/' . $invitation->invitation_key);
}
if ($invoice->due_date) { if ($invoice->due_date) {
$carbonDueDate = \Carbon::parse($invoice->due_date); $carbonDueDate = \Carbon::parse($invoice->due_date);

View File

@ -75,6 +75,7 @@ class ReportController extends BaseController
'activity', 'activity',
'aging', 'aging',
'client', 'client',
'credit',
'document', 'document',
'expense', 'expense',
'invoice', 'invoice',

View File

@ -134,6 +134,7 @@ class SubscriptionController extends BaseController
$subscription = Subscription::scope($subscriptionPublicId)->firstOrFail(); $subscription = Subscription::scope($subscriptionPublicId)->firstOrFail();
} else { } else {
$subscription = Subscription::createNew(); $subscription = Subscription::createNew();
$subscriptionPublicId = $subscription->public_id;
} }
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);
@ -154,6 +155,14 @@ class SubscriptionController extends BaseController
Session::flash('message', $message); Session::flash('message', $message);
} }
return Redirect::to('settings/' . ACCOUNT_API_TOKENS); return redirect('/settings/api_tokens');
/*
if ($subscriptionPublicId) {
return Redirect::to('subscriptions/' . $subscriptionPublicId . '/edit');
} else {
return redirect('/settings/api_tokens');
}
*/
} }
} }

View File

@ -55,8 +55,16 @@ class TaskKanbanController extends BaseController
$task->task_status_sort_order = $i++; $task->task_status_sort_order = $i++;
$task->save(); $task->save();
} }
// otherwise, check that the tasks orders are correct // otherwise, check that the orders are correct
} else { } else {
for ($i=0; $i<$statuses->count(); $i++) {
$status = $statuses[$i];
if ($status->sort_order != $i) {
$status->sort_order = $i;
$status->save();
}
}
$firstStatus = $statuses[0]; $firstStatus = $statuses[0];
$counts = []; $counts = [];
foreach ($tasks as $task) { foreach ($tasks as $task) {

View File

@ -118,7 +118,7 @@ class UserController extends BaseController
} }
if (! Auth::user()->confirmed) { if (! Auth::user()->confirmed) {
Session::flash('error', trans('texts.confirmation_required')); Session::flash('error', trans('texts.confirmation_required', ['link' => link_to('/resend_confirmation', trans('texts.click_here'))]));
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
} }

View File

@ -100,8 +100,8 @@ class ApiCheck
return Response::json("Please wait {$wait} second(s)", 403, $headers); return Response::json("Please wait {$wait} second(s)", 403, $headers);
} }
Cache::put("hour_throttle:{$key}", $new_hour_throttle, 10); Cache::put("hour_throttle:{$key}", $new_hour_throttle, 60);
Cache::put("last_api_request:{$key}", time(), 10); Cache::put("last_api_request:{$key}", time(), 60);
} }
return $next($request); return $next($request);

View File

@ -4,6 +4,7 @@ namespace App\Http\Middleware;
use App\Models\Contact; use App\Models\Contact;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\ProposalInvitation;
use Auth; use Auth;
use Closure; use Closure;
use Session; use Session;
@ -25,13 +26,14 @@ class Authenticate
public function handle($request, Closure $next, $guard = 'user') public function handle($request, Closure $next, $guard = 'user')
{ {
$authenticated = Auth::guard($guard)->check(); $authenticated = Auth::guard($guard)->check();
$invitationKey = $request->invitation_key ?: $request->proposal_invitation_key;
if ($guard == 'client') { if ($guard == 'client') {
if (! empty($request->invitation_key)) { if (! empty($request->invitation_key) || ! empty($request->proposal_invitation_key)) {
$contact_key = session('contact_key'); $contact_key = session('contact_key');
if ($contact_key) { if ($contact_key) {
$contact = $this->getContact($contact_key); $contact = $this->getContact($contact_key);
$invitation = $this->getInvitation($request->invitation_key); $invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key));
if (! $invitation) { if (! $invitation) {
return response()->view('error', [ return response()->view('error', [
@ -59,7 +61,7 @@ class Authenticate
$contact = false; $contact = false;
if ($contact_key) { if ($contact_key) {
$contact = $this->getContact($contact_key); $contact = $this->getContact($contact_key);
} elseif ($invitation = $this->getInvitation($request->invitation_key)) { } elseif ($invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key))) {
$contact = $invitation->contact; $contact = $invitation->contact;
Session::put('contact_key', $contact->contact_key); Session::put('contact_key', $contact->contact_key);
} }
@ -89,6 +91,7 @@ class Authenticate
if ($authenticated) { if ($authenticated) {
$request->merge(['contact' => $contact]); $request->merge(['contact' => $contact]);
$account->loadLocalizationSettings($contact->client);
} }
} }
@ -108,7 +111,7 @@ class Authenticate
* *
* @return \Illuminate\Database\Eloquent\Model|null|static * @return \Illuminate\Database\Eloquent\Model|null|static
*/ */
protected function getInvitation($key) protected function getInvitation($key, $isProposal = false)
{ {
if (! $key) { if (! $key) {
return false; return false;
@ -118,7 +121,12 @@ class Authenticate
list($key) = explode('&', $key); list($key) = explode('&', $key);
$key = substr($key, 0, RANDOM_KEY_LENGTH); $key = substr($key, 0, RANDOM_KEY_LENGTH);
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first(); if ($isProposal) {
$invitation = ProposalInvitation::withTrashed()->where('invitation_key', '=', $key)->first();
} else {
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
}
if ($invitation && ! $invitation->is_deleted) { if ($invitation && ! $invitation->is_deleted) {
return $invitation; return $invitation;
} else { } else {

View File

@ -7,6 +7,7 @@ use Closure;
use App\Models\LookupAccount; use App\Models\LookupAccount;
use App\Models\LookupContact; use App\Models\LookupContact;
use App\Models\LookupInvitation; use App\Models\LookupInvitation;
use App\Models\LookupProposalInvitation;
use App\Models\LookupAccountToken; use App\Models\LookupAccountToken;
use App\Models\LookupUser; use App\Models\LookupUser;
use Auth; use Auth;
@ -43,6 +44,8 @@ class DatabaseLookup
} elseif ($guard == 'contact') { } elseif ($guard == 'contact') {
if ($key = request()->invitation_key) { if ($key = request()->invitation_key) {
LookupInvitation::setServerByField('invitation_key', $key); LookupInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->proposal_invitation_key) {
LookupProposalInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->contact_key ?: session('contact_key')) { } elseif ($key = request()->contact_key ?: session('contact_key')) {
LookupContact::setServerByField('contact_key', $key); LookupContact::setServerByField('contact_key', $key);
} elseif ($key = request()->account_key) { } elseif ($key = request()->account_key) {

View File

@ -36,8 +36,13 @@ class StartupCheck
// Set up trusted X-Forwarded-Proto proxies // Set up trusted X-Forwarded-Proto proxies
// TRUSTED_PROXIES accepts a comma delimited list of subnets // TRUSTED_PROXIES accepts a comma delimited list of subnets
// ie, TRUSTED_PROXIES='10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' // ie, TRUSTED_PROXIES='10.0.0.0/8,172.16.0.0/12,192.168.0.0/16'
// set TRUSTED_PROXIES=* if you want to trust every proxy.
if (isset($_ENV['TRUSTED_PROXIES'])) { if (isset($_ENV['TRUSTED_PROXIES'])) {
$request->setTrustedProxies(array_map('trim', explode(',', env('TRUSTED_PROXIES')))); if (env('TRUSTED_PROXIES') == '*') {
$request->setTrustedProxies(['127.0.0.1', $request->server->get('REMOTE_ADDR')]);
} else{
$request->setTrustedProxies(array_map('trim', explode(',', env('TRUSTED_PROXIES'))));
}
} }
// Ensure all request are over HTTPS in production // Ensure all request are over HTTPS in production
@ -218,7 +223,7 @@ class StartupCheck
// Show message to IE 8 and before users // Show message to IE 8 and before users
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT'])) { if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT'])) {
Session::flash('error', trans('texts.old_browser', ['link' => OUTDATE_BROWSER_URL])); Session::flash('error', trans('texts.old_browser', ['link' => link_to(OUTDATE_BROWSER_URL, trans('texts.newer_browser'), ['target' => '_blank'])]));
} }
$response = $next($request); $response = $next($request);

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use App\Models\Invoice;
class CreatePaymentTermAPIRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_PAYMENT_TERM);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = [
'num_days' => 'required|numeric|unique:payment_terms',
];
return $rules;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class CreateProposalCategoryRequest extends ProposalCategoryRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_PROPOSAL_CATEGORY);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => sprintf('required|unique:proposal_categories,name,,id,account_id,%s', $this->user()->account_id),
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class CreateProposalRequest extends ProposalRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_PROPOSAL);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'invoice_id' => 'required',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class CreateProposalSnippetRequest extends ProposalSnippetRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_PROPOSAL_SNIPPET);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => sprintf('required|unique:proposal_snippets,name,,id,account_id,%s', $this->user()->account_id),
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class CreateProposalTemplateRequest extends ProposalTemplateRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_PROPOSAL_TEMPLATE);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => sprintf('required|unique:proposal_templates,name,,id,account_id,%s', $this->user()->account_id),
];
}
}

View File

@ -35,6 +35,7 @@ class EntityRequest extends Request
if (! $publicId) { if (! $publicId) {
$publicId = Input::get('public_id') ?: Input::get('id'); $publicId = Input::get('public_id') ?: Input::get('id');
} }
if (! $publicId) { if (! $publicId) {
return null; return null;
} }

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class PaymentTermRequest extends EntityRequest
{
protected $entityType = ENTITY_PAYMENT_TERM;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ProposalCategoryRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL_CATEGORY;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ProposalRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL;
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use App\Models\ProposalCategory;
class ProposalSnippetRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL_SNIPPET;
public function sanitize()
{
$input = $this->all();
// check if we're creating a new proposal category
if ($this->proposal_category_id == '-1') {
$data = [
'name' => trim($this->proposal_category_name)
];
if (ProposalCategory::validate($data) === true) {
$category = app('App\Ninja\Repositories\ProposalCategoryRepository')->save($data);
$input['proposal_category_id'] = $category->id;
} else {
$input['proposal_category_id'] = null;
}
} elseif ($this->proposal_category_id) {
$input['proposal_category_id'] = ProposalCategory::getPrivateId($this->proposal_category_id);
}
$this->replace($input);
return $this->all();
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ProposalTemplateRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL_TEMPLATE;
}

View File

@ -34,7 +34,7 @@ class UpdateInvoiceAPIRequest extends InvoiceRequest
$invoiceId = $this->entity()->id; $invoiceId = $this->entity()->id;
$rules = [ $rules = [
'invoice_items' => 'valid_invoice_items', 'invoice_items' => 'required|valid_invoice_items',
'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
//'invoice_date' => 'date', //'invoice_date' => 'date',

View File

@ -31,7 +31,7 @@ class UpdateInvoiceRequest extends InvoiceRequest
$rules = [ $rules = [
'client' => 'required', 'client' => 'required',
'invoice_items' => 'valid_invoice_items', 'invoice_items' => 'required|valid_invoice_items',
'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
'invoice_date' => 'required', 'invoice_date' => 'required',

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Requests;
class UpdatePaymentTermRequest extends EntityRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
class UpdateProposalCategoryRequest extends ProposalCategoryRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
return [
'name' => sprintf('required|unique:proposal_categories,name,,id,account_id,%s', $this->user()->account_id),
];
}
}

View File

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

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
class UpdateProposalSnippetRequest extends ProposalSnippetRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
return [
'name' => sprintf('required|unique:proposal_snippets,name,%s,id,account_id,%s', $this->entity()->id, $this->user()->account_id),
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
class UpdateProposalTemplateRequest extends ProposalTemplateRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
return [
'name' => sprintf('required|unique:proposal_templates,name,%s,id,account_id,%s', $this->entity()->id, $this->user()->account_id),
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\ViewComposers;
use Illuminate\View\View;
use App\Models\ProposalSnippet;
use App\Models\Document;
/**
* ClientPortalHeaderComposer.php.
*
* @copyright See LICENSE file that was distributed with this source code.
*/
class ProposalComposer
{
/**
* Bind data to the view.
*
* @param View $view
*
* @return void
*/
public function compose(View $view)
{
$snippets = ProposalSnippet::scope()
->with('proposal_category')
->orderBy('name')
->get();
$view->with('snippets', $snippets);
$documents = Document::scope()
->whereNull('invoice_id')
->whereNull('expense_id')
->get();
$data = [];
foreach ($documents as $document) {
$data[] = [
'src' => $document->getProposalUrl(),
'public_id' => $document->public_id,
];
}
$view->with('documents', $data);
}
}

View File

@ -0,0 +1,157 @@
<?php
namespace App\Jobs;
use App\Jobs\Job;
use CleverIt\UBL\Invoice\Generator;
use CleverIt\UBL\Invoice\Invoice;
use CleverIt\UBL\Invoice\Party;
use CleverIt\UBL\Invoice\Address;
use CleverIt\UBL\Invoice\Country;
use CleverIt\UBL\Invoice\Contact;
use CleverIt\UBL\Invoice\TaxTotal;
use CleverIt\UBL\Invoice\TaxSubTotal;
use CleverIt\UBL\Invoice\TaxCategory;
use CleverIt\UBL\Invoice\TaxScheme;
use CleverIt\UBL\Invoice\InvoiceLine;
use CleverIt\UBL\Invoice\Item;
use CleverIt\UBL\Invoice\LegalMonetaryTotal;
class ConvertInvoiceToUbl extends Job
{
const INVOICE_TYPE_STANDARD = 380;
const INVOICE_TYPE_CREDIT = 381;
public function __construct($invoice)
{
$this->invoice = $invoice;
}
public function handle()
{
$invoice = $this->invoice;
$account = $invoice->account;
$client = $invoice->client;
$ublInvoice = new Invoice();
// invoice
$ublInvoice->setId($invoice->invoice_number);
$ublInvoice->setIssueDate(date_create($invoice->invoice_date));
$ublInvoice->setInvoiceTypeCode($invoice->amount < 0 ? self::INVOICE_TYPE_CREDIT : self::INVOICE_TYPE_STANDARD);
$supplierParty = $this->createParty($account, $invoice->user);
$ublInvoice->setAccountingSupplierParty($supplierParty);
$customerParty = $this->createParty($client, $client->contacts[0]);
$ublInvoice->setAccountingCustomerParty($customerParty);
// line items
$invoiceLine = [];
$taxable = $invoice->getTaxable();
foreach ($invoice->invoice_items as $index => $item) {
$itemTaxable = $invoice->getItemTaxable($item, $taxable);
$item->setRelation('invoice', $invoice);
$invoiceLines[] = $this->createInvoiceLine($index, $item, $itemTaxable);
}
$ublInvoice->setInvoiceLines($invoiceLines);
if ($invoice->hasTaxes()) {
$taxtotal = new TaxTotal();
$taxAmount1 = $taxAmount2 = 0;
if ($invoice->tax_name1 || floatval($invoice->tax_rate1)) {
$taxAmount1 = $this->createTaxRate($taxtotal, $taxable, $invoice->tax_rate1, $invoice->tax_name1);
}
if ($invoice->tax_name2 || floatval($invoice->tax_rate2)) {
$taxAmount2 = $this->createTaxRate($taxtotal, $taxable, $invoice->tax_rate2, $invoice->tax_name2);
}
$taxtotal->setTaxAmount($taxAmount1 + $taxAmount2);
$ublInvoice->setTaxTotal($taxtotal);
}
$ublInvoice->setLegalMonetaryTotal((new LegalMonetaryTotal())
//->setLineExtensionAmount()
->setTaxExclusiveAmount($taxable)
->setPayableAmount($invoice->balance));
return Generator::invoice($ublInvoice, $invoice->client->getCurrencyCode());
}
private function createParty($company, $user)
{
$party = new Party();
$party->setName($company->name);
$address = (new Address())
->setCityName($company->city)
->setStreetName($company->address1)
->setBuildingNumber($company->address2)
->setPostalZone($company->postal_code);
if ($company->country_id) {
$country = new Country();
$country->setIdentificationCode($company->country->iso_3166_2);
$address->setCountry($country);
}
$party->setPostalAddress($address);
$party->setPhysicalLocation($address);
$contact = new Contact();
$contact->setElectronicMail($user->email);
$party->setContact($contact);
return $party;
}
private function createInvoiceLine($index, $item, $taxable)
{
$invoiceLine = (new InvoiceLine())
->setId($index + 1)
->setInvoicedQuantity($item->qty)
->setLineExtensionAmount($item->costWithDiscount())
->setItem((new Item())
->setName($item->product_key)
->setDescription($item->description));
//->setSellersItemIdentification("1ABCD"));
if ($item->hasTaxes()) {
$taxtotal = new TaxTotal();
$itemTaxAmount1 = $itemTaxAmount2 = 0;
if ($item->tax_name1 || floatval($item->tax_rate1)) {
$itemTaxAmount1 = $this->createTaxRate($taxtotal, $taxable, $item->tax_rate1, $item->tax_name1);
}
if ($item->tax_name2 || floatval($item->tax_rate2)) {
$itemTaxAmount2 = $this->createTaxRate($taxtotal, $taxable, $item->tax_rate2, $item->tax_name2);
}
$taxtotal->setTaxAmount($itemTaxAmount1 + $itemTaxAmount2);
$invoiceLine->setTaxTotal($taxtotal);
}
return $invoiceLine;
}
private function createTaxRate(&$taxtotal, $taxable, $taxRate, $taxName)
{
$invoice = $this->invoice;
$taxAmount = $invoice->taxAmount($taxable, $taxRate);
$taxScheme = ((new TaxScheme()))->setId($taxName);
$taxtotal->addTaxSubTotal((new TaxSubTotal())
->setTaxAmount($taxAmount)
->setTaxableAmount($taxable)
->setTaxCategory((new TaxCategory())
->setId($taxName)
->setName($taxName)
->setTaxScheme($taxScheme)
->setPercent($taxRate)));
return $taxAmount;
}
}

View File

@ -46,6 +46,10 @@ class DownloadInvoices extends Job
*/ */
public function handle(UserMailer $userMailer) public function handle(UserMailer $userMailer)
{ {
if (! extension_loaded('GMP')) {
die(trans('texts.gmp_required'));
}
$zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoice_pdfs'))); $zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoice_pdfs')));
foreach ($this->invoices as $invoice) { foreach ($this->invoices as $invoice) {
@ -54,34 +58,5 @@ class DownloadInvoices extends Job
$zip->finish(); $zip->finish();
exit; exit;
/*
// if queues are disabled download a zip file
if (config('queue.default') === 'sync' || count($this->invoices) <= 10) {
$zip = Archive::instance_by_useragent(date('Y-m-d') . '-Invoice_PDFs');
foreach ($this->invoices as $invoice) {
$zip->add_file($invoice->getFileName(), $invoice->getPDFString());
}
$zip->finish();
exit;
// otherwise sends the PDFs in an email
} else {
$data = [];
foreach ($this->invoices as $invoice) {
$data[] = [
'name' => $invoice->getFileName(),
'data' => $invoice->getPDFString(),
];
}
$subject = trans('texts.invoices_are_attached');
$data = [
'documents' => $data
];
$userMailer->sendMessage($this->user, $subject, false, $data);
}
*/
} }
} }

View File

@ -50,6 +50,11 @@ class PurgeAccountData extends Job
'vendors', 'vendors',
'contacts', 'contacts',
'clients', 'clients',
'proposals',
'proposal_templates',
'proposal_snippets',
'proposal_categories',
'proposal_invitations',
]; ];
foreach ($tables as $table) { foreach ($tables as $table) {
@ -71,6 +76,7 @@ class PurgeAccountData extends Job
$lookupAccount = LookupAccount::whereAccountKey($account->account_key)->firstOrFail(); $lookupAccount = LookupAccount::whereAccountKey($account->account_key)->firstOrFail();
DB::table('lookup_contacts')->where('lookup_account_id', '=', $lookupAccount->id)->delete(); DB::table('lookup_contacts')->where('lookup_account_id', '=', $lookupAccount->id)->delete();
DB::table('lookup_invitations')->where('lookup_account_id', '=', $lookupAccount->id)->delete(); DB::table('lookup_invitations')->where('lookup_account_id', '=', $lookupAccount->id)->delete();
DB::table('lookup_proposal_invitations')->where('lookup_account_id', '=', $lookupAccount->id)->delete();
config(['database.default' => $current]); config(['database.default' => $current]);
} }

View File

@ -43,6 +43,11 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/ */
protected $server; protected $server;
/**
* @var Proposal
*/
protected $proposal;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -51,12 +56,13 @@ class SendInvoiceEmail extends Job implements ShouldQueue
* @param bool $reminder * @param bool $reminder
* @param mixed $pdfString * @param mixed $pdfString
*/ */
public function __construct(Invoice $invoice, $userId = false, $reminder = false, $template = false) public function __construct(Invoice $invoice, $userId = false, $reminder = false, $template = false, $proposal = false)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->userId = $userId; $this->userId = $userId;
$this->reminder = $reminder; $this->reminder = $reminder;
$this->template = $template; $this->template = $template;
$this->proposal = $proposal;
$this->server = config('database.default'); $this->server = config('database.default');
} }
@ -72,7 +78,7 @@ class SendInvoiceEmail extends Job implements ShouldQueue
Auth::onceUsingId($this->userId); Auth::onceUsingId($this->userId);
} }
$mailer->sendInvoice($this->invoice, $this->reminder, $this->template); $mailer->sendInvoice($this->invoice, $this->reminder, $this->template, $this->proposal);
if (App::runningInConsole() && $this->userId) { if (App::runningInConsole() && $this->userId) {
Auth::logout(); Auth::logout();

View File

@ -61,4 +61,17 @@ class HTMLUtils
return $previous; return $previous;
} }
} }
public static function getEnvForAccount($field, $default = '')
{
$key = '';
if ($user = auth()->user()) {
$key .= $user->account->id . '_';
}
$key .= $field;
return env($key, env($field, $default));
}
} }

View File

@ -12,6 +12,7 @@ class HistoryUtils
public static function loadHistory($users) public static function loadHistory($users)
{ {
$userIds = []; $userIds = [];
session([RECENTLY_VIEWED => false]);
if (is_array($users)) { if (is_array($users)) {
foreach ($users as $user) { foreach ($users as $user) {
@ -37,7 +38,7 @@ class HistoryUtils
ACTIVITY_TYPE_VIEW_QUOTE, ACTIVITY_TYPE_VIEW_QUOTE,
]; ];
$activities = Activity::with(['client.contacts', 'invoice', 'task', 'expense']) $activities = Activity::with(['client.contacts', 'invoice', 'task.project', 'expense'])
->whereIn('user_id', $userIds) ->whereIn('user_id', $userIds)
->whereIn('activity_type_id', $activityTypes) ->whereIn('activity_type_id', $activityTypes)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
@ -53,6 +54,12 @@ class HistoryUtils
continue; continue;
} }
$entity->setRelation('client', $activity->client); $entity->setRelation('client', $activity->client);
if ($entity->project) {
$project = $entity->project;
$project->setRelation('client', $activity->client);
static::trackViewed($project);
}
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_EXPENSE || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_EXPENSE) { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_EXPENSE || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_EXPENSE) {
$entity = $activity->expense; $entity = $activity->expense;
if (! $entity) { if (! $entity) {
@ -80,6 +87,8 @@ class HistoryUtils
ENTITY_QUOTE, ENTITY_QUOTE,
ENTITY_TASK, ENTITY_TASK,
ENTITY_EXPENSE, ENTITY_EXPENSE,
ENTITY_PROJECT,
ENTITY_PROPOSAL,
//ENTITY_RECURRING_EXPENSE, //ENTITY_RECURRING_EXPENSE,
]; ];
@ -87,6 +96,10 @@ class HistoryUtils
return; return;
} }
if ($entity->is_deleted) {
return;
}
$object = static::convertToObject($entity); $object = static::convertToObject($entity);
$history = Session::get(RECENTLY_VIEWED) ?: []; $history = Session::get(RECENTLY_VIEWED) ?: [];
$accountHistory = isset($history[$entity->account_id]) ? $history[$entity->account_id] : []; $accountHistory = isset($history[$entity->account_id]) ? $history[$entity->account_id] : [];
@ -135,6 +148,9 @@ class HistoryUtils
} elseif (method_exists($entity, 'client') && $entity->client) { } elseif (method_exists($entity, 'client') && $entity->client) {
$object->client_id = $entity->client->public_id; $object->client_id = $entity->client->public_id;
$object->client_name = $entity->client->getDisplayName(); $object->client_name = $entity->client->getDisplayName();
} elseif (method_exists($entity, 'invoice') && $entity->invoice) {
$object->client_id = $entity->invoice->client->public_id;
$object->client_name = $entity->invoice->client->getDisplayName();
} else { } else {
$object->client_id = 0; $object->client_id = 0;
$object->client_name = 0; $object->client_name = 0;
@ -175,7 +191,8 @@ class HistoryUtils
$button = ''; $button = '';
} }
$str .= sprintf('<li>%s<a href="%s"><div>%s %s</div></a></li>', $button, $link, $icon, $name); $padding = $str ? 16 : 0;
$str .= sprintf('<li style="margin-top: %spx">%s<a href="%s"><div>%s %s</div></a></li>', $padding, $button, $link, $icon, $name);
$lastClientId = $item->client_id; $lastClientId = $item->client_id;
} }

View File

@ -364,7 +364,9 @@ class Utils
if ($field == 'checkbox') { if ($field == 'checkbox') {
$data[] = $field; $data[] = $field;
} elseif ($field) { } elseif ($field) {
if ($module) { if (substr($field, 0, 1) == '-') {
$data[] = substr($field, 1);
} elseif ($module) {
$data[] = mtrans($module, $field); $data[] = mtrans($module, $field);
} else { } else {
$data[] = trans("texts.$field"); $data[] = trans("texts.$field");
@ -564,6 +566,10 @@ class Utils
if ($type === ENTITY_EXPENSE_CATEGORY) { if ($type === ENTITY_EXPENSE_CATEGORY) {
return 'expense_categories'; return 'expense_categories';
} elseif ($type === ENTITY_PROPOSAL_CATEGORY) {
return 'proposal_categories';
} elseif ($type === ENTITY_TASK_STATUS) {
return 'task_statuses';
} else { } else {
return $type . 's'; return $type . 's';
} }
@ -1087,6 +1093,25 @@ class Utils
} }
} }
public static function getCustomLabel($value)
{
if (strpos($value, '|') !== false) {
return explode('|', $value)[0];
} else {
return $value;
}
}
public static function getCustomValues($value)
{
if (strpos($value, '|') !== false) {
$values = explode(',', explode('|', $value)[1]);
return array_combine($values, $values);
} else {
return $value;
}
}
public static function formatWebsite($link) public static function formatWebsite($link)
{ {
if (! $link) { if (! $link) {
@ -1260,7 +1285,7 @@ class Utils
$tax1 = round($amount * $taxRate1 / 100, 2); $tax1 = round($amount * $taxRate1 / 100, 2);
$tax2 = round($amount * $taxRate2 / 100, 2); $tax2 = round($amount * $taxRate2 / 100, 2);
return round($amount + $tax1 + $tax2, 2); return round($tax1 + $tax2, 2);
} }
public static function roundSignificant($value, $precision = 2) { public static function roundSignificant($value, $precision = 2) {

View File

@ -2,6 +2,7 @@
namespace App\Listeners; namespace App\Listeners;
use App\Events\SubdomainWasRemoved;
use App\Events\SubdomainWasUpdated; use App\Events\SubdomainWasUpdated;
use App\Ninja\DNS\Cloudflare; use App\Ninja\DNS\Cloudflare;
@ -19,4 +20,11 @@ class DNSListener
if(env("CLOUDFLARE_DNS_ENABLED")) if(env("CLOUDFLARE_DNS_ENABLED"))
Cloudflare::addDNSRecord($event->account); Cloudflare::addDNSRecord($event->account);
} }
public function removeDNSRecord(SubdomainWasRemoved $event)
{
if(env("CLOUDFLARE_DNS_ENABLED"))
Cloudflare::removeDNSRecord($event->account);
}
} }

View File

@ -102,7 +102,7 @@ class HandleUserLoggedIn
if (in_array(config('app.key'), ['SomeRandomString', 'SomeRandomStringSomeRandomString', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'])) { if (in_array(config('app.key'), ['SomeRandomString', 'SomeRandomStringSomeRandomString', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'])) {
Session::flash('error', trans('texts.error_app_key_set_to_default')); Session::flash('error', trans('texts.error_app_key_set_to_default'));
} elseif (in_array($appCipher, ['MCRYPT_RIJNDAEL_256', 'MCRYPT_RIJNDAEL_128'])) { } elseif (in_array($appCipher, ['MCRYPT_RIJNDAEL_256', 'MCRYPT_RIJNDAEL_128'])) {
Session::flash('error', trans('texts.mcrypt_warning')); Session::flash('error', trans('texts.mcrypt_warning', ['command' => '<code>php artisan ninja:update-key --legacy=true</code>']));
} }
} }
} }

View File

@ -46,6 +46,8 @@ class HandleUserSettingsChanged
if ($event->user && $event->user->isEmailBeingChanged()) { if ($event->user && $event->user->isEmailBeingChanged()) {
$this->userMailer->sendConfirmation($event->user); $this->userMailer->sendConfirmation($event->user);
$this->userMailer->sendEmailChanged($event->user);
Session::flash('warning', trans('texts.verify_email')); Session::flash('warning', trans('texts.verify_email'));
} }
} }

View File

@ -254,19 +254,30 @@ class SubscriptionListener
return; return;
} }
// generate JSON data
$manager = new Manager(); $manager = new Manager();
$manager->setSerializer(new ArraySerializer()); $manager->setSerializer(new ArraySerializer());
$manager->parseIncludes($include); $manager->parseIncludes($include);
$resource = new Item($entity, $transformer, $entity->getEntityType()); $resource = new Item($entity, $transformer, $entity->getEntityType());
$data = $manager->createData($resource)->toArray(); $jsonData = $manager->createData($resource)->toArray();
// For legacy Zapier support // For legacy Zapier support
if (isset($data['client_id'])) { if (isset($jsonData['client_id'])) {
$data['client_name'] = $entity->client->getDisplayName(); $jsonData['client_name'] = $entity->client->getDisplayName();
} }
foreach ($subscriptions as $subscription) { foreach ($subscriptions as $subscription) {
switch ($subscription->format) {
case SUBSCRIPTION_FORMAT_JSON:
$data = $jsonData;
break;
case SUBSCRIPTION_FORMAT_UBL:
$data = $ublData;
break;
}
self::notifySubscription($subscription, $data); self::notifySubscription($subscription, $data);
} }
} }

View File

@ -146,6 +146,7 @@ class Account extends Eloquent
'invoice_fields', 'invoice_fields',
'invoice_embed_documents', 'invoice_embed_documents',
'document_email_attachment', 'document_email_attachment',
'ubl_email_attachment',
'enable_client_portal_dashboard', 'enable_client_portal_dashboard',
'page_size', 'page_size',
'live_preview', 'live_preview',
@ -237,6 +238,8 @@ class Account extends Eloquent
'hours', 'hours',
'id_number', 'id_number',
'invoice', 'invoice',
'invoice_date',
'invoice_number',
'item', 'item',
'line_total', 'line_total',
'outstanding', 'outstanding',
@ -245,6 +248,8 @@ class Account extends Eloquent
'po_number', 'po_number',
'quantity', 'quantity',
'quote', 'quote',
'quote_date',
'quote_number',
'rate', 'rate',
'service', 'service',
'subtotal', 'subtotal',
@ -500,7 +505,7 @@ class Account extends Eloquent
if ($gatewayId) { if ($gatewayId) {
return $this->getGatewayConfig($gatewayId) != false; return $this->getGatewayConfig($gatewayId) != false;
} else { } else {
return count($this->account_gateways) > 0; return $this->account_gateways->count() > 0;
} }
} }
@ -1484,6 +1489,14 @@ class Account extends Eloquent
return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment; return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment;
} }
/**
* @return bool
*/
public function attachUBL()
{
return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->ubl_email_attachment;
}
/** /**
* @return mixed * @return mixed
*/ */
@ -1643,6 +1656,7 @@ class Account extends Eloquent
ENTITY_EXPENSE, ENTITY_EXPENSE,
ENTITY_VENDOR, ENTITY_VENDOR,
ENTITY_PROJECT, ENTITY_PROJECT,
ENTITY_PROPOSAL,
])) { ])) {
return true; return true;
} }
@ -1651,6 +1665,8 @@ class Account extends Eloquent
$entityType = ENTITY_EXPENSE; $entityType = ENTITY_EXPENSE;
} elseif ($entityType == ENTITY_PROJECT) { } elseif ($entityType == ENTITY_PROJECT) {
$entityType = ENTITY_TASK; $entityType = ENTITY_TASK;
} elseif ($entityType == ENTITY_PROPOSAL) {
$entityType = ENTITY_QUOTE;
} }
// note: single & checks bitmask match // note: single & checks bitmask match

View File

@ -38,6 +38,7 @@ class AccountEmailSettings extends Eloquent
public static $templates = [ public static $templates = [
TEMPLATE_INVOICE, TEMPLATE_INVOICE,
TEMPLATE_QUOTE, TEMPLATE_QUOTE,
TEMPLATE_PROPOSAL,
//TEMPLATE_PARTIAL, //TEMPLATE_PARTIAL,
TEMPLATE_PAYMENT, TEMPLATE_PAYMENT,
TEMPLATE_REMINDER1, TEMPLATE_REMINDER1,

View File

@ -268,4 +268,13 @@ class AccountGateway extends EntityModel
return \URL::to(env('WEBHOOK_PREFIX', '').'payment_hook/'.$account->account_key.'/'.$this->gateway_id.env('WEBHOOK_SUFFIX', '')); return \URL::to(env('WEBHOOK_PREFIX', '').'payment_hook/'.$account->account_key.'/'.$this->gateway_id.env('WEBHOOK_SUFFIX', ''));
} }
public function isTestMode()
{
if ($this->isGateway(GATEWAY_STRIPE)) {
return strpos($this->getPublishableStripeKey(), 'test') !== false;
} else {
return $this->getConfigField('testMode');
}
}
} }

View File

@ -351,7 +351,7 @@ class Client extends EntityModel
return $this->name; return $this->name;
} }
if (! count($this->contacts)) { if (! $this->contacts->count()) {
return ''; return '';
} }
@ -386,6 +386,29 @@ class Client extends EntityModel
return $this->hasAddress() && env('GOOGLE_MAPS_ENABLED') !== false; return $this->hasAddress() && env('GOOGLE_MAPS_ENABLED') !== false;
} }
/**
* @return bool
*/
public function addressesMatch()
{
$fields = [
'address1',
'address2',
'city',
'state',
'postal_code',
'country_id',
];
foreach ($fields as $field) {
if ($this->$field != $this->{'shipping_' . $field}) {
return false;
}
}
return true;
}
/** /**
* @return bool * @return bool
*/ */

View File

@ -131,6 +131,21 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
} }
} }
/**
* @return mixed|string
*/
public function getSearchName()
{
$name = $this->getFullName();
$email = $this->email;
if ($name && $email) {
return sprintf('%s <%s>', $name, $email);
} else {
return $name ?: $email;
}
}
/** /**
* @param $contact_key * @param $contact_key
* *

View File

@ -41,6 +41,6 @@ class Country extends Eloquent
*/ */
public function getName() public function getName()
{ {
return $this->name; return trans('texts.country_' . $this->name);
} }
} }

View File

@ -272,6 +272,15 @@ class Document extends EntityModel
return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name); return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name);
} }
public function getProposalUrl()
{
if (! $this->is_proposal || ! $this->document_key) {
return '';
}
return url('proposal/image/'. $this->account->account_key . '/' . $this->document_key . '/' . $this->name);
}
/** /**
* @return bool * @return bool
*/ */

View File

@ -321,6 +321,7 @@ class EntityModel extends Eloquent
'recurring_expenses' => 'files-o', 'recurring_expenses' => 'files-o',
'credits' => 'credit-card', 'credits' => 'credit-card',
'quotes' => 'file-text-o', 'quotes' => 'file-text-o',
'proposals' => 'th-large',
'tasks' => 'clock-o', 'tasks' => 'clock-o',
'expenses' => 'file-image-o', 'expenses' => 'file-image-o',
'vendors' => 'building', 'vendors' => 'building',
@ -354,6 +355,15 @@ class EntityModel extends Eloquent
return false; return false;
} }
public static function getFormUrl($entityType)
{
if (in_array($entityType, [ENTITY_PROPOSAL_CATEGORY, ENTITY_PROPOSAL_SNIPPET, ENTITY_PROPOSAL_TEMPLATE])) {
return str_replace('_', 's/', Utils::pluralizeEntityType($entityType));
} else {
return Utils::pluralizeEntityType($entityType);
}
}
public static function getStates($entityType = false) public static function getStates($entityType = false)
{ {
$data = []; $data = [];

View File

@ -61,6 +61,7 @@ class Expense extends EntityModel
'vendor', 'vendor',
'amount', 'amount',
'public_notes', 'public_notes',
'private_notes',
'expense_category', 'expense_category',
'expense_date', 'expense_date',
]; ];
@ -73,7 +74,8 @@ class Expense extends EntityModel
'category' => 'expense_category', 'category' => 'expense_category',
'client' => 'client', 'client' => 'client',
'vendor' => 'vendor', 'vendor' => 'vendor',
'notes|details' => 'public_notes', 'notes|details^private' => 'public_notes',
'notes|details^public' => 'private_notes',
'date' => 'expense_date', 'date' => 'expense_date',
]; ];
} }
@ -253,6 +255,11 @@ class Expense extends EntityModel
} }
public function amountWithTax() public function amountWithTax()
{
return $this->amount + $this->taxAmount();
}
public function taxAmount()
{ {
return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2); return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2);
} }

View File

@ -2,10 +2,9 @@
namespace App\Models; namespace App\Models;
use Carbon;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Utils;
use App\Models\LookupInvitation; use App\Models\LookupInvitation;
use App\Models\Traits\Inviteable;
/** /**
* Class Invitation. * Class Invitation.
@ -13,6 +12,8 @@ use App\Models\LookupInvitation;
class Invitation extends EntityModel class Invitation extends EntityModel
{ {
use SoftDeletes; use SoftDeletes;
use Inviteable;
/** /**
* @var array * @var array
*/ */
@ -58,102 +59,6 @@ class Invitation extends EntityModel
return $this->belongsTo('App\Models\Account'); return $this->belongsTo('App\Models\Account');
} }
// If we're getting the link for PhantomJS to generate the PDF
// we need to make sure it's served from our site
/**
* @param string $type
* @param bool $forceOnsite
*
* @return string
*/
public function getLink($type = 'view', $forceOnsite = false, $forcePlain = false)
{
if (! $this->account) {
$this->load('account');
}
$account = $this->account;
$iframe_url = $account->iframe_url;
$url = trim(SITE_URL, '/');
if (env('REQUIRE_HTTPS')) {
$url = str_replace('http://', 'https://', $url);
}
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
$url = $account->present()->clientPortalLink();
}
if ($iframe_url && ! $forceOnsite) {
return "{$iframe_url}?{$this->invitation_key}";
} elseif ($this->account->subdomain && ! $forcePlain) {
$url = Utils::replaceSubdomain($url, $account->subdomain);
}
}
return "{$url}/{$type}/{$this->invitation_key}";
}
/**
* @return bool|string
*/
public function getStatus()
{
$hasValue = false;
$parts = [];
$statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
foreach ($statuses as $status) {
$field = "{$status}_date";
$date = '';
if ($this->$field && $this->field != '0000-00-00 00:00:00') {
$date = Utils::dateToString($this->$field);
$hasValue = true;
$parts[] = trans('texts.invitation_status_' . $status) . ': ' . $date;
}
}
return $hasValue ? implode($parts, '<br/>') : false;
}
/**
* @return mixed
*/
public function getName()
{
return $this->invitation_key;
}
/**
* @param null $messageId
*/
public function markSent($messageId = null)
{
$this->message_id = $messageId;
$this->email_error = null;
$this->sent_date = Carbon::now()->toDateTimeString();
$this->save();
}
public function isSent()
{
return $this->sent_date && $this->sent_date != '0000-00-00 00:00:00';
}
public function markViewed()
{
$invoice = $this->invoice;
$client = $invoice->client;
$this->viewed_date = Carbon::now()->toDateTimeString();
$this->save();
$invoice->markViewed();
$client->markLoggedIn();
}
public function signatureDiv() public function signatureDiv()
{ {
if (! $this->signature_base64) { if (! $this->signature_base64) {

View File

@ -451,6 +451,23 @@ class Invoice extends EntityModel implements BalanceAffecting
->where('is_recurring', '=', false); ->where('is_recurring', '=', false);
} }
/**
* @param $query
*
* @return mixed
*/
public function scopeUnapprovedQuotes($query, $includeInvoiceId = false)
{
return $query->quotes()
->where(function ($query) use ($includeInvoiceId) {
$query->whereId($includeInvoiceId)
->orWhere(function ($query) {
$query->where('invoice_status_id', '<', INVOICE_STATUS_APPROVED)
->whereNull('quote_invoice_id');
});
});
}
/** /**
* @param $query * @param $query
* @param $typeId * @param $typeId
@ -710,11 +727,11 @@ class Invoice extends EntityModel implements BalanceAffecting
/** /**
* @return string * @return string
*/ */
public function getFileName() public function getFileName($extension = 'pdf')
{ {
$entityType = $this->getEntityType(); $entityType = $this->getEntityType();
return trans("texts.$entityType") . '_' . $this->invoice_number . '.pdf'; return trans("texts.$entityType") . '_' . $this->invoice_number . '.' . $extension;
} }
/** /**
@ -841,6 +858,14 @@ class Invoice extends EntityModel implements BalanceAffecting
return $this->invoice_status_id >= INVOICE_STATUS_VIEWED; return $this->invoice_status_id >= INVOICE_STATUS_VIEWED;
} }
/**
* @return bool
*/
public function isApproved()
{
return $this->invoice_status_id >= INVOICE_STATUS_APPROVED || $this->quote_invoice_id;
}
/** /**
* @return bool * @return bool
*/ */
@ -1403,21 +1428,12 @@ class Invoice extends EntityModel implements BalanceAffecting
$paidAmount = $this->getAmountPaid($calculatePaid); $paidAmount = $this->getAmountPaid($calculatePaid);
if ($this->tax_name1) { if ($this->tax_name1) {
if ($account->inclusive_taxes) { $invoiceTaxAmount = $this->taxAmount($taxable, $this->tax_rate1);
$invoiceTaxAmount = round($taxable - ($taxable / (1 + ($this->tax_rate1 / 100))), 2);
} else {
$invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2);
}
$invoicePaidAmount = floatval($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; $invoicePaidAmount = floatval($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
$this->calculateTax($taxes, $this->tax_name1, $this->tax_rate1, $invoiceTaxAmount, $invoicePaidAmount); $this->calculateTax($taxes, $this->tax_name1, $this->tax_rate1, $invoiceTaxAmount, $invoicePaidAmount);
} }
if ($this->tax_name2) { if ($this->tax_name2) {
if ($account->inclusive_taxes) { $invoiceTaxAmount = $this->taxAmount($taxable, $this->tax_rate2);
$invoiceTaxAmount = round($taxable - ($taxable / (1 + ($this->tax_rate2 / 100))), 2);
} else {
$invoiceTaxAmount = round($taxable * ($this->tax_rate2 / 100), 2);
}
$invoicePaidAmount = floatval($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; $invoicePaidAmount = floatval($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
$this->calculateTax($taxes, $this->tax_name2, $this->tax_rate2, $invoiceTaxAmount, $invoicePaidAmount); $this->calculateTax($taxes, $this->tax_name2, $this->tax_rate2, $invoiceTaxAmount, $invoicePaidAmount);
} }
@ -1426,21 +1442,12 @@ class Invoice extends EntityModel implements BalanceAffecting
$itemTaxable = $this->getItemTaxable($invoiceItem, $taxable); $itemTaxable = $this->getItemTaxable($invoiceItem, $taxable);
if ($invoiceItem->tax_name1) { if ($invoiceItem->tax_name1) {
if ($account->inclusive_taxes) { $itemTaxAmount = $this->taxAmount($itemTaxable, $invoiceItem->tax_rate1);
$itemTaxAmount = round($taxable - ($taxable / (1 + ($invoiceItem->tax_rate1 / 100))), 2);
} else {
$itemTaxAmount = round($itemTaxable * ($invoiceItem->tax_rate1 / 100), 2);
}
$itemPaidAmount = floatval($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; $itemPaidAmount = floatval($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
$this->calculateTax($taxes, $invoiceItem->tax_name1, $invoiceItem->tax_rate1, $itemTaxAmount, $itemPaidAmount); $this->calculateTax($taxes, $invoiceItem->tax_name1, $invoiceItem->tax_rate1, $itemTaxAmount, $itemPaidAmount);
} }
if ($invoiceItem->tax_name2) { if ($invoiceItem->tax_name2) {
if ($account->inclusive_taxes) { $itemTaxAmount = $this->taxAmount($itemTaxable, $invoiceItem->tax_rate2);
$itemTaxAmount = round($taxable - ($taxable / (1 + ($invoiceItem->tax_rate2 / 100))), 2);
} else {
$itemTaxAmount = round($itemTaxable * ($invoiceItem->tax_rate2 / 100), 2);
}
$itemPaidAmount = floatval($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; $itemPaidAmount = floatval($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
$this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount); $this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount);
} }
@ -1449,6 +1456,28 @@ class Invoice extends EntityModel implements BalanceAffecting
return $taxes; return $taxes;
} }
public function getTaxTotal()
{
$total = 0;
foreach ($this->getTaxes() as $tax) {
$total += $tax['amount'];
}
return $total;
}
public function taxAmount($taxable, $rate)
{
$account = $this->account;
if ($account->inclusive_taxes) {
return round($taxable - ($taxable / (1 + ($rate / 100))), 2);
} else {
return round($taxable * ($rate / 100), 2);
}
}
/** /**
* @param $taxes * @param $taxes
* @param $name * @param $name
@ -1484,18 +1513,18 @@ class Invoice extends EntityModel implements BalanceAffecting
*/ */
public function countDocuments($expenses = false) public function countDocuments($expenses = false)
{ {
$count = count($this->documents); $count = $this->documents->count();
foreach ($this->expenses as $expense) { foreach ($this->expenses as $expense) {
if ($expense->invoice_documents) { if ($expense->invoice_documents) {
$count += count($expense->documents); $count += $expense->documents->count();
} }
} }
if ($expenses) { if ($expenses) {
foreach ($expenses as $expense) { foreach ($expenses as $expense) {
if ($expense->invoice_documents) { if ($expense->invoice_documents) {
$count += count($expense->documents); $count += $expense->documents->count();
} }
} }
} }
@ -1525,7 +1554,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function hasExpenseDocuments() public function hasExpenseDocuments()
{ {
foreach ($this->expenses as $expense) { foreach ($this->expenses as $expense) {
if ($expense->invoice_documents && count($expense->documents)) { if ($expense->invoice_documents && $expense->documents->count()) {
return true; return true;
} }
} }
@ -1606,6 +1635,28 @@ class Invoice extends EntityModel implements BalanceAffecting
return true; return true;
} }
public function hasTaxes()
{
if ($this->tax_name1 || $this->tax_rate1) {
return true;
}
if ($this->tax_name2 || $this->tax_rate2) {
return false;
}
return false;
}
public function isLocked()
{
if (! config('ninja.lock_sent_invoices')) {
return false;
}
return $this->isSent() && ! $this->is_recurring;
}
} }
Invoice::creating(function ($invoice) { Invoice::creating(function ($invoice) {

View File

@ -107,4 +107,33 @@ class InvoiceItem extends EntityModel
$this->save(); $this->save();
} }
} }
public function hasTaxes()
{
if ($this->tax_name1 || $this->tax_rate1) {
return true;
}
if ($this->tax_name2 || $this->tax_rate2) {
return false;
}
return false;
}
public function costWithDiscount()
{
$cost = $this->cost;
if ($this->discount != 0) {
if ($this->invoice->is_amount_discount) {
$cost -= $discount / $this->qty;
} else {
$cost -= $cost * $discount / 100;
}
}
return $cost;
}
} }

View File

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class LookupProposalInvitation extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'lookup_account_id',
'invitation_key',
'message_id',
];
public static function updateInvitation($accountKey, $invitation)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
if (! $invitation->message_id) {
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupAccount = LookupAccount::whereAccountKey($accountKey)
->firstOrFail();
$lookupInvitation = LookupProposalInvitation::whereLookupAccountId($lookupAccount->id)
->whereInvitationKey($invitation->invitation_key)
->firstOrFail();
$lookupInvitation->message_id = $invitation->message_id;
$lookupInvitation->save();
config(['database.default' => $current]);
}
}

View File

@ -47,6 +47,8 @@ class Product extends EntityModel
'product_key', 'product_key',
'notes', 'notes',
'cost', 'cost',
'custom_value1',
'custom_value2',
]; ];
} }
@ -59,6 +61,8 @@ class Product extends EntityModel
'product|item' => 'product_key', 'product|item' => 'product_key',
'notes|description|details' => 'notes', 'notes|description|details' => 'notes',
'cost|amount|price' => 'cost', 'cost|amount|price' => 'cost',
'custom_value1' => 'custom_value1',
'custom_value2' => 'custom_value2',
]; ];
} }

107
app/Models/Proposal.php Normal file
View File

@ -0,0 +1,107 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
/**
* Class ExpenseCategory.
*/
class Proposal extends EntityModel
{
use SoftDeletes;
use PresentableTrait;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var string
*/
protected $presenter = 'App\Ninja\Presenters\ProposalPresenter';
/**
* @var array
*/
protected $fillable = [
'private_notes',
'html',
'css',
];
/**
* @var string
*/
//protected $presenter = 'App\Ninja\Presenters\ProjectPresenter';
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_PROPOSAL;
}
/**
* @return string
*/
public function getRoute()
{
return "/proposals/{$this->public_id}";
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
/**
* @return mixed
*/
public function invoice()
{
return $this->belongsTo('App\Models\Invoice')->withTrashed();
}
/**
* @return mixed
*/
public function invitations()
{
return $this->hasMany('App\Models\ProposalInvitation')->orderBy('proposal_invitations.contact_id');
}
/**
* @return mixed
*/
public function proposal_invitations()
{
return $this->hasMany('App\Models\ProposalInvitation')->orderBy('proposal_invitations.contact_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function proposal_template()
{
return $this->belongsTo('App\Models\ProposalTemplate')->withTrashed();
}
public function getDisplayName()
{
return $this->invoice->invoice_number;
}
}
Proposal::creating(function ($project) {
$project->setNullValues();
});
Proposal::updating(function ($project) {
$project->setNullValues();
});

View File

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
/**
* Class ExpenseCategory.
*/
class ProposalCategory extends EntityModel
{
use SoftDeletes;
use PresentableTrait;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $fillable = [
'name',
];
/**
* @var string
*/
//protected $presenter = 'App\Ninja\Presenters\ProjectPresenter';
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_PROPOSAL_CATEGORY;
}
/**
* @return string
*/
public function getRoute()
{
return "/proposals/categories/{$this->public_id}";
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function getDisplayName()
{
return $this->name;
}
}
/*
Proposal::creating(function ($project) {
$project->setNullValues();
});
Proposal::updating(function ($project) {
$project->setNullValues();
});
*/

View File

@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\LookupProposalInvitation;
use App\Models\Traits\Inviteable;
/**
* Class Invitation.
*/
class ProposalInvitation extends EntityModel
{
use SoftDeletes;
use Inviteable;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_PROPOSAL_INVITATION;
}
/**
* @return mixed
*/
public function proposal()
{
return $this->belongsTo('App\Models\Proposal')->withTrashed();
}
/**
* @return mixed
*/
public function contact()
{
return $this->belongsTo('App\Models\Contact')->withTrashed();
}
/**
* @return mixed
*/
public function user()
{
return $this->belongsTo('App\Models\User')->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
}
ProposalInvitation::creating(function ($invitation)
{
LookupProposalInvitation::createNew($invitation->account->account_key, [
'invitation_key' => $invitation->invitation_key,
]);
});
ProposalInvitation::updating(function ($invitation)
{
$dirty = $invitation->getDirty();
if (array_key_exists('message_id', $dirty)) {
LookupProposalInvitation::updateInvitation($invitation->account->account_key, $invitation);
}
});
ProposalInvitation::deleted(function ($invitation)
{
if ($invitation->forceDeleting) {
LookupProposalInvitation::deleteWhere([
'invitation_key' => $invitation->invitation_key,
]);
}
});

View File

@ -0,0 +1,84 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
/**
* Class ExpenseCategory.
*/
class ProposalSnippet extends EntityModel
{
use SoftDeletes;
use PresentableTrait;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $fillable = [
'name',
'icon',
'private_notes',
'proposal_category_id',
'html',
'css',
];
/**
* @var string
*/
protected $presenter = 'App\Ninja\Presenters\ProposalSnippetPresenter';
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_PROPOSAL_SNIPPET;
}
/**
* @return string
*/
public function getRoute()
{
return "/proposals/snippets/{$this->public_id}";
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function proposal_category()
{
return $this->belongsTo('App\Models\ProposalCategory')->withTrashed();
}
public function getDisplayName()
{
return $this->name;
}
}
/*
Proposal::creating(function ($project) {
$project->setNullValues();
});
Proposal::updating(function ($project) {
$project->setNullValues();
});
*/

View File

@ -0,0 +1,74 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
/**
* Class ExpenseCategory.
*/
class ProposalTemplate extends EntityModel
{
use SoftDeletes;
use PresentableTrait;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $fillable = [
'name',
'private_notes',
'html',
'css',
];
/**
* @var string
*/
protected $presenter = 'App\Ninja\Presenters\ProposalTemplatePresenter';
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_PROPOSAL_TEMPLATE;
}
/**
* @return string
*/
public function getRoute()
{
return "/proposals/templates/{$this->public_id}";
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function getDisplayName()
{
return $this->name;
}
}
/*
Proposal::creating(function ($project) {
$project->setNullValues();
});
Proposal::updating(function ($project) {
$project->setNullValues();
});
*/

View File

@ -129,7 +129,7 @@ class RecurringExpense extends EntityModel
public function amountWithTax() public function amountWithTax()
{ {
return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2); return $this->amount + Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2);
} }
} }

View File

@ -28,6 +28,7 @@ class Subscription extends EntityModel
protected $fillable = [ protected $fillable = [
'event_id', 'event_id',
'target_url', 'target_url',
'format',
]; ];
/** /**

View File

@ -129,17 +129,27 @@ class Task extends EntityModel
* *
* @return int * @return int
*/ */
public static function calcDuration($task) public static function calcDuration($task, $startTimeCutoff = 0, $endTimeCutoff = 0)
{ {
$duration = 0; $duration = 0;
$parts = json_decode($task->time_log) ?: []; $parts = json_decode($task->time_log) ?: [];
foreach ($parts as $part) { foreach ($parts as $part) {
$startTime = $part[0];
if (count($part) == 1 || ! $part[1]) { if (count($part) == 1 || ! $part[1]) {
$duration += time() - $part[0]; $endTime = time();
} else { } else {
$duration += $part[1] - $part[0]; $endTime = $part[1];
} }
if ($startTimeCutoff) {
$startTime = max($startTime, $startTimeCutoff);
}
if ($endTimeCutoff) {
$endTime = min($endTime, $endTimeCutoff);
}
$duration += $endTime - $startTime;
} }
return $duration; return $duration;
@ -148,9 +158,9 @@ class Task extends EntityModel
/** /**
* @return int * @return int
*/ */
public function getDuration() public function getDuration($startTimeCutoff = 0, $endTimeCutoff = 0)
{ {
return self::calcDuration($this); return self::calcDuration($this, $startTimeCutoff, $endTimeCutoff);
} }
/** /**
@ -230,8 +240,11 @@ class Task extends EntityModel
public function scopeDateRange($query, $startDate, $endDate) public function scopeDateRange($query, $startDate, $endDate)
{ {
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) >= ' . $startDate->format('U')); $query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->modify('+1 day')->format('U'))
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->modify('+1 day')->format('U')); ->whereRaw('case
when is_running then unix_timestamp()
else cast(substring(time_log, length(time_log) - 11, 10) as unsigned)
end >= ' . $startDate->format('U'));
return $query; return $query;
} }

View File

@ -0,0 +1,113 @@
<?php
namespace App\Models\Traits;
use Carbon;
use Utils;
/**
* Class SendsEmails.
*/
trait Inviteable
{
// If we're getting the link for PhantomJS to generate the PDF
// we need to make sure it's served from our site
/**
* @param string $type
* @param bool $forceOnsite
*
* @return string
*/
public function getLink($type = 'view', $forceOnsite = false, $forcePlain = false)
{
if (! $this->account) {
$this->load('account');
}
if ($this->proposal_id) {
$type = 'proposal';
}
$account = $this->account;
$iframe_url = $account->iframe_url;
$url = trim(SITE_URL, '/');
if (env('REQUIRE_HTTPS')) {
$url = str_replace('http://', 'https://', $url);
}
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
$url = $account->present()->clientPortalLink();
}
if ($iframe_url && ! $forceOnsite) {
return "{$iframe_url}?{$this->invitation_key}";
} elseif ($this->account->subdomain && ! $forcePlain) {
$url = Utils::replaceSubdomain($url, $account->subdomain);
}
}
return "{$url}/{$type}/{$this->invitation_key}";
}
/**
* @return bool|string
*/
public function getStatus()
{
$hasValue = false;
$parts = [];
$statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
foreach ($statuses as $status) {
$field = "{$status}_date";
$date = '';
if ($this->$field && $this->field != '0000-00-00 00:00:00') {
$date = Utils::dateToString($this->$field);
$hasValue = true;
$parts[] = trans('texts.invitation_status_' . $status) . ': ' . $date;
}
}
return $hasValue ? implode($parts, '<br/>') : false;
}
/**
* @return mixed
*/
public function getName()
{
return $this->invitation_key;
}
/**
* @param null $messageId
*/
public function markSent($messageId = null)
{
$this->message_id = $messageId;
$this->email_error = null;
$this->sent_date = Carbon::now()->toDateTimeString();
$this->save();
}
public function isSent()
{
return $this->sent_date && $this->sent_date != '0000-00-00 00:00:00';
}
public function markViewed()
{
$this->viewed_date = Carbon::now()->toDateTimeString();
$this->save();
if ($this->invoice) {
$invoice = $this->invoice;
$client = $invoice->client;
$invoice->markViewed();
$client->markLoggedIn();
}
}
}

Some files were not shown because too many files have changed in this diff Show More