Merge branch 'release-4.0.0'

This commit is contained in:
Hillel Coren 2017-12-11 23:02:29 +02:00
commit 730b378b5b
228 changed files with 9249 additions and 3264 deletions

View File

@ -114,6 +114,8 @@ after_script:
- mysql -u root -e 'select * from credits;' ninja
- mysql -u root -e 'select * from expenses;' ninja
- mysql -u root -e 'select * from accounts;' ninja
- mysql -u root -e 'select * from fonts;' ninja
- mysql -u root -e 'select * from banks;' ninja
- cat storage/logs/laravel-error.log
- cat storage/logs/laravel-info.log
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr)

View File

@ -21,7 +21,7 @@ class InitLookup extends Command
*
* @var string
*/
protected $signature = 'ninja:init-lookup {--truncate=} {--validate=} {--update=} {--company_id=} {--page_size=100} {--database=db-ninja-1}';
protected $signature = 'ninja:init-lookup {--truncate=} {--subdomain} {--validate=} {--update=} {--company_id=} {--page_size=100} {--database=db-ninja-1}';
/**
* The console command description.
@ -57,9 +57,12 @@ class InitLookup extends Command
$database = $this->option('database');
$dbServer = DbServer::whereName($database)->first();
if ($this->option('truncate')) {
if ($this->option('subdomain')) {
$this->logMessage('Updating subdomains...');
$this->popuplateSubdomains();
} else if ($this->option('truncate')) {
$this->logMessage('Truncating data...');
$this->truncateTables();
$this->logMessage('Truncated');
} else {
config(['database.default' => $this->option('database')]);
@ -87,6 +90,30 @@ class InitLookup extends Command
}
}
private function popuplateSubdomains()
{
$data = [];
config(['database.default' => $this->option('database')]);
$accounts = DB::table('accounts')
->orderBy('id')
->where('subdomain', '!=', '')
->get(['account_key', 'subdomain']);
foreach ($accounts as $account) {
$data[$account->account_key] = $account->subdomain;
}
config(['database.default' => DB_NINJA_LOOKUP]);
$validate = $this->option('validate');
$update = $this->option('update');
foreach ($data as $accountKey => $subdomain) {
LookupAccount::whereAccountKey($accountKey)->update(['subdomain' => $subdomain]);
}
}
private function initCompanies($dbServerId, $offset = 0)
{
$data = [];
@ -340,6 +367,7 @@ class InitLookup extends Command
protected function getOptions()
{
return [
['subdomain', null, InputOption::VALUE_OPTIONAL, 'Subdomain', null],
['truncate', null, InputOption::VALUE_OPTIONAL, 'Truncate', null],
['company_id', null, InputOption::VALUE_OPTIONAL, 'Company Id', null],
['page_size', null, InputOption::VALUE_OPTIONAL, 'Page Size', null],

View File

@ -116,8 +116,10 @@ class SendRecurringInvoices extends Command
try {
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice && ! $invoice->isPaid()) {
$this->info('Sending Invoice');
$this->info('Not billed - Sending Invoice');
$this->mailer->sendInvoice($invoice);
} elseif ($invoice) {
$this->info('Successfully billed invoice');
}
} catch (Exception $exception) {
$this->info('Error: ' . $exception->getMessage());

View File

@ -2,12 +2,18 @@
namespace App\Console\Commands;
use Carbon;
use Str;
use App\Models\Invoice;
use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Mailers\UserMailer;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\InvoiceRepository;
use App\Models\ScheduledReport;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use App\Jobs\ExportReportResults;
use App\Jobs\RunReport;
/**
* Class SendReminders.
@ -46,13 +52,14 @@ class SendReminders extends Command
* @param InvoiceRepository $invoiceRepo
* @param accountRepository $accountRepo
*/
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo)
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, UserMailer $userMailer)
{
parent::__construct();
$this->mailer = $mailer;
$this->invoiceRepo = $invoiceRepo;
$this->accountRepo = $accountRepo;
$this->userMailer = $userMailer;
}
public function fire()
@ -63,6 +70,23 @@ class SendReminders extends Command
config(['database.default' => $database]);
}
$this->chargeLateFees();
$this->setReminderEmails();
$this->sendScheduledReports();
$this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("SendReminders [{$database}]: Finished successfully");
});
}
}
private function chargeLateFees()
{
$accounts = $this->accountRepo->findWithFees();
$this->info(count($accounts) . ' accounts found with fees');
@ -79,17 +103,20 @@ class SendReminders extends Command
$this->info('Charge fee: ' . $invoice->id);
$account->loadLocalizationSettings($invoice->client); // support trans to add fee line item
$number = preg_replace('/[^0-9]/', '', $reminder);
$amount = $account->account_email_settings->{"late_fee{$number}_amount"};
$percent = $account->account_email_settings->{"late_fee{$number}_percent"};
$this->invoiceRepo->setLateFee($invoice, $amount, $percent);
}
}
}
}
private function setReminderEmails()
{
$accounts = $this->accountRepo->findWithReminders();
$this->info(count($accounts) . ' accounts found with reminders');
/** @var \App\Models\Account $account */
foreach ($accounts as $account) {
if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
continue;
@ -98,7 +125,6 @@ class SendReminders extends Command
$invoices = $this->invoiceRepo->findNeedingReminding($account);
$this->info($account->name . ': ' . count($invoices) . ' invoices found');
/** @var Invoice $invoice */
foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice)) {
$this->info('Send email: ' . $invoice->id);
@ -106,15 +132,34 @@ class SendReminders extends Command
}
}
}
}
$this->info('Done');
private function sendScheduledReports()
{
$scheduledReports = ScheduledReport::where('send_date', '<=', date('Y-m-d'))
->with('user', 'account.company')
->get();
$this->info(count($scheduledReports) . ' scheduled reports');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("SendReminders [{$database}]: Finished successfully");
});
foreach ($scheduledReports as $scheduledReport) {
$user = $scheduledReport->user;
$account = $scheduledReport->account;
if (! $account->hasFeature(FEATURE_REPORTS)) {
continue;
}
$config = (array) json_decode($scheduledReport->config);
$reportType = $config['report_type'];
$report = dispatch(new RunReport($scheduledReport->user, $reportType, $config, true));
$file = dispatch(new ExportReportResults($scheduledReport->user, $config['export_format'], $reportType, $report->exportParams));
if ($file) {
$this->userMailer->sendScheduledReport($scheduledReport, $file);
}
$scheduledReport->updateSendDate();
}
}

View File

@ -7,11 +7,11 @@ use Symfony\Component\Console\Input\InputOption;
use App\Models\AccountGateway;
use App\Models\BankAccount;
use Artisan;
use Crypt;
use Illuminate\Encryption\Encrypter;
use Laravel\LegacyEncrypter\McryptEncrypter;
/**
* Class PruneData.
* Class UpdateKey
*/
class UpdateKey extends Command
{
@ -34,17 +34,30 @@ class UpdateKey extends Command
exit;
}
$legacy = false;
if ($this->option('legacy') == 'true') {
$legacy = new McryptEncrypter(env('APP_KEY'));
}
// load the current values
$gatewayConfigs = [];
$bankUsernames = [];
foreach (AccountGateway::all() as $gateway) {
if ($legacy) {
$gatewayConfigs[$gateway->id] = json_decode($legacy->decrypt($gateway->config));
} else {
$gatewayConfigs[$gateway->id] = $gateway->getConfig();
}
}
foreach (BankAccount::all() as $bank) {
if ($legacy) {
$bankUsernames[$bank->id] = $legacy->decrypt($bank->username);
} else {
$bankUsernames[$bank->id] = $bank->getUsername();
}
}
// check if we can write to the .env file
$envPath = base_path() . '/.env';
@ -57,7 +70,8 @@ class UpdateKey extends Command
$key = str_random(32);
}
$crypt = new Encrypter($key, config('app.cipher'));
$cipher = $legacy ? 'AES-256-CBC' : config('app.cipher');
$crypt = new Encrypter($key, $cipher);
// update values using the new key/encrypter
foreach (AccountGateway::all() as $gateway) {
@ -72,11 +86,21 @@ class UpdateKey extends Command
$bank->save();
}
$message = date('r') . ' Successfully updated ';
if ($envWriteable) {
$this->info(date('r') . ' Successfully update the key');
if ($legacy) {
$message .= 'the key, set the cipher in the .env file to AES-256-CBC';
} else {
$this->info(date('r') . ' Successfully update data, make sure to set the new app key: ' . $key);
$message .= 'the key';
}
} else {
if ($legacy) {
$message .= 'the data, make sure to set the new cipher/key: AES-256-CBC/' . $key;
} else {
$message .= 'the data, make sure to set the new key: ' . $key;
}
}
$this->info($message);
}
/**
@ -92,6 +116,8 @@ class UpdateKey extends Command
*/
protected function getOptions()
{
return [];
return [
['legacy', null, InputOption::VALUE_OPTIONAL, 'Legacy', null],
];
}
}

View File

