Merge branch 'release-3.9.0'

This commit is contained in:
Hillel Coren 2017-11-08 20:34:45 +02:00
commit 6bb62d1046
216 changed files with 6986 additions and 3788 deletions

View File

@ -99,3 +99,9 @@ WEPAY_THEME='{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":
BLUEVINE_PARTNER_UNIQUE_ID= BLUEVINE_PARTNER_UNIQUE_ID=
BLUEVINE_PARTNER_TOKEN= BLUEVINE_PARTNER_TOKEN=
CLOUDFLARE_DNS_ENABLED=false
CLOUDFLARE_API_KEY=
CLOUDFLARE_EMAIL=
CLOUDFLARE_TARGET_IP_ADDRESS=
CLOUDFLARE_ZONE_IDS={}

View File

@ -119,6 +119,7 @@ after_script:
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr) - FILES=$(find tests/_output -type f -name '*.png' | sort -nr)
- for i in $FILES; do echo $i; base64 "$i"; break; done - for i in $FILES; do echo $i; base64 "$i"; break; done
notifications: notifications:
email: email:
on_success: never on_success: never

View File

@ -34,152 +34,10 @@ module.exports = function(grunt) {
// Return the computed object // Return the computed object
return out; return out;
}()), }())
concat: {
options: {
process: function(src, filepath) {
var basepath = filepath.substring(7, filepath.lastIndexOf('/') + 1);
// Fix relative paths for css files
if(filepath.indexOf('.css', filepath.length - 4) !== -1) {
return src.replace(/(url\s*[\("']+)\s*([^'"\)]+)(['"\)]+;?)/gi, function(match, start, url, end, offset, string) {
if(url.indexOf('data:') === 0) {
// Skip data urls
return match;
} else if(url.indexOf('/') === 0) {
// Skip absolute urls
return match;
} else {
return start + basepath + url + end;
}
});
// Fix source maps locations
} else if(filepath.indexOf('.js', filepath.length - 4) !== -1) {
return src.replace(/(\/[*\/][#@]\s*sourceMappingURL=)([^\s]+)/gi, function(match, start, url, offset, string) {
if(url.indexOf('/') === 0) {
// Skip absolute urls
return match;
} else {
return start + basepath + url;
}
});
// Don't do anything for unknown file types
} else {
return src;
}
},
},
js: {
src: [
'public/vendor/jquery/dist/jquery.js',
'public/vendor/jquery-ui/jquery-ui.min.js',
'public/vendor/bootstrap/dist/js/bootstrap.min.js',
'public/vendor/datatables/media/js/jquery.dataTables.js',
'public/vendor/datatables-bootstrap3/BS3/assets/js/datatables.js',
'public/vendor/knockout.js/knockout.js',
'public/vendor/knockout-mapping/build/output/knockout.mapping-latest.js',
'public/vendor/knockout-sortable/build/knockout-sortable.min.js',
'public/vendor/underscore/underscore.js',
'public/vendor/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.de.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.da.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.pt-BR.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.nl.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.fr.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.it.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.lt.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.no.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.es.min.js',
'public/vendor/bootstrap-datepicker/dist/locales/bootstrap-datepicker.sv.min.js',
'public/vendor/dropzone/dist/min/dropzone.min.js',
'public/vendor/typeahead.js/dist/typeahead.jquery.min.js',
'public/vendor/accounting/accounting.min.js',
'public/vendor/spectrum/spectrum.js',
'public/vendor/jspdf/dist/jspdf.min.js',
'public/vendor/moment/min/moment.min.js',
'public/vendor/moment-timezone/builds/moment-timezone-with-data.min.js',
'public/vendor/stacktrace-js/dist/stacktrace-with-polyfills.min.js',
'public/vendor/fuse.js/src/fuse.min.js',
'public/vendor/sweetalert/dist/sweetalert.min.js',
//'public/vendor/moment-duration-format/lib/moment-duration-format.js',
//'public/vendor/pdfmake/build/pdfmake.min.js',
//'public/vendor/pdfmake/build/vfs_fonts.js',
//'public/js/vfs_fonts.js',
'public/js/bootstrap-combobox.js',
'public/js/script.js',
'public/js/pdf.pdfmake.js',
],
dest: 'public/built.js',
nonull: true
},
/*js_public: {
src: [
'public/js/simpleexpand.js',
'public/js/valign.js',
'public/js/bootstrap.min.js',
'public/js/simpleexpand.js',
'public/vendor/bootstrap/dist/js/bootstrap.min.js',
'public/js/bootstrap-combobox.js',
],
dest: 'public/built.public.js',
nonull: true
},
css: {
src: [
'public/vendor/bootstrap/dist/css/bootstrap.min.css',
'public/vendor/datatables/media/css/jquery.dataTables.css',
'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css',
'public/vendor/font-awesome/css/font-awesome.min.css',
'public/vendor/bootstrap-datepicker/dist/css/bootstrap-datepicker3.css',
'public/vendor/dropzone/dist/min/dropzone.min.css',
'public/vendor/spectrum/spectrum.css',
'public/css/bootstrap-combobox.css',
'public/css/typeahead.js-bootstrap.css',
'public/vendor/sweetalert/dist/sweetalert.css',
'public/css/style.css',
],
dest: 'public/css/built.css',
nonull: true,
options: {
process: false
}
},*/
/*css_public: {
src: [
'public/vendor/bootstrap/dist/css/bootstrap.min.css',
'public/vendor/font-awesome/css/font-awesome.min.css',
'public/css/bootstrap-combobox.css',
'public/vendor/datatables/media/css/jquery.dataTables.css',
'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css',
'public/css/public.style.css',
],
dest: 'public/css/built.public.css',
nonull: true,
options: {
process: false
}
},*/
/*js_pdf: {
src: [
'public/js/pdf_viewer.js',
'public/js/compatibility.js',
'public/js/pdfmake.min.js',
'public/js/vfs.js',
],
dest: 'public/pdf.built.js',
nonull: true
}*/
}
}); });
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-dump-dir'); grunt.loadNpmTasks('grunt-dump-dir');
grunt.registerTask('default', ['dump_dir']);
grunt.registerTask('default', ['dump_dir', 'concat']);
}; };

View File

@ -59,7 +59,7 @@ class ChargeRenewalInvoices extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d').' ChargeRenewalInvoices...'); $this->info(date('r').' ChargeRenewalInvoices...');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {
config(['database.default' => $database]); config(['database.default' => $database]);

View File

@ -42,6 +42,10 @@ Options:
By default the script only checks for errors, adding this option By default the script only checks for errors, adding this option
makes the script apply the fixes. makes the script apply the fixes.
--fast=true
Skip using phantomjs
*/ */
/** /**
@ -144,7 +148,7 @@ class CheckData extends Command
return; return;
} }
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true' || $this->option('fast') == 'true') {
return; return;
} }
@ -792,6 +796,7 @@ class CheckData extends Command
{ {
return [ return [
['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null], ['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null],
['fast', null, InputOption::VALUE_OPTIONAL, 'Fast', null],
['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null], ['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null], ['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
]; ];

View File

@ -83,7 +83,7 @@ class CreateTestData extends Command
return false; return false;
} }
$this->info(date('Y-m-d').' Running CreateTestData...'); $this->info(date('r').' Running CreateTestData...');
$this->count = $this->argument('count'); $this->count = $this->argument('count');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {

View File

@ -23,7 +23,7 @@ class PruneData extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d').' Running PruneData...'); $this->info(date('r').' Running PruneData...');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {
config(['database.default' => $database]); config(['database.default' => $database]);

View File

@ -23,7 +23,7 @@ class RemoveOrphanedDocuments extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d').' Running RemoveOrphanedDocuments...'); $this->info(date('r').' Running RemoveOrphanedDocuments...');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {
config(['database.default' => $database]); config(['database.default' => $database]);

View File

@ -23,7 +23,7 @@ class ResetData extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d') . ' Running ResetData...'); $this->info(date('r') . ' Running ResetData...');
if (! Utils::isNinjaDev()) { if (! Utils::isNinjaDev()) {
return; return;

View File

@ -65,7 +65,7 @@ class SendRecurringInvoices extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d H:i:s') . ' Running SendRecurringInvoices...'); $this->info(date('r') . ' Running SendRecurringInvoices...');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {
config(['database.default' => $database]); config(['database.default' => $database]);
@ -76,7 +76,7 @@ class SendRecurringInvoices extends Command
$this->billInvoices(); $this->billInvoices();
$this->createExpenses(); $this->createExpenses();
$this->info(date('Y-m-d H:i:s') . ' Done'); $this->info(date('r') . ' Done');
} }
private function resetCounters() private function resetCounters()

View File

@ -57,7 +57,7 @@ class SendReminders extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d') . ' Running SendReminders...'); $this->info(date('r') . ' Running SendReminders...');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {
config(['database.default' => $database]); config(['database.default' => $database]);

View File

@ -50,7 +50,7 @@ class SendRenewalInvoices extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d').' Running SendRenewalInvoices...'); $this->info(date('r').' Running SendRenewalInvoices...');
if ($database = $this->option('database')) { if ($database = $this->option('database')) {
config(['database.default' => $database]); config(['database.default' => $database]);

View File

@ -39,6 +39,6 @@ class TestOFX extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d').' Running TestOFX...'); $this->info(date('r').' Running TestOFX...');
} }
} }

View File

@ -27,10 +27,10 @@ class UpdateKey extends Command
public function fire() public function fire()
{ {
$this->info(date('Y-m-d h:i:s') . ' Running UpdateKey...'); $this->info(date('r') . ' Running UpdateKey...');
if (! env('APP_KEY') || ! env('APP_CIPHER')) { if (! env('APP_KEY') || ! env('APP_CIPHER')) {
$this->info(date('Y-m-d h:i:s') . ' Error: app key and cipher are not set'); $this->info(date('r') . ' Error: app key and cipher are not set');
exit; exit;
} }
@ -73,9 +73,9 @@ class UpdateKey extends Command
} }
if ($envWriteable) { if ($envWriteable) {
$this->info(date('Y-m-d h:i:s') . ' Successfully update the key'); $this->info(date('r') . ' Successfully update the key');
} else { } else {
$this->info(date('Y-m-d h:i:s') . ' Successfully update data, make sure to set the new app key: ' . $key); $this->info(date('r') . ' Successfully update data, make sure to set the new app key: ' . $key);
} }
} }

View File

@ -224,8 +224,9 @@ if (! defined('APP_NAME')) {
define('FREQUENCY_MONTHLY', 4); define('FREQUENCY_MONTHLY', 4);
define('FREQUENCY_TWO_MONTHS', 5); define('FREQUENCY_TWO_MONTHS', 5);
define('FREQUENCY_THREE_MONTHS', 6); define('FREQUENCY_THREE_MONTHS', 6);
define('FREQUENCY_SIX_MONTHS', 7); define('FREQUENCY_FOUR_MONTHS', 7);
define('FREQUENCY_ANNUALLY', 8); define('FREQUENCY_SIX_MONTHS', 8);
define('FREQUENCY_ANNUALLY', 9);
define('SESSION_TIMEZONE', 'timezone'); define('SESSION_TIMEZONE', 'timezone');
define('SESSION_CURRENCY', 'currency'); define('SESSION_CURRENCY', 'currency');
@ -309,7 +310,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest')); define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01'); define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '3.8.1' . env('NINJA_VERSION_SUFFIX')); define('NINJA_VERSION', '3.9.0' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@ -408,6 +409,8 @@ if (! defined('APP_NAME')) {
define('PAYMENT_TYPE_ALIPAY', 28); define('PAYMENT_TYPE_ALIPAY', 28);
define('PAYMENT_TYPE_SOFORT', 29); define('PAYMENT_TYPE_SOFORT', 29);
define('PAYMENT_TYPE_SEPA', 30); define('PAYMENT_TYPE_SEPA', 30);
define('PAYMENT_TYPE_GOCARDLESS', 31);
define('PAYMENT_TYPE_BITCOIN', 32);
define('PAYMENT_METHOD_STATUS_NEW', 'new'); define('PAYMENT_METHOD_STATUS_NEW', 'new');
define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed'); define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed');
@ -422,6 +425,7 @@ if (! defined('APP_NAME')) {
define('GATEWAY_TYPE_ALIPAY', 7); define('GATEWAY_TYPE_ALIPAY', 7);
define('GATEWAY_TYPE_SOFORT', 8); define('GATEWAY_TYPE_SOFORT', 8);
define('GATEWAY_TYPE_SEPA', 9); define('GATEWAY_TYPE_SEPA', 9);
define('GATEWAY_TYPE_GOCARDLESS', 10);
define('GATEWAY_TYPE_TOKEN', 'token'); define('GATEWAY_TYPE_TOKEN', 'token');
define('TEMPLATE_INVOICE', 'invoice'); define('TEMPLATE_INVOICE', 'invoice');
@ -552,6 +556,8 @@ if (! defined('APP_NAME')) {
define('INVOICE_FIELDS_CLIENT', 'client_fields'); define('INVOICE_FIELDS_CLIENT', 'client_fields');
define('INVOICE_FIELDS_INVOICE', 'invoice_fields'); define('INVOICE_FIELDS_INVOICE', 'invoice_fields');
define('INVOICE_FIELDS_ACCOUNT', 'account_fields'); define('INVOICE_FIELDS_ACCOUNT', 'account_fields');
define('INVOICE_FIELDS_PRODUCT', 'product_fields');
define('INVOICE_FIELDS_TASK', 'task_fields');
$creditCards = [ $creditCards = [
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],

View File

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

View File

@ -6,6 +6,7 @@ use App\Events\UserSignedUp;
use App\Http\Requests\RegisterRequest; use App\Http\Requests\RegisterRequest;
use App\Http\Requests\UpdateAccountRequest; use App\Http\Requests\UpdateAccountRequest;
use App\Models\Account; use App\Models\Account;
use App\Models\User;
use App\Ninja\OAuth\OAuth; use App\Ninja\OAuth\OAuth;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Transformers\AccountTransformer; use App\Ninja\Transformers\AccountTransformer;
@ -46,7 +47,7 @@ class AccountApiController extends BaseAPIController
$account = $this->accountRepo->create($request->first_name, $request->last_name, $request->email, $request->password); $account = $this->accountRepo->create($request->first_name, $request->last_name, $request->email, $request->password);
$user = $account->users()->first(); $user = $account->users()->first();
Auth::login($user, true); Auth::login($user);
event(new UserSignedUp()); event(new UserSignedUp());
return $this->processLogin($request); return $this->processLogin($request);
@ -54,11 +55,26 @@ class AccountApiController extends BaseAPIController
public function login(Request $request) public function login(Request $request)
{ {
$user = User::where('email', '=', $request->email)->first();
if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
sleep(ERROR_DELAY);
return $this->errorResponse(['message' => 'Invalid credentials'], 401);
}
if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) { if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) {
if ($user && $user->failed_logins > 0) {
$user->failed_logins = 0;
$user->save();
}
return $this->processLogin($request); return $this->processLogin($request);
} else { } else {
error_log('login failed');
if ($user) {
$user->failed_logins = $user->failed_logins + 1;
$user->save();
}
sleep(ERROR_DELAY); sleep(ERROR_DELAY);
return $this->errorResponse(['message' => 'Invalid credentials'], 401); return $this->errorResponse(['message' => 'Invalid credentials'], 401);
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\SubdomainWasUpdated;
use App\Events\UserSettingsChanged; use App\Events\UserSettingsChanged;
use App\Events\UserSignedUp; use App\Events\UserSignedUp;
use App\Http\Requests\SaveClientPortalSettings; use App\Http\Requests\SaveClientPortalSettings;
@ -768,7 +769,12 @@ class AccountController extends BaseController
*/ */
public function saveClientPortalSettings(SaveClientPortalSettings $request) public function saveClientPortalSettings(SaveClientPortalSettings $request)
{ {
$account = $request->user()->account; $account = $request->user()->account;
if($account->subdomain !== $request->subdomain)
event(new SubdomainWasUpdated($account));
$account->fill($request->all()); $account->fill($request->all());
$account->client_view_css = $request->client_view_css; $account->client_view_css = $request->client_view_css;
$account->subdomain = $request->subdomain; $account->subdomain = $request->subdomain;
@ -1123,6 +1129,11 @@ class AccountController extends BaseController
} }
$rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id']; $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
if ($user->google_2fa_secret) {
$rules['phone'] = 'required';
}
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) { if ($validator->fails()) {
@ -1144,6 +1155,10 @@ class AccountController extends BaseController
$user->notify_approved = Input::get('notify_approved'); $user->notify_approved = Input::get('notify_approved');
} }
if ($user->google_2fa_secret && ! Input::get('enable_two_factor')) {
$user->google_2fa_secret = null;
}
if (Utils::isNinja()) { if (Utils::isNinja()) {
if (Input::get('referral_code') && ! $user->referral_code) { if (Input::get('referral_code') && ! $user->referral_code) {
$user->referral_code = strtolower(str_random(RANDOM_KEY_LENGTH)); $user->referral_code = strtolower(str_random(RANDOM_KEY_LENGTH));

View File

@ -209,7 +209,8 @@ class AccountGatewayController extends BaseController
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) { if ($validator->fails()) {
return Redirect::to('gateways/create?other_providers=' . ($gatewayId == GATEWAY_WEPAY ? 'false' : 'true')) $url = $accountGatewayPublicId ? "/gateways/{$accountGatewayPublicId}/edit" : 'gateways/create?other_providers=' . ($gatewayId == GATEWAY_WEPAY ? 'false' : 'true');
return Redirect::to($url)
->withErrors($validator) ->withErrors($validator)
->withInput(); ->withInput();
} else { } else {
@ -294,6 +295,8 @@ class AccountGatewayController extends BaseController
if ($gatewayId == GATEWAY_STRIPE) { if ($gatewayId == GATEWAY_STRIPE) {
$config->enableAlipay = boolval(Input::get('enable_alipay')); $config->enableAlipay = boolval(Input::get('enable_alipay'));
$config->enableSofort = boolval(Input::get('enable_sofort')); $config->enableSofort = boolval(Input::get('enable_sofort'));
$config->enableSepa = boolval(Input::get('enable_sepa'));
$config->enableBitcoin = boolval(Input::get('enable_bitcoin'));
} }
if ($gatewayId == GATEWAY_STRIPE || $gatewayId == GATEWAY_WEPAY) { if ($gatewayId == GATEWAY_STRIPE || $gatewayId == GATEWAY_WEPAY) {

View File

@ -14,6 +14,9 @@ use Illuminate\Http\Request;
use Lang; use Lang;
use Session; use Session;
use Utils; use Utils;
use Cache;
use Illuminate\Contracts\Auth\Authenticatable;
use App\Http\Requests\ValidateTwoFactorRequest;
class AuthController extends Controller class AuthController extends Controller
{ {
@ -151,15 +154,12 @@ class AuthController extends Controller
if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) { if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
Session::flash('error', trans('texts.invalid_credentials')); Session::flash('error', trans('texts.invalid_credentials'));
return redirect()->to('login'); return redirect()->to('login');
} }
$response = self::postLogin($request); $response = self::postLogin($request);
if (Auth::check()) { if (Auth::check()) {
Event::fire(new UserLoggedIn());
/* /*
$users = false; $users = false;
// we're linking a new account // we're linking a new account
@ -171,10 +171,8 @@ class AuthController extends Controller
$users = $this->accountRepo->loadAccounts(Auth::user()->id); $users = $this->accountRepo->loadAccounts(Auth::user()->id);
} }
*/ */
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
Session::put(SESSION_USER_ACCOUNTS, $users);
} elseif ($user) { } elseif ($user) {
error_log('login failed');
$user->failed_logins = $user->failed_logins + 1; $user->failed_logins = $user->failed_logins + 1;
$user->save(); $user->save();
} }
@ -182,6 +180,60 @@ class AuthController extends Controller
return $response; 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 * @return \Illuminate\Http\Response
*/ */

View File

@ -2,6 +2,8 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use Event;
use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
@ -18,7 +20,9 @@ class PasswordController extends Controller
| |
*/ */
use ResetsPasswords; use ResetsPasswords {
getResetSuccessResponse as protected traitGetResetSuccessResponse;
}
/** /**
* @var string * @var string
@ -49,4 +53,18 @@ class PasswordController extends Controller
return $this->getEmail(); return $this->getEmail();
} }
protected function getResetSuccessResponse($response)
{
$user = auth()->user();
if ($user->google_2fa_secret) {
auth()->logout();
session(['2fa:user:id' => $user->id]);
return redirect('/validate_two_factor/' . $user->account->account_key);
} else {
Event::fire(new UserLoggedIn());
return $this->traitGetResetSuccessResponse($response);
}
}
} }

View File

@ -83,6 +83,7 @@ class ClientPortalController extends BaseController
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
$invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date);
$invoice->partial_due_date = Utils::fromSqlDate($invoice->partial_due_date);
$invoice->features = [ $invoice->features = [
'customize_invoice_design' => $account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), 'customize_invoice_design' => $account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN),
'remove_created_by' => $account->hasFeature(FEATURE_REMOVE_CREATED_BY), 'remove_created_by' => $account->hasFeature(FEATURE_REMOVE_CREATED_BY),
@ -346,6 +347,7 @@ class ClientPortalController extends BaseController
'title' => trans('texts.recurring_invoices'), 'title' => trans('texts.recurring_invoices'),
'entityType' => ENTITY_RECURRING_INVOICE, 'entityType' => ENTITY_RECURRING_INVOICE,
'columns' => Utils::trans($columns), 'columns' => Utils::trans($columns),
'sortColumn' => 1,
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);
@ -373,6 +375,7 @@ class ClientPortalController extends BaseController
'title' => trans('texts.invoices'), 'title' => trans('texts.invoices'),
'entityType' => ENTITY_INVOICE, 'entityType' => ENTITY_INVOICE,
'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date', 'status']), 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date', 'status']),
'sortColumn' => 1,
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);
@ -417,6 +420,7 @@ class ClientPortalController extends BaseController
'entityType' => ENTITY_PAYMENT, 'entityType' => ENTITY_PAYMENT,
'title' => trans('texts.payments'), 'title' => trans('texts.payments'),
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status']), 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status']),
'sortColumn' => 4,
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);
@ -501,6 +505,7 @@ class ClientPortalController extends BaseController
'title' => trans('texts.quotes'), 'title' => trans('texts.quotes'),
'entityType' => ENTITY_QUOTE, 'entityType' => ENTITY_QUOTE,
'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date', 'status']), 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date', 'status']),
'sortColumn' => 1,
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);
@ -536,6 +541,7 @@ class ClientPortalController extends BaseController
'title' => trans('texts.credits'), 'title' => trans('texts.credits'),
'entityType' => ENTITY_CREDIT, 'entityType' => ENTITY_CREDIT,
'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance', 'notes']), 'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance', 'notes']),
'sortColumn' => 0,
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);
@ -571,6 +577,7 @@ class ClientPortalController extends BaseController
'title' => trans('texts.documents'), 'title' => trans('texts.documents'),
'entityType' => ENTITY_DOCUMENT, 'entityType' => ENTITY_DOCUMENT,
'columns' => Utils::trans(['invoice_number', 'name', 'document_date', 'document_size']), 'columns' => Utils::trans(['invoice_number', 'name', 'document_date', 'document_size']),
'sortColumn' => 2,
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);

View File

@ -149,9 +149,16 @@ class HomeController extends BaseController
$subject = 'Customer Message: '; $subject = 'Customer Message: ';
if (Utils::isNinjaProd()) { if (Utils::isNinjaProd()) {
$subject .= str_replace('db-', '', config('database.default')); $subject .= str_replace('db-', '', config('database.default'));
$account = Auth::user()->account;
if ($account->isEnterprise()) {
$subject .= 'E';
} elseif ($account->isPro()) {
$subject .= 'P';
}
} else { } else {
$subject .= 'Self-Host'; $subject .= 'Self-Host';
} }
$subject .= ' | ' . date('r');
$message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com')) $message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com'))
->from(CONTACT_EMAIL, Auth::user()->present()->fullName) ->from(CONTACT_EMAIL, Auth::user()->present()->fullName)
->replyTo(Auth::user()->email, Auth::user()->present()->fullName) ->replyTo(Auth::user()->email, Auth::user()->present()->fullName)

View File

@ -200,15 +200,13 @@ class InvoiceApiController extends BaseAPIController
if ($isEmailInvoice) { if ($isEmailInvoice) {
if ($payment) { if ($payment) {
app('App\Ninja\Mailers\ContactMailer')->sendPaymentConfirmation($payment); $this->dispatch(new SendPaymentEmail($payment));
//$this->dispatch(new SendPaymentEmail($payment));
} else { } else {
if ($invoice->is_recurring && $recurringInvoice = $this->invoiceRepo->createRecurringInvoice($invoice)) { if ($invoice->is_recurring && $recurringInvoice = $this->invoiceRepo->createRecurringInvoice($invoice)) {
$invoice = $recurringInvoice; $invoice = $recurringInvoice;
} }
$reminder = isset($data['email_type']) ? $data['email_type'] : false; $reminder = isset($data['email_type']) ? $data['email_type'] : false;
app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice, $reminder); $this->dispatch(new SendInvoiceEmail($invoice, auth()->user()->id, $reminder));
//$this->dispatch(new SendInvoiceEmail($invoice));
} }
} }
@ -290,14 +288,23 @@ class InvoiceApiController extends BaseAPIController
private function prepareItem($item) private function prepareItem($item)
{ {
// if only the product key is set we'll load the cost and notes // if only the product key is set we'll load the cost and notes
if (! empty($item['product_key']) && empty($item['cost']) && empty($item['notes'])) { if (! empty($item['product_key'])) {
$product = Product::findProductByKey($item['product_key']); $product = Product::findProductByKey($item['product_key']);
if ($product) { if ($product) {
if (empty($item['cost'])) { $fields = [
$item['cost'] = $product->cost; 'cost',
} 'notes',
if (empty($item['notes'])) { 'custom_value1',
$item['notes'] = $product->notes; 'custom_value2',
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
];
foreach ($fields as $field) {
if (! isset($item[$field])) {
$item[$field] = $product->$field;
}
} }
} }
} }
@ -326,11 +333,13 @@ class InvoiceApiController extends BaseAPIController
$invoice = $recurringInvoice; $invoice = $recurringInvoice;
} }
//$this->dispatch(new SendInvoiceEmail($invoice)); if (config('queue.default') !== 'sync') {
$result = app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice); $this->dispatch(new SendInvoiceEmail($invoice, auth()->user()->id));
} else {
if ($result !== true) { $result = app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);
return $this->errorResponse($result, 500); if ($result !== true) {
return $this->errorResponse($result, 500);
}
} }
$headers = Utils::getApiHeaders(); $headers = Utils::getApiHeaders();

View File

@ -105,6 +105,7 @@ class InvoiceController extends BaseController
$invoice->invoice_type_id = $clone; $invoice->invoice_type_id = $clone;
$invoice->invoice_number = $account->getNextNumber($invoice); $invoice->invoice_number = $account->getNextNumber($invoice);
$invoice->due_date = null; $invoice->due_date = null;
$invoice->partial_due_date = null;
$invoice->balance = $invoice->amount; $invoice->balance = $invoice->amount;
$invoice->invoice_status_id = 0; $invoice->invoice_status_id = 0;
$invoice->invoice_date = date_create()->format('Y-m-d'); $invoice->invoice_date = date_create()->format('Y-m-d');
@ -123,6 +124,8 @@ class InvoiceController extends BaseController
$invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->start_date = Utils::fromSqlDate($invoice->start_date);
$invoice->end_date = Utils::fromSqlDate($invoice->end_date); $invoice->end_date = Utils::fromSqlDate($invoice->end_date);
$invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date); $invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date);
$invoice->partial_due_date = Utils::fromSqlDate($invoice->partial_due_date);
$invoice->features = [ $invoice->features = [
'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN),
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),

View File

@ -9,20 +9,23 @@ use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\PaymentRepository;
use App\Services\PaymentService;
use Input; use Input;
use Response; use Response;
class PaymentApiController extends BaseAPIController class PaymentApiController extends BaseAPIController
{ {
protected $paymentRepo; protected $paymentRepo;
protected $paymentService;
protected $entityType = ENTITY_PAYMENT; protected $entityType = ENTITY_PAYMENT;
public function __construct(PaymentRepository $paymentRepo, ContactMailer $contactMailer) public function __construct(PaymentRepository $paymentRepo, PaymentService $paymentService, ContactMailer $contactMailer)
{ {
parent::__construct(); parent::__construct();
$this->paymentRepo = $paymentRepo; $this->paymentRepo = $paymentRepo;
$this->paymentService = $paymentService;
$this->contactMailer = $contactMailer; $this->contactMailer = $contactMailer;
} }
@ -108,7 +111,7 @@ class PaymentApiController extends BaseAPIController
// check payment has been marked sent // check payment has been marked sent
$request->invoice->markSentIfUnsent(); $request->invoice->markSentIfUnsent();
$payment = $this->paymentRepo->save($request->input()); $payment = $this->paymentService->save($request->input(), null, $request->invoice);
if (Input::get('email_receipt')) { if (Input::get('email_receipt')) {
$this->contactMailer->sendPaymentConfirmation($payment); $this->contactMailer->sendPaymentConfirmation($payment);

View File

@ -191,16 +191,10 @@ class PaymentController extends BaseController
// if the payment amount is more than the balance create a credit // if the payment amount is more than the balance create a credit
if ($amount > $request->invoice->balance) { if ($amount > $request->invoice->balance) {
$credit = Credit::createNew(); $credit = true;
$credit->client_id = $request->invoice->client_id;
$credit->credit_date = date_create()->format('Y-m-d');
$credit->amount = $credit->balance = $amount - $request->invoice->balance;
$credit->private_notes = trans('texts.credit_created_by', ['transaction_reference' => $input['transaction_reference']]);
$credit->save();
$input['amount'] = $request->invoice->balance;
} }
$payment = $this->paymentService->save($input); $payment = $this->paymentService->save($input, null, $request->invoice);
if (Input::get('email_receipt')) { if (Input::get('email_receipt')) {
$this->contactMailer->sendPaymentConfirmation($payment); $this->contactMailer->sendPaymentConfirmation($payment);

View File

@ -149,6 +149,10 @@ class ProductController extends BaseController
$message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product'); $message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product');
Session::flash('message', $message); Session::flash('message', $message);
if (in_array(request('action'), ['archive', 'delete', 'restore', 'invoice'])) {
return self::bulk();
}
return Redirect::to("products/{$product->public_id}/edit"); return Redirect::to("products/{$product->public_id}/edit");
} }
@ -159,7 +163,17 @@ class ProductController extends BaseController
{ {
$action = Input::get('action'); $action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->productService->bulk($ids, $action);
if ($action == 'invoice') {
$products = Product::scope($ids)->get();
$data = [];
foreach ($products as $product) {
$data[] = $product->product_key;
}
return redirect("invoices/create")->with('selectedProducts', $data);
} else {
$count = $this->productService->bulk($ids, $action);
}
$message = Utils::pluralize($action.'d_product', $count); $message = Utils::pluralize($action.'d_product', $count);
Session::flash('message', $message); Session::flash('message', $message);

View File

@ -118,12 +118,12 @@ class ProjectApiController extends BaseAPIController
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="body",
* @SWG\Schema(ref="#/definitions/project") * @SWG\Schema(ref="#/definitions/Project")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="New project", * description="New project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
* ), * ),
* @SWG\Response( * @SWG\Response(
* response="default", * response="default",
@ -155,12 +155,12 @@ class ProjectApiController extends BaseAPIController
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="project", * name="project",
* @SWG\Schema(ref="#/definitions/project") * @SWG\Schema(ref="#/definitions/Project")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Updated project", * description="Updated project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
* ), * ),
* @SWG\Response( * @SWG\Response(
* response="default", * response="default",
@ -200,7 +200,7 @@ class ProjectApiController extends BaseAPIController
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Deleted project", * description="Deleted project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
* ), * ),
* @SWG\Response( * @SWG\Response(
* response="default", * response="default",

View File

@ -51,6 +51,7 @@ class ProjectController extends BaseController
public function create(ProjectRequest $request) public function create(ProjectRequest $request)
{ {
$data = [ $data = [
'account' => auth()->user()->account,
'project' => null, 'project' => null,
'method' => 'POST', 'method' => 'POST',
'url' => 'projects', 'url' => 'projects',
@ -67,6 +68,7 @@ class ProjectController extends BaseController
$project = $request->entity(); $project = $request->entity();
$data = [ $data = [
'account' => auth()->user()->account,
'project' => $project, 'project' => $project,
'method' => 'PUT', 'method' => 'PUT',
'url' => 'projects/' . $project->public_id, 'url' => 'projects/' . $project->public_id,

View File

@ -72,6 +72,7 @@ class ReportController extends BaseController
'activity', 'activity',
'aging', 'aging',
'client', 'client',
'document',
'expense', 'expense',
'invoice', 'invoice',
'payment', 'payment',
@ -98,6 +99,8 @@ class ReportController extends BaseController
'date_field' => $dateField, 'date_field' => $dateField,
'invoice_status' => request()->invoice_status, 'invoice_status' => request()->invoice_status,
'group_dates_by' => request()->group_dates_by, 'group_dates_by' => request()->group_dates_by,
'document_filter' => request()->document_filter,
'export_format' => $format,
]; ];
$report = new $reportClass($startDate, $endDate, $isExport, $options); $report = new $reportClass($startDate, $endDate, $isExport, $options);
if (Input::get('report_type')) { if (Input::get('report_type')) {
@ -138,61 +141,87 @@ class ReportController extends BaseController
$filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report"; $filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report";
$formats = ['csv', 'pdf', 'xlsx']; $formats = ['csv', 'pdf', 'xlsx', 'zip'];
if(!in_array($format, $formats)) { if (! in_array($format, $formats)) {
throw new \Exception("Invalid format request to export report"); throw new \Exception("Invalid format request to export report");
} }
//Get labeled header //Get labeled header
$columns_labeled = $report->tableHeaderArray(); $data = array_merge(
[
array_map(function($col) {
return $col['label'];
}, $report->tableHeaderArray())
],
$data
);
/*$summary = []; $summary = [];
if(count(array_values($totals))) { if (count(array_values($totals))) {
$summary[] = array_merge([ $summary[] = array_merge([
trans("texts.totals") trans("texts.totals")
], array_map(function ($key) {return trans("texts.{$key}");}, array_keys(array_values(array_values($totals)[0])[0]))); ], array_map(function ($key) {
return trans("texts.{$key}");
}, array_keys(array_values(array_values($totals)[0])[0])));
} }
foreach ($totals as $currencyId => $each) { foreach ($totals as $currencyId => $each) {
foreach ($each as $dimension => $val) { foreach ($each as $dimension => $val) {
$tmp = []; $tmp = [];
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : ''); $tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
foreach ($val as $id => $field) {
foreach ($val as $id => $field) $tmp[] = Utils::formatMoney($field, $currencyId); $tmp[] = Utils::formatMoney($field, $currencyId);
}
$summary[] = $tmp; $summary[] = $tmp;
} }
} }
dd($summary);*/ return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $summary) {
return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $columns_labeled) {
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $columns_labeled) {
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $summary) {
$sheet->setOrientation('landscape'); $sheet->setOrientation('landscape');
$sheet->freezeFirstRow(); $sheet->freezeFirstRow();
if ($format == 'pdf') {
//Add border on PDF
if($format == 'pdf')
$sheet->setAllBorders('thin'); $sheet->setAllBorders('thin');
}
$sheet->rows(array_merge( if ($format == 'csv') {
[array_map(function($col) {return $col['label'];}, $columns_labeled)], $sheet->rows(array_merge($data, [[]], $summary));
$data } else {
)); $sheet->rows($data);
}
//Styling header // Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($columns_labeled)-1).'1', function($cells) { $sheet->cells('A1:'.Utils::num2alpha(count($data[0])-1).'1', function($cells) {
$cells->setBackground('#777777'); $cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF'); $cells->setFontColor('#FFFFFF');
$cells->setFontSize(13); $cells->setFontSize(13);
$cells->setFontFamily('Calibri'); $cells->setFontFamily('Calibri');
$cells->setFontWeight('bold'); $cells->setFontWeight('bold');
}); });
$sheet->setAutoSize(true); $sheet->setAutoSize(true);
}); });
$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); })->export($format);
} }
} }