@ -2,6 +2,7 @@
if (! defined('APP_NAME')) {
define('APP_NAME', env('APP_NAME', 'Invoice Ninja'));
define('APP_DOMAIN', env('APP_DOMAIN', 'invoiceninja.com'));
define('CONTACT_EMAIL', env('MAIL_FROM_ADDRESS', env('MAIL_USERNAME')));
define('CONTACT_NAME', env('MAIL_FROM_NAME'));
define('SITE_URL', env('APP_URL'));
@ -39,6 +40,7 @@ if (! defined('APP_NAME')) {
define('ENTITY_PROJECT', 'project');
define('ENTITY_RECURRING_EXPENSE', 'recurring_expense');
define('ENTITY_CUSTOMER', 'customer');
define('ENTITY_SUBSCRIPTION', 'subscription');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
@ -228,6 +230,11 @@ if (! defined('APP_NAME')) {
define('FREQUENCY_SIX_MONTHS', 8);
define('FREQUENCY_ANNUALLY', 9);
define('REPORT_FREQUENCY_DAILY', 'daily');
define('REPORT_FREQUENCY_WEEKLY', 'weekly');
define('REPORT_FREQUENCY_BIWEEKLY', 'biweekly');
define('REPORT_FREQUENCY_MONTHLY', 'monthly');
define('SESSION_TIMEZONE', 'timezone');
define('SESSION_CURRENCY', 'currency');
define('SESSION_CURRENCY_DECORATOR', 'currency_decorator');
@ -310,7 +317,7 @@ if (! defined('APP_NAME')) {
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_DATE', '2000-01-01');
define('NINJA_VERSION', '3.9.2' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '4.0.0' . env('NINJA_VERSION_SUFFIX'));
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'));
@ -426,6 +433,7 @@ if (! defined('APP_NAME')) {
define('GATEWAY_TYPE_SOFORT', 8);
define('GATEWAY_TYPE_SEPA', 9);
define('GATEWAY_TYPE_GOCARDLESS', 10);
define('GATEWAY_TYPE_APPLE_PAY', 11);
define('GATEWAY_TYPE_TOKEN', 'token');
define('TEMPLATE_INVOICE', 'invoice');
@ -451,6 +459,9 @@ if (! defined('APP_NAME')) {
define('FILTER_INVOICE_DATE', 'invoice_date');
define('FILTER_PAYMENT_DATE', 'payment_date');
define('ADDRESS_BILLING', 'billing_address');
define('ADDRESS_SHIPPING', 'shipping_address');
define('SOCIAL_GOOGLE', 'Google');
define('SOCIAL_FACEBOOK', 'Facebook');
define('SOCIAL_GITHUB', 'GitHub');

View File

@ -4,6 +4,7 @@ namespace App\Exceptions;
use Crawler;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
@ -28,7 +29,7 @@ class Handler extends ExceptionHandler
*/
protected $dontReport = [
TokenMismatchException::class,
//ModelNotFoundException::class,
ModelNotFoundException::class,
//AuthorizationException::class,
//HttpException::class,
//ValidationException::class,
@ -150,4 +151,31 @@ class Handler extends ExceptionHandler
return parent::render($request, $e);
}
}
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
$guard = array_get($exception->guards(), 0);
switch ($guard) {
case 'client':
$url = '/client/login';
break;
default:
$url = '/login';
break;
}
return redirect()->guest($url);
}
}

View File

@ -16,6 +16,7 @@ use Auth;
use Cache;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Response;
use Socialite;
use Utils;
@ -118,7 +119,13 @@ class AccountApiController extends BaseAPIController
public function getUserAccounts(Request $request)
{
return $this->processLogin($request);
$user = Auth::user();
$users = $this->accountRepo->findUsers($user, 'account.account_tokens');
$transformer = new UserAccountTransformer($user->account, $request->serializer, $request->token_name);
$data = $this->createCollection($users, $transformer, 'user_account');
return $this->response($data);
}
public function update(UpdateAccountRequest $request)
@ -140,7 +147,7 @@ class AccountApiController extends BaseAPIController
$devices = json_decode($account->devices, true);
for ($x = 0; $x < count($devices); $x++) {
if ($devices[$x]['email'] == Auth::user()->username) {
if ($devices[$x]['email'] == $request->email) {
$devices[$x]['token'] = $request->token; //update
$devices[$x]['device'] = $request->device;
$account->devices = json_encode($devices);
@ -171,6 +178,26 @@ class AccountApiController extends BaseAPIController
return $this->response($newDevice);
}
public function removeDeviceToken(Request $request) {
$account = Auth::user()->account;
$devices = json_decode($account->devices, true);
foreach($devices as $key => $value)
{
if($request->token == $value['token'])
unset($devices[$key]);
}
$account->devices = json_encode(array_values($devices));
$account->save();
return $this->response(['success']);
}
public function updatePushNotifications(Request $request)
{
$account = Auth::user()->account;
@ -220,4 +247,11 @@ class AccountApiController extends BaseAPIController
return $this->errorResponse(['message' => 'Invalid credentials'], 401);
}
public function iosSubscriptionStatus() {
//stubbed for iOS callbacks
}
}

View File

@ -494,6 +494,8 @@ class AccountController extends BaseController
'account' => Auth::user()->account,
'title' => trans('texts.tax_rates'),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->get(),
'countInvoices' => Invoice::scope()->withTrashed()->count(),
'hasInclusiveTaxRates' => TaxRate::scope()->whereIsInclusive(true)->count() ? true : false,
];
return View::make('accounts.tax_rates', $data);
@ -769,11 +771,20 @@ class AccountController extends BaseController
*/
public function saveClientPortalSettings(SaveClientPortalSettings $request)
{
$account = $request->user()->account;
if($account->subdomain !== $request->subdomain)
// check subdomain is unique in the lookup tables
if (request()->subdomain) {
if (! \App\Models\LookupAccount::validateField('subdomain', request()->subdomain, $account)) {
return Redirect::to('settings/' . ACCOUNT_CLIENT_PORTAL)
->withError(trans('texts.subdomain_taken'))
->withInput();
}
}
if ($account->subdomain !== $request->subdomain) {
event(new SubdomainWasUpdated($account));
}
$account->fill($request->all());
$account->client_view_css = $request->client_view_css;

View File

@ -17,6 +17,7 @@ use Utils;
use Validator;
use View;
use WePay;
use File;
class AccountGatewayController extends BaseController
{
@ -119,9 +120,9 @@ class AccountGatewayController extends BaseController
$creditCards = [];
foreach ($creditCardsArray as $card => $name) {
if ($selectedCards > 0 && ($selectedCards & $card) == $card) {
$creditCards[$name['text']] = ['value' => $card, 'data-imageUrl' => asset($name['card']), 'checked' => 'checked'];
$creditCards['<div>' . $name['text'] . '</div>'] = ['value' => $card, 'data-imageUrl' => asset($name['card']), 'checked' => 'checked'];
} else {
$creditCards[$name['text']] = ['value' => $card, 'data-imageUrl' => asset($name['card'])];
$creditCards['<div>' . $name['text'] . '</div>'] = ['value' => $card, 'data-imageUrl' => asset($name['card'])];
}
}
@ -297,6 +298,13 @@ class AccountGatewayController extends BaseController
$config->enableSofort = boolval(Input::get('enable_sofort'));
$config->enableSepa = boolval(Input::get('enable_sepa'));
$config->enableBitcoin = boolval(Input::get('enable_bitcoin'));
$config->enableApplePay = boolval(Input::get('enable_apple_pay'));
if ($config->enableApplePay && $uploadedFile = request()->file('apple_merchant_id')) {
$config->appleMerchantId = File::get($uploadedFile);
} elseif ($oldConfig && ! empty($oldConfig->appleMerchantId)) {
$config->appleMerchantId = $oldConfig->appleMerchantId;
}
}
if ($gatewayId == GATEWAY_STRIPE || $gatewayId == GATEWAY_WEPAY) {
@ -316,6 +324,7 @@ class AccountGatewayController extends BaseController
$accountGateway->accepted_credit_cards = $cardCount;
$accountGateway->show_address = Input::get('show_address') ? true : false;
$accountGateway->show_shipping_address = Input::get('show_shipping_address') ? true : false;
$accountGateway->update_address = Input::get('update_address') ? true : false;
$accountGateway->setConfig($config);

View File

@ -269,9 +269,21 @@ class AppController extends BaseController
public function update()
{
if (! Utils::isNinjaProd()) {
if ($password = env('UPDATE_SECRET')) {
if (! hash_equals($password, request('secret') ?: '')) {
abort(400, 'Invalid secret: /update?secret=<value>');
}
}
try {
set_time_limit(60 * 5);
$this->checkInnoDB();
$cacheCompiled = base_path('bootstrap/cache/compiled.php');
if (file_exists($cacheCompiled)) { unlink ($cacheCompiled); }
$cacheServices = base_path('bootstrap/cache/services.json');
if (file_exists($cacheServices)) { unlink ($cacheServices); }
Artisan::call('clear-compiled');
Artisan::call('cache:clear');
Artisan::call('debugbar:clear');

View File

@ -2,42 +2,13 @@
namespace App\Http\Controllers\Auth;
use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use App\Ninja\Repositories\AccountRepository;
use App\Services\AuthService;
use Auth;
use Event;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Http\Request;
use Lang;
use Session;
use Utils;
use Cache;
use Illuminate\Contracts\Auth\Authenticatable;
use App\Http\Requests\ValidateTwoFactorRequest;
use App\Http\Controllers\Controller;
class AuthController extends Controller
{
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
|
*/
use AuthenticatesAndRegistersUsers;
/**
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* @var AuthService
*/
@ -63,43 +34,13 @@ class AuthController extends Controller
$this->authService = $authService;
}
/**
* @param array $data
*
* @return mixed
*/
public function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
*
* @return User
*/
public function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
/**
* @param $provider
* @param Request $request
*
* @return \Illuminate\Http\RedirectResponse
*/
public function authLogin($provider, Request $request)
public function oauthLogin($provider, Request $request)
{
return $this->authService->execute($provider, $request->has('code'));
}
@ -107,161 +48,12 @@ class AuthController extends Controller
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function authUnlink()
public function oauthUnlink()
{
$this->accountRepo->unlinkUserFromOauth(Auth::user());
$this->accountRepo->unlinkUserFromOauth(auth()->user());
Session::flash('message', trans('texts.updated_settings'));
session()->flash('message', trans('texts.updated_settings'));
return redirect()->to('/settings/' . ACCOUNT_USER_DETAILS);
}
/**
* @return \Illuminate\Http\Response
*/
public function getLoginWrapper()
{
if (auth()->check()) {
return redirect('/');
}
if (! Utils::isNinja() && ! User::count()) {
return redirect()->to('/setup');
}
if (Utils::isNinja() && ! Utils::isTravis()) {
// make sure the user is on SITE_URL/login to ensure OAuth works
$requestURL = request()->url();
$loginURL = SITE_URL . '/login';
$subdomain = Utils::getSubdomain(request()->url());
if ($requestURL != $loginURL && ! strstr($subdomain, 'webapp-')) {
return redirect()->to($loginURL);
}
}
return self::getLogin();
}
/**
* @param Request $request
*
* @return \Illuminate\Http\Response
*/
public function postLoginWrapper(Request $request)
{
$userId = Auth::check() ? Auth::user()->id : null;
$user = User::where('email', '=', $request->input('email'))->first();
if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
Session::flash('error', trans('texts.invalid_credentials'));
return redirect()->to('login');
}
$response = self::postLogin($request);
if (Auth::check()) {
/*
$users = false;
// we're linking a new account
if ($request->link_accounts && $userId && Auth::user()->id != $userId) {
$users = $this->accountRepo->associateAccounts($userId, Auth::user()->id);
Session::flash('message', trans('texts.associated_accounts'));
// check if other accounts are linked
} else {
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
}
*/
} elseif ($user) {
error_log('login failed');
$user->failed_logins = $user->failed_logins + 1;
$user->save();
}
return $response;
}
/**
* Send the post-authentication response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return \Illuminate\Http\Response
*/
private function authenticated(Request $request, Authenticatable $user)
{
if ($user->google_2fa_secret) {
Auth::logout();
$request->session()->put('2fa:user:id', $user->id);
return redirect('/validate_two_factor/' . $user->account->account_key);
}
Event::fire(new UserLoggedIn());
return redirect()->intended($this->redirectTo);
}
/**
*
* @return \Illuminate\Http\Response
*/
public function getValidateToken()
{
if (session('2fa:user:id')) {
return view('auth.two_factor');
}
return redirect('login');
}
/**
*
* @param App\Http\Requests\ValidateSecretRequest $request
* @return \Illuminate\Http\Response
*/
public function postValidateToken(ValidateTwoFactorRequest $request)
{
//get user id and create cache key
$userId = $request->session()->pull('2fa:user:id');
$key = $userId . ':' . $request->totp;
//use cache to store token to blacklist
Cache::add($key, true, 4);
//login and redirect user
Auth::loginUsingId($userId);
Event::fire(new UserLoggedIn());
return redirect()->intended($this->redirectTo);
}
/**
* @return \Illuminate\Http\Response
*/
public function getLogoutWrapper()
{
if (Auth::check() && ! Auth::user()->registered) {
if (request()->force_logout) {
$account = Auth::user()->account;
$this->accountRepo->unlinkAccount($account);
if (! $account->hasMultipleAccounts()) {
$account->company->forceDelete();
}
$account->forceDelete();
} else {
return redirect('/');
}
}
$response = self::getLogout();
Session::flush();
$reason = htmlentities(request()->reason);
if (!empty($reason) && Lang::has("texts.{$reason}_logout")) {
Session::flash('warning', trans("texts.{$reason}_logout"));
}
return $response;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
}

View File

@ -0,0 +1,214 @@
<?php
namespace App\Http\Controllers\Auth;
use Utils;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Contracts\Auth\Authenticatable;
use Event;
use Cache;
use Lang;
use App\Events\UserLoggedIn;
use App\Http\Requests\ValidateTwoFactorRequest;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'getLogoutWrapper']);
}
/**
* @return \Illuminate\Http\Response
*/
public function getLoginWrapper(Request $request)
{
if (auth()->check()) {
return redirect('/');
}
if (! Utils::isNinja() && ! User::count()) {
return redirect()->to('/setup');
}
if (Utils::isNinja() && ! Utils::isTravis()) {
// make sure the user is on SITE_URL/login to ensure OAuth works
$requestURL = request()->url();
$loginURL = SITE_URL . '/login';
$subdomain = Utils::getSubdomain(request()->url());
if ($requestURL != $loginURL && ! strstr($subdomain, 'webapp-')) {
return redirect()->to($loginURL);
}
}
return self::showLoginForm($request);
}
/**
* @param Request $request
*
* @return \Illuminate\Http\Response
*/
public function postLoginWrapper(Request $request)
{
$userId = auth()->check() ? auth()->user()->id : null;
$user = User::where('email', '=', $request->input('email'))->first();
if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
session()->flash('error', trans('texts.invalid_credentials'));
return redirect()->to('login');
}
$response = self::login($request);
if (auth()->check()) {
/*
$users = false;
// we're linking a new account
if ($request->link_accounts && $userId && Auth::user()->id != $userId) {
$users = $this->accountRepo->associateAccounts($userId, Auth::user()->id);
Session::flash('message', trans('texts.associated_accounts'));
// check if other accounts are linked
} else {
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
}
*/
} else {
$stacktrace = sprintf("%s %s %s %s\n", date('Y-m-d h:i:s'), $request->input('email'), \Request::getClientIp(), array_get($_SERVER, 'HTTP_USER_AGENT'));
file_put_contents(storage_path('logs/failed-logins.log'), $stacktrace, FILE_APPEND);
error_log('login failed');
if ($user) {
$user->failed_logins = $user->failed_logins + 1;
$user->save();
}
}
return $response;
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
->withErrors([
$this->username() => trans('texts.invalid_credentials'),
]);
}
/**
* Send the post-authentication response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return \Illuminate\Http\Response
*/
private function authenticated(Request $request, Authenticatable $user)
{
if ($user->google_2fa_secret) {
auth()->logout();
session()->put('2fa:user:id', $user->id);
return redirect('/validate_two_factor/' . $user->account->account_key);
}
Event::fire(new UserLoggedIn());
return redirect()->intended($this->redirectTo);
}
/**
*
* @return \Illuminate\Http\Response
*/
public function getValidateToken()
{
if (session('2fa:user:id')) {
return view('auth.two_factor');
}
return redirect('login');
}
/**
*
* @param App\Http\Requests\ValidateSecretRequest $request
* @return \Illuminate\Http\Response
*/
public function postValidateToken(ValidateTwoFactorRequest $request)
{
//get user id and create cache key
$userId = session()->pull('2fa:user:id');
$key = $userId . ':' . $request->totp;
//use cache to store token to blacklist
Cache::add($key, true, 4);
//login and redirect user
auth()->loginUsingId($userId);
Event::fire(new UserLoggedIn());
return redirect()->intended($this->redirectTo);
}
/**
* @return \Illuminate\Http\Response
*/
public function getLogoutWrapper(Request $request)
{
if (auth()->check() && ! auth()->user()->registered) {
if (request()->force_logout) {
$account = auth()->user()->account;
app('App\Ninja\Repositories\AccountRepository')->unlinkAccount($account);
if (! $account->hasMultipleAccounts()) {
$account->company->forceDelete();
}
$account->forceDelete();
} else {
return redirect('/');
}
}
$response = self::logout($request);
$reason = htmlentities(request()->reason);
if (!empty($reason) && Lang::has("texts.{$reason}_logout")) {
session()->flash('warning', trans("texts.{$reason}_logout"));
}
return $response;
}
}

View File

@ -3,11 +3,13 @@
namespace App\Http\Controllers\Auth;
use Event;
use Illuminate\Http\Request;
use App\Models\PasswordReset;
use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
@ -21,40 +23,27 @@ class PasswordController extends Controller
*/
use ResetsPasswords {
getResetSuccessResponse as protected traitGetResetSuccessResponse;
sendResetResponse as protected traitSendResetResponse;
}
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new password controller instance.
* Create a new controller instance.
*
* @internal param \Illuminate\Contracts\Auth\Guard $auth
* @internal param \Illuminate\Contracts\Auth\PasswordBroker $passwords
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Display the form to request a password reset link.
*
* @return \Illuminate\Http\Response
*/
public function getEmailWrapper()
{
if (auth()->check()) {
return redirect('/');
}
return $this->getEmail();
}
protected function getResetSuccessResponse($response)
protected function sendResetResponse($response)
{
$user = auth()->user();
@ -64,7 +53,20 @@ class PasswordController extends Controller
return redirect('/validate_two_factor/' . $user->account->account_key);
} else {
Event::fire(new UserLoggedIn());
return $this->traitGetResetSuccessResponse($response);
return $this->traitSendResetResponse($response);
}
}
public function showResetForm(Request $request, $token = null)
{
$passwordReset = PasswordReset::whereToken($token)->first();
if (! $passwordReset) {
return redirect('login')->withMessage(trans('texts.invalid_code'));
}
return view('auth.passwords.reset')->with(
['token' => $token, 'email' => $passwordReset->email]
);
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace App\Http\Controllers\ClientAuth;
use App\Http\Controllers\Controller;
use App\Models\Contact;
use App\Models\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Session;
class AuthController extends Controller
{
use AuthenticatesUsers;
/**
* @var string
*/
protected $guard = 'client';
/**
* @var string
*/
protected $redirectTo = '/client/dashboard';
/**
* @return mixed
*/
public function showLoginForm()
{
$data = [
'clientauth' => true,
];
return view('clientauth.login')->with($data);
}
/**
* Get the needed authorization credentials from the request.
*
* @param \Illuminate\Http\Request $request
*
* @return array
*/
protected function getCredentials(Request $request)
{
$credentials = $request->only('password');
$credentials['id'] = null;
$contactKey = session('contact_key');
if ($contactKey) {
$contact = Contact::where('contact_key', '=', $contactKey)->first();
if ($contact && ! $contact->is_deleted) {
$credentials['id'] = $contact->id;
}
}
return $credentials;
}
/**
* Validate the user login request.
*
* @param \Illuminate\Http\Request $request
*
* @return void
*/
protected function validateLogin(Request $request)
{
$this->validate($request, [
'password' => 'required',
]);
}
/**
* @return mixed
*/
public function getSessionExpired()
{
return view('clientauth.sessionexpired')->with(['clientauth' => true]);
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\ClientAuth;
use Password;
use Config;
use App\Models\Contact;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:client');
//Config::set('auth.defaults.passwords', 'client');
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function showLinkRequestForm()
{
$data = [
'clientauth' => true,
];
if (! session('contact_key')) {
return \Redirect::to('/client/session_expired');
}
return view('clientauth.passwords.email')->with($data);
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\Response
*/
public function sendResetLinkEmail(Request $request)
{
$contactId = null;
$contactKey = session('contact_key');
if ($contactKey) {
$contact = Contact::where('contact_key', '=', $contactKey)->first();
if ($contact && ! $contact->is_deleted && $contact->email) {
$contactId = $contact->id;
}
}
$response = $this->broker()->sendResetLink(['id' => $contactId], function (Message $message) {
$message->subject($this->getEmailSubject());
});
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($response)
: $this->sendResetLinkFailedResponse($request, $response);
}
protected function broker()
{
return Password::broker('clients');
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace App\Http\Controllers\ClientAuth;
use Utils;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Contact;
use App\Models\Account;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Contracts\Auth\Authenticatable;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/client/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:client', ['except' => 'logout']);
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return auth()->guard('client');
}
/**
* @return mixed
*/
public function showLoginForm()
{
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
$hasAccountIndentifier = request()->account_key || ($subdomain && $subdomain != 'app');
if (! session('contact_key')) {
if (Utils::isNinja()) {
if (! $hasAccountIndentifier) {
return redirect('/client/session_expired');
}
} else {
if (! $hasAccountIndentifier && Account::count() > 1) {
return redirect('/client/session_expired');
}
}
}
return view('clientauth.login')->with(['clientauth' => true]);
}
/**
* Get the needed authorization credentials from the request.
*
* @param \Illuminate\Http\Request $request
*
* @return array
*/
protected function credentials(Request $request)
{
if ($contactKey = session('contact_key')) {
$credentials = $request->only('password');
$credentials['contact_key'] = $contactKey;
} else {
$credentials = $request->only('email', 'password');
$account = false;
// resovle the email to a contact/account
if ($accountKey = request()->account_key) {
$account = Account::whereAccountKey($accountKey)->first();
} else {
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
if ($subdomain != 'app') {
$account = Account::whereSubdomain($subdomain)->first();
}
}
if ($account) {
$credentials['account_id'] = $account->id;
} else {
abort(500, 'Account not resolved in client login');
}
}
return $credentials;
}
/**
* Send the post-authentication response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return \Illuminate\Http\Response
*/
private function authenticated(Request $request, Authenticatable $contact)
{
session(['contact_key' => $contact->contact_key]);
return redirect()->intended($this->redirectPath());
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
->withErrors([
$this->username() => trans('texts.invalid_credentials'),
]);
}
/**
* Validate the user login request - don't require the email
*
* @param \Illuminate\Http\Request $request
*
* @return void
*/
protected function validateLogin(Request $request)
{
$rules = [
'password' => 'required',
];
if (! session('contact_key')) {
$rules['email'] = 'required|email';
}
$this->validate($request, $rules);
}
/**
* @return mixed
*/
public function getSessionExpired()
{
return view('clientauth.sessionexpired')->with(['clientauth' => true]);
}
}

View File

@ -13,86 +13,6 @@ use Illuminate\Support\Facades\Password;
class PasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* @var string
*/
protected $redirectTo = '/client/dashboard';
/**
* Create a new password controller instance.
*
* @internal param \Illuminate\Contracts\Auth\Guard $auth
* @internal param \Illuminate\Contracts\Auth\PasswordBroker $passwords
*/
public function __construct()
{
$this->middleware('guest');
Config::set('auth.defaults.passwords', 'client');
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function showLinkRequestForm()
{
$data = [
'clientauth' => true,
];
if (! session('contact_key')) {
return \Redirect::to('/client/sessionexpired');
}
return view('clientauth.password')->with($data);
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\Response
*/
public function sendResetLinkEmail(Request $request)
{
$broker = $this->getBroker();
$contactId = null;
$contactKey = session('contact_key');
if ($contactKey) {
$contact = Contact::where('contact_key', '=', $contactKey)->first();
if ($contact && ! $contact->is_deleted && $contact->email) {
$contactId = $contact->id;
}
}
$response = Password::broker($broker)->sendResetLink(['id' => $contactId], function (Message $message) {
$message->subject($this->getEmailSubject());
});
switch ($response) {
case Password::RESET_LINK_SENT:
return $this->getSendResetLinkEmailSuccessResponse($response);
case Password::INVALID_USER:
default:
return $this->getSendResetLinkEmailFailureResponse($response);
}
}
/**
* Display the password reset view for the given token.
*
@ -116,7 +36,7 @@ class PasswordController extends Controller
);
if (! session('contact_key')) {
return \Redirect::to('/client/sessionexpired');
return \Redirect::to('/client/session_expired');
}
return view('clientauth.reset')->with($data);

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\ClientAuth;
use Password;
use Config;
use App\Models\PasswordReset;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use App\Models\PasswordReset;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/client/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:client');
//Config::set('auth.defaults.passwords', 'client');
}
protected function broker()
{
return Password::broker('clients');
}
protected function guard()
{
return auth()->guard('client');
}
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(
['token' => $token, 'email' => $passwordReset->email]
);
}
}

View File

@ -14,6 +14,7 @@ use App\Ninja\Repositories\CreditRepository;
use App\Ninja\Repositories\DocumentRepository;
use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\TaskRepository;
use App\Services\PaymentService;
use Auth;
use Barracuda\ArchiveStream\ZipArchive;
@ -36,7 +37,14 @@ class ClientPortalController extends BaseController
private $paymentRepo;
private $documentRepo;
public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, DocumentRepository $documentRepo, PaymentService $paymentService, CreditRepository $creditRepo)
public function __construct(
InvoiceRepository $invoiceRepo,
PaymentRepository $paymentRepo,
ActivityRepository $activityRepo,
DocumentRepository $documentRepo,
PaymentService $paymentService,
CreditRepository $creditRepo,
TaskRepository $taskRepo)
{
$this->invoiceRepo = $invoiceRepo;
$this->paymentRepo = $paymentRepo;
@ -44,6 +52,7 @@ class ClientPortalController extends BaseController
$this->documentRepo = $documentRepo;
$this->paymentService = $paymentService;
$this->creditRepo = $creditRepo;
$this->taskRepo = $taskRepo;
}
public function view($invitationKey)
@ -133,9 +142,6 @@ class ClientPortalController extends BaseController
}
$showApprove = $invoice->quote_invoice_id ? false : true;
if ($invoice->due_date) {
$showApprove = time() < strtotime($invoice->getOriginal('due_date'));
}
if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) {
$showApprove = false;
}
@ -556,6 +562,46 @@ class ClientPortalController extends BaseController
return $this->creditRepo->getClientDatatable($contact->client_id);
}
public function taskIndex()
{
if (! $contact = $this->getContact()) {
return $this->returnError();
}
$account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (! $contact->client->show_tasks_in_portal) {
return redirect()->to($account->enable_client_portal_dashboard ? '/client/dashboard' : '/client/payment_methods/');
}
if (! $account->enable_client_portal) {
return $this->returnError();
}
$color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [
'color' => $color,
'account' => $account,
'title' => trans('texts.tasks'),
'entityType' => ENTITY_TASK,
'columns' => Utils::trans(['project', 'date', 'duration', 'description']),
'sortColumn' => 1,
];
return response()->view('public_list', $data);
}
public function taskDatatable()
{
if (! $contact = $this->getContact()) {
return false;
}
return $this->taskRepo->getClientDatatable($contact->client_id);
}
public function documentIndex()
{
if (! $contact = $this->getContact()) {
@ -601,7 +647,7 @@ class ClientPortalController extends BaseController
return response()->view('error', [
'error' => $error ?: trans('texts.invoice_not_found'),
'hideHeader' => true,
'account' => $this->getContact()->account,
'account' => $this->getContact() ? $this->getContact()->account : false,
]);
}

View File

@ -83,6 +83,7 @@ class DashboardController extends BaseController
'tasks' => $tasks,
'showBlueVinePromo' => $showBlueVinePromo,
'showWhiteLabelExpired' => $showWhiteLabelExpired,
'showExpenses' => count($expenses) && $account->isModuleEnabled(ENTITY_EXPENSE),
'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',
];

View File

@ -170,7 +170,7 @@ class ExportController extends BaseController
if ($request->input('include') === 'all' || $request->input('clients')) {
$data['clients'] = Client::scope()
->with('user', 'contacts', 'country', 'currency')
->with('user', 'contacts', 'country', 'currency', 'shipping_country')
->withArchived()
->get();
}

View File

@ -24,15 +24,8 @@ class IntegrationController extends Controller
return Response::json('Event is invalid', 500);
}
$subscription = Subscription::where('account_id', '=', Auth::user()->account_id)
->where('event_id', '=', $eventId)->first();
if (! $subscription) {
$subscription = new Subscription();
$subscription->account_id = Auth::user()->account_id;
$subscription = Subscription::createNew();
$subscription->event_id = $eventId;
}
$subscription->target_url = trim(Input::get('target_url'));
$subscription->save();

View File

@ -93,7 +93,7 @@ class InvoiceController extends BaseController
->where('invitations.invoice_id', '=', $invoice->id)
->where('invitations.account_id', '=', Auth::user()->account_id)
->where('invitations.deleted_at', '=', null)
->select('contacts.public_id')->lists('public_id');
->select('contacts.public_id')->pluck('public_id');
$clients = Client::scope()->withTrashed()->with('contacts', 'country');
@ -590,6 +590,28 @@ class InvoiceController extends BaseController
return View::make('invoices.history', $data);
}
public function deliveryNote(InvoiceRequest $request)
{
$invoice = $request->entity();
$invoice->load('user', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'account.country', 'client.contacts', 'client.country', 'client.shipping_country');
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
$invoice->features = [
'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN),
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS),
];
$invoice->invoice_type_id = intval($invoice->invoice_type_id);
$data = [
'invoice' => $invoice,
'invoiceDesigns' => InvoiceDesign::getDesigns(),
'invoiceFonts' => Cache::get('fonts'),
];
return View::make('invoices.delivery_note', $data);
}
public function checkInvoiceNumber($invoicePublicId = false)
{
$invoiceNumber = request()->invoice_number;

View File

@ -114,10 +114,16 @@ class OnlinePaymentController extends BaseController
*
* @return \Illuminate\Http\RedirectResponse
*/
public function doPayment(CreateOnlinePaymentRequest $request)
public function doPayment(CreateOnlinePaymentRequest $request, $invitationKey, $gatewayTypeAlias = false)
{
$invitation = $request->invitation;
if ($gatewayTypeAlias) {
$gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias);
} else {
$gatewayTypeId = Session::get($invitation->id . 'gateway_type');
}
$paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId);
if (! $invitation->invoice->canBePaid() && ! request()->update) {
@ -184,7 +190,9 @@ class OnlinePaymentController extends BaseController
private function completePurchase($invitation, $isOffsite = false)
{
if ($redirectUrl = session('redirect_url:' . $invitation->invitation_key)) {
if (request()->wantsJson()) {
return response()->json(RESULT_SUCCESS);
} elseif ($redirectUrl = session('redirect_url:' . $invitation->invitation_key)) {
$separator = strpos($redirectUrl, '?') === false ? '?' : '&';
return redirect()->to($redirectUrl . $separator . 'invoice_id=' . $invitation->invoice->public_id);
@ -412,4 +420,28 @@ class OnlinePaymentController extends BaseController
return redirect()->to($link);
}
}
public function showAppleMerchantId()
{
if (Utils::isNinja()) {
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
$account = Account::whereSubdomain($subdomain)->first();
} else {
$account = Account::first();
}
if (! $account) {
exit("Account not found");
}
$accountGateway = $account->account_gateways()
->whereGatewayId(GATEWAY_STRIPE)->first();
if (! $account) {
exit("Apple merchant id not set");
}
echo $accountGateway->getConfigField('appleMerchantId');
exit;
}
}

View File

@ -6,6 +6,7 @@ use App\Http\Requests\CreateProjectRequest;
use App\Http\Requests\ProjectRequest;
use App\Http\Requests\UpdateProjectRequest;
use App\Models\Client;
use App\Models\Project;
use App\Ninja\Datatables\ProjectDatatable;
use App\Ninja\Repositories\ProjectRepository;
use App\Services\ProjectService;
@ -95,6 +96,11 @@ class ProjectController extends BaseController
Session::flash('message', trans('texts.updated_project'));
$action = Input::get('action');
if (in_array($action, ['archive', 'delete', 'restore', 'invoice'])) {
return self::bulk();
}
return redirect()->to($project->getRoute());
}
@ -102,6 +108,42 @@ class ProjectController extends BaseController
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
if ($action == 'invoice') {
$data = [];
$clientPublicId = false;
$lastClientId = false;
$lastProjectId = false;
$projects = Project::scope($ids)
->with(['client', 'tasks' => function ($query) {
$query->whereNull('invoice_id');
}])
->get();
foreach ($projects as $project) {
if (! $clientPublicId) {
$clientPublicId = $project->client->public_id;
}
if ($lastClientId && $lastClientId != $project->client_id) {
return redirect('projects')->withError(trans('texts.project_error_multiple_clients'));
}
$lastClientId = $project->client_id;
foreach ($project->tasks as $task) {
if ($task->is_running) {
return redirect('projects')->withError(trans('texts.task_error_running'));
}
$showProject = $lastProjectId != $task->project_id;
$data[] = [
'publicId' => $task->public_id,
'description' => $task->present()->invoiceDescription(auth()->user()->account, $showProject),
'duration' => $task->getHours(),
'cost' => $task->getRate(),
];
$lastProjectId = $task->project_id;
}
}
return redirect("invoices/create/{$clientPublicId}")->with('tasks', $data);
} else {
$count = $this->projectService->bulk($ids, $action);
if ($count > 0) {
@ -112,4 +154,5 @@ class ProjectController extends BaseController
return redirect()->to('/projects');
}
}
}

View File

@ -149,6 +149,13 @@ class QuoteController extends BaseController
$invitation = Invitation::with('invoice.invoice_items', 'invoice.invitations')->where('invitation_key', '=', $invitationKey)->firstOrFail();
$invoice = $invitation->invoice;
if ($invoice->due_date) {
$carbonDueDate = \Carbon::parse($invoice->due_date);
if (! $carbonDueDate->isToday() && ! $carbonDueDate->isFuture()) {
return redirect("view/{$invitationKey}")->withError(trans('texts.quote_has_expired'));
}
}
$invitationKey = $this->invoiceService->approveQuote($invoice, $invitation);
Session::flash('message', trans('texts.quote_is_approved'));

View File

@ -2,13 +2,16 @@
namespace App\Http\Controllers;
use App\Jobs\ExportReportResults;
use App\Jobs\RunReport;
use App\Models\Account;
use App\Models\ScheduledReport;
use Auth;
use Input;
use Str;
use Utils;
use View;
use Excel;
use Carbon;
use Validator;
/**
* Class ReportController.
@ -94,22 +97,30 @@ class ReportController extends BaseController
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
$isExport = $action == 'export';
$reportClass = '\\App\\Ninja\\Reports\\' . Str::studly($reportType) . 'Report';
$options = [
$config = [
'date_field' => $dateField,
'invoice_status' => request()->invoice_status,
'status_ids' => request()->status_ids,
'group_dates_by' => request()->group_dates_by,
'document_filter' => request()->document_filter,
'currency_type' => request()->currency_type,
'export_format' => $format,
'start_date' => $params['startDate'],
'end_date' => $params['endDate'],
];
$report = new $reportClass($startDate, $endDate, $isExport, $options);
if (Input::get('report_type')) {
$report->run();
}
$params['report'] = $report;
$params = array_merge($params, $report->results());
if ($isExport) {
return self::export($format, $reportType, $params);
$report = dispatch(new RunReport(auth()->user(), $reportType, $config, $isExport));
$params = array_merge($params, $report->exportParams);
switch ($action) {
case 'export':
return dispatch(new ExportReportResults(auth()->user(), $format, $reportType, $params))->export($format);
break;
case 'schedule':
self::schedule($params, $config);
return redirect('/reports');
break;
case 'cancel_schedule':
self::cancelSchdule();
return redirect('/reports');
break;
}
} else {
$params['columns'] = [];
@ -118,112 +129,47 @@ class ReportController extends BaseController
$params['report'] = false;
}
return View::make('reports.chart_builder', $params);
$params['scheduledReports'] = ScheduledReport::scope()->whereUserId(auth()->user()->id)->get();
return View::make('reports.report_builder', $params);
}
/**
* @param $format
* @param $reportType
* @param $params
* @todo: Add summary to export
*/
private function export($format, $reportType, $params)
private function schedule($params, $options)
{
if (! Auth::user()->hasPermission('view_all')) {
exit;
}
$validator = Validator::make(request()->all(), [
'frequency' => 'required|in:daily,weekly,biweekly,monthly',
'send_date' => 'required',
]);
$format = strtolower($format);
$data = $params['displayData'];
$columns = $params['columns'];
$totals = $params['reportTotals'];
$report = $params['report'];
$filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report";
$formats = ['csv', 'pdf', 'xlsx', 'zip'];
if (! in_array($format, $formats)) {
throw new \Exception("Invalid format request to export report");
}
//Get labeled header
$data = array_merge(
[
array_map(function($col) {
return $col['label'];
}, $report->tableHeaderArray())
],
$data
);
$summary = [];
if (count(array_values($totals))) {
$summary[] = array_merge([
trans("texts.totals")
], array_map(function ($key) {
return trans("texts.{$key}");
}, array_keys(array_values(array_values($totals)[0])[0])));
}
foreach ($totals as $currencyId => $each) {
foreach ($each as $dimension => $val) {
$tmp = [];
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
foreach ($val as $id => $field) {
$tmp[] = Utils::formatMoney($field, $currencyId);
}
$summary[] = $tmp;
}
}
return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $summary) {
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $summary) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
if ($format == 'csv') {
$sheet->rows(array_merge($data, [[]], $summary));
if ($validator->fails()) {
session()->now('message', trans('texts.scheduled_report_error'));
} else {
$sheet->rows($data);
$options['report_type'] = $params['reportType'];
$options['range'] = request('range');
$options['start_date_offset'] = $options['range'] ? '' : Carbon::parse($params['startDate'])->diffInDays(null, false); // null,false to get the relative/non-absolute diff
$options['end_date_offset'] = $options['range'] ? '' : Carbon::parse($params['endDate'])->diffInDays(null, false);
unset($options['start_date']);
unset($options['end_date']);
unset($options['group_dates_by']);
$schedule = ScheduledReport::createNew();
$schedule->config = json_encode($options);
$schedule->frequency = request('frequency');
$schedule->send_date = Utils::toSqlDate(request('send_date'));
$schedule->save();
session()->flash('message', trans('texts.created_scheduled_report'));
}
}
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($data[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
private function cancelSchdule()
{
ScheduledReport::scope()
->whereUserId(auth()->user()->id)
->wherePublicId(request('scheduled_report_id'))
->delete();
if (count($summary)) {
$excel->sheet(trans("texts.totals"), function($sheet) use($report, $summary, $format) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
$sheet->rows($summary);
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($summary[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
}
})->export($format);
session()->flash('message', trans('texts.deleted_scheduled_report'));
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers;
use App\Models\Subscription;
use App\Services\SubscriptionService;
use Auth;
use Input;
use Redirect;
use Session;
use URL;
use Validator;
use View;
/**
* Class SubscriptionController.
*/
class SubscriptionController extends BaseController
{
/**
* @var SubscriptionService
*/
protected $subscriptionService;
/**
* SubscriptionController constructor.
*
* @param SubscriptionService $subscriptionService
*/
public function __construct(SubscriptionService $subscriptionService)
{
//parent::__construct();
$this->subscriptionService = $subscriptionService;
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function index()
{
return Redirect::to('settings/' . ACCOUNT_API_TOKENS);
}
/**
* @return \Illuminate\Http\JsonResponse
*/
public function getDatatable()
{
return $this->subscriptionService->getDatatable(Auth::user()->account_id);
}
/**
* @param $publicId
*
* @return \Illuminate\Contracts\View\View
*/
public function edit($publicId)
{
$subscription = Subscription::scope($publicId)->firstOrFail();
$data = [
'subscription' => $subscription,
'method' => 'PUT',
'url' => 'subscriptions/' . $publicId,
'title' => trans('texts.edit_subscription'),
];
return View::make('accounts.subscription', $data);
}
/**
* @param $publicId
*
* @return \Illuminate\Http\RedirectResponse
*/
public function update($publicId)
{
return $this->save($publicId);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function store()
{
return $this->save();
}
/**
* @return \Illuminate\Contracts\View\View
*/
public function create()
{
$data = [
'subscription' => null,
'method' => 'POST',
'url' => 'subscriptions',
'title' => trans('texts.add_subscription'),
];
return View::make('accounts.subscription', $data);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function bulk()
{
$action = Input::get('bulk_action');
$ids = Input::get('bulk_public_id');
$count = $this->subscriptionService->bulk($ids, $action);
Session::flash('message', trans('texts.archived_subscription'));
return Redirect::to('settings/' . ACCOUNT_API_TOKENS);
}
/**
* @param bool $subscriptionPublicId
*
* @return $this|\Illuminate\Http\RedirectResponse
*/
public function save($subscriptionPublicId = false)
{
if (Auth::user()->account->hasFeature(FEATURE_API)) {
$rules = [
'event_id' => 'required',
'target_url' => 'required|url',
];
if ($subscriptionPublicId) {
$subscription = Subscription::scope($subscriptionPublicId)->firstOrFail();
} else {
$subscription = Subscription::createNew();
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
return Redirect::to($subscriptionPublicId ? 'subscriptions/edit' : 'subscriptions/create')->withInput()->withErrors($validator);
}
$subscription->fill(request()->all());
$subscription->save();
if ($subscriptionPublicId) {
$message = trans('texts.updated_subscription');
} else {
$message = trans('texts.created_subscription');
}
Session::flash('message', $message);
}
return Redirect::to('settings/' . ACCOUNT_API_TOKENS);
}
}

View File

@ -308,7 +308,6 @@ class TaskController extends BaseController
}
} else {
$count = $this->taskService->bulk($ids, $action);
if (request()->wantsJson()) {
return response()->json($count);
} else {

View File

@ -9,33 +9,57 @@ class Kernel extends HttpKernel
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'App\Http\Middleware\VerifyCsrfToken',
'App\Http\Middleware\DuplicateSubmissionCheck',
'App\Http\Middleware\QueryLogging',
'App\Http\Middleware\StartupCheck',
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
//\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\DuplicateSubmissionCheck::class,
\App\Http\Middleware\QueryLogging::class,
\App\Http\Middleware\StartupCheck::class,
],
'api' => [
\App\Http\Middleware\ApiCheck::class,
],
/*
'api' => [
'throttle:60,1',
'bindings',
],
*/
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'lookup' => 'App\Http\Middleware\DatabaseLookup',
'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'permissions.required' => 'App\Http\Middleware\PermissionsRequired',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
'api' => 'App\Http\Middleware\ApiCheck',
'cors' => '\Barryvdh\Cors\HandleCors',
'throttle' => 'Illuminate\Routing\Middleware\ThrottleRequests',
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'lookup' => \App\Http\Middleware\DatabaseLookup::class,
'permissions.required' => \App\Http\Middleware\PermissionsRequired::class,
];
}

View File

@ -28,7 +28,8 @@ class ApiCheck
{
$loggingIn = $request->is('api/v1/login')
|| $request->is('api/v1/register')
|| $request->is('api/v1/oauth_login');
|| $request->is('api/v1/oauth_login')
|| $request->is('api/v1/ios_subscription_status');
$headers = Utils::getApiHeaders();
$hasApiSecret = false;

View File

@ -64,8 +64,9 @@ class Authenticate
Session::put('contact_key', $contact->contact_key);
}
if (! $contact) {
return \Redirect::to('client/sessionexpired');
return \Redirect::to('client/session_expired');
}
$account = $contact->account;
if (Auth::guard('user')->check() && Auth::user('user')->account_id == $account->id) {
@ -86,8 +87,8 @@ class Authenticate
$authenticated = true;
}
if (env('PHANTOMJS_SECRET') && $request->phantomjs_secret && hash_equals(env('PHANTOMJS_SECRET'), $request->phantomjs_secret)) {
$authenticated = true;
if ($authenticated) {
$request->merge(['contact' => $contact]);
}
}

View File

@ -10,6 +10,7 @@ use App\Models\LookupInvitation;
use App\Models\LookupAccountToken;
use App\Models\LookupUser;
use Auth;
use Utils;
class DatabaseLookup
{
@ -44,6 +45,13 @@ class DatabaseLookup
LookupInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->contact_key ?: session('contact_key')) {
LookupContact::setServerByField('contact_key', $key);
} elseif ($key = request()->account_key) {
LookupAccount::setServerByField('account_key', $key);
} else {
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
if ($subdomain != 'app') {
LookupAccount::setServerByField('subdomain', $subdomain);
}
}
} elseif ($guard == 'postmark') {
LookupInvitation::setServerByField('message_id', request()->MessageID);

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
class EncryptCookies extends BaseEncrypter
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View File

@ -39,12 +39,21 @@ class RedirectIfAuthenticated
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
public function handle(Request $request, Closure $next, $guard = null)
{
if ($this->auth->check() && Client::scope()->count() > 0) {
if (auth()->guard($guard)->check()) {
Session::reflash();
return new RedirectResponse(url('/dashboard'));
switch ($guard) {
case 'client':
if (session('contact_key')) {
return redirect('/client/dashboard');
}
break;
default:
return redirect('/dashboard');
break;
}
}
return $next($request);

View File

@ -55,8 +55,8 @@ class StartupCheck
$file = storage_path() . '/version.txt';
$version = @file_get_contents($file);
if ($version != NINJA_VERSION) {
if (version_compare(phpversion(), '5.5.9', '<')) {
dd('Please update PHP to >= 5.5.9');
if (version_compare(phpversion(), '7.0.0', '<')) {
dd('Please update PHP to >= 7.0.0');
}
$handle = fopen($file, 'w');
fwrite($handle, NINJA_VERSION);

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests;
use App\Models\Invitation;
use App\Models\GatewayType;
class CreateOnlinePaymentRequest extends Request
{
@ -39,7 +40,12 @@ class CreateOnlinePaymentRequest extends Request
->firstOrFail();
$input['invitation'] = $invitation;
if ($gatewayTypeAlias = request()->gateway_type) {
$input['gateway_type'] = GatewayType::getIdFromAlias($gatewayTypeAlias);
} else {
$input['gateway_type'] = session($invitation->id . 'gateway_type');
}
$this->replace($input);

View File

@ -22,7 +22,7 @@ class CreateProjectRequest extends ProjectRequest
public function rules()
{
return [
'name' => sprintf('required|unique:projects,name,,id,account_id,%s', $this->user()->account_id),
'name' => 'required',
'client_id' => 'required',
];
}

View File

@ -26,7 +26,7 @@ class UpdateProjectRequest extends ProjectRequest
}
return [
'name' => sprintf('required|unique:projects,name,%s,id,account_id,%s', $this->entity()->id, $this->user()->account_id),
'name' => 'required',
];
}
}

View File

@ -0,0 +1,126 @@
<?php
namespace App\Jobs;
use Utils;
use Excel;
use App\Jobs\Job;
class ExportReportResults extends Job
{
public function __construct($user, $format, $reportType, $params)
{
$this->user = $user;
$this->format = strtolower($format);
$this->reportType = $reportType;
$this->params = $params;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if (! $this->user->hasPermission('view_all')) {
return false;
}
$format = $this->format;
$reportType = $this->reportType;
$params = $this->params;
$data = $params['displayData'];
$columns = $params['columns'];
$totals = $params['reportTotals'];
$report = $params['report'];
$filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report";
$formats = ['csv', 'pdf', 'xlsx', 'zip'];
if (! in_array($format, $formats)) {
throw new \Exception("Invalid format request to export report");
}
//Get labeled header
$data = array_merge(
[
array_map(function($col) {
return $col['label'];
}, $report->tableHeaderArray())
],
$data
);
$summary = [];
if (count(array_values($totals))) {
$summary[] = array_merge([
trans("texts.totals")
], array_map(function ($key) {
return trans("texts.{$key}");
}, array_keys(array_values(array_values($totals)[0])[0])));
}
foreach ($totals as $currencyId => $each) {
foreach ($each as $dimension => $val) {
$tmp = [];
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
foreach ($val as $id => $field) {
$tmp[] = Utils::formatMoney($field, $currencyId);
}
$summary[] = $tmp;
}
}
return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $summary) {
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $summary) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
if ($format == 'csv') {
$sheet->rows(array_merge($data, [[]], $summary));
} else {
$sheet->rows($data);
}
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($data[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
if (count($summary)) {
$excel->sheet(trans("texts.totals"), function($sheet) use($report, $summary, $format) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
$sheet->rows($summary);
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($summary[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
}
});
}
}

86
app/Jobs/RunReport.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace App\Jobs;
use App;
use Str;
use Utils;
use Carbon;
use App\Jobs\Job;
class RunReport extends Job
{
public function __construct($user, $reportType, $config, $isExport = false)
{
$this->user = $user;
$this->reportType = $reportType;
$this->config = $config;
$this->isExport = $isExport;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if (! $this->user->hasPermission('view_all')) {
return false;
}
$reportType = $this->reportType;
$config = $this->config;
$isExport = $this->isExport;
$reportClass = '\\App\\Ninja\\Reports\\' . Str::studly($reportType) . 'Report';
if (! empty($config['range'])) {
switch ($config['range']) {
case 'this_month':
$startDate = Carbon::now()->firstOfMonth()->toDateString();
$endDate = Carbon::now()->lastOfMonth()->toDateString();
break;
case 'last_month':
$startDate = Carbon::now()->subMonth()->firstOfMonth()->toDateString();
$endDate = Carbon::now()->subMonth()->lastOfMonth()->toDateString();
break;
case 'this_year':
$startDate = Carbon::now()->firstOfYear()->toDateString();
$endDate = Carbon::now()->lastOfYear()->toDateString();
break;
case 'last_year':
$startDate = Carbon::now()->subYear()->firstOfYear()->toDateString();
$endDate = Carbon::now()->subYear()->lastOfYear()->toDateString();
break;
}
} elseif (! empty($config['start_date_offset'])) {
$startDate = Carbon::now()->subDays($config['start_date_offset'])->toDateString();
$endDate = Carbon::now()->subDays($config['end_date_offset'])->toDateString();
} else {
$startDate = $config['start_date'];
$endDate = $config['end_date'];
}
// send email as user
if (App::runningInConsole() && $this->user) {
auth()->onceUsingId($this->user->id);
}
$report = new $reportClass($startDate, $endDate, $isExport, $config);
$report->run();
if (App::runningInConsole() && $this->user) {
auth()->logout();
}
$params = [
'startDate' => $startDate,
'endDate' => $endDate,
'report' => $report,
];
$report->exportParams = array_merge($params, $report->results());
return $report;
}
}

View File

@ -108,6 +108,11 @@ class Utils
return self::getResllerType() ? true : false;
}
public static function isRootFolder()
{
return strlen(preg_replace('/[^\/]/', '', url('/'))) == 2;
}
public static function clientViewCSS()
{
$account = false;
@ -459,6 +464,11 @@ class Utils
public static function parseFloat($value)
{
// check for comma as decimal separator
if (preg_match('/,[\d]{1,2}$/', $value)) {
$value = str_replace(',', '.', $value);
}
$value = preg_replace('/[^0-9\.\-]/', '', $value);
return floatval($value);

View File

@ -101,6 +101,8 @@ class HandleUserLoggedIn
// warn if using the default app key
if (in_array(config('app.key'), ['SomeRandomString', 'SomeRandomStringSomeRandomString', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'])) {
Session::flash('error', trans('texts.error_app_key_set_to_default'));
} elseif (in_array($appCipher, ['MCRYPT_RIJNDAEL_256', 'MCRYPT_RIJNDAEL_128'])) {
Session::flash('error', trans('texts.mcrypt_warning'));
}
}
}

View File

@ -132,9 +132,12 @@ class SubscriptionListener
return;
}
$subscription = $entity->account->getSubscription($eventId);
$subscriptions = $entity->account->getSubscriptions($eventId);
if (! $subscriptions->count()) {
return;
}
if ($subscription) {
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$manager->parseIncludes($include);
@ -147,6 +150,7 @@ class SubscriptionListener
$data['client_name'] = $entity->client->getDisplayName();
}
foreach ($subscriptions as $subscription) {
Utils::notifyZapier($subscription, $data);
}
}

View File

@ -177,6 +177,7 @@ class Account extends Eloquent
'credit_number_prefix',
'credit_number_pattern',
'task_rate',
'inclusive_taxes',
];
/**
@ -216,7 +217,6 @@ class Account extends Eloquent
ENTITY_QUOTE => 4,
ENTITY_TASK => 8,
ENTITY_EXPENSE => 16,
ENTITY_VENDOR => 32,
];
public static $dashboardSections = [
@ -233,6 +233,7 @@ class Account extends Eloquent
'due_date',
'hours',
'id_number',
'invoice',
'item',
'line_total',
'outstanding',
@ -240,6 +241,7 @@ class Account extends Eloquent
'partial_due',
'po_number',
'quantity',
'quote',
'rate',
'service',
'subtotal',
@ -1008,6 +1010,15 @@ class Account extends Eloquent
$this->company->save();
}
public function hasReminders()
{
if (! $this->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
return false;
}
return $this->enable_reminder1 || $this->enable_reminder2 || $this->enable_reminder3;
}
/**
* @param $feature
*
@ -1293,9 +1304,9 @@ class Account extends Eloquent
*
* @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getSubscription($eventId)
public function getSubscriptions($eventId)
{
return Subscription::where('account_id', '=', $this->id)->where('event_id', '=', $eventId)->first();
return Subscription::where('account_id', '=', $this->id)->where('event_id', '=', $eventId)->get();
}
/**
@ -1625,10 +1636,17 @@ class Account extends Eloquent
ENTITY_TASK,
ENTITY_EXPENSE,
ENTITY_VENDOR,
ENTITY_PROJECT,
])) {
return true;
}
if ($entityType == ENTITY_VENDOR) {
$entityType = ENTITY_EXPENSE;
} elseif ($entityType == ENTITY_PROJECT) {
$entityType = ENTITY_TASK;
}
// note: single & checks bitmask match
return $this->enabled_modules & static::$modules[$entityType];
}
@ -1692,6 +1710,11 @@ class Account extends Eloquent
return $this->company->accounts->count() > 1;
}
public function getPrimaryAccount()
{
return $this->company->accounts()->orderBy('id')->first();
}
public function financialYearStart()
{
if (! $this->financial_year_start) {
@ -1712,6 +1735,29 @@ class Account extends Eloquent
{
return $this->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->enable_portal_password;
}
public function getBaseUrl()
{
if ($this->hasFeature(FEATURE_CUSTOM_URL)) {
if ($this->iframe_url) {
return $this->iframe_url;
}
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
$url = $this->present()->clientPortalLink();
} else {
$url = url('/');
}
if ($this->subdomain) {
$url = Utils::replaceSubdomain($url, $this->subdomain);
}
return $url;
} else {
return url('/');
}
}
}
Account::creating(function ($account)
@ -1719,6 +1765,13 @@ Account::creating(function ($account)
LookupAccount::createAccount($account->account_key, $account->company_id);
});
Account::updating(function ($account) {
$dirty = $account->getDirty();
if (array_key_exists('subdomain', $dirty)) {
LookupAccount::updateAccount($account->account_key, $account);
}
});
Account::updated(function ($account) {
// prevent firing event if the invoice/quote counter was changed
// TODO: remove once counters are moved to separate table

View File

@ -136,6 +136,15 @@ class AccountGateway extends EntityModel
return $this->getConfigField('publishableKey');
}
public function getAppleMerchantId()
{
if (! $this->isGateway(GATEWAY_STRIPE)) {
return false;
}
return $this->getConfigField('appleMerchantId');
}
/**
* @return bool
*/
@ -144,6 +153,14 @@ class AccountGateway extends EntityModel
return ! empty($this->getConfigField('enableAch'));
}
/**
* @return bool
*/
public function getApplePayEnabled()
{
return ! empty($this->getConfigField('enableApplePay'));
}
/**
* @return bool
*/

View File

@ -53,10 +53,16 @@ class Client extends EntityModel
'quote_number_counter',
'public_notes',
'task_rate',
'shipping_address1',
'shipping_address2',
'shipping_city',
'shipping_state',
'shipping_postal_code',
'shipping_country_id',
'show_tasks_in_portal',
'send_reminders',
];
/**
* @return array
*/
@ -179,6 +185,14 @@ class Client extends EntityModel
return $this->belongsTo('App\Models\Country');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function shipping_country()
{
return $this->belongsTo('App\Models\Country');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
@ -375,7 +389,7 @@ class Client extends EntityModel
/**
* @return bool
*/
public function hasAddress()
public function hasAddress($shipping = false)
{
$fields = [
'address1',
@ -387,6 +401,9 @@ class Client extends EntityModel
];
foreach ($fields as $field) {
if ($shipping) {
$field = 'shipping_' . $field;
}
if ($this->$field) {
return true;
}
@ -489,6 +506,20 @@ class Client extends EntityModel
return $this->account->currency ? $this->account->currency->code : 'USD';
}
public function getCountryCode()
{
if ($country = $this->country) {
return $country->iso_3166_2;
}
if (! $this->account) {
$this->load('account');
}
return $this->account->country ? $this->account->country->iso_3166_2 : 'US';
}
/**
* @param $isQuote
*

View File

@ -9,13 +9,20 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\LookupContact;
use Illuminate\Notifications\Notifiable;
/**
* Class Contact.
*/
class Contact extends EntityModel implements AuthenticatableContract, CanResetPasswordContract
{
use SoftDeletes, Authenticatable, CanResetPassword;
use SoftDeletes;
use Authenticatable;
use CanResetPassword;
use Notifiable;
protected $guard = 'client';
/**
* @var array
*/
@ -42,6 +49,17 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
'custom_value2',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
'confirmation_code',
];
/**
* @var string
*/
@ -165,6 +183,12 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
return "{$url}/client/dashboard/{$this->contact_key}";
}
public function sendPasswordResetNotification($token)
{
//$this->notify(new ResetPasswordNotification($token));
app('App\Ninja\Mailers\ContactMailer')->sendPasswordReset($this, $token);
}
}
Contact::creating(function ($contact)

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Eloquent;
use Str;
/**
* Class Currency.
@ -28,4 +29,12 @@ class Currency extends Eloquent
{
return $this->name;
}
/**
* @return mixed
*/
public function getTranslatedName()
{
return trans('texts.currency_' . Str::slug($this->name, '_'));
}
}

View File

@ -161,6 +161,7 @@ class EntityModel extends Eloquent
$query->where($this->getTable() .'.account_id', '=', $accountId);
// If 'false' is passed as the publicId return nothing rather than everything
if (func_num_args() > 1 && ! $publicId && ! $accountId) {
$query->where('id', '=', 0);
return $query;
@ -326,6 +327,7 @@ class EntityModel extends Eloquent
'settings' => 'cog',
'self-update' => 'download',
'reports' => 'th-list',
'projects' => 'briefcase',
];
return array_get($icons, $entityType);

View File

@ -42,9 +42,8 @@ class Gateway extends Eloquent
*/
public static $preferred = [
GATEWAY_PAYPAL_EXPRESS,
GATEWAY_BITPAY,
GATEWAY_DWOLLA,
GATEWAY_STRIPE,
GATEWAY_WEPAY,
GATEWAY_BRAINTREE,
GATEWAY_AUTHORIZE_NET,
GATEWAY_MOLLIE,
@ -140,7 +139,6 @@ class Gateway extends Eloquent
public function scopePrimary($query, $accountGatewaysIds)
{
$query->where('payment_library_id', '=', 1)
->where('id', '!=', GATEWAY_WEPAY)
->whereIn('id', static::$preferred)
->whereIn('id', $accountGatewaysIds);
}
@ -152,7 +150,6 @@ class Gateway extends Eloquent
public function scopeSecondary($query, $accountGatewaysIds)
{
$query->where('payment_library_id', '=', 1)
->where('id', '!=', GATEWAY_WEPAY)
->whereNotIn('id', static::$preferred)
->whereIn('id', $accountGatewaysIds);
}
@ -178,11 +175,13 @@ class Gateway extends Eloquent
$link = 'https://applications.sagepay.com/apply/2C02C252-0F8A-1B84-E10D-CF933EFCAA99';
} elseif ($this->id == GATEWAY_STRIPE) {
$link = 'https://dashboard.stripe.com/account/apikeys';
} elseif ($this->id == GATEWAY_WEPAY) {
$link = url('/gateways/create?wepay=true');
}
$key = 'texts.gateway_help_'.$this->id;
$str = trans($key, [
'link' => "<a href='$link' target='_blank'>Click here</a>",
'link' => "<a href='$link' >Click here</a>",
'complete_link' => url('/complete'),
]);

View File

@ -460,6 +460,38 @@ class Invoice extends EntityModel implements BalanceAffecting
return $query->where('invoice_type_id', '=', $typeId);
}
/**
* @param $query
* @param $typeId
*
* @return mixed
*/
public function scopeStatusIds($query, $statusIds)
{
if (! $statusIds || (is_array($statusIds) && ! count($statusIds))) {
return $query;
}
return $query->where(function ($query) use ($statusIds) {
foreach ($statusIds as $statusId) {
$query->orWhere('invoice_status_id', '=', $statusId);
}
if (in_array(INVOICE_STATUS_UNPAID, $statusIds)) {
$query->orWhere(function ($query) {
$query->where('balance', '>', 0)
->where('is_public', '=', true);
});
}
if (in_array(INVOICE_STATUS_OVERDUE, $statusIds)) {
$query->orWhere(function ($query) {
$query->where('balance', '>', 0)
->where('due_date', '<', date('Y-m-d'))
->where('is_public', '=', true);
});
}
});
}
/**
* @param $typeId
*
@ -955,6 +987,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'include_item_taxes_inline',
'invoice_fields',
'show_currency_code',
'inclusive_taxes',
]);
foreach ($this->invoice_items as $invoiceItem) {
@ -1319,17 +1352,26 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getTaxes($calculatePaid = false)
{
$taxes = [];
$account = $this->account;
$taxable = $this->getTaxable();
$paidAmount = $this->getAmountPaid($calculatePaid);
if ($this->tax_name1) {
if ($account->inclusive_taxes) {
$invoiceTaxAmount = round(($taxable * 100) / (100 + ($this->tax_rate1 * 100)), 2);
} else {
$invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2);
}
$invoicePaidAmount = floatval($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
$this->calculateTax($taxes, $this->tax_name1, $this->tax_rate1, $invoiceTaxAmount, $invoicePaidAmount);
}
if ($this->tax_name2) {
if ($account->inclusive_taxes) {
$invoiceTaxAmount = round(($taxable * 100) / (100 + ($this->tax_rate2 * 100)), 2);
} else {
$invoiceTaxAmount = round($taxable * ($this->tax_rate2 / 100), 2);
}
$invoicePaidAmount = floatval($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
$this->calculateTax($taxes, $this->tax_name2, $this->tax_rate2, $invoiceTaxAmount, $invoicePaidAmount);
}
@ -1338,13 +1380,21 @@ class Invoice extends EntityModel implements BalanceAffecting
$itemTaxable = $this->getItemTaxable($invoiceItem, $taxable);
if ($invoiceItem->tax_name1) {
if ($account->inclusive_taxes) {
$itemTaxAmount = round(($itemTaxable * 100) / (100 + ($invoiceItem->tax_rate1 * 100)), 2);
} else {
$itemTaxAmount = round($itemTaxable * ($invoiceItem->tax_rate1 / 100), 2);
}
$itemPaidAmount = floatval($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
$this->calculateTax($taxes, $invoiceItem->tax_name1, $invoiceItem->tax_rate1, $itemTaxAmount, $itemPaidAmount);
}
if ($invoiceItem->tax_name2) {
if ($account->inclusive_taxes) {
$itemTaxAmount = round(($itemTaxable * 100) / (100 + ($invoiceItem->tax_rate2 * 100)), 2);
} else {
$itemTaxAmount = round($itemTaxable * ($invoiceItem->tax_rate2 / 100), 2);
}
$itemPaidAmount = floatval($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
$this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount);
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Eloquent;
use Str;
/**
* Class InvoiceStatus.
@ -35,4 +36,12 @@ class InvoiceStatus extends Eloquent
return false;
}
}
/**
* @return mixed
*/
public function getTranslatedName()
{
return trans('texts.status_' . Str::slug($this->name, '_'));
}
}

View File

@ -55,4 +55,44 @@ class LookupAccount extends LookupModel
return $this->lookupCompany->dbServer->name;
}
public static function updateAccount($accountKey, $account)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupAccount = LookupAccount::whereAccountKey($accountKey)
->firstOrFail();
$lookupAccount->subdomain = $account->subdomain ?: null;
$lookupAccount->save();
config(['database.default' => $current]);
}
public static function validateField($field, $value, $account = false)
{
if (! env('MULTI_DB_ENABLED')) {
return true;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupAccount = LookupAccount::where($field, '=', $value)->first();
if ($account) {
$isValid = ! $lookupAccount || ($lookupAccount->account_key == $account->account_key);
} else {
$isValid = ! $lookupAccount;
}
config(['database.default' => $current]);
return $isValid;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class Client.
*/
class PasswordReset extends Eloquent
{
}

View File

@ -23,7 +23,10 @@ class Payment extends EntityModel
* @var array
*/
protected $fillable = [
'transaction_reference',
'private_notes',
'exchange_rate',
'exchange_currency_id',
];
public static $statusClasses = [
@ -303,6 +306,14 @@ class Payment extends EntityModel
return $this->getCompletedAmount() > 0 && ($this->isCompleted() || $this->isPartiallyRefunded());
}
/**
* @return bool
*/
public function isExchanged()
{
return $this->exchange_currency_id || $this->exchange_rate != 1;
}
/**
* @return mixed|null|\stdClass|string
*/

View File

@ -256,7 +256,7 @@ class PaymentMethod extends EntityModel
PaymentMethod::deleting(function ($paymentMethod) {
$accountGatewayToken = $paymentMethod->account_gateway_token;
if ($accountGatewayToken->default_payment_method_id == $paymentMethod->id) {
$newDefault = $accountGatewayToken->payment_methods->first(function ($i, $paymentMethdod) use ($accountGatewayToken) {
$newDefault = $accountGatewayToken->payment_methods->first(function ($paymentMethdod) use ($accountGatewayToken) {
return $paymentMethdod->id != $accountGatewayToken->default_payment_method_id;
});
$accountGatewayToken->default_payment_method_id = $newDefault ? $newDefault->id : null;

View File

@ -55,6 +55,14 @@ class Project extends EntityModel
{
return $this->belongsTo('App\Models\Client')->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function tasks()
{
return $this->hasMany('App\Models\Task');
}
}
Project::creating(function ($project) {

View File

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Carbon;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Scheduled Report
*/
class ScheduledReport extends EntityModel
{
use SoftDeletes;
/**
* @var array
*/
protected $fillable = [
'frequency',
'config',
'send_date',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
/**
* @return mixed
*/
public function user()
{
return $this->belongsTo('App\Models\User')->withTrashed();
}
public function updateSendDate()
{
switch ($this->frequency) {
case REPORT_FREQUENCY_DAILY;
$this->send_date = Carbon::now()->addDay()->toDateString();
break;
case REPORT_FREQUENCY_WEEKLY:
$this->send_date = Carbon::now()->addWeek()->toDateString();
break;
case REPORT_FREQUENCY_BIWEEKLY:
$this->send_date = Carbon::now()->addWeeks(2)->toDateString();
break;
case REPORT_FREQUENCY_MONTHLY:
$this->send_date = Carbon::now()->addMonth()->toDateString();
break;
}
$this->save();
}
}

View File

@ -8,15 +8,42 @@ use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Subscription.
*/
class Subscription extends Eloquent
class Subscription extends EntityModel
{
/**
* @var bool
*/
public $timestamps = true;
use SoftDeletes;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $fillable = [
'event_id',
'target_url',
];
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_SUBSCRIPTION;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
}

View File

@ -40,7 +40,7 @@ trait HasRecurrence
$monthsSinceLastSent = ($diff->format('%y') * 12) + $diff->format('%m');
// check we don't send a few hours early due to timezone difference
if (Carbon::now()->format('Y-m-d') != Carbon::now($timezone)->format('Y-m-d')) {
if (Utils::isNinja() && Carbon::now()->format('Y-m-d') != Carbon::now($timezone)->format('Y-m-d')) {
return false;
}

View File

@ -331,6 +331,7 @@ trait PresentsInvoice
'unit_cost',
'custom_value1',
'custom_value2',
'delivery_note',
];
foreach ($fields as $field) {

View File

@ -11,6 +11,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Laracasts\Presenter\PresentableTrait;
use Session;
use App\Models\LookupUser;
use Illuminate\Notifications\Notifiable;
/**
* Class User.
@ -19,6 +20,7 @@ class User extends Authenticatable
{
use PresentableTrait;
use SoftDeletes;
use Notifiable;
/**
* @var string
@ -176,7 +178,7 @@ class User extends Authenticatable
} elseif ($this->email) {
return $this->email;
} else {
return 'Guest';
return trans('texts.guest');
}
}
@ -427,6 +429,12 @@ class User extends Authenticatable
{
return $this->account->company->accounts->sortBy('id')->first();
}
public function sendPasswordResetNotification($token)
{
//$this->notify(new ResetPasswordNotification($token));
app('App\Ninja\Mailers\UserMailer')->sendPasswordReset($this, $token);
}
}
User::created(function ($user)

View File

@ -68,7 +68,10 @@ class InvoiceDatatable extends EntityDatatable
function ($model) {
$str = '';
if ($model->partial_due_date) {
$str = Utils::fromSqlDate($model->partial_due_date) . ', ';
$str = Utils::fromSqlDate($model->partial_due_date);
if ($model->due_date_sql && $model->due_date_sql != '0000-00-00') {
$str .= ', ';
}
}
return $str . Utils::fromSqlDate($model->due_date_sql);
},
@ -106,11 +109,20 @@ class InvoiceDatatable extends EntityDatatable
},
],
[
trans('texts.view_history'),
trans("texts.{$entityType}_history"),
function ($model) use ($entityType) {
return URL::to("{$entityType}s/{$entityType}_history/{$model->public_id}");
},
],
[
trans('texts.delivery_note'),
function ($model) use ($entityType) {
return url("invoices/delivery_note/{$model->public_id}");
},
function ($model) use ($entityType) {
return $entityType == ENTITY_INVOICE;
},
],
[
'--divider--', function () {
return false;

View File

@ -91,7 +91,13 @@ class PaymentDatatable extends EntityDatatable
[
'amount',
function ($model) {
return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id);
$amount = Utils::formatMoney($model->amount, $model->currency_id, $model->country_id);
if ($model->exchange_currency_id && $model->exchange_rate != 1) {
$amount .= ' | ' . Utils::formatMoney($model->amount * $model->exchange_rate, $model->exchange_currency_id, $model->country_id);
}
return $amount;
},
],
[

View File

@ -59,6 +59,15 @@ class ProjectDatatable extends EntityDatatable
return Auth::user()->can('editByOwner', [ENTITY_PROJECT, $model->user_id]);
},
],
[
trans('texts.invoice_project'),
function ($model) {
return "javascript:submitForm_project('invoice', {$model->public_id})";
},
function ($model) {
return Auth::user()->can('create', ENTITY_INVOICE);
},
],
];
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Ninja\Datatables;
use URL;
class SubscriptionDatatable extends EntityDatatable
{
public $entityType = ENTITY_SUBSCRIPTION;
public function columns()
{
return [
[
'event',
function ($model) {
return trans('texts.subscription_event_' . $model->event);
},
],
[
'target',
function ($model) {
return $model->target;
},
],
];
}
public function actions()
{
return [
[
uctrans('texts.edit_subscription'),
function ($model) {
return URL::to("subscriptions/{$model->public_id}/edit");
},
],
];
}
}

View File

@ -26,7 +26,11 @@ class TaxRateDatatable extends EntityDatatable
[
'type',
function ($model) {
if (auth()->user()->account->inclusive_taxes) {
return trans('texts.inclusive');
} else {
return $model->is_inclusive ? trans('texts.inclusive') : trans('texts.exclusive');
}
},
],
];

View File

@ -327,4 +327,19 @@ class ContactMailer extends Mailer
$this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
public function sendPasswordReset($contact, $token)
{
if (! $contact->email) {
return;
}
$subject = trans('texts.your_password_reset_link');
$view = 'client_password';
$data = [
'token' => $token,
];
$this->sendTo($contact->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
}

View File

@ -154,4 +154,42 @@ class UserMailer extends Mailer
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
public function sendPasswordReset($user, $token)
{
if (! $user->email) {
return;
}
$subject = trans('texts.your_password_reset_link');
$view = 'password';
$data = [
'token' => $token,
];
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
public function sendScheduledReport($scheduledReport, $file)
{
$user = $scheduledReport->user;
$config = json_decode($scheduledReport->config);
if (! $user->email) {
return;
}
$subject = sprintf('%s - %s %s', APP_NAME, trans('texts.' . $config->report_type), trans('texts.report'));
$view = 'user_message';
$data = [
'userName' => $user->getDisplayName(),
'primaryMessage' => trans('texts.scheduled_report_attached', ['type' => trans('texts.' . $config->report_type)]),
'documents' => [[
'name' => $file->filename . '.' . $config->export_format,
'data' => $file->string($config->export_format),
]]
];
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
}

View File

@ -173,7 +173,6 @@ class BasePaymentDriver
'accountGateway' => $this->accountGateway,
'acceptedCreditCardTypes' => $this->accountGateway->getCreditcardTypes(),
'gateway' => $gateway,
'showAddress' => $this->accountGateway->show_address,
'showBreadcrumbs' => false,
'url' => $url,
'amount' => $this->invoice()->getRequestedAmount(),
@ -407,20 +406,31 @@ class BasePaymentDriver
$this->contact()->save();
}
if (! $this->accountGateway->show_address || ! $this->accountGateway->update_address) {
return;
}
// update the address info
$client = $this->client();
if ($this->accountGateway->show_address && $this->accountGateway->update_address) {
$client->address1 = trim($this->input['address1']);
$client->address2 = trim($this->input['address2']);
$client->city = trim($this->input['city']);
$client->state = trim($this->input['state']);
$client->postal_code = trim($this->input['postal_code']);
$client->country_id = trim($this->input['country_id']);
}
if ($this->accountGateway->show_shipping_address) {
$client->shipping_address1 = trim($this->input['shipping_address1']);
$client->shipping_address2 = trim($this->input['shipping_address2']);
$client->shipping_city = trim($this->input['shipping_city']);
$client->shipping_state = trim($this->input['shipping_state']);
$client->shipping_postal_code = trim($this->input['shipping_postal_code']);
$client->shipping_country_id = trim($this->input['shipping_country_id']);
}
if ($client->isDirty()) {
$client->save();
}
}
protected function paymentDetails($paymentMethod = false)
{
@ -474,22 +484,23 @@ class BasePaymentDriver
}
if (isset($input['address1'])) {
// TODO use cache instead
$country = Country::find($input['country_id']);
$hasShippingAddress = $this->accountGateway->show_shipping_address;
$country = Utils::getFromCache($input['country_id'], 'countries');
$shippingCountry = $hasShippingAddress ? Utils::getFromCache($input['shipping_country_id'], 'countries') : $country;
$data = array_merge($data, [
'billingAddress1' => $input['address1'],
'billingAddress2' => $input['address2'],
'billingCity' => $input['city'],
'billingState' => $input['state'],
'billingPostcode' => $input['postal_code'],
'billingAddress1' => trim($input['address1']),
'billingAddress2' => trim($input['address2']),
'billingCity' => trim($input['city']),
'billingState' => trim($input['state']),
'billingPostcode' => trim($input['postal_code']),
'billingCountry' => $country->iso_3166_2,
'shippingAddress1' => $input['address1'],
'shippingAddress2' => $input['address2'],
'shippingCity' => $input['city'],
'shippingState' => $input['state'],
'shippingPostcode' => $input['postal_code'],
'shippingCountry' => $country->iso_3166_2,
'shippingAddress1' => $hasShippingAddress ? trim($this->input['shipping_address1']) : trim($input['address1']),
'shippingAddress2' => $hasShippingAddress ? trim($this->input['shipping_address2']) : trim($input['address2']),
'shippingCity' => $hasShippingAddress ? trim($this->input['shipping_city']) : trim($input['city']),
'shippingState' => $hasShippingAddress ? trim($this->input['shipping_state']) : trim($input['state']),
'shippingPostcode' => $hasShippingAddress ? trim($this->input['shipping_postal_code']) : trim($input['postal_code']),
'shippingCountry' => $hasShippingAddress ? $shippingCountry->iso_3166_2 : $country->iso_3166_2,
]);
}
@ -501,6 +512,7 @@ class BasePaymentDriver
$invoice = $this->invoice();
$client = $this->client();
$contact = $this->invitation->contact ?: $client->contacts()->first();
$hasShippingAddress = $this->accountGateway->show_shipping_address;
return [
'email' => $contact->email,
@ -514,12 +526,12 @@ class BasePaymentDriver
'billingState' => $client->state,
'billingCountry' => $client->country ? $client->country->iso_3166_2 : '',
'billingPhone' => $contact->phone,
'shippingAddress1' => $client->address1,
'shippingAddress2' => $client->address2,
'shippingCity' => $client->city,
'shippingPostcode' => $client->postal_code,
'shippingState' => $client->state,
'shippingCountry' => $client->country ? $client->country->iso_3166_2 : '',
'shippingAddress1' => $client->shipping_address1 ? $client->shipping_address1 : $client->address1,
'shippingAddress2' => $client->shipping_address1 ? $client->shipping_address1 : $client->address2,
'shippingCity' => $client->shipping_address1 ? $client->shipping_address1 : $client->city,
'shippingPostcode' => $client->shipping_address1 ? $client->shipping_address1 : $client->postal_code,
'shippingState' => $client->shipping_address1 ? $client->shipping_address1 : $client->state,
'shippingCountry' => $client->shipping_address1 ? ($client->shipping_country ? $client->shipping_country->iso_3166_2 : '') : ($client->country ? $client->country->iso_3166_2 : ''),
'shippingPhone' => $contact->phone,
];
}
@ -867,23 +879,32 @@ class BasePaymentDriver
return $payment;
}
protected function updateClientFromOffsite($transRef, $paymentRef)
{
// do nothing
}
public function completeOffsitePurchase($input)
{
$this->input = $input;
$ref = array_get($this->input, 'token') ?: $this->invitation->transaction_reference;
$transRef = array_get($this->input, 'token') ?: $this->invitation->transaction_reference;
if (method_exists($this->gateway(), 'completePurchase')) {
$details = $this->paymentDetails();
$response = $this->gateway()->completePurchase($details)->send();
$ref = $response->getTransactionReference() ?: $ref;
$paymentRef = $response->getTransactionReference() ?: $transRef;
if ($response->isCancelled()) {
return false;
} elseif (! $response->isSuccessful()) {
throw new Exception($response->getMessage());
}
} else {
$paymentRef = $transRef;
}
$this->updateClientFromOffsite($transRef, $paymentRef);
// check invoice still has balance
if (! floatval($this->invoice()->balance)) {
throw new Exception(trans('texts.payment_error_code', ['code' => 'NB']));
@ -891,12 +912,12 @@ class BasePaymentDriver
// check this isn't a duplicate transaction reference
if (Payment::whereAccountId($this->invitation->account_id)
->whereTransactionReference($ref)
->whereTransactionReference($paymentRef)
->first()) {
throw new Exception(trans('texts.payment_error_code', ['code' => 'DT']));
}
return $this->createPayment($ref);
return $this->createPayment($paymentRef);
}
public function tokenLinks()

View File

@ -41,4 +41,33 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
}
}
protected function updateClientFromOffsite($transRef, $paymentRef)
{
$response = $this->gateway()->fetchCheckout([
'token' => $transRef
])->send();
$data = $response->getData();
$client = $this->client();
if (empty($data['SHIPTOSTREET'])) {
return;
}
$client->shipping_address1 = trim($data['SHIPTOSTREET']);
$client->shipping_address2 = '';
$client->shipping_city = trim($data['SHIPTOCITY']);
$client->shipping_state = trim($data['SHIPTOSTATE']);
$client->shipping_postal_code = trim($data['SHIPTOZIP']);
if ($country = cache('countries')->filter(function ($item) use ($data) {
return strtolower($item->iso_3166_2) == strtolower(trim($data['SHIPTOCOUNTRYCODE']));
})->first()) {
$client->shipping_country_id = $country->id;
} else {
$client->shipping_country_id = null;
}
$client->save();
}
}

View File

@ -53,6 +53,9 @@ class StripePaymentDriver extends BasePaymentDriver
if ($gateway->getAlipayEnabled()) {
$types[] = GATEWAY_TYPE_ALIPAY;
}
if ($gateway->getApplePayEnabled()) {
$types[] = GATEWAY_TYPE_APPLE_PAY;
}
}
return $types;
@ -67,6 +70,10 @@ class StripePaymentDriver extends BasePaymentDriver
{
$rules = parent::rules();
if ($this->isGatewayType(GATEWAY_TYPE_APPLE_PAY)) {
return ['sourceToken' => 'required'];
}
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
$rules['authorize_ach'] = 'required';
}
@ -224,7 +231,9 @@ class StripePaymentDriver extends BasePaymentDriver
// For older users the Stripe account may just have the customer token but not the card version
// In that case we'd use GATEWAY_TYPE_TOKEN even though we're creating the credit card
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD) || $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)
|| $this->isGatewayType(GATEWAY_TYPE_APPLE_PAY)
|| $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
$paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01';
$paymentMethod->payment_type_id = PaymentType::parseCardType($source['brand']);
} elseif ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {

View File

@ -236,4 +236,31 @@ class AccountPresenter extends Presenter
return $data;
}
public function clientLoginUrl()
{
$account = $this->entity;
if (Utils::isNinjaProd()) {
$url = 'https://';
$url .= $account->subdomain ?: 'app';
$url .= '.' . Domain::getDomainFromId($account->domain_id);
} else {
$url = SITE_URL;
}
$url .= '/client/login';
if (Utils::isNinja()) {
if (! $account->subdomain) {
$url .= '?account_key=' . $account->account_key;
}
} else {
if (Account::count() > 1) {
$url .= '?account_key=' . $account->account_key;
}
}
return $url;
}
}

View File

@ -11,6 +11,11 @@ class ClientPresenter extends EntityPresenter
return $this->entity->country ? $this->entity->country->name : '';
}
public function shipping_country()
{
return $this->entity->shipping_country ? $this->entity->shipping_country->name : '';
}
public function balance()
{
$client = $this->entity;
@ -51,6 +56,53 @@ class ClientPresenter extends EntityPresenter
return sprintf('%s: %s %s', trans('texts.payment_terms'), trans('texts.payment_terms_net'), $client->defaultDaysDue());
}
public function address($addressType = ADDRESS_BILLING)
{
$str = '';
$prefix = $addressType == ADDRESS_BILLING ? '' : 'shipping_';
$client = $this->entity;
if ($address1 = $client->{$prefix . 'address1'}) {
$str .= e($address1) . '<br/>';
}
if ($address2 = $client->{$prefix . 'address2'}) {
$str .= e($address2) . '<br/>';
}
if ($cityState = $this->getCityState($addressType)) {
$str .= e($cityState) . '<br/>';
}
if ($country = $client->{$prefix . 'country'}) {
$str .= e($country->name) . '<br/>';
}
if ($str) {
$str = '<b>' . trans('texts.' . $addressType) . '</b><br/>' . $str;
}
return $str;
}
/**
* @return string
*/
public function getCityState($addressType = ADDRESS_BILLING)
{
$client = $this->entity;
$prefix = $addressType == ADDRESS_BILLING ? '' : 'shipping_';
$swap = $client->{$prefix . 'country'} && $client->{$prefix . 'country'}->swap_postal_code;
$city = e($client->{$prefix . 'city'});
$state = e($client->{$prefix . 'state'});
$postalCode = e($client->{$prefix . 'post_code'});
if ($city || $state || $postalCode) {
return Utils::cityStateZip($city, $state, $postalCode, $swap);
} else {
return false;
}
}
/**
* @return string
*/

View File

@ -2,6 +2,7 @@
namespace App\Ninja\Presenters;
use Str;
use stdClass;
class InvoiceItemPresenter extends EntityPresenter
@ -16,4 +17,9 @@ class InvoiceItemPresenter extends EntityPresenter
return $data;
}
public function notes()
{
return Str::limit($this->entity->notes);
}
}

View File

@ -242,6 +242,11 @@ class InvoicePresenter extends EntityPresenter
}
$actions[] = ['url' => url("{$entityType}s/{$entityType}_history/{$invoice->public_id}"), 'label' => trans('texts.view_history')];
if ($entityType == ENTITY_INVOICE) {
$actions[] = ['url' => url("invoices/delivery_note/{$invoice->public_id}"), 'label' => trans('texts.delivery_note')];
}
$actions[] = DropdownButton::DIVIDER;
if ($entityType == ENTITY_QUOTE) {

View File

@ -18,8 +18,8 @@ class ActivityReport extends AbstractReport
{
$account = Auth::user()->account;
$startDate = $this->startDate->format('Y-m-d');
$endDate = $this->endDate->format('Y-m-d');
$startDate = $this->startDate;;
$endDate = $this->endDate;
$activities = Activity::scope()
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'task', 'expense', 'account')
@ -32,7 +32,7 @@ class ActivityReport extends AbstractReport
$activity->present()->createdAt,
$client ? ($this->isExport ? $client->getDisplayName() : $client->present()->link) : '',
$activity->present()->user,
$activity->getMessage(),
$this->isExport ? strip_tags($activity->getMessage()) : $activity->getMessage(),
];
}

View File

@ -38,7 +38,8 @@ class ExpenseReport extends AbstractReport
$zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.expense_documents')));
foreach ($expenses->get() as $expense) {
foreach ($expense->documents as $document) {
$name = sprintf('%s_%s_%s_%s', date('Y-m-d'), trans('texts.expense'), $expense->public_id, $document->name);
$expenseId = str_pad($expense->public_id, $account->invoice_number_padding, '0', STR_PAD_LEFT);
$name = sprintf('%s_%s_%s_%s', date('Y-m-d'), trans('texts.expense'), $expenseId, $document->name);
$name = str_replace(' ', '_', $name);
$zip->add_file($name, $document->getRaw());
}

View File

@ -22,21 +22,17 @@ class InvoiceReport extends AbstractReport
public function run()
{
$account = Auth::user()->account;
$status = $this->options['invoice_status'];
$statusIds = $this->options['status_ids'];
$exportFormat = $this->options['export_format'];
$clients = Client::scope()
->orderBy('name')
->withArchived()
->with('contacts')
->with(['invoices' => function ($query) use ($status) {
if ($status == 'draft') {
$query->whereIsPublic(false);
} elseif (in_array($status, ['paid', 'unpaid', 'sent'])) {
$query->whereIsPublic(true);
}
->with(['invoices' => function ($query) use ($statusIds) {
$query->invoices()
->withArchived()
->statusIds($statusIds)
->where('invoice_date', '>=', $this->startDate)
->where('invoice_date', '<=', $this->endDate)
->with(['payments' => function ($query) {
@ -65,17 +61,12 @@ class InvoiceReport extends AbstractReport
foreach ($client->invoices as $invoice) {
$payments = count($invoice->payments) ? $invoice->payments : [false];
foreach ($payments as $payment) {
if (! $payment && $status == 'paid') {
continue;
} elseif ($payment && $status == 'unpaid') {
continue;
}
$this->data[] = [
$this->isExport ? $client->getDisplayName() : $client->present()->link,
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
$invoice->present()->invoice_date,
$account->formatMoney($invoice->amount, $client),
$invoice->present()->status(),
$invoice->statusLabel(),
$payment ? $payment->present()->payment_date : '',
$payment ? $account->formatMoney($payment->getCompletedAmount(), $client) : '',
$payment ? $payment->present()->method : '',

View File

@ -4,6 +4,7 @@ namespace App\Ninja\Reports;
use App\Models\Payment;
use Auth;
use Utils;
class PaymentReport extends AbstractReport
{
@ -20,6 +21,7 @@ class PaymentReport extends AbstractReport
public function run()
{
$account = Auth::user()->account;
$currencyType = $this->options['currency_type'];
$invoiceMap = [];
$payments = Payment::scope()
@ -39,22 +41,36 @@ class PaymentReport extends AbstractReport
foreach ($payments->get() as $payment) {
$invoice = $payment->invoice;
$client = $payment->client;
$amount = $payment->getCompletedAmount();
if ($currencyType == 'converted') {
$amount *= $payment->exchange_rate;
$this->addToTotals($payment->exchange_currency_id, 'paid', $amount);
$amount = Utils::formatMoney($amount, $payment->exchange_currency_id);
} else {
$this->addToTotals($client->currency_id, 'paid', $amount);
$amount = $account->formatMoney($amount, $client);
}
$this->data[] = [
$this->isExport ? $client->getDisplayName() : $client->present()->link,
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
$invoice->present()->invoice_date,
$account->formatMoney($invoice->amount, $client),
$payment->present()->payment_date,
$account->formatMoney($payment->getCompletedAmount(), $client),
$amount,
$payment->present()->method,
];
if (! isset($invoiceMap[$invoice->id])) {
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
$invoiceMap[$invoice->id] = true;
}
$this->addToTotals($client->currency_id, 'paid', $payment->getCompletedAmount());
if ($currencyType == 'converted') {
$this->addToTotals($payment->exchange_currency_id, 'amount', $invoice->amount * $payment->exchange_rate);
} else {
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
}
}
}
}
}

View File

@ -13,6 +13,7 @@ class ProductReport extends AbstractReport
'invoice_number',
'invoice_date',
'product',
'description',
'qty',
'cost',
//'tax_rate1',
@ -22,20 +23,16 @@ class ProductReport extends AbstractReport
public function run()
{
$account = Auth::user()->account;
$status = $this->options['invoice_status'];
$statusIds = $this->options['status_ids'];
$clients = Client::scope()
->orderBy('name')
->withArchived()
->with('contacts')
->with(['invoices' => function ($query) use ($status) {
if ($status == 'draft') {
$query->whereIsPublic(false);
} elseif (in_array($status, ['paid', 'unpaid', 'sent'])) {
$query->whereIsPublic(true);
}
->with(['invoices' => function ($query) use ($statusIds) {
$query->invoices()
->withArchived()
->statusIds($statusIds)
->where('invoice_date', '>=', $this->startDate)
->where('invoice_date', '<=', $this->endDate)
->with(['invoice_items']);
@ -43,17 +40,13 @@ class ProductReport extends AbstractReport
foreach ($clients->get() as $client) {
foreach ($client->invoices as $invoice) {
if (! $invoice->isPaid() && $status == 'paid') {
continue;
} elseif ($invoice->isPaid() && $status == 'unpaid') {
continue;
}
foreach ($invoice->invoice_items as $item) {
$this->data[] = [
$this->isExport ? $client->getDisplayName() : $client->present()->link,
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
$invoice->present()->invoice_date,
$item->product_key,
$this->isExport ? $item->notes : $item->present()->notes,
Utils::roundSignificant($item->qty, 0),
Utils::roundSignificant($item->cost, 2),
];

View File

@ -59,7 +59,7 @@ class ProfitAndLossReport extends AbstractReport
$this->data[] = [
trans('texts.expense'),
$client ? ($this->isExport ? $client->getDisplayName() : $client->present()->link) : '',
$expense->present()->amount,
'-' . $expense->present()->amount,
$expense->present()->expense_date,
$expense->present()->category,
];

View File

@ -60,6 +60,7 @@ class AccountRepository
$account->ip = Request::getClientIp();
$account->account_key = strtolower(str_random(RANDOM_KEY_LENGTH));
$account->company_id = $company->id;
$account->currency_id = DEFAULT_CURRENCY;
// Set default language/currency based on IP
if (\Cache::get('currencies')) {
@ -151,12 +152,17 @@ class AccountRepository
if ($user->hasPermission('view_all')) {
$clients = Client::scope()
->with('contacts', 'invoices')
->get();
->withArchived()
->with(['contacts', 'invoices' => function ($query) use ($user) {
$query->withArchived();
}])->get();
} else {
$clients = Client::scope()
->where('user_id', '=', $user->id)
->withArchived()
->with(['contacts', 'invoices' => function ($query) use ($user) {
$query->where('user_id', '=', $user->id);
$query->withArchived()
->where('user_id', '=', $user->id);
}])->get();
}

View File

@ -159,7 +159,7 @@ class DashboardRepository
$records->select(DB::raw('sum(expenses.amount + (expenses.amount * expenses.tax_rate1 / 100) + (expenses.amount * expenses.tax_rate2 / 100)) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy));
}
return $records->get();
return $records->get()->all();
}
public function totals($accountId, $userId, $viewAll)

View File

@ -607,10 +607,12 @@ class InvoiceRepository extends BaseRepository
$total += $invoice->custom_value2;
}
if (! $account->inclusive_taxes) {
$taxAmount1 = round($total * ($invoice->tax_rate1 ? $invoice->tax_rate1 : 0) / 100, 2);
$taxAmount2 = round($total * ($invoice->tax_rate2 ? $invoice->tax_rate2 : 0) / 100, 2);
$total = round($total + $taxAmount1 + $taxAmount2, 2);
$total += $itemTax;
}
// custom fields not charged taxes
if ($invoice->custom_value1 && ! $invoice->custom_taxes1) {
@ -1174,7 +1176,10 @@ class InvoiceRepository extends BaseRepository
$sql = implode(' OR ', $dates);
$invoices = Invoice::invoiceType(INVOICE_TYPE_STANDARD)
->with('invoice_items')
->with('client', 'invoice_items')
->whereHas('client', function ($query) {
$query->whereSendReminders(true);
})
->whereAccountId($account->id)
->where('balance', '>', 0)
->where('is_recurring', '=', false)

View File

@ -64,6 +64,8 @@ class PaymentRepository extends BaseRepository
'payments.routing_number',
'payments.bank_name',
'payments.private_notes',
'payments.exchange_rate',
'payments.exchange_currency_id',
'invoices.is_deleted as invoice_is_deleted',
'gateways.name as gateway_name',
'gateways.id as gateway_id',
@ -187,12 +189,7 @@ class PaymentRepository extends BaseRepository
$payment->payment_date = date('Y-m-d');
}
if (isset($input['transaction_reference'])) {
$payment->transaction_reference = trim($input['transaction_reference']);
}
if (isset($input['private_notes'])) {
$payment->private_notes = trim($input['private_notes']);
}
$payment->fill($input);
if (! $publicId) {
$clientId = $input['client_id'];

View File

@ -0,0 +1,29 @@
<?php
namespace App\Ninja\Repositories;
use App\Models\Subscription;
use DB;
class SubscriptionRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\Subscription';
}
public function find($accountId)
{
$query = DB::table('subscriptions')
->where('subscriptions.account_id', '=', $accountId)
->whereNull('subscriptions.deleted_at')
->select(
'subscriptions.public_id',
'subscriptions.target_url as target',
'subscriptions.event_id as event',
'subscriptions.deleted_at'
);
return $query;
}
}

View File

@ -8,6 +8,7 @@ use App\Models\Task;
use Auth;
use Session;
use DB;
use Utils;
class TaskRepository extends BaseRepository
{
@ -101,6 +102,38 @@ class TaskRepository extends BaseRepository
return $query;
}
public function getClientDatatable($clientId)
{
$query = DB::table('tasks')
->leftJoin('projects', 'projects.id', '=', 'tasks.project_id')
->where('tasks.client_id', '=', $clientId)
->where('tasks.is_deleted', '=', false)
->whereNull('tasks.invoice_id')
->select(
'tasks.description',
'tasks.time_log',
'tasks.time_log as duration',
DB::raw("SUBSTRING(time_log, 3, 10) date"),
'projects.name as project'
);
$table = \Datatable::query($query)
->addColumn('project', function ($model) {
return $model->project;
})
->addColumn('date', function ($model) {
return Task::calcStartTime($model);
})
->addColumn('duration', function ($model) {
return Utils::formatTime(Task::calcDuration($model));
})
->addColumn('description', function ($model) {
return $model->description;
});
return $table->make();
}
public function save($publicId, $data, $task = null)
{
if ($task) {

View File

@ -43,6 +43,12 @@ class AccountEmailSettingsTransformer extends EntityTransformer
'email_template_reminder1' => $settings->email_template_reminder1,
'email_template_reminder2' => $settings->email_template_reminder2,
'email_template_reminder3' => $settings->email_template_reminder3,
'late_fee1_amount' => $settings->late_fee1_amount,
'late_fee1_percent' => $settings->late_fee1_percent,
'late_fee2_amount' => $settings->late_fee2_amount,
'late_fee2_percent' => $settings->late_fee2_percent,
'late_fee3_amount' => $settings->late_fee3_amount,
'late_fee3_percent' => $settings->late_fee3_percent,
];
}
}

View File

@ -275,6 +275,7 @@ class AccountTransformer extends EntityTransformer
'custom_contact_label1' => $account->custom_contact_label1,
'custom_contact_label2' => $account->custom_contact_label2,
'task_rate' => (float) $account->task_rate,
'inclusive_taxes' => (bool) $account->inclusive_taxes,
];
}
}

View File

@ -38,6 +38,15 @@ class ClientTransformer extends EntityTransformer
* @SWG\Property(property="id_number", type="string", example="123456")
* @SWG\Property(property="language_id", type="integer", example=1)
* @SWG\Property(property="task_rate", type="number", format="float", example=10)
* @SWG\Property(property="shipping_address1", type="string", example="10 Main St.")
* @SWG\Property(property="shipping_address2", type="string", example="1st Floor")
* @SWG\Property(property="shipping_city", type="string", example="New York")
* @SWG\Property(property="shipping_state", type="string", example="NY")
* @SWG\Property(property="shipping_postal_code", type="string", example=10010)
* @SWG\Property(property="shipping_country_id", type="integer", example=840)
* @SWG\Property(property="show_tasks_in_portal", type="boolean", example=false)
* @SWG\Property(property="send_reminders", type="boolean", example=false)
* @SWG\Property(property="credit_number_counter", type="integer", example=1)
*/
protected $defaultIncludes = [
'contacts',
@ -137,6 +146,15 @@ class ClientTransformer extends EntityTransformer
'invoice_number_counter' => (int) $client->invoice_number_counter,
'quote_number_counter' => (int) $client->quote_number_counter,
'task_rate' => (float) $client->task_rate,
'shipping_address1' => $client->shipping_address1,
'shipping_address2' => $client->shipping_address2,
'shipping_city' => $client->shipping_city,
'shipping_state' => $client->shipping_state,
'shipping_postal_code' => $client->shipping_postal_code,
'shipping_country_id' => (int) $client->shipping_country_id,
'show_tasks_in_portal' => (bool) $client->show_tasks_in_portal,
'send_reminders' => (bool) $client->send_reminders,
'credit_number_counter' => (int) $client->credit_number_counter,
]);
}
}

View File

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

View File

@ -60,6 +60,8 @@ class PaymentTransformer extends EntityTransformer
'invoice_id' => (int) ($this->invoice ? $this->invoice->public_id : $payment->invoice->public_id),
'invoice_number' => $this->invoice ? $this->invoice->invoice_number : $payment->invoice->invoice_number,
'private_notes' => $payment->private_notes,
'exchange_rate' => (float) $payment->exchange_rate,
'exchange_currency_id' => (int) $payment->exchange_currency_id,
]);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Policies;
use App\Models\User;
class SubscriptionPolicy extends EntityPolicy
{
public static function edit(User $user, $item)
{
return $user->hasPermission('admin');
}
public static function create(User $user, $item)
{
return $user->hasPermission('admin');
}
}

View File

@ -10,6 +10,8 @@ use Utils;
use Validator;
use Queue;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\Facades\Route;
/**
* Class AppServiceProvider.
@ -23,6 +25,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
Route::singularResourceParameters(false);
// support selecting job database
Queue::before(function (JobProcessing $event) {
$body = $event->job->getRawBody();

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use Gate;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -28,6 +29,7 @@ class AuthServiceProvider extends ServiceProvider
\App\Models\TaxRate::class => \App\Policies\TaxRatePolicy::class,
\App\Models\AccountGateway::class => \App\Policies\AccountGatewayPolicy::class,
\App\Models\AccountToken::class => \App\Policies\TokenPolicy::class,
\App\Models\Subscription::class => \App\Policies\SubscriptionPolicy::class,
\App\Models\BankAccount::class => \App\Policies\BankAccountPolicy::class,
\App\Models\PaymentTerm::class => \App\Policies\PaymentTermPolicy::class,
\App\Models\Project::class => \App\Policies\ProjectPolicy::class,
@ -41,12 +43,12 @@ class AuthServiceProvider extends ServiceProvider
*
* @return void
*/
public function boot(GateContract $gate)
public function boot()
{
foreach (get_class_methods(new \App\Policies\GenericEntityPolicy()) as $method) {
$gate->define($method, "App\Policies\GenericEntityPolicy@{$method}");
Gate::define($method, "App\Policies\GenericEntityPolicy@{$method}");
}
$this->registerPolicies($gate);
$this->registerPolicies();
}
}

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
@ -222,9 +221,9 @@ class EventServiceProvider extends ServiceProvider
*
* @return void
*/
public function boot(DispatcherContract $events)
public function boot()
{
parent::boot($events);
parent::boot();
//
}

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