View File

@ -149,6 +149,10 @@ class TaskApiController extends BaseAPIController
*/ */
public function update(UpdateTaskRequest $request) public function update(UpdateTaskRequest $request)
{ {
if ($request->action) {
return $this->handleAction($request);
}
$task = $request->entity(); $task = $request->entity();
$task = $this->taskRepo->save($task->public_id, \Illuminate\Support\Facades\Input::all()); $task = $this->taskRepo->save($task->public_id, \Illuminate\Support\Facades\Input::all());

View File

@ -262,7 +262,7 @@ class TaskController extends BaseController
$this->taskRepo->save($ids, ['action' => $action]); $this->taskRepo->save($ids, ['action' => $action]);
return Redirect::to('tasks')->withMessage(trans($action == 'stop' ? 'texts.stopped_task' : 'texts.resumed_task')); return Redirect::to('tasks')->withMessage(trans($action == 'stop' ? 'texts.stopped_task' : 'texts.resumed_task'));
} elseif ($action == 'invoice' || $action == 'add_to_invoice') { } elseif ($action == 'invoice' || $action == 'add_to_invoice') {
$tasks = Task::scope($ids)->with('client')->orderBy('project_id', 'id')->get(); $tasks = Task::scope($ids)->with('account', 'client', 'project')->orderBy('project_id', 'id')->get();
$clientPublicId = false; $clientPublicId = false;
$data = []; $data = [];
@ -294,6 +294,7 @@ class TaskController extends BaseController
'publicId' => $task->public_id, 'publicId' => $task->public_id,
'description' => $task->present()->invoiceDescription($account, $showProject), 'description' => $task->present()->invoiceDescription($account, $showProject),
'duration' => $task->getHours(), 'duration' => $task->getHours(),
'cost' => $task->getRate(),
]; ];
$lastProjectId = $task->project_id; $lastProjectId = $task->project_id;
} }

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers;
use PragmaRX\Google2FA\Google2FA;
use Crypt;
class TwoFactorController extends Controller
{
public function setupTwoFactor()
{
$user = auth()->user();
if ($user->google_2fa_secret || ! $user->phone || ! $user->confirmed) {
return redirect('/settings/user_details');
}
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
session(['2fa:secret' => $secret]);
$qrCode = $google2fa->getQRCodeGoogleUrl(
APP_NAME,
$user->email,
$secret
);
$data = [
'secret' => $secret,
'qrCode' => $qrCode,
];
return view('users.two_factor', $data);
}
public function enableTwoFactor()
{
$user = auth()->user();
$secret = session()->pull('2fa:secret');
if ($secret && ! $user->google_2fa_secret && $user->phone && $user->confirmed) {
$user->google_2fa_secret = Crypt::encrypt($secret);
$user->save();
session()->flash('message', trans('texts.enabled_two_factor'));
}
return redirect('settings/user_details');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\DeleteVendorRequest;
use App\Http\Requests\VendorRequest; use App\Http\Requests\VendorRequest;
use App\Http\Requests\CreateVendorRequest; use App\Http\Requests\CreateVendorRequest;
use App\Http\Requests\UpdateVendorRequest; use App\Http\Requests\UpdateVendorRequest;
@ -186,7 +187,7 @@ class VendorApiController extends BaseAPIController
* ) * )
* ) * )
*/ */
public function destroy(UpdateVendorRequest $request) public function destroy(DeleteVendorRequest $request)
{ {
$vendor = $request->entity(); $vendor = $request->entity();

View File

@ -36,5 +36,6 @@ class Kernel extends HttpKernel
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated', 'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
'api' => 'App\Http\Middleware\ApiCheck', 'api' => 'App\Http\Middleware\ApiCheck',
'cors' => '\Barryvdh\Cors\HandleCors', 'cors' => '\Barryvdh\Cors\HandleCors',
'throttle' => 'Illuminate\Routing\Middleware\ThrottleRequests',
]; ];
} }

View File

@ -30,7 +30,7 @@ class CreatePaymentAPIRequest extends PaymentRequest
]; ];
} }
$this->invoice = $invoice = Invoice::scope($this->invoice_id) $this->invoice = $invoice = Invoice::scope($this->invoice_public_id ?: $this->invoice_id)
->withArchived() ->withArchived()
->invoices() ->invoices()
->first(); ->first();

View File

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

View File

@ -0,0 +1,78 @@
<?php
namespace App\Http\Requests;
use Cache;
use Crypt;
use Google2FA;
use App\Models\User;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory as ValidatonFactory;
class ValidateTwoFactorRequest extends Request
{
/**
*
* @var \App\User
*/
private $user;
/**
* Create a new FormRequest instance.
*
* @param \Illuminate\Validation\Factory $factory
* @return void
*/
public function __construct(ValidatonFactory $factory)
{
$factory->extend(
'valid_token',
function ($attribute, $value, $parameters, $validator) {
$secret = Crypt::decrypt($this->user->google_2fa_secret);
return Google2FA::verifyKey($secret, $value);
},
trans('texts.invalid_code')
);
$factory->extend(
'used_token',
function ($attribute, $value, $parameters, $validator) {
$key = $this->user->id . ':' . $value;
return !Cache::has($key);
},
trans('texts.invalid_code')
);
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
try {
$this->user = User::findOrFail(
session('2fa:user:id')
);
} catch (Exception $exc) {
return false;
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'totp' => 'bail|required|digits:6|valid_token|used_token',
];
}
}

View File

@ -79,6 +79,8 @@ Route::group(['middleware' => 'lookup:postmark'], function () {
Route::group(['middleware' => 'lookup:account'], function () { Route::group(['middleware' => 'lookup:account'], function () {
Route::post('/payment_hook/{account_key}/{gateway_id}', 'OnlinePaymentController@handlePaymentWebhook'); Route::post('/payment_hook/{account_key}/{gateway_id}', 'OnlinePaymentController@handlePaymentWebhook');
Route::match(['GET', 'POST', 'OPTIONS'], '/buy_now/{gateway_type?}', 'OnlinePaymentController@handleBuyNow'); Route::match(['GET', 'POST', 'OPTIONS'], '/buy_now/{gateway_type?}', 'OnlinePaymentController@handleBuyNow');
Route::get('validate_two_factor/{account_key}', 'Auth\AuthController@getValidateToken');
Route::post('validate_two_factor/{account_key}', ['middleware' => 'throttle:5', 'uses' => 'Auth\AuthController@postValidateToken']);
}); });
//Route::post('/hook/bot/{platform?}', 'BotController@handleMessage'); //Route::post('/hook/bot/{platform?}', 'BotController@handleMessage');
@ -141,6 +143,8 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails');
Route::post('settings/payment_gateway_limits', 'AccountGatewayController@savePaymentGatewayLimits'); Route::post('settings/payment_gateway_limits', 'AccountGatewayController@savePaymentGatewayLimits');
Route::post('users/change_password', 'UserController@changePassword'); Route::post('users/change_password', 'UserController@changePassword');
Route::get('settings/enable_two_factor', 'TwoFactorController@setupTwoFactor');
Route::post('settings/enable_two_factor', 'TwoFactorController@enableTwoFactor');
Route::resource('clients', 'ClientController'); Route::resource('clients', 'ClientController');
Route::get('api/clients', 'ClientController@getDatatable'); Route::get('api/clients', 'ClientController@getDatatable');

View File

@ -46,7 +46,7 @@ class DownloadInvoices extends Job
*/ */
public function handle(UserMailer $userMailer) public function handle(UserMailer $userMailer)
{ {
$zip = Archive::instance_by_useragent(date('Y-m-d') . '-Invoice_PDFs'); $zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoice_pdfs')));
foreach ($this->invoices as $invoice) { foreach ($this->invoices as $invoice) {
$zip->add_file($invoice->getFileName(), $invoice->getPDFString()); $zip->add_file($invoice->getFileName(), $invoice->getPDFString());

View File

@ -89,6 +89,11 @@ class Utils
return env('NINJA_DEV') == 'true'; return env('NINJA_DEV') == 'true';
} }
public static function isTimeTracker()
{
return array_get($_SERVER, 'HTTP_USER_AGENT') == TIME_TRACKER_USER_AGENT;
}
public static function requireHTTPS() public static function requireHTTPS()
{ {
if (Request::root() === 'http://ninja.dev' || Request::root() === 'http://ninja.dev:8000') { if (Request::root() === 'http://ninja.dev' || Request::root() === 'http://ninja.dev:8000') {
@ -1064,7 +1069,7 @@ class Utils
{ {
$name = trim($name); $name = trim($name);
$lastName = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name); $lastName = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name);
$firstName = trim(preg_replace('#'.$lastName.'#', '', $name)); $firstName = trim(preg_replace('#' . preg_quote($lastName, '/') . '#', '', $name));
return [$firstName, $lastName]; return [$firstName, $lastName];
} }

View File

@ -0,0 +1,22 @@
<?php
namespace App\Listeners;
use App\Events\SubdomainWasUpdated;
use App\Ninja\DNS\Cloudflare;
/**
* Class DNSListener.
*/
class DNSListener
{
/**
* @param DNSListener $event
*/
public function addDNSRecord(SubdomainWasUpdated $event)
{
if(env("CLOUDFLARE_DNS_ENABLED"))
Cloudflare::addDNSRecord($event->account);
}
}

View File

@ -41,7 +41,8 @@ class HandleUserLoggedIn
*/ */
public function handle(UserLoggedIn $event) public function handle(UserLoggedIn $event)
{ {
$account = Auth::user()->account; $user = auth()->user();
$account = $user->account;
if (! Utils::isNinja() && empty($account->last_login)) { if (! Utils::isNinja() && empty($account->last_login)) {
event(new UserSignedUp()); event(new UserSignedUp());
@ -50,6 +51,11 @@ class HandleUserLoggedIn
$account->last_login = Carbon::now()->toDateTimeString(); $account->last_login = Carbon::now()->toDateTimeString();
$account->save(); $account->save();
if ($user->failed_logins > 0) {
$user->failed_logins = 0;
$user->save();
}
$users = $this->accountRepo->loadAccounts(Auth::user()->id); $users = $this->accountRepo->loadAccounts(Auth::user()->id);
Session::put(SESSION_USER_ACCOUNTS, $users); Session::put(SESSION_USER_ACCOUNTS, $users);
HistoryUtils::loadHistory($users ?: Auth::user()->id); HistoryUtils::loadHistory($users ?: Auth::user()->id);

View File

@ -176,6 +176,7 @@ class Account extends Eloquent
'credit_number_counter', 'credit_number_counter',
'credit_number_prefix', 'credit_number_prefix',
'credit_number_pattern', 'credit_number_pattern',
'task_rate',
]; ];
/** /**
@ -809,7 +810,7 @@ class Account extends Eloquent
$available = true; $available = true;
foreach ($gatewayTypes as $type) { foreach ($gatewayTypes as $type) {
if ($paymentDriver->handles($type)) { if ($type != GATEWAY_TYPE_TOKEN && $paymentDriver->handles($type)) {
$available = false; $available = false;
break; break;
} }
@ -1084,6 +1085,11 @@ class Account extends Eloquent
} }
} }
public function isPaid()
{
return Utils::isNinja() ? $this->isPro() : Utils::isWhiteLabel();
}
/** /**
* @param null $plan_details * @param null $plan_details
* *

View File

@ -160,6 +160,22 @@ class AccountGateway extends EntityModel
return ! empty($this->getConfigField('enableSofort')); return ! empty($this->getConfigField('enableSofort'));
} }
/**
* @return bool
*/
public function getSepaEnabled()
{
return ! empty($this->getConfigField('enableSepa'));
}
/**
* @return bool
*/
public function getBitcoinEnabled()
{
return ! empty($this->getConfigField('enableBitcoin'));
}
/** /**
* @return bool * @return bool
*/ */

View File

@ -126,7 +126,7 @@ class Activity extends Eloquent
'user' => $isSystem ? '<i>' . trans('texts.system') . '</i>' : e($user->getDisplayName()), 'user' => $isSystem ? '<i>' . trans('texts.system') . '</i>' : e($user->getDisplayName()),
'invoice' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null, 'invoice' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null, 'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
'contact' => $contactId ? e($client->getDisplayName()) : e($user->getDisplayName()), 'contact' => $contactId ? link_to($client->getRoute(), $client->getDisplayName()) : e($user->getDisplayName()),
'payment' => $payment ? e($payment->transaction_reference) : null, 'payment' => $payment ? e($payment->transaction_reference) : null,
'payment_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null, 'payment_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null,
'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null, 'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null,

View File

@ -52,6 +52,7 @@ class Client extends EntityModel
'invoice_number_counter', 'invoice_number_counter',
'quote_number_counter', 'quote_number_counter',
'public_notes', 'public_notes',
'task_rate',
]; ];

View File

@ -161,6 +161,11 @@ class EntityModel extends Eloquent
$query->where($this->getTable() .'.account_id', '=', $accountId); $query->where($this->getTable() .'.account_id', '=', $accountId);
if (func_num_args() > 1 && ! $publicId) {
$query->where('id', '=', 0);
return $query;
}
if ($publicId) { if ($publicId) {
if (is_array($publicId)) { if (is_array($publicId)) {
$query->whereIn('public_id', $publicId); $query->whereIn('public_id', $publicId);

View File

@ -32,6 +32,7 @@ class Gateway extends Eloquent
GATEWAY_TYPE_BITCOIN, GATEWAY_TYPE_BITCOIN,
GATEWAY_TYPE_DWOLLA, GATEWAY_TYPE_DWOLLA,
GATEWAY_TYPE_TOKEN, GATEWAY_TYPE_TOKEN,
GATEWAY_TYPE_GOCARDLESS,
]; ];
// these will appear in the primary gateway select // these will appear in the primary gateway select
@ -58,6 +59,7 @@ class Gateway extends Eloquent
*/ */
public static $alternate = [ public static $alternate = [
GATEWAY_PAYPAL_EXPRESS, GATEWAY_PAYPAL_EXPRESS,
GATEWAY_GOCARDLESS,
GATEWAY_BITPAY, GATEWAY_BITPAY,
GATEWAY_DWOLLA, GATEWAY_DWOLLA,
GATEWAY_CUSTOM, GATEWAY_CUSTOM,
@ -87,6 +89,8 @@ class Gateway extends Eloquent
'developerMode', 'developerMode',
// Dwolla // Dwolla
'sandbox', 'sandbox',
// Payfast
'pdtKey',
]; ];
/** /**

View File

@ -216,6 +216,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'public_notes', 'public_notes',
'invoice_footer', 'invoice_footer',
'partial', 'partial',
'partial_due_date',
] as $field) { ] as $field) {
if ($this->$field != $this->getOriginal($field)) { if ($this->$field != $this->getOriginal($field)) {
return true; return true;
@ -634,6 +635,15 @@ class Invoice extends EntityModel implements BalanceAffecting
if ($this->partial > 0) { if ($this->partial > 0) {
$this->partial = $partial; $this->partial = $partial;
// clear the partial due date and set the due date
// using payment terms if it's blank
if (! $this->partial && $this->partial_due_date) {
$this->partial_due_date = null;
if (! $this->due_date) {
$this->due_date = $this->account->defaultDueDate($this->client);
}
}
} }
$this->save(); $this->save();
@ -682,7 +692,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if ($quoteInvoiceId) { if ($quoteInvoiceId) {
$label = 'converted'; $label = 'converted';
} elseif ($class == 'danger') { } elseif ($class == 'danger') {
$label = $entityType == ENTITY_INVOICE ? 'overdue' : 'expired'; $label = $entityType == ENTITY_INVOICE ? 'past_due' : 'expired';
} else { } else {
$label = 'status_' . strtolower($status); $label = 'status_' . strtolower($status);
} }
@ -719,7 +729,8 @@ class Invoice extends EntityModel implements BalanceAffecting
public function statusClass() public function statusClass()
{ {
return static::calcStatusClass($this->invoice_status_id, $this->balance, $this->getOriginal('due_date'), $this->is_recurring); $dueDate = $this->getOriginal('partial_due_date') ?: $this->getOriginal('due_date');
return static::calcStatusClass($this->invoice_status_id, $this->balance, $dueDate, $this->is_recurring);
} }
public function statusLabel() public function statusLabel()
@ -808,7 +819,7 @@ class Invoice extends EntityModel implements BalanceAffecting
*/ */
public function isOverdue() public function isOverdue()
{ {
return static::calcIsOverdue($this->balance, $this->due_date); return static::calcIsOverdue($this->balance, $this->partial_due_date ?: $this->due_date);
} }
/** /**
@ -878,6 +889,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'custom_taxes1', 'custom_taxes1',
'custom_taxes2', 'custom_taxes2',
'partial', 'partial',
'partial_due_date',
'has_tasks', 'has_tasks',
'custom_text_value1', 'custom_text_value1',
'custom_text_value2', 'custom_text_value2',
@ -1198,7 +1210,7 @@ class Invoice extends EntityModel implements BalanceAffecting
} }
$invitation = $this->invitations[0]; $invitation = $this->invitations[0];
$link = $invitation->getLink('view', true); $link = $invitation->getLink('view', true, true);
$pdfString = false; $pdfString = false;
$phantomjsSecret = env('PHANTOMJS_SECRET'); $phantomjsSecret = env('PHANTOMJS_SECRET');
$phantomjsLink = $link . "?phantomjs=true&phantomjs_secret={$phantomjsSecret}"; $phantomjsLink = $link . "?phantomjs=true&phantomjs_secret={$phantomjsSecret}";
@ -1208,8 +1220,11 @@ class Invoice extends EntityModel implements BalanceAffecting
// we see occasional 408 errors // we see occasional 408 errors
for ($i=1; $i<=5; $i++) { for ($i=1; $i<=5; $i++) {
$pdfString = CurlUtils::phantom('GET', $phantomjsLink); $pdfString = CurlUtils::phantom('GET', $phantomjsLink);
if ($pdfString) { $pdfString = strip_tags($pdfString);
if (strpos($pdfString, 'data') === 0) {
break; break;
} else {
$pdfString = false;
} }
} }
} }
@ -1217,9 +1232,8 @@ class Invoice extends EntityModel implements BalanceAffecting
if (! $pdfString && ($key = env('PHANTOMJS_CLOUD_KEY'))) { if (! $pdfString && ($key = env('PHANTOMJS_CLOUD_KEY'))) {
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%26phantomjs_secret={$phantomjsSecret}%22,renderType:%22html%22%7D"; $url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%26phantomjs_secret={$phantomjsSecret}%22,renderType:%22html%22%7D";
$pdfString = CurlUtils::get($url); $pdfString = CurlUtils::get($url);
$pdfString = strip_tags($pdfString);
} }
$pdfString = strip_tags($pdfString);
} catch (\Exception $exception) { } catch (\Exception $exception) {
Utils::logError("PhantomJS - Failed to load {$phantomjsLink}: {$exception->getMessage()}"); Utils::logError("PhantomJS - Failed to load {$phantomjsLink}: {$exception->getMessage()}");
return false; return false;
@ -1234,7 +1248,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if ($pdf = Utils::decodePDF($pdfString)) { if ($pdf = Utils::decodePDF($pdfString)) {
return $pdf; return $pdf;
} else { } else {
Utils::logError("PhantomJS - Unable to decode {$phantomjsLink}: {$pdfString}"); Utils::logError("PhantomJS - Unable to decode {$phantomjsLink}");
return false; return false;
} }
} else { } else {
@ -1465,7 +1479,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if ($entityType == ENTITY_INVOICE) { if ($entityType == ENTITY_INVOICE) {
$statuses[INVOICE_STATUS_UNPAID] = trans('texts.unpaid'); $statuses[INVOICE_STATUS_UNPAID] = trans('texts.unpaid');
$statuses[INVOICE_STATUS_OVERDUE] = trans('texts.overdue'); $statuses[INVOICE_STATUS_OVERDUE] = trans('texts.past_due');
} }
return $statuses; return $statuses;

View File

@ -24,6 +24,7 @@ class Project extends EntityModel
*/ */
protected $fillable = [ protected $fillable = [
'name', 'name',
'task_rate',
]; ];
/** /**

View File

@ -145,6 +145,24 @@ class Task extends EntityModel
return self::calcDuration($this); return self::calcDuration($this);
} }
/**
* @return float
*/
public function getRate()
{
$value = 0;
if ($this->project && floatval($this->project->task_rate)) {
$value = $this->project->task_rate;
} elseif ($this->client && floatval($this->client->task_rate)) {
$value = $this->client->task_rate;
} else {
$value = $this->account->task_rate;
}
return Utils::roundSignificant($value);
}
/** /**
* @return int * @return int
*/ */

View File

@ -345,6 +345,9 @@ trait GeneratesNumbers
case FREQUENCY_THREE_MONTHS: case FREQUENCY_THREE_MONTHS:
$resetDate->addMonths(3); $resetDate->addMonths(3);
break; break;
case FREQUENCY_FOUR_MONTHS:
$resetDate->addMonths(4);
break;
case FREQUENCY_SIX_MONTHS: case FREQUENCY_SIX_MONTHS:
$resetDate->addMonths(6); $resetDate->addMonths(6);
break; break;

View File

@ -63,6 +63,8 @@ trait HasRecurrence
return $monthsSinceLastSent >= 2; return $monthsSinceLastSent >= 2;
case FREQUENCY_THREE_MONTHS: case FREQUENCY_THREE_MONTHS:
return $monthsSinceLastSent >= 3; return $monthsSinceLastSent >= 3;
case FREQUENCY_FOUR_MONTHS:
return $monthsSinceLastSent >= 4;
case FREQUENCY_SIX_MONTHS: case FREQUENCY_SIX_MONTHS:
return $monthsSinceLastSent >= 6; return $monthsSinceLastSent >= 6;
case FREQUENCY_ANNUALLY: case FREQUENCY_ANNUALLY:
@ -100,6 +102,9 @@ trait HasRecurrence
case FREQUENCY_THREE_MONTHS: case FREQUENCY_THREE_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=3;'; $rule = 'FREQ=MONTHLY;INTERVAL=3;';
break; break;
case FREQUENCY_FOUR_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=4;';
break;
case FREQUENCY_SIX_MONTHS: case FREQUENCY_SIX_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=6;'; $rule = 'FREQ=MONTHLY;INTERVAL=6;';
break; break;

View File

@ -12,6 +12,29 @@ trait PresentsInvoice
if ($this->invoice_fields) { if ($this->invoice_fields) {
$fields = json_decode($this->invoice_fields, true); $fields = json_decode($this->invoice_fields, true);
if (! isset($fields['product_fields'])) {
$fields['product_fields'] = [
'product.item',
'product.description',
'product.custom_value1',
'product.custom_value2',
'product.unit_cost',
'product.quantity',
'product.tax',
'product.line_total',
];
$fields['task_fields'] = [
'product.service',
'product.description',
'product.custom_value1',
'product.custom_value2',
'product.rate',
'product.hours',
'product.tax',
'product.line_total',
];
}
return $this->applyLabels($fields); return $this->applyLabels($fields);
} else { } else {
return $this->getDefaultInvoiceFields(); return $this->getDefaultInvoiceFields();
@ -54,6 +77,26 @@ trait PresentsInvoice
'account.city_state_postal', 'account.city_state_postal',
'account.country', 'account.country',
], ],
'product_fields' => [
'product.item',
'product.description',
'product.custom_value1',
'product.custom_value2',
'product.unit_cost',
'product.quantity',
'product.tax',
'product.line_total',
],
'task_fields' => [
'product.service',
'product.description',
'product.custom_value1',
'product.custom_value2',
'product.rate',
'product.hours',
'product.tax',
'product.line_total',
]
]; ];
if ($this->custom_invoice_text_label1) { if ($this->custom_invoice_text_label1) {
@ -136,6 +179,26 @@ trait PresentsInvoice
'account.custom_value2', 'account.custom_value2',
'.blank', '.blank',
], ],
INVOICE_FIELDS_PRODUCT => [
'product.item',
'product.description',
'product.custom_value1',
'product.custom_value2',
'product.unit_cost',
'product.quantity',
'product.tax',
'product.line_total',
],
INVOICE_FIELDS_TASK => [
'product.service',
'product.description',
'product.custom_value1',
'product.custom_value2',
'product.rate',
'product.hours',
'product.tax',
'product.line_total',
],
]; ];
return $this->applyLabels($fields); return $this->applyLabels($fields);
@ -264,6 +327,10 @@ trait PresentsInvoice
'invoice_due_date', 'invoice_due_date',
'quote_due_date', 'quote_due_date',
'service', 'service',
'product_key',
'unit_cost',
'custom_value1',
'custom_value2',
]; ];
foreach ($fields as $field) { foreach ($fields as $field) {
@ -289,6 +356,8 @@ trait PresentsInvoice
'client.custom_value2' => 'custom_client_label2', 'client.custom_value2' => 'custom_client_label2',
'contact.custom_value1' => 'custom_contact_label1', 'contact.custom_value1' => 'custom_contact_label1',
'contact.custom_value2' => 'custom_contact_label2', 'contact.custom_value2' => 'custom_contact_label2',
'product.custom_value1' => 'custom_invoice_item_label1',
'product.custom_value2' => 'custom_invoice_item_label2',
] as $field => $property) { ] as $field => $property) {
$data[$field] = e($this->$property) ?: trans('texts.custom_field'); $data[$field] = e($this->$property) ?: trans('texts.custom_field');
} }
@ -307,4 +376,10 @@ trait PresentsInvoice
return null; return null;
} }
public function hideQuantity() {
$fields = $this->getInvoiceFields();
return ! isset($fields['product_fields']['product.quantity']);
}
} }

View File

@ -155,9 +155,15 @@ trait SendsEmails
{ {
for ($i = 1; $i <= 3; $i++) { for ($i = 1; $i <= 3; $i++) {
if ($date = $this->getReminderDate($i, $filterEnabled)) { if ($date = $this->getReminderDate($i, $filterEnabled)) {
$field = $this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date'; if ($this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE) {
if ($invoice->$field == $date) { if (($invoice->partial && $invoice->partial_due_date == $date)
return "reminder{$i}"; || $invoice->due_date == $date) {
return "reminder{$i}";
}
} else {
if ($invoice->invoice_date == $date) {
return "reminder{$i}";
}
} }
} }
} }

View File

@ -59,7 +59,15 @@ class User extends Authenticatable
* *
* @var array * @var array
*/ */
protected $hidden = ['password', 'remember_token', 'confirmation_code']; protected $hidden = [
'password',
'remember_token',
'confirmation_code',
'oauth_user_id',
'oauth_provider_id',
'google_2fa_secret',
'google_2fa_phone',
];
/** /**
* @var array * @var array

View File

@ -0,0 +1,50 @@
<?php
namespace App\Ninja\DNS;
use App\Libraries\Utils;
use App\Models\Account;
class Cloudflare
{
public static function addDNSRecord(Account $account){
$zones = json_decode(env('CLOUDFLARE_ZONE_IDS',''), true);
foreach($zones as $zone)
{
$curl = curl_init();
$jsonEncodedData = json_encode(['type'=>'A', 'name'=>$account->subdomain, 'content'=>env('CLOUDFLARE_TARGET_IP_ADDRESS',''),'proxied'=>true]);
$opts = [
CURLOPT_URL => 'https://api.cloudflare.com/client/v4/zones/'.$zone.'/dns_records',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $jsonEncodedData,
CURLOPT_HTTPHEADER => [ 'Content-Type: application/json',
'Content-Length: '.strlen($jsonEncodedData),
'X-Auth-Email: '.env('CLOUDFLARE_EMAIL', ''),
'X-Auth-Key: '.env('CLOUDFLARE_API_KEY', '')
],
];
curl_setopt_array($curl, $opts);
$result = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($status != 200)
Utils::logError('unable to update subdomain ' . $account->subdomain . ' @ Cloudflare - '.$result);
}
}
}

View File

@ -15,8 +15,17 @@ class ActivityDatatable extends EntityDatatable
'activities.id', 'activities.id',
function ($model) { function ($model) {
$str = Utils::timestampToDateTimeString(strtotime($model->created_at)); $str = Utils::timestampToDateTimeString(strtotime($model->created_at));
$activityTypes = [
ACTIVITY_TYPE_VIEW_INVOICE,
ACTIVITY_TYPE_VIEW_QUOTE,
ACTIVITY_TYPE_CREATE_PAYMENT,
ACTIVITY_TYPE_APPROVE_QUOTE,
];
if ($model->contact_id) { if ($model->contact_id
&& ! $model->is_system
&& in_array($model->activity_type_id, $activityTypes)
&& ! in_array($model->ip, ['127.0.0.1', '192.168.255.33'])) {
$ipLookUpLink = IP_LOOKUP_URL . $model->ip; $ipLookUpLink = IP_LOOKUP_URL . $model->ip;
$str .= sprintf(' &nbsp; <i class="fa fa-globe" style="cursor:pointer" title="%s" onclick="openUrl(\'%s\', \'IP Lookup\')"></i>', $model->ip, $ipLookUpLink); $str .= sprintf(' &nbsp; <i class="fa fa-globe" style="cursor:pointer" title="%s" onclick="openUrl(\'%s\', \'IP Lookup\')"></i>', $model->ip, $ipLookUpLink);
} }

View File

@ -66,7 +66,11 @@ class InvoiceDatatable extends EntityDatatable
[ [
$entityType == ENTITY_INVOICE ? 'due_date' : 'valid_until', $entityType == ENTITY_INVOICE ? 'due_date' : 'valid_until',
function ($model) { function ($model) {
return Utils::fromSqlDate($model->due_date_sql); $str = '';
if ($model->partial_due_date) {
$str = Utils::fromSqlDate($model->partial_due_date) . ', ';
}
return $str . Utils::fromSqlDate($model->due_date_sql);
}, },
], ],
[ [
@ -165,7 +169,7 @@ class InvoiceDatatable extends EntityDatatable
private function getStatusLabel($model) private function getStatusLabel($model)
{ {
$class = Invoice::calcStatusClass($model->invoice_status_id, $model->balance, $model->due_date_sql, $model->is_recurring); $class = Invoice::calcStatusClass($model->invoice_status_id, $model->balance, $model->partial_due_date ?: $model->due_date_sql, $model->is_recurring);
$label = Invoice::calcStatusLabel($model->invoice_status_name, $class, $this->entityType, $model->quote_invoice_id); $label = Invoice::calcStatusLabel($model->invoice_status_name, $class, $this->entityType, $model->quote_invoice_id);
return "<h4><div class=\"label label-{$class}\">$label</div></h4>"; return "<h4><div class=\"label label-{$class}\">$label</div></h4>";

View File

@ -69,7 +69,7 @@ class PaymentDatatable extends EntityDatatable
} elseif ($model->email) { } elseif ($model->email) {
return $model->email; return $model->email;
} elseif ($model->payment_type) { } elseif ($model->payment_type) {
return trans('texts.payment_type_' . $model->payment_type); return trans('texts.payment_type_' . strtolower($model->payment_type));
} }
} elseif ($model->last4) { } elseif ($model->last4) {
if ($model->bank_name) { if ($model->bank_name) {

View File

@ -30,7 +30,7 @@ class ProductDatatable extends EntityDatatable
[ [
'cost', 'cost',
function ($model) { function ($model) {
return Utils::formatMoney($model->cost); return Utils::roundSignificant($model->cost);
}, },
], ],
[ [
@ -52,6 +52,15 @@ class ProductDatatable extends EntityDatatable
return URL::to("products/{$model->public_id}/edit"); return URL::to("products/{$model->public_id}/edit");
}, },
], ],
[
trans('texts.invoice_product'),
function ($model) {
return "javascript:submitForm_product('invoice', {$model->public_id})";
},
function ($model) {
return (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE);
},
],
]; ];
} }
} }

View File

@ -38,6 +38,12 @@ class ProjectDatatable extends EntityDatatable
} }
}, },
], ],
[
'task_rate',
function ($model) {
return floatval($model->task_rate) ? Utils::roundSignificant($model->task_rate) : '';
}
],
]; ];
} }

View File

@ -259,10 +259,21 @@ class BaseTransformer extends TransformerAbstract
{ {
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber);
return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE][$invoiceNumber] : null; return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE][$invoiceNumber] : null;
} }
/**
* @param $invoiceNumber
*
* @return null
*/
public function getInvoicePublicId($invoiceNumber)
{
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($invoiceNumber);
return isset($this->maps['invoices'][$invoiceNumber]) ? $this->maps['invoices'][$invoiceNumber]->public_id : null;
}
/** /**
* @param $invoiceNumber * @param $invoiceNumber
* *

View File

@ -27,6 +27,7 @@ class PaymentTransformer extends BaseTransformer
'payment_date_sql' => $this->getDate($data, 'payment_date'), 'payment_date_sql' => $this->getDate($data, 'payment_date'),
'client_id' => $this->getInvoiceClientId($data->invoice_num), 'client_id' => $this->getInvoiceClientId($data->invoice_num),
'invoice_id' => $this->getInvoiceId($data->invoice_num), 'invoice_id' => $this->getInvoiceId($data->invoice_num),
'invoice_public_id' => $this->getInvoicePublicId($data->invoice_num),
]; ];
}); });
} }

View File

@ -10,6 +10,7 @@ class AuthorizeNetAIMPaymentDriver extends BasePaymentDriver
{ {
$data = parent::paymentDetails(); $data = parent::paymentDetails();
$data['solutionId'] = $this->accountGateway->getConfigField('testMode') ? 'AAA100303' : 'AAA172036'; $data['solutionId'] = $this->accountGateway->getConfigField('testMode') ? 'AAA100303' : 'AAA172036';
$data['invoiceNumber'] = $this->invoice()->invoice_number;
return $data; return $data;
} }

View File

@ -318,7 +318,11 @@ class BasePaymentDriver
// parse the transaction reference // parse the transaction reference
if ($this->transactionReferenceParam) { if ($this->transactionReferenceParam) {
$ref = $this->purchaseResponse[$this->transactionReferenceParam]; if (! empty($this->purchaseResponse[$this->transactionReferenceParam])) {
$ref = $this->purchaseResponse[$this->transactionReferenceParam];
} else {
throw new Exception($response->getMessage() ?: trans('texts.payment_error'));
}
} else { } else {
$ref = $response->getTransactionReference(); $ref = $response->getTransactionReference();
} }

View File

@ -12,7 +12,7 @@ class GoCardlessV2RedirectPaymentDriver extends BasePaymentDriver
public function gatewayTypes() public function gatewayTypes()
{ {
$types = [ $types = [
GATEWAY_TYPE_BANK_TRANSFER, GATEWAY_TYPE_GOCARDLESS,
GATEWAY_TYPE_TOKEN, GATEWAY_TYPE_TOKEN,
]; ];
@ -112,6 +112,10 @@ class GoCardlessV2RedirectPaymentDriver extends BasePaymentDriver
continue; continue;
} }
if ($payment->is_deleted || $payment->invoice->is_deleted) {
continue;
}
if ($action == 'failed' || $action == 'charged_back') { if ($action == 'failed' || $action == 'charged_back') {
if (! $payment->isFailed()) { if (! $payment->isFailed()) {
$payment->markFailed($event['details']['description']); $payment->markFailed($event['details']['description']);

View File

@ -8,6 +8,14 @@ class PayFastPaymentDriver extends BasePaymentDriver
{ {
protected $transactionReferenceParam = 'm_payment_id'; protected $transactionReferenceParam = 'm_payment_id';
protected function paymentDetails($paymentMethod = false)
{
$data = parent::paymentDetails();
$data['notifyUrl'] = $this->invitation->getLink('complete', true);
return $data;
}
public function completeOffsitePurchase($input) public function completeOffsitePurchase($input)
{ {
parent::completeOffsitePurchase([ parent::completeOffsitePurchase([

View File

@ -43,6 +43,13 @@ class StripePaymentDriver extends BasePaymentDriver
} elseif ($sofortEnabled) { } elseif ($sofortEnabled) {
$types[] = GATEWAY_TYPE_SOFORT; $types[] = GATEWAY_TYPE_SOFORT;
} }
if ($gateway->getSepaEnabled()) {
$types[] = GATEWAY_TYPE_SEPA;
}
if ($gateway->getBitcoinEnabled()) {
$types[] = GATEWAY_TYPE_BITCOIN;
}
if ($gateway->getAlipayEnabled()) { if ($gateway->getAlipayEnabled()) {
$types[] = GATEWAY_TYPE_ALIPAY; $types[] = GATEWAY_TYPE_ALIPAY;
} }
@ -84,7 +91,7 @@ class StripePaymentDriver extends BasePaymentDriver
public function shouldUseSource() public function shouldUseSource()
{ {
return in_array($this->gatewayType, [GATEWAY_TYPE_ALIPAY, GATEWAY_TYPE_SOFORT]); return in_array($this->gatewayType, [GATEWAY_TYPE_ALIPAY, GATEWAY_TYPE_SOFORT, GATEWAY_TYPE_BITCOIN]);
} }
protected function checkCustomerExists($customer) protected function checkCustomerExists($customer)
@ -240,13 +247,16 @@ class StripePaymentDriver extends BasePaymentDriver
$isBank = $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod); $isBank = $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod);
$isAlipay = $this->isGatewayType(GATEWAY_TYPE_ALIPAY, $paymentMethod); $isAlipay = $this->isGatewayType(GATEWAY_TYPE_ALIPAY, $paymentMethod);
$isSofort = $this->isGatewayType(GATEWAY_TYPE_SOFORT, $paymentMethod); $isSofort = $this->isGatewayType(GATEWAY_TYPE_SOFORT, $paymentMethod);
$isBitcoin = $this->isGatewayType(GATEWAY_TYPE_BITCOIN, $paymentMethod);
if ($isBank || $isAlipay || $isSofort) { if ($isBank || $isAlipay || $isSofort || $isBitcoin) {
$payment->payment_status_id = $this->purchaseResponse['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING; $payment->payment_status_id = $this->purchaseResponse['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING;
if ($isAlipay) { if ($isAlipay) {
$payment->payment_type_id = PAYMENT_TYPE_ALIPAY; $payment->payment_type_id = PAYMENT_TYPE_ALIPAY;
} elseif ($isSofort) { } elseif ($isSofort) {
$payment->payment_type_id = PAYMENT_TYPE_SOFORT; $payment->payment_type_id = PAYMENT_TYPE_SOFORT;
} elseif ($isBitcoin) {
$payment->payment_type_id = PAYMENT_TYPE_BITCOIN;
} }
} }
@ -351,6 +361,7 @@ class StripePaymentDriver extends BasePaymentDriver
$amount = intval($this->invoice()->getRequestedAmount() * 100); $amount = intval($this->invoice()->getRequestedAmount() * 100);
$invoiceNumber = $this->invoice()->invoice_number; $invoiceNumber = $this->invoice()->invoice_number;
$currency = $this->client()->getCurrencyCode(); $currency = $this->client()->getCurrencyCode();
$email = $this->contact()->email;
$gatewayType = GatewayType::getAliasFromId($this->gatewayType); $gatewayType = GatewayType::getAliasFromId($this->gatewayType);
$redirect = url("/complete_source/{$this->invitation->invitation_key}/{$gatewayType}"); $redirect = url("/complete_source/{$this->invitation->invitation_key}/{$gatewayType}");
$country = $this->client()->country ? $this->client()->country->iso_3166_2 : ($this->account()->country ? $this->account()->country->iso_3166_2 : ''); $country = $this->client()->country ? $this->client()->country->iso_3166_2 : ($this->account()->country ? $this->account()->country->iso_3166_2 : '');
@ -361,6 +372,12 @@ class StripePaymentDriver extends BasePaymentDriver
throw new Exception('Alipay is not enabled'); throw new Exception('Alipay is not enabled');
} }
$type = 'alipay'; $type = 'alipay';
} elseif ($this->gatewayType == GATEWAY_TYPE_BITCOIN) {
if (! $this->accountGateway->getBitcoinEnabled()) {
throw new Exception('Bitcoin is not enabled');
}
$type = 'bitcoin';
$extra = "&owner[email]={$email}";
} else { } else {
if (! $this->accountGateway->getSofortEnabled()) { if (! $this->accountGateway->getSofortEnabled()) {
throw new Exception('Sofort is not enabled'); throw new Exception('Sofort is not enabled');
@ -376,7 +393,18 @@ class StripePaymentDriver extends BasePaymentDriver
$this->invitation->transaction_reference = $response['id']; $this->invitation->transaction_reference = $response['id'];
$this->invitation->save(); $this->invitation->save();
return redirect($response['redirect']['url']); if ($this->gatewayType == GATEWAY_TYPE_BITCOIN) {
return view('payments/stripe/bitcoin', [
'client' => $this->client(),
'account' => $this->account(),
'invitation' => $this->invitation,
'invoiceNumber' => $invoiceNumber,
'amount' => $amount,
'source' => $response,
]);
} else {
return redirect($response['redirect']['url']);
}
} else { } else {
throw new Exception($response); throw new Exception($response);
} }
@ -474,6 +502,10 @@ class StripePaymentDriver extends BasePaymentDriver
return false; return false;
} }
if ($payment->is_deleted || $payment->invoice->is_deleted) {
return false;
}
if ($eventType == 'charge.failed') { if ($eventType == 'charge.failed') {
if (! $payment->isFailed()) { if (! $payment->isFailed()) {
$payment->markFailed($source['failure_message']); $payment->markFailed($source['failure_message']);

View File

@ -276,6 +276,10 @@ class WePayPaymentDriver extends BasePaymentDriver
throw new Exception('Unknown payment'); throw new Exception('Unknown payment');
} }
if ($payment->is_deleted || $payment->invoice->is_deleted) {
throw new Exception('Payment is deleted');
}
$wepay = Utils::setupWePay($accountGateway); $wepay = Utils::setupWePay($accountGateway);
$checkout = $wepay->request('checkout', [ $checkout = $wepay->request('checkout', [
'checkout_id' => intval($objectId), 'checkout_id' => intval($objectId),

View File

@ -52,6 +52,18 @@ class AccountPresenter extends Presenter
return Utils::addHttp($this->entity->website); return Utils::addHttp($this->entity->website);
} }
/**
* @return string
*/
public function taskRate()
{
if ($this->entity->task_rate) {
return Utils::roundSignificant($this->entity->task_rate);
} else {
return '';
}
}
/** /**
* @return mixed * @return mixed
*/ */

View File

@ -50,4 +50,16 @@ class ClientPresenter extends EntityPresenter
return sprintf('%s: %s %s', trans('texts.payment_terms'), trans('texts.payment_terms_net'), $client->defaultDaysDue()); return sprintf('%s: %s %s', trans('texts.payment_terms'), trans('texts.payment_terms_net'), $client->defaultDaysDue());
} }
/**
* @return string
*/
public function taskRate()
{
if ($this->entity->task_rate) {
return Utils::roundSignificant($this->entity->task_rate);
} else {
return '';
}
}
} }

View File

@ -84,7 +84,7 @@ class EntityPresenter extends Presenter
$entity = $this->entity; $entity = $this->entity;
$entityType = $entity->getEntityType(); $entityType = $entity->getEntityType();
return sprintf('%s: %s', trans('texts.' . $entityType), $entity->getDisplayName()); return sprintf('%s %s', trans('texts.' . $entityType), $entity->getDisplayName());
} }
public function calendarEvent($subColors = false) public function calendarEvent($subColors = false)

View File

@ -67,11 +67,14 @@ class InvoicePresenter extends EntityPresenter
public function age() public function age()
{ {
if (! $this->entity->due_date || $this->entity->date_date == '0000-00-00') { $invoice = $this->entity;
$dueDate = $invoice->partial_due_date ?: $invoice->due_date;
if (! $dueDate || $dueDate == '0000-00-00') {
return 0; return 0;
} }
$date = Carbon::parse($this->entity->due_date); $date = Carbon::parse($dueDate);
if ($date->isFuture()) { if ($date->isFuture()) {
return 0; return 0;
@ -155,6 +158,11 @@ class InvoicePresenter extends EntityPresenter
return Utils::fromSqlDate($this->entity->due_date); return Utils::fromSqlDate($this->entity->due_date);
} }
public function partial_due_date()
{
return Utils::fromSqlDate($this->entity->partial_due_date);
}
public function frequency() public function frequency()
{ {
$frequency = $this->entity->frequency ? $this->entity->frequency->name : ''; $frequency = $this->entity->frequency ? $this->entity->frequency->name : '';
@ -248,7 +256,7 @@ class InvoicePresenter extends EntityPresenter
} }
if ($invoice->onlyHasTasks()) { if ($invoice->onlyHasTasks()) {
$actions[] = ['url' => 'javascript:onAddItemClick()', 'label' => trans('texts.add_item')]; $actions[] = ['url' => 'javascript:onAddItemClick()', 'label' => trans('texts.add_product')];
} }
if ($invoice->canBePaid()) { if ($invoice->canBePaid()) {

View File

@ -2,6 +2,7 @@
namespace App\Ninja\Presenters; namespace App\Ninja\Presenters;
use DropdownButton;
use App\Libraries\Skype\HeroCard; use App\Libraries\Skype\HeroCard;
class ProductPresenter extends EntityPresenter class ProductPresenter extends EntityPresenter
@ -22,4 +23,25 @@ class ProductPresenter extends EntityPresenter
return $card; return $card;
} }
public function moreActions()
{
$product = $this->entity;
if (! $product->trashed()) {
if (auth()->user()->can('create', ENTITY_INVOICE)) {
$actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans('texts.invoice_product')];
$actions[] = DropdownButton::DIVIDER;
}
$actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans("texts.archive_product")];
} else {
$actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans("texts.restore_product")];
}
if (! $product->is_deleted) {
$actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans("texts.delete_product")];
}
return $actions;
}
} }

View File

@ -41,7 +41,7 @@ class AgingReport extends AbstractReport
$this->isExport ? $client->getDisplayName() : $client->present()->link, $this->isExport ? $client->getDisplayName() : $client->present()->link,
$this->isExport ? $invoice->invoice_number : $invoice->present()->link, $this->isExport ? $invoice->invoice_number : $invoice->present()->link,
$invoice->present()->invoice_date, $invoice->present()->invoice_date,
$invoice->present()->due_date, $invoice->present()->partial_due_date ?: $invoice->present()->due_date,
$invoice->present()->age, $invoice->present()->age,
$account->formatMoney($invoice->amount, $client), $account->formatMoney($invoice->amount, $client),
$account->formatMoney($invoice->balance, $client), $account->formatMoney($invoice->balance, $client),

View File

@ -0,0 +1,74 @@
<?php
namespace App\Ninja\Reports;
use App\Models\Invoice;
use App\Models\Expense;
use Barracuda\ArchiveStream\Archive;
class DocumentReport extends AbstractReport
{
public $columns = [
'document',
'client',
'invoice_or_expense',
'date',
];
public function run()
{
$account = auth()->user()->account;
$filter = $this->options['document_filter'];
$exportFormat = $this->options['export_format'];
$records = false;
if (! $filter || $filter == ENTITY_INVOICE) {
$records = Invoice::scope()
->withArchived()
->with(['documents'])
->where('invoice_date', '>=', $this->startDate)
->where('invoice_date', '<=', $this->endDate)
->get();
}
if (! $filter || $filter == ENTITY_EXPENSE){
$expenses = Expense::scope()
->withArchived()
->with(['documents'])
->where('expense_date', '>=', $this->startDate)
->where('expense_date', '<=', $this->endDate)
->get();
if ($records) {
$records = $records->merge($expenses);
} else {
$records = $expenses;
}
}
if ($this->isExport && $exportFormat == 'zip') {
$zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.documents')));
foreach ($records as $record) {
foreach ($record->documents as $document) {
$name = sprintf('%s_%s_%s', date('Y-m-d'), $record->present()->titledName, $document->name);
$name = str_replace(' ', '_', $name);
$name = str_replace('#', '', $name);
$zip->add_file($name, $document->getRaw());
}
}
$zip->finish();
exit;
}
foreach ($records as $record) {
foreach ($record->documents as $document) {
$this->data[] = [
$this->isExport ? $document->name : link_to($document->getUrl(), $document->name),
$record->client ? ($this->isExport ? $record->client->getDisplayName() : $record->client->present()->link) : '',
$this->isExport ? $record->present()->titledName : ($filter ? $record->present()->link : link_to($record->present()->url, $record->present()->titledName)),
$record->getEntityType() == ENTITY_INVOICE ? $record->invoice_date : $record->expense_date,
];
}
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Ninja\Reports; namespace App\Ninja\Reports;
use Barracuda\ArchiveStream\Archive;
use App\Models\Expense; use App\Models\Expense;
use Auth; use Auth;
use Utils; use Utils;
@ -19,6 +20,12 @@ class ExpenseReport extends AbstractReport
public function run() public function run()
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$exportFormat = $this->options['export_format'];
$with = ['client.contacts', 'vendor'];
if ($exportFormat == 'zip') {
$with[] = ['documents'];
}
$expenses = Expense::scope() $expenses = Expense::scope()
->orderBy('expense_date', 'desc') ->orderBy('expense_date', 'desc')
@ -27,6 +34,19 @@ class ExpenseReport extends AbstractReport
->where('expense_date', '>=', $this->startDate) ->where('expense_date', '>=', $this->startDate)
->where('expense_date', '<=', $this->endDate); ->where('expense_date', '<=', $this->endDate);
if ($this->isExport && $exportFormat == 'zip') {
$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);
$name = str_replace(' ', '_', $name);
$zip->add_file($name, $document->getRaw());
}
}
$zip->finish();
exit;
}
foreach ($expenses->get() as $expense) { foreach ($expenses->get() as $expense) {
$amount = $expense->amountWithTax(); $amount = $expense->amountWithTax();

View File

@ -4,6 +4,7 @@ namespace App\Ninja\Reports;
use App\Models\Client; use App\Models\Client;
use Auth; use Auth;
use Barracuda\ArchiveStream\Archive;
class InvoiceReport extends AbstractReport class InvoiceReport extends AbstractReport
{ {
@ -22,6 +23,7 @@ class InvoiceReport extends AbstractReport
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$status = $this->options['invoice_status']; $status = $this->options['invoice_status'];
$exportFormat = $this->options['export_format'];
$clients = Client::scope() $clients = Client::scope()
->orderBy('name') ->orderBy('name')
@ -44,6 +46,21 @@ class InvoiceReport extends AbstractReport
}, 'invoice_items']); }, 'invoice_items']);
}]); }]);
if ($this->isExport && $exportFormat == 'zip') {
$zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoice_documents')));
foreach ($clients->get() as $client) {
foreach ($client->invoices as $invoice) {
foreach ($invoice->documents as $document) {
$name = sprintf('%s_%s_%s', date('Y-m-d'), $invoice->present()->titledName, $document->name);
$zip->add_file($name, $document->getRaw());
}
}
}
$zip->finish();
exit;
}
foreach ($clients->get() as $client) { foreach ($clients->get() as $client) {
foreach ($client->invoices as $invoice) { foreach ($client->invoices as $invoice) {
$payments = count($invoice->payments) ? $invoice->payments : [false]; $payments = count($invoice->payments) ? $invoice->payments : [false];

View File

@ -4,6 +4,7 @@ namespace App\Ninja\Reports;
use App\Models\Client; use App\Models\Client;
use Auth; use Auth;
use Barracuda\ArchiveStream\Archive;
class QuoteReport extends AbstractReport class QuoteReport extends AbstractReport
{ {
@ -19,6 +20,7 @@ class QuoteReport extends AbstractReport
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$status = $this->options['invoice_status']; $status = $this->options['invoice_status'];
$exportFormat = $this->options['export_format'];
$clients = Client::scope() $clients = Client::scope()
->orderBy('name') ->orderBy('name')
@ -35,6 +37,21 @@ class QuoteReport extends AbstractReport
->with(['invoice_items']); ->with(['invoice_items']);
}]); }]);
if ($this->isExport && $exportFormat == 'zip') {
$zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.quote_documents')));
foreach ($clients->get() as $client) {
foreach ($client->invoices as $invoice) {
foreach ($invoice->documents as $document) {
$name = sprintf('%s_%s_%s', date('Y-m-d'), $invoice->present()->titledName, $document->name);
$name = str_replace(' ', '_', $name);
$zip->add_file($name, $document->getRaw());
}
}
}
$zip->finish();
exit;
}
foreach ($clients->get() as $client) { foreach ($clients->get() as $client) {
foreach ($client->invoices as $invoice) { foreach ($client->invoices as $invoice) {
$this->data[] = [ $this->data[] = [

View File

@ -58,10 +58,30 @@ class AccountRepository
$account->account_key = strtolower(str_random(RANDOM_KEY_LENGTH)); $account->account_key = strtolower(str_random(RANDOM_KEY_LENGTH));
$account->company_id = $company->id; $account->company_id = $company->id;
if ($locale = Session::get(SESSION_LOCALE)) { // Set default language/currency based on IP
if ($language = Language::whereLocale($locale)->first()) { $data = unserialize(file_get_contents('http://www.geoplugin.net/php.gp?ip=' . $account->ip));
$account->language_id = $language->id; $currencyCode = strtolower($data['geoplugin_currencyCode']);
} $countryCode = strtolower($data['geoplugin_countryCode']);
$currency = \Cache::get('currencies')->filter(function ($item) use ($currencyCode) {
return strtolower($item->code) == $currencyCode;
})->first();
if ($currency) {
$account->currency_id = $currency->id;
}
$country = \Cache::get('countries')->filter(function ($item) use ($countryCode) {
return strtolower($item->iso_3166_2) == $countryCode || strtolower($item->iso_3166_3) == $countryCode;
})->first();
if ($country) {
$account->country_id = $country->id;
}
$language = \Cache::get('languages')->filter(function ($item) use ($countryCode) {
return strtolower($item->locale) == $countryCode;
})->first();
if ($language) {
$account->language_id = $language->id;
} }
$account->save(); $account->save();

View File

@ -309,13 +309,13 @@ class DashboardRepository
->where('invoices.deleted_at', '=', null) ->where('invoices.deleted_at', '=', null)
->where('invoices.is_public', '=', true) ->where('invoices.is_public', '=', true)
->where('contacts.is_primary', '=', true) ->where('contacts.is_primary', '=', true)
->where('invoices.due_date', '<', date('Y-m-d')); ->where(DB::raw("coalesce(invoices.partial_due_date, invoices.due_date)"), '<', date('Y-m-d'));
if (! $viewAll) { if (! $viewAll) {
$pastDue = $pastDue->where('invoices.user_id', '=', $userId); $pastDue = $pastDue->where('invoices.user_id', '=', $userId);
} }
return $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id']) return $pastDue->select([DB::raw("coalesce(invoices.partial_due_date, invoices.due_date) due_date"), 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->orderBy('invoices.due_date', 'asc') ->orderBy('invoices.due_date', 'asc')
->take(50) ->take(50)
->get(); ->get();
@ -337,8 +337,7 @@ class DashboardRepository
->where('invoices.is_public', '=', true) ->where('invoices.is_public', '=', true)
->where('contacts.is_primary', '=', true) ->where('contacts.is_primary', '=', true)
->where(function($query) { ->where(function($query) {
$query->where('invoices.due_date', '>=', date('Y-m-d')) $query->where(DB::raw("coalesce(invoices.partial_due_date, invoices.due_date)"), '>=', date('Y-m-d'));
->orWhereNull('invoices.due_date');
}) })
->orderBy('invoices.due_date', 'asc'); ->orderBy('invoices.due_date', 'asc');
@ -347,7 +346,7 @@ class DashboardRepository
} }
return $upcoming->take(50) return $upcoming->take(50)
->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id']) ->select([DB::raw("coalesce(invoices.partial_due_date, invoices.due_date) due_date"), 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id'])
->get(); ->get();
} }

View File

@ -74,9 +74,10 @@ class InvoiceRepository extends BaseRepository
'invoices.balance', 'invoices.balance',
'invoices.invoice_date', 'invoices.invoice_date',
'invoices.due_date as due_date_sql', 'invoices.due_date as due_date_sql',
'invoices.partial_due_date',
DB::raw("CONCAT(invoices.invoice_date, invoices.created_at) as date"), DB::raw("CONCAT(invoices.invoice_date, invoices.created_at) as date"),
DB::raw("CONCAT(invoices.due_date, invoices.created_at) as due_date"), DB::raw("CONCAT(COALESCE(invoices.partial_due_date, invoices.due_date), invoices.created_at) as due_date"),
DB::raw("CONCAT(invoices.due_date, invoices.created_at) as valid_until"), DB::raw("CONCAT(COALESCE(invoices.partial_due_date, invoices.due_date), invoices.created_at) as valid_until"),
'invoice_statuses.name as status', 'invoice_statuses.name as status',
'invoice_statuses.name as invoice_status_name', 'invoice_statuses.name as invoice_status_name',
'contacts.first_name', 'contacts.first_name',
@ -338,7 +339,7 @@ class InvoiceRepository extends BaseRepository
} elseif (Invoice::calcIsOverdue($model->balance, $model->due_date)) { } elseif (Invoice::calcIsOverdue($model->balance, $model->due_date)) {
$class = 'danger'; $class = 'danger';
if ($entityType == ENTITY_INVOICE) { if ($entityType == ENTITY_INVOICE) {
$label = trans('texts.overdue'); $label = trans('texts.past_due');
} else { } else {
$label = trans('texts.expired'); $label = trans('texts.expired');
} }
@ -389,7 +390,7 @@ class InvoiceRepository extends BaseRepository
$invoice->custom_taxes2 = $account->custom_invoice_taxes2 ?: false; $invoice->custom_taxes2 = $account->custom_invoice_taxes2 ?: false;
// set the default due date // set the default due date
if ($entityType == ENTITY_INVOICE) { if ($entityType == ENTITY_INVOICE && empty($data['partial_due_date'])) {
$client = Client::scope()->whereId($data['client_id'])->first(); $client = Client::scope()->whereId($data['client_id'])->first();
$invoice->due_date = $account->defaultDueDate($client); $invoice->due_date = $account->defaultDueDate($client);
} }
@ -402,6 +403,8 @@ class InvoiceRepository extends BaseRepository
if ($invoice->is_deleted) { if ($invoice->is_deleted) {
return $invoice; return $invoice;
} elseif ($invoice->isSent() && config('ninja.lock_sent_invoices')) {
return $invoice;
} }
if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) { if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
@ -445,6 +448,7 @@ class InvoiceRepository extends BaseRepository
if (isset($data['is_amount_discount'])) { if (isset($data['is_amount_discount'])) {
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
} }
if (isset($data['invoice_date_sql'])) { if (isset($data['invoice_date_sql'])) {
$invoice->invoice_date = $data['invoice_date_sql']; $invoice->invoice_date = $data['invoice_date_sql'];
} elseif (isset($data['invoice_date'])) { } elseif (isset($data['invoice_date'])) {
@ -499,8 +503,13 @@ class InvoiceRepository extends BaseRepository
$invoice->terms = ''; $invoice->terms = '';
} }
$invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (! $publicId && $account->invoice_footer ? $account->invoice_footer : ''); if (isset($data['invoice_footer']) && trim($data['invoice_footer'])) {
$invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : ''; $invoice->invoice_footer = trim($data['invoice_footer']);
} elseif ($isNew && ! $invoice->is_recurring && $account->invoice_footer) {
$invoice->invoice_footer = $account->invoice_footer;
} else {
$invoice->invoice_footer = '';
}
// process date variables if not recurring // process date variables if not recurring
if (! $invoice->is_recurring) { if (! $invoice->is_recurring) {
@ -619,6 +628,14 @@ class InvoiceRepository extends BaseRepository
$invoice->partial = max(0, min(round(Utils::parseFloat($data['partial']), 2), $invoice->balance)); $invoice->partial = max(0, min(round(Utils::parseFloat($data['partial']), 2), $invoice->balance));
} }
if ($invoice->partial) {
if (isset($data['partial_due_date'])) {
$invoice->partial_due_date = Utils::toSqlDate($data['partial_due_date']);
}
} else {
$invoice->partial_due_date = null;
}
$invoice->amount = $total; $invoice->amount = $total;
$invoice->save(); $invoice->save();
@ -881,7 +898,7 @@ class InvoiceRepository extends BaseRepository
if ($account->invoice_terms) { if ($account->invoice_terms) {
$clone->terms = $account->invoice_terms; $clone->terms = $account->invoice_terms;
} }
if ($account->auto_convert_quote) { if (! auth()->check()) {
$clone->is_public = true; $clone->is_public = true;
$clone->invoice_status_id = INVOICE_STATUS_SENT; $clone->invoice_status_id = INVOICE_STATUS_SENT;
} }
@ -1137,8 +1154,11 @@ class InvoiceRepository extends BaseRepository
for ($i = 1; $i <= 3; $i++) { for ($i = 1; $i <= 3; $i++) {
if ($date = $account->getReminderDate($i, $filterEnabled)) { if ($date = $account->getReminderDate($i, $filterEnabled)) {
$field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date'; if ($account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE) {
$dates[] = "$field = '$date'"; $dates[] = "(due_date = '$date' OR partial_due_date = '$date')";
} else {
$dates[] = "invoice_date = '$date'";
}
} }
} }

View File

@ -35,6 +35,7 @@ class ProjectRepository extends BaseRepository
'projects.public_id', 'projects.public_id',
'projects.user_id', 'projects.user_id',
'projects.deleted_at', 'projects.deleted_at',
'projects.task_rate',
'projects.is_deleted', 'projects.is_deleted',
DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"),
'clients.user_id as client_user_id', 'clients.user_id as client_user_id',

View File

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

View File

@ -37,6 +37,7 @@ class ClientTransformer extends EntityTransformer
* @SWG\Property(property="vat_number", type="string", example="123456") * @SWG\Property(property="vat_number", type="string", example="123456")
* @SWG\Property(property="id_number", type="string", example="123456") * @SWG\Property(property="id_number", type="string", example="123456")
* @SWG\Property(property="language_id", type="integer", example=1) * @SWG\Property(property="language_id", type="integer", example=1)
* @SWG\Property(property="task_rate", type="number", format="float", example=10)
*/ */
protected $defaultIncludes = [ protected $defaultIncludes = [
'contacts', 'contacts',
@ -135,6 +136,7 @@ class ClientTransformer extends EntityTransformer
'custom_value2' => $client->custom_value2, 'custom_value2' => $client->custom_value2,
'invoice_number_counter' => (int) $client->invoice_number_counter, 'invoice_number_counter' => (int) $client->invoice_number_counter,
'quote_number_counter' => (int) $client->quote_number_counter, 'quote_number_counter' => (int) $client->quote_number_counter,
'task_rate' => (float) $client->task_rate,
]); ]);
} }
} }

View File

@ -5,10 +5,18 @@ namespace App\Ninja\Transformers;
use App\Models\Credit; use App\Models\Credit;
/** /**
* Class CreditTransformer. * @SWG\Definition(definition="Credit", required={"client_id"}, @SWG\Xml(name="Credit"))
*/ */
class CreditTransformer extends EntityTransformer class CreditTransformer extends EntityTransformer
{ {
/**
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="amount", type="number", format="float", example=10, readOnly=true)
* @SWG\Property(property="client_id", type="integer", example=1)
* @SWG\Property(property="private_notes", type="string", example="Notes...")
* @SWG\Property(property="public_notes", type="string", example="Notes...")
*/
/** /**
* @param Credit $credit * @param Credit $credit
* *

View File

@ -119,6 +119,7 @@ class InvoiceTransformer extends EntityTransformer
'is_amount_discount' => (bool) $invoice->is_amount_discount, 'is_amount_discount' => (bool) $invoice->is_amount_discount,
'invoice_footer' => $invoice->invoice_footer, 'invoice_footer' => $invoice->invoice_footer,
'partial' => (float) $invoice->partial, 'partial' => (float) $invoice->partial,
'partial_due_date' => $invoice->partial_due_date,
'has_tasks' => (bool) $invoice->has_tasks, 'has_tasks' => (bool) $invoice->has_tasks,
'auto_bill' => (bool) $invoice->auto_bill, 'auto_bill' => (bool) $invoice->auto_bill,
'custom_value1' => (float) $invoice->custom_value1, 'custom_value1' => (float) $invoice->custom_value1,

View File

@ -16,6 +16,7 @@ class ProjectTransformer extends EntityTransformer
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true) * @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true) * @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="is_deleted", type="boolean", example=false, readOnly=true) * @SWG\Property(property="is_deleted", type="boolean", example=false, readOnly=true)
* @SWG\Property(property="task_rate", type="number", format="float", example=10)
*/ */
public function transform(Project $project) public function transform(Project $project)
{ {
@ -26,6 +27,7 @@ class ProjectTransformer extends EntityTransformer
'updated_at' => $this->getTimestamp($project->updated_at), 'updated_at' => $this->getTimestamp($project->updated_at),
'archived_at' => $this->getTimestamp($project->deleted_at), 'archived_at' => $this->getTimestamp($project->deleted_at),
'is_deleted' => (bool) $project->is_deleted, 'is_deleted' => (bool) $project->is_deleted,
'task_rate' => (float) $project->task_rate,
]); ]);
} }
} }

View File

@ -200,6 +200,11 @@ class EventServiceProvider extends ServiceProvider
'Illuminate\Queue\Events\JobExceptionOccurred' => [ 'Illuminate\Queue\Events\JobExceptionOccurred' => [
'App\Listeners\InvoiceListener@jobFailed' 'App\Listeners\InvoiceListener@jobFailed'
],
//DNS
'App\Events\SubdomainWasUpdated' => [
'App\Listeners\DNSListener@addDNSRecord'
] ]
/* /*

View File

@ -83,10 +83,15 @@ class AuthService
} }
} else { } else {
LookupUser::setServerByField('oauth_user_key', $providerId . '-' . $oauthUserId); LookupUser::setServerByField('oauth_user_key', $providerId . '-' . $oauthUserId);
\Log::info("Find user: $providerId, $oauthUserId");
if ($user = $this->accountRepo->findUserByOauth($providerId, $oauthUserId)) { if ($user = $this->accountRepo->findUserByOauth($providerId, $oauthUserId)) {
Auth::login($user, true); if ($user->google_2fa_secret) {
event(new UserLoggedIn()); session(['2fa:user:id' => $user->id]);
return redirect('/validate_two_factor/' . $user->account->account_key);
} else {
Auth::login($user);
event(new UserLoggedIn());
}
} else { } else {
Session::flash('error', trans('texts.invalid_credentials')); Session::flash('error', trans('texts.invalid_credentials'));

View File

@ -443,7 +443,9 @@ class ImportService
// update the entity maps // update the entity maps
if ($entityType != ENTITY_CUSTOMER) { if ($entityType != ENTITY_CUSTOMER) {
$mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps'; $mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
$this->$mapFunction($entity); if (method_exists($this, $mapFunction)) {
$this->$mapFunction($entity);
}
} }
// if the invoice is paid we'll also create a payment record // if the invoice is paid we'll also create a payment record
@ -929,6 +931,7 @@ class ImportService
private function addInvoiceToMaps(Invoice $invoice) private function addInvoiceToMaps(Invoice $invoice)
{ {
if ($number = strtolower(trim($invoice->invoice_number))) { if ($number = strtolower(trim($invoice->invoice_number))) {
$this->maps['invoices'][$number] = $invoice;
$this->maps['invoice'][$number] = $invoice->id; $this->maps['invoice'][$number] = $invoice->id;
$this->maps['invoice_client'][$number] = $invoice->client_id; $this->maps['invoice_client'][$number] = $invoice->client_id;
$this->maps['invoice_ids'][$invoice->public_id] = $invoice->id; $this->maps['invoice_ids'][$invoice->public_id] = $invoice->id;

View File

@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\Account; use App\Models\Account;
use App\Models\Activity; use App\Models\Activity;
use App\Models\Client; use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Ninja\Datatables\PaymentDatatable; use App\Ninja\Datatables\PaymentDatatable;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
@ -149,8 +150,19 @@ class PaymentService extends BaseService
} }
} }
public function save($input, $payment = null) public function save($input, $payment = null, $invoice = null)
{ {
// if the payment amount is more than the balance create a credit
if ($invoice && $input['amount'] > $invoice->balance) {
$credit = Credit::createNew();
$credit->client_id = $invoice->client_id;
$credit->credit_date = date_create()->format('Y-m-d');
$credit->amount = $credit->balance = $input['amount'] - $invoice->balance;
$credit->private_notes = trans('texts.credit_created_by', ['transaction_reference' => isset($input['transaction_reference']) ? $input['transaction_reference'] : '']);
$credit->save();
$input['amount'] = $invoice->balance;
}
return $this->paymentRepo->save($input, $payment); return $this->paymentRepo->save($input, $payment);
} }

View File

@ -45,7 +45,7 @@ class TemplateService
'$emailSignature' => $account->getEmailFooter(), '$emailSignature' => $account->getEmailFooter(),
'$client' => $client->getDisplayName(), '$client' => $client->getDisplayName(),
'$account' => $account->getDisplayName(), '$account' => $account->getDisplayName(),
'$dueDate' => $account->formatDate($invoice->due_date), '$dueDate' => $account->formatDate($invoice->partial_due_date ?: $invoice->due_date),
'$invoiceDate' => $account->formatDate($invoice->invoice_date), '$invoiceDate' => $account->formatDate($invoice->invoice_date),
'$contact' => $contact->getDisplayName(), '$contact' => $contact->getDisplayName(),
'$firstName' => $contact->first_name, '$firstName' => $contact->first_name,

View File

@ -38,7 +38,8 @@
"card": "^2.1.1", "card": "^2.1.1",
"fullcalendar": "^3.5.1", "fullcalendar": "^3.5.1",
"toastr": "^2.1.3", "toastr": "^2.1.3",
"jt.timepicker": "jquery-timepicker-jt#^1.11.12" "jt.timepicker": "jquery-timepicker-jt#^1.11.12",
"qrcode.js": "qrcode-js#*"
}, },
"resolutions": { "resolutions": {
"jquery": "~1.11" "jquery": "~1.11"

View File

@ -17,11 +17,13 @@
"ext-gd": "*", "ext-gd": "*",
"ext-gmp": "*", "ext-gmp": "*",
"Dwolla/omnipay-dwolla": "dev-master", "Dwolla/omnipay-dwolla": "dev-master",
"abdala/omnipay-pagseguro": "0.2",
"agmscode/omnipay-agms": "~1.0", "agmscode/omnipay-agms": "~1.0",
"alfaproject/omnipay-skrill": "dev-master", "alfaproject/omnipay-skrill": "dev-master",
"anahkiasen/former": "4.0.*@dev", "anahkiasen/former": "4.0.*@dev",
"andreas22/omnipay-fasapay": "1.*", "andreas22/omnipay-fasapay": "1.*",
"asgrim/ofxparser": "^1.1", "asgrim/ofxparser": "^1.1",
"bacon/bacon-qr-code": "^1.0",
"barracudanetworks/archivestream-php": "^1.0", "barracudanetworks/archivestream-php": "^1.0",
"barryvdh/laravel-cors": "^0.9.1", "barryvdh/laravel-cors": "^0.9.1",
"barryvdh/laravel-debugbar": "~2.2", "barryvdh/laravel-debugbar": "~2.2",
@ -70,6 +72,7 @@
"mpdf/mpdf": "6.1.3", "mpdf/mpdf": "6.1.3",
"nwidart/laravel-modules": "^1.14", "nwidart/laravel-modules": "^1.14",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248", "omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/authorizenet": "dev-solution-id as 2.5.0",
"omnipay/bitpay": "dev-master", "omnipay/bitpay": "dev-master",
"omnipay/braintree": "~2.0@dev", "omnipay/braintree": "~2.0@dev",
"omnipay/gocardless": "dev-master", "omnipay/gocardless": "dev-master",
@ -77,6 +80,7 @@
"omnipay/omnipay": "~2.3", "omnipay/omnipay": "~2.3",
"omnipay/stripe": "dev-master", "omnipay/stripe": "dev-master",
"patricktalmadge/bootstrapper": "5.5.x", "patricktalmadge/bootstrapper": "5.5.x",
"pragmarx/google2fa-laravel": "^0.1.2",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"simshaun/recurr": "dev-master", "simshaun/recurr": "dev-master",
"softcommerce/omnipay-paytrace": "~1.0", "softcommerce/omnipay-paytrace": "~1.0",
@ -86,10 +90,8 @@
"webpatser/laravel-countries": "dev-master", "webpatser/laravel-countries": "dev-master",
"websight/l5-google-cloud-storage": "dev-master", "websight/l5-google-cloud-storage": "dev-master",
"wepay/php-sdk": "^0.2", "wepay/php-sdk": "^0.2",
"wildbit/laravel-postmark-provider": "3.0", "wildbit/laravel-postmark-provider": "3.0"
"abdala/omnipay-pagseguro": "0.2", },
"omnipay/authorizenet": "dev-solution-id as 2.5.0"
},
"require-dev": { "require-dev": {
"codeception/c3": "~2.0", "codeception/c3": "~2.0",
"codeception/codeception": "2.3.3", "codeception/codeception": "2.3.3",

551
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -158,6 +158,7 @@ return [
Codedge\Updater\UpdaterServiceProvider::class, Codedge\Updater\UpdaterServiceProvider::class,
Nwidart\Modules\LaravelModulesServiceProvider::class, Nwidart\Modules\LaravelModulesServiceProvider::class,
Barryvdh\Cors\ServiceProvider::class, Barryvdh\Cors\ServiceProvider::class,
PragmaRX\Google2FALaravel\ServiceProvider::class,
/* /*
* Application Service Providers... * Application Service Providers...
@ -171,6 +172,7 @@ return [
'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider', 'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
'Davibennun\LaravelPushNotification\LaravelPushNotificationServiceProvider', 'Davibennun\LaravelPushNotification\LaravelPushNotificationServiceProvider',
], ],
/* /*
@ -266,6 +268,8 @@ return [
'DateUtils' => App\Libraries\DateUtils::class, 'DateUtils' => App\Libraries\DateUtils::class,
'HTMLUtils' => App\Libraries\HTMLUtils::class, 'HTMLUtils' => App\Libraries\HTMLUtils::class,
'Domain' => App\Constants\Domain::class, 'Domain' => App\Constants\Domain::class,
'Google2FA' => PragmaRX\Google2FALaravel\Facade::class,
], ],
]; ];

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