mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-23 20:00:33 -04:00
Merge branch 'release-3.9.0'
This commit is contained in:
commit
6bb62d1046
@ -99,3 +99,9 @@ WEPAY_THEME='{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":
|
||||
|
||||
BLUEVINE_PARTNER_UNIQUE_ID=
|
||||
BLUEVINE_PARTNER_TOKEN=
|
||||
|
||||
CLOUDFLARE_DNS_ENABLED=false
|
||||
CLOUDFLARE_API_KEY=
|
||||
CLOUDFLARE_EMAIL=
|
||||
CLOUDFLARE_TARGET_IP_ADDRESS=
|
||||
CLOUDFLARE_ZONE_IDS={}
|
@ -119,6 +119,7 @@ after_script:
|
||||
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr)
|
||||
- for i in $FILES; do echo $i; base64 "$i"; break; done
|
||||
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
|
146
Gruntfile.js
146
Gruntfile.js
@ -34,152 +34,10 @@ module.exports = function(grunt) {
|
||||
|
||||
// Return the computed object
|
||||
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.registerTask('default', ['dump_dir', 'concat']);
|
||||
grunt.registerTask('default', ['dump_dir']);
|
||||
|
||||
};
|
||||
|
@ -59,7 +59,7 @@ class ChargeRenewalInvoices extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' ChargeRenewalInvoices...');
|
||||
$this->info(date('r').' ChargeRenewalInvoices...');
|
||||
|
||||
if ($database = $this->option('database')) {
|
||||
config(['database.default' => $database]);
|
||||
|
@ -42,6 +42,10 @@ Options:
|
||||
By default the script only checks for errors, adding this option
|
||||
makes the script apply the fixes.
|
||||
|
||||
--fast=true
|
||||
|
||||
Skip using phantomjs
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -144,7 +148,7 @@ class CheckData extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->option('fix') == 'true') {
|
||||
if ($this->option('fix') == 'true' || $this->option('fast') == 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -792,6 +796,7 @@ class CheckData extends Command
|
||||
{
|
||||
return [
|
||||
['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null],
|
||||
['fast', null, InputOption::VALUE_OPTIONAL, 'Fast', null],
|
||||
['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null],
|
||||
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
|
||||
];
|
||||
|
@ -83,7 +83,7 @@ class CreateTestData extends Command
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info(date('Y-m-d').' Running CreateTestData...');
|
||||
$this->info(date('r').' Running CreateTestData...');
|
||||
$this->count = $this->argument('count');
|
||||
|
||||
if ($database = $this->option('database')) {
|
||||
|
@ -23,7 +23,7 @@ class PruneData extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' Running PruneData...');
|
||||
$this->info(date('r').' Running PruneData...');
|
||||
|
||||
if ($database = $this->option('database')) {
|
||||
config(['database.default' => $database]);
|
||||
|
@ -23,7 +23,7 @@ class RemoveOrphanedDocuments extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' Running RemoveOrphanedDocuments...');
|
||||
$this->info(date('r').' Running RemoveOrphanedDocuments...');
|
||||
|
||||
if ($database = $this->option('database')) {
|
||||
config(['database.default' => $database]);
|
||||
|
@ -23,7 +23,7 @@ class ResetData extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d') . ' Running ResetData...');
|
||||
$this->info(date('r') . ' Running ResetData...');
|
||||
|
||||
if (! Utils::isNinjaDev()) {
|
||||
return;
|
||||
|
@ -65,7 +65,7 @@ class SendRecurringInvoices extends Command
|
||||
|
||||
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')) {
|
||||
config(['database.default' => $database]);
|
||||
@ -76,7 +76,7 @@ class SendRecurringInvoices extends Command
|
||||
$this->billInvoices();
|
||||
$this->createExpenses();
|
||||
|
||||
$this->info(date('Y-m-d H:i:s') . ' Done');
|
||||
$this->info(date('r') . ' Done');
|
||||
}
|
||||
|
||||
private function resetCounters()
|
||||
|
@ -57,7 +57,7 @@ class SendReminders extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d') . ' Running SendReminders...');
|
||||
$this->info(date('r') . ' Running SendReminders...');
|
||||
|
||||
if ($database = $this->option('database')) {
|
||||
config(['database.default' => $database]);
|
||||
|
@ -50,7 +50,7 @@ class SendRenewalInvoices extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' Running SendRenewalInvoices...');
|
||||
$this->info(date('r').' Running SendRenewalInvoices...');
|
||||
|
||||
if ($database = $this->option('database')) {
|
||||
config(['database.default' => $database]);
|
||||
|
@ -39,6 +39,6 @@ class TestOFX extends Command
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' Running TestOFX...');
|
||||
$this->info(date('r').' Running TestOFX...');
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ class UpdateKey extends Command
|
||||
|
||||
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')) {
|
||||
$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;
|
||||
}
|
||||
|
||||
@ -73,9 +73,9 @@ class UpdateKey extends Command
|
||||
}
|
||||
|
||||
if ($envWriteable) {
|
||||
$this->info(date('Y-m-d h:i:s') . ' Successfully update the key');
|
||||
$this->info(date('r') . ' Successfully update the key');
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,8 +224,9 @@ if (! defined('APP_NAME')) {
|
||||
define('FREQUENCY_MONTHLY', 4);
|
||||
define('FREQUENCY_TWO_MONTHS', 5);
|
||||
define('FREQUENCY_THREE_MONTHS', 6);
|
||||
define('FREQUENCY_SIX_MONTHS', 7);
|
||||
define('FREQUENCY_ANNUALLY', 8);
|
||||
define('FREQUENCY_FOUR_MONTHS', 7);
|
||||
define('FREQUENCY_SIX_MONTHS', 8);
|
||||
define('FREQUENCY_ANNUALLY', 9);
|
||||
|
||||
define('SESSION_TIMEZONE', 'timezone');
|
||||
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_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
|
||||
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_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_SOFORT', 29);
|
||||
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_VERIFICATION_FAILED', 'verification_failed');
|
||||
@ -422,6 +425,7 @@ if (! defined('APP_NAME')) {
|
||||
define('GATEWAY_TYPE_ALIPAY', 7);
|
||||
define('GATEWAY_TYPE_SOFORT', 8);
|
||||
define('GATEWAY_TYPE_SEPA', 9);
|
||||
define('GATEWAY_TYPE_GOCARDLESS', 10);
|
||||
define('GATEWAY_TYPE_TOKEN', 'token');
|
||||
|
||||
define('TEMPLATE_INVOICE', 'invoice');
|
||||
@ -552,6 +556,8 @@ if (! defined('APP_NAME')) {
|
||||
define('INVOICE_FIELDS_CLIENT', 'client_fields');
|
||||
define('INVOICE_FIELDS_INVOICE', 'invoice_fields');
|
||||
define('INVOICE_FIELDS_ACCOUNT', 'account_fields');
|
||||
define('INVOICE_FIELDS_PRODUCT', 'product_fields');
|
||||
define('INVOICE_FIELDS_TASK', 'task_fields');
|
||||
|
||||
$creditCards = [
|
||||
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],
|
||||
|
21
app/Events/SubdomainWasUpdated.php
Normal file
21
app/Events/SubdomainWasUpdated.php
Normal 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;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ use App\Events\UserSignedUp;
|
||||
use App\Http\Requests\RegisterRequest;
|
||||
use App\Http\Requests\UpdateAccountRequest;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Ninja\OAuth\OAuth;
|
||||
use App\Ninja\Repositories\AccountRepository;
|
||||
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);
|
||||
$user = $account->users()->first();
|
||||
|
||||
Auth::login($user, true);
|
||||
Auth::login($user);
|
||||
event(new UserSignedUp());
|
||||
|
||||
return $this->processLogin($request);
|
||||
@ -54,11 +55,26 @@ class AccountApiController extends BaseAPIController
|
||||
|
||||
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 ($user && $user->failed_logins > 0) {
|
||||
$user->failed_logins = 0;
|
||||
$user->save();
|
||||
}
|
||||
return $this->processLogin($request);
|
||||
} else {
|
||||
error_log('login failed');
|
||||
if ($user) {
|
||||
$user->failed_logins = $user->failed_logins + 1;
|
||||
$user->save();
|
||||
}
|
||||
sleep(ERROR_DELAY);
|
||||
|
||||
return $this->errorResponse(['message' => 'Invalid credentials'], 401);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\SubdomainWasUpdated;
|
||||
use App\Events\UserSettingsChanged;
|
||||
use App\Events\UserSignedUp;
|
||||
use App\Http\Requests\SaveClientPortalSettings;
|
||||
@ -768,7 +769,12 @@ class AccountController extends BaseController
|
||||
*/
|
||||
public function saveClientPortalSettings(SaveClientPortalSettings $request)
|
||||
{
|
||||
|
||||
$account = $request->user()->account;
|
||||
|
||||
if($account->subdomain !== $request->subdomain)
|
||||
event(new SubdomainWasUpdated($account));
|
||||
|
||||
$account->fill($request->all());
|
||||
$account->client_view_css = $request->client_view_css;
|
||||
$account->subdomain = $request->subdomain;
|
||||
@ -1123,6 +1129,11 @@ class AccountController extends BaseController
|
||||
}
|
||||
|
||||
$rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
|
||||
|
||||
if ($user->google_2fa_secret) {
|
||||
$rules['phone'] = 'required';
|
||||
}
|
||||
|
||||
$validator = Validator::make(Input::all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@ -1144,6 +1155,10 @@ class AccountController extends BaseController
|
||||
$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 (Input::get('referral_code') && ! $user->referral_code) {
|
||||
$user->referral_code = strtolower(str_random(RANDOM_KEY_LENGTH));
|
||||
|
@ -209,7 +209,8 @@ class AccountGatewayController extends BaseController
|
||||
$validator = Validator::make(Input::all(), $rules);
|
||||
|
||||
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)
|
||||
->withInput();
|
||||
} else {
|
||||
@ -294,6 +295,8 @@ class AccountGatewayController extends BaseController
|
||||
if ($gatewayId == GATEWAY_STRIPE) {
|
||||
$config->enableAlipay = boolval(Input::get('enable_alipay'));
|
||||
$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) {
|
||||
|
@ -14,6 +14,9 @@ use Illuminate\Http\Request;
|
||||
use Lang;
|
||||
use Session;
|
||||
use Utils;
|
||||
use Cache;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use App\Http\Requests\ValidateTwoFactorRequest;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
@ -151,15 +154,12 @@ class AuthController extends Controller
|
||||
|
||||
if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
|
||||
Session::flash('error', trans('texts.invalid_credentials'));
|
||||
|
||||
return redirect()->to('login');
|
||||
}
|
||||
|
||||
$response = self::postLogin($request);
|
||||
|
||||
if (Auth::check()) {
|
||||
Event::fire(new UserLoggedIn());
|
||||
|
||||
/*
|
||||
$users = false;
|
||||
// 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);
|
||||
Session::put(SESSION_USER_ACCOUNTS, $users);
|
||||
} elseif ($user) {
|
||||
error_log('login failed');
|
||||
$user->failed_logins = $user->failed_logins + 1;
|
||||
$user->save();
|
||||
}
|
||||
@ -182,6 +180,60 @@ class AuthController extends Controller
|
||||
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
|
||||
*/
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use Event;
|
||||
use App\Events\UserLoggedIn;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
|
||||
@ -18,7 +20,9 @@ class PasswordController extends Controller
|
||||
|
|
||||
*/
|
||||
|
||||
use ResetsPasswords;
|
||||
use ResetsPasswords {
|
||||
getResetSuccessResponse as protected traitGetResetSuccessResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string
|
||||
@ -49,4 +53,18 @@ class PasswordController extends Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ class ClientPortalController extends BaseController
|
||||
|
||||
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
|
||||
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
|
||||
$invoice->partial_due_date = Utils::fromSqlDate($invoice->partial_due_date);
|
||||
$invoice->features = [
|
||||
'customize_invoice_design' => $account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN),
|
||||
'remove_created_by' => $account->hasFeature(FEATURE_REMOVE_CREATED_BY),
|
||||
@ -346,6 +347,7 @@ class ClientPortalController extends BaseController
|
||||
'title' => trans('texts.recurring_invoices'),
|
||||
'entityType' => ENTITY_RECURRING_INVOICE,
|
||||
'columns' => Utils::trans($columns),
|
||||
'sortColumn' => 1,
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
@ -373,6 +375,7 @@ class ClientPortalController extends BaseController
|
||||
'title' => trans('texts.invoices'),
|
||||
'entityType' => ENTITY_INVOICE,
|
||||
'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date', 'status']),
|
||||
'sortColumn' => 1,
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
@ -417,6 +420,7 @@ class ClientPortalController extends BaseController
|
||||
'entityType' => ENTITY_PAYMENT,
|
||||
'title' => trans('texts.payments'),
|
||||
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status']),
|
||||
'sortColumn' => 4,
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
@ -501,6 +505,7 @@ class ClientPortalController extends BaseController
|
||||
'title' => trans('texts.quotes'),
|
||||
'entityType' => ENTITY_QUOTE,
|
||||
'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date', 'status']),
|
||||
'sortColumn' => 1,
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
@ -536,6 +541,7 @@ class ClientPortalController extends BaseController
|
||||
'title' => trans('texts.credits'),
|
||||
'entityType' => ENTITY_CREDIT,
|
||||
'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance', 'notes']),
|
||||
'sortColumn' => 0,
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
@ -571,6 +577,7 @@ class ClientPortalController extends BaseController
|
||||
'title' => trans('texts.documents'),
|
||||
'entityType' => ENTITY_DOCUMENT,
|
||||
'columns' => Utils::trans(['invoice_number', 'name', 'document_date', 'document_size']),
|
||||
'sortColumn' => 2,
|
||||
];
|
||||
|
||||
return response()->view('public_list', $data);
|
||||
|
@ -149,9 +149,16 @@ class HomeController extends BaseController
|
||||
$subject = 'Customer Message: ';
|
||||
if (Utils::isNinjaProd()) {
|
||||
$subject .= str_replace('db-', '', config('database.default'));
|
||||
$account = Auth::user()->account;
|
||||
if ($account->isEnterprise()) {
|
||||
$subject .= 'E';
|
||||
} elseif ($account->isPro()) {
|
||||
$subject .= 'P';
|
||||
}
|
||||
} else {
|
||||
$subject .= 'Self-Host';
|
||||
}
|
||||
$subject .= ' | ' . date('r');
|
||||
$message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com'))
|
||||
->from(CONTACT_EMAIL, Auth::user()->present()->fullName)
|
||||
->replyTo(Auth::user()->email, Auth::user()->present()->fullName)
|
||||
|
@ -200,15 +200,13 @@ class InvoiceApiController extends BaseAPIController
|
||||
|
||||
if ($isEmailInvoice) {
|
||||
if ($payment) {
|
||||
app('App\Ninja\Mailers\ContactMailer')->sendPaymentConfirmation($payment);
|
||||
//$this->dispatch(new SendPaymentEmail($payment));
|
||||
$this->dispatch(new SendPaymentEmail($payment));
|
||||
} else {
|
||||
if ($invoice->is_recurring && $recurringInvoice = $this->invoiceRepo->createRecurringInvoice($invoice)) {
|
||||
$invoice = $recurringInvoice;
|
||||
}
|
||||
$reminder = isset($data['email_type']) ? $data['email_type'] : false;
|
||||
app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice, $reminder);
|
||||
//$this->dispatch(new SendInvoiceEmail($invoice));
|
||||
$this->dispatch(new SendInvoiceEmail($invoice, auth()->user()->id, $reminder));
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,14 +288,23 @@ class InvoiceApiController extends BaseAPIController
|
||||
private function prepareItem($item)
|
||||
{
|
||||
// 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']);
|
||||
if ($product) {
|
||||
if (empty($item['cost'])) {
|
||||
$item['cost'] = $product->cost;
|
||||
}
|
||||
if (empty($item['notes'])) {
|
||||
$item['notes'] = $product->notes;
|
||||
$fields = [
|
||||
'cost',
|
||||
'notes',
|
||||
'custom_value1',
|
||||
'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;
|
||||
}
|
||||
|
||||
//$this->dispatch(new SendInvoiceEmail($invoice));
|
||||
$result = app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);
|
||||
|
||||
if ($result !== true) {
|
||||
return $this->errorResponse($result, 500);
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->dispatch(new SendInvoiceEmail($invoice, auth()->user()->id));
|
||||
} else {
|
||||
$result = app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);
|
||||
if ($result !== true) {
|
||||
return $this->errorResponse($result, 500);
|
||||
}
|
||||
}
|
||||
|
||||
$headers = Utils::getApiHeaders();
|
||||
|
@ -105,6 +105,7 @@ class InvoiceController extends BaseController
|
||||
$invoice->invoice_type_id = $clone;
|
||||
$invoice->invoice_number = $account->getNextNumber($invoice);
|
||||
$invoice->due_date = null;
|
||||
$invoice->partial_due_date = null;
|
||||
$invoice->balance = $invoice->amount;
|
||||
$invoice->invoice_status_id = 0;
|
||||
$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->end_date = Utils::fromSqlDate($invoice->end_date);
|
||||
$invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date);
|
||||
$invoice->partial_due_date = Utils::fromSqlDate($invoice->partial_due_date);
|
||||
|
||||
$invoice->features = [
|
||||
'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN),
|
||||
'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY),
|
||||
|
@ -9,20 +9,23 @@ use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Ninja\Mailers\ContactMailer;
|
||||
use App\Ninja\Repositories\PaymentRepository;
|
||||
use App\Services\PaymentService;
|
||||
use Input;
|
||||
use Response;
|
||||
|
||||
class PaymentApiController extends BaseAPIController
|
||||
{
|
||||
protected $paymentRepo;
|
||||
protected $paymentService;
|
||||
|
||||
protected $entityType = ENTITY_PAYMENT;
|
||||
|
||||
public function __construct(PaymentRepository $paymentRepo, ContactMailer $contactMailer)
|
||||
public function __construct(PaymentRepository $paymentRepo, PaymentService $paymentService, ContactMailer $contactMailer)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->paymentRepo = $paymentRepo;
|
||||
$this->paymentService = $paymentService;
|
||||
$this->contactMailer = $contactMailer;
|
||||
}
|
||||
|
||||
@ -108,7 +111,7 @@ class PaymentApiController extends BaseAPIController
|
||||
// check payment has been marked sent
|
||||
$request->invoice->markSentIfUnsent();
|
||||
|
||||
$payment = $this->paymentRepo->save($request->input());
|
||||
$payment = $this->paymentService->save($request->input(), null, $request->invoice);
|
||||
|
||||
if (Input::get('email_receipt')) {
|
||||
$this->contactMailer->sendPaymentConfirmation($payment);
|
||||
|
@ -191,16 +191,10 @@ class PaymentController extends BaseController
|
||||
|
||||
// if the payment amount is more than the balance create a credit
|
||||
if ($amount > $request->invoice->balance) {
|
||||
$credit = Credit::createNew();
|
||||
$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;
|
||||
$credit = true;
|
||||
}
|
||||
|
||||
$payment = $this->paymentService->save($input);
|
||||
$payment = $this->paymentService->save($input, null, $request->invoice);
|
||||
|
||||
if (Input::get('email_receipt')) {
|
||||
$this->contactMailer->sendPaymentConfirmation($payment);
|
||||
|
@ -149,6 +149,10 @@ class ProductController extends BaseController
|
||||
$message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product');
|
||||
Session::flash('message', $message);
|
||||
|
||||
if (in_array(request('action'), ['archive', 'delete', 'restore', 'invoice'])) {
|
||||
return self::bulk();
|
||||
}
|
||||
|
||||
return Redirect::to("products/{$product->public_id}/edit");
|
||||
}
|
||||
|
||||
@ -159,7 +163,17 @@ class ProductController extends BaseController
|
||||
{
|
||||
$action = Input::get('action');
|
||||
$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);
|
||||
Session::flash('message', $message);
|
||||
|
@ -118,12 +118,12 @@ class ProjectApiController extends BaseAPIController
|
||||
* @SWG\Parameter(
|
||||
* in="body",
|
||||
* name="body",
|
||||
* @SWG\Schema(ref="#/definitions/project")
|
||||
* @SWG\Schema(ref="#/definitions/Project")
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="New project",
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project"))
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response="default",
|
||||
@ -155,12 +155,12 @@ class ProjectApiController extends BaseAPIController
|
||||
* @SWG\Parameter(
|
||||
* in="body",
|
||||
* name="project",
|
||||
* @SWG\Schema(ref="#/definitions/project")
|
||||
* @SWG\Schema(ref="#/definitions/Project")
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Updated project",
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project"))
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response="default",
|
||||
@ -200,7 +200,7 @@ class ProjectApiController extends BaseAPIController
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Deleted project",
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project"))
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response="default",
|
||||
|
@ -51,6 +51,7 @@ class ProjectController extends BaseController
|
||||
public function create(ProjectRequest $request)
|
||||
{
|
||||
$data = [
|
||||
'account' => auth()->user()->account,
|
||||
'project' => null,
|
||||
'method' => 'POST',
|
||||
'url' => 'projects',
|
||||
@ -67,6 +68,7 @@ class ProjectController extends BaseController
|
||||
$project = $request->entity();
|
||||
|
||||
$data = [
|
||||
'account' => auth()->user()->account,
|
||||
'project' => $project,
|
||||
'method' => 'PUT',
|
||||
'url' => 'projects/' . $project->public_id,
|
||||
|
@ -72,6 +72,7 @@ class ReportController extends BaseController
|
||||
'activity',
|
||||
'aging',
|
||||
'client',
|
||||
'document',
|
||||
'expense',
|
||||
'invoice',
|
||||
'payment',
|
||||
@ -98,6 +99,8 @@ class ReportController extends BaseController
|
||||
'date_field' => $dateField,
|
||||
'invoice_status' => request()->invoice_status,
|
||||
'group_dates_by' => request()->group_dates_by,
|
||||
'document_filter' => request()->document_filter,
|
||||
'export_format' => $format,
|
||||
];
|
||||
$report = new $reportClass($startDate, $endDate, $isExport, $options);
|
||||
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";
|
||||
|
||||
$formats = ['csv', 'pdf', 'xlsx'];
|
||||
if(!in_array($format, $formats)) {
|
||||
$formats = ['csv', 'pdf', 'xlsx', 'zip'];
|
||||
if (! in_array($format, $formats)) {
|
||||
throw new \Exception("Invalid format request to export report");
|
||||
}
|
||||
|
||||
//Get labeled header
|
||||
$columns_labeled = $report->tableHeaderArray();
|
||||
$data = array_merge(
|
||||
[
|
||||
array_map(function($col) {
|
||||
return $col['label'];
|
||||
}, $report->tableHeaderArray())
|
||||
],
|
||||
$data
|
||||
);
|
||||
|
||||
/*$summary = [];
|
||||
if(count(array_values($totals))) {
|
||||
$summary = [];
|
||||
if (count(array_values($totals))) {
|
||||
$summary[] = array_merge([
|
||||
trans("texts.totals")
|
||||
], array_map(function ($key) {return trans("texts.{$key}");}, array_keys(array_values(array_values($totals)[0])[0])));
|
||||
], array_map(function ($key) {
|
||||
return trans("texts.{$key}");
|
||||
}, array_keys(array_values(array_values($totals)[0])[0])));
|
||||
}
|
||||
|
||||
foreach ($totals as $currencyId => $each) {
|
||||
foreach ($each as $dimension => $val) {
|
||||
$tmp = [];
|
||||
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
|
||||
|
||||
foreach ($val as $id => $field) $tmp[] = Utils::formatMoney($field, $currencyId);
|
||||
|
||||
foreach ($val as $id => $field) {
|
||||
$tmp[] = Utils::formatMoney($field, $currencyId);
|
||||
}
|
||||
$summary[] = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
dd($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) {
|
||||
return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $summary) {
|
||||
|
||||
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $summary) {
|
||||
$sheet->setOrientation('landscape');
|
||||
$sheet->freezeFirstRow();
|
||||
|
||||
//Add border on PDF
|
||||
if($format == 'pdf')
|
||||
if ($format == 'pdf') {
|
||||
$sheet->setAllBorders('thin');
|
||||
}
|
||||
|
||||
$sheet->rows(array_merge(
|
||||
[array_map(function($col) {return $col['label'];}, $columns_labeled)],
|
||||
$data
|
||||
));
|
||||
if ($format == 'csv') {
|
||||
$sheet->rows(array_merge($data, [[]], $summary));
|
||||
} else {
|
||||
$sheet->rows($data);
|
||||
}
|
||||
|
||||
//Styling header
|
||||
$sheet->cells('A1:'.Utils::num2alpha(count($columns_labeled)-1).'1', function($cells) {
|
||||
// Styling header
|
||||
$sheet->cells('A1:'.Utils::num2alpha(count($data[0])-1).'1', function($cells) {
|
||||
$cells->setBackground('#777777');
|
||||
$cells->setFontColor('#FFFFFF');
|
||||
$cells->setFontSize(13);
|
||||
$cells->setFontFamily('Calibri');
|
||||
$cells->setFontWeight('bold');
|
||||
});
|
||||
|
||||
|
||||
$sheet->setAutoSize(true);
|
||||
});
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
@ -149,8 +149,12 @@ class TaskApiController extends BaseAPIController
|
||||
*/
|
||||
public function update(UpdateTaskRequest $request)
|
||||
{
|
||||
if ($request->action) {
|
||||
return $this->handleAction($request);
|
||||
}
|
||||
|
||||
$task = $request->entity();
|
||||
|
||||
|
||||
$task = $this->taskRepo->save($task->public_id, \Illuminate\Support\Facades\Input::all());
|
||||
|
||||
return $this->itemResponse($task);
|
||||
|
@ -262,7 +262,7 @@ class TaskController extends BaseController
|
||||
$this->taskRepo->save($ids, ['action' => $action]);
|
||||
return Redirect::to('tasks')->withMessage(trans($action == 'stop' ? 'texts.stopped_task' : 'texts.resumed_task'));
|
||||
} 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;
|
||||
$data = [];
|
||||
|
||||
@ -294,6 +294,7 @@ class TaskController extends BaseController
|
||||
'publicId' => $task->public_id,
|
||||
'description' => $task->present()->invoiceDescription($account, $showProject),
|
||||
'duration' => $task->getHours(),
|
||||
'cost' => $task->getRate(),
|
||||
];
|
||||
$lastProjectId = $task->project_id;
|
||||
}
|
||||
|
51
app/Http/Controllers/TwoFactorController.php
Normal file
51
app/Http/Controllers/TwoFactorController.php
Normal 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');
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\DeleteVendorRequest;
|
||||
use App\Http\Requests\VendorRequest;
|
||||
use App\Http\Requests\CreateVendorRequest;
|
||||
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();
|
||||
|
||||
|
@ -36,5 +36,6 @@ class Kernel extends HttpKernel
|
||||
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
|
||||
'api' => 'App\Http\Middleware\ApiCheck',
|
||||
'cors' => '\Barryvdh\Cors\HandleCors',
|
||||
'throttle' => 'Illuminate\Routing\Middleware\ThrottleRequests',
|
||||
];
|
||||
}
|
||||
|
@ -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()
|
||||
->invoices()
|
||||
->first();
|
||||
|
26
app/Http/Requests/DeleteVendorRequest.php
Normal file
26
app/Http/Requests/DeleteVendorRequest.php
Normal 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 [];
|
||||
}
|
||||
}
|
78
app/Http/Requests/ValidateTwoFactorRequest.php
Normal file
78
app/Http/Requests/ValidateTwoFactorRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
@ -79,6 +79,8 @@ Route::group(['middleware' => 'lookup:postmark'], function () {
|
||||
Route::group(['middleware' => 'lookup:account'], function () {
|
||||
Route::post('/payment_hook/{account_key}/{gateway_id}', 'OnlinePaymentController@handlePaymentWebhook');
|
||||
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');
|
||||
@ -141,6 +143,8 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
|
||||
Route::post('settings/user_details', 'AccountController@saveUserDetails');
|
||||
Route::post('settings/payment_gateway_limits', 'AccountGatewayController@savePaymentGatewayLimits');
|
||||
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::get('api/clients', 'ClientController@getDatatable');
|
||||
|
@ -46,7 +46,7 @@ class DownloadInvoices extends Job
|
||||
*/
|
||||
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) {
|
||||
$zip->add_file($invoice->getFileName(), $invoice->getPDFString());
|
||||
|
@ -89,6 +89,11 @@ class Utils
|
||||
return env('NINJA_DEV') == 'true';
|
||||
}
|
||||
|
||||
public static function isTimeTracker()
|
||||
{
|
||||
return array_get($_SERVER, 'HTTP_USER_AGENT') == TIME_TRACKER_USER_AGENT;
|
||||
}
|
||||
|
||||
public static function requireHTTPS()
|
||||
{
|
||||
if (Request::root() === 'http://ninja.dev' || Request::root() === 'http://ninja.dev:8000') {
|
||||
@ -1064,7 +1069,7 @@ class Utils
|
||||
{
|
||||
$name = trim($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];
|
||||
}
|
||||
|
22
app/Listeners/DNSListener.php
Normal file
22
app/Listeners/DNSListener.php
Normal 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);
|
||||
}
|
||||
}
|
@ -41,7 +41,8 @@ class HandleUserLoggedIn
|
||||
*/
|
||||
public function handle(UserLoggedIn $event)
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$user = auth()->user();
|
||||
$account = $user->account;
|
||||
|
||||
if (! Utils::isNinja() && empty($account->last_login)) {
|
||||
event(new UserSignedUp());
|
||||
@ -50,6 +51,11 @@ class HandleUserLoggedIn
|
||||
$account->last_login = Carbon::now()->toDateTimeString();
|
||||
$account->save();
|
||||
|
||||
if ($user->failed_logins > 0) {
|
||||
$user->failed_logins = 0;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
|
||||
Session::put(SESSION_USER_ACCOUNTS, $users);
|
||||
HistoryUtils::loadHistory($users ?: Auth::user()->id);
|
||||
|
@ -176,6 +176,7 @@ class Account extends Eloquent
|
||||
'credit_number_counter',
|
||||
'credit_number_prefix',
|
||||
'credit_number_pattern',
|
||||
'task_rate',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -809,7 +810,7 @@ class Account extends Eloquent
|
||||
$available = true;
|
||||
|
||||
foreach ($gatewayTypes as $type) {
|
||||
if ($paymentDriver->handles($type)) {
|
||||
if ($type != GATEWAY_TYPE_TOKEN && $paymentDriver->handles($type)) {
|
||||
$available = false;
|
||||
break;
|
||||
}
|
||||
@ -1084,6 +1085,11 @@ class Account extends Eloquent
|
||||
}
|
||||
}
|
||||
|
||||
public function isPaid()
|
||||
{
|
||||
return Utils::isNinja() ? $this->isPro() : Utils::isWhiteLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $plan_details
|
||||
*
|
||||
|
@ -160,6 +160,22 @@ class AccountGateway extends EntityModel
|
||||
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
|
||||
*/
|
||||
|
@ -126,7 +126,7 @@ class Activity extends Eloquent
|
||||
'user' => $isSystem ? '<i>' . trans('texts.system') . '</i>' : e($user->getDisplayName()),
|
||||
'invoice' => $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_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null,
|
||||
'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null,
|
||||
|
@ -52,6 +52,7 @@ class Client extends EntityModel
|
||||
'invoice_number_counter',
|
||||
'quote_number_counter',
|
||||
'public_notes',
|
||||
'task_rate',
|
||||
];
|
||||
|
||||
|
||||
|
@ -161,6 +161,11 @@ class EntityModel extends Eloquent
|
||||
|
||||
$query->where($this->getTable() .'.account_id', '=', $accountId);
|
||||
|
||||
if (func_num_args() > 1 && ! $publicId) {
|
||||
$query->where('id', '=', 0);
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($publicId) {
|
||||
if (is_array($publicId)) {
|
||||
$query->whereIn('public_id', $publicId);
|
||||
|
@ -32,6 +32,7 @@ class Gateway extends Eloquent
|
||||
GATEWAY_TYPE_BITCOIN,
|
||||
GATEWAY_TYPE_DWOLLA,
|
||||
GATEWAY_TYPE_TOKEN,
|
||||
GATEWAY_TYPE_GOCARDLESS,
|
||||
];
|
||||
|
||||
// these will appear in the primary gateway select
|
||||
@ -58,6 +59,7 @@ class Gateway extends Eloquent
|
||||
*/
|
||||
public static $alternate = [
|
||||
GATEWAY_PAYPAL_EXPRESS,
|
||||
GATEWAY_GOCARDLESS,
|
||||
GATEWAY_BITPAY,
|
||||
GATEWAY_DWOLLA,
|
||||
GATEWAY_CUSTOM,
|
||||
@ -87,6 +89,8 @@ class Gateway extends Eloquent
|
||||
'developerMode',
|
||||
// Dwolla
|
||||
'sandbox',
|
||||
// Payfast
|
||||
'pdtKey',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -216,6 +216,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
'public_notes',
|
||||
'invoice_footer',
|
||||
'partial',
|
||||
'partial_due_date',
|
||||
] as $field) {
|
||||
if ($this->$field != $this->getOriginal($field)) {
|
||||
return true;
|
||||
@ -634,6 +635,15 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
if ($this->partial > 0) {
|
||||
$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();
|
||||
@ -682,7 +692,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
if ($quoteInvoiceId) {
|
||||
$label = 'converted';
|
||||
} elseif ($class == 'danger') {
|
||||
$label = $entityType == ENTITY_INVOICE ? 'overdue' : 'expired';
|
||||
$label = $entityType == ENTITY_INVOICE ? 'past_due' : 'expired';
|
||||
} else {
|
||||
$label = 'status_' . strtolower($status);
|
||||
}
|
||||
@ -719,7 +729,8 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
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()
|
||||
@ -808,7 +819,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
*/
|
||||
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_taxes2',
|
||||
'partial',
|
||||
'partial_due_date',
|
||||
'has_tasks',
|
||||
'custom_text_value1',
|
||||
'custom_text_value2',
|
||||
@ -1198,7 +1210,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
}
|
||||
|
||||
$invitation = $this->invitations[0];
|
||||
$link = $invitation->getLink('view', true);
|
||||
$link = $invitation->getLink('view', true, true);
|
||||
$pdfString = false;
|
||||
$phantomjsSecret = env('PHANTOMJS_SECRET');
|
||||
$phantomjsLink = $link . "?phantomjs=true&phantomjs_secret={$phantomjsSecret}";
|
||||
@ -1208,8 +1220,11 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
// we see occasional 408 errors
|
||||
for ($i=1; $i<=5; $i++) {
|
||||
$pdfString = CurlUtils::phantom('GET', $phantomjsLink);
|
||||
if ($pdfString) {
|
||||
$pdfString = strip_tags($pdfString);
|
||||
if (strpos($pdfString, 'data') === 0) {
|
||||
break;
|
||||
} else {
|
||||
$pdfString = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1217,9 +1232,8 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
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";
|
||||
$pdfString = CurlUtils::get($url);
|
||||
$pdfString = strip_tags($pdfString);
|
||||
}
|
||||
|
||||
$pdfString = strip_tags($pdfString);
|
||||
} catch (\Exception $exception) {
|
||||
Utils::logError("PhantomJS - Failed to load {$phantomjsLink}: {$exception->getMessage()}");
|
||||
return false;
|
||||
@ -1234,7 +1248,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
if ($pdf = Utils::decodePDF($pdfString)) {
|
||||
return $pdf;
|
||||
} else {
|
||||
Utils::logError("PhantomJS - Unable to decode {$phantomjsLink}: {$pdfString}");
|
||||
Utils::logError("PhantomJS - Unable to decode {$phantomjsLink}");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@ -1465,7 +1479,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
if ($entityType == ENTITY_INVOICE) {
|
||||
$statuses[INVOICE_STATUS_UNPAID] = trans('texts.unpaid');
|
||||
$statuses[INVOICE_STATUS_OVERDUE] = trans('texts.overdue');
|
||||
$statuses[INVOICE_STATUS_OVERDUE] = trans('texts.past_due');
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
|
@ -24,6 +24,7 @@ class Project extends EntityModel
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'task_rate',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -145,6 +145,24 @@ class Task extends EntityModel
|
||||
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
|
||||
*/
|
||||
|
@ -345,6 +345,9 @@ trait GeneratesNumbers
|
||||
case FREQUENCY_THREE_MONTHS:
|
||||
$resetDate->addMonths(3);
|
||||
break;
|
||||
case FREQUENCY_FOUR_MONTHS:
|
||||
$resetDate->addMonths(4);
|
||||
break;
|
||||
case FREQUENCY_SIX_MONTHS:
|
||||
$resetDate->addMonths(6);
|
||||
break;
|
||||
|
@ -63,6 +63,8 @@ trait HasRecurrence
|
||||
return $monthsSinceLastSent >= 2;
|
||||
case FREQUENCY_THREE_MONTHS:
|
||||
return $monthsSinceLastSent >= 3;
|
||||
case FREQUENCY_FOUR_MONTHS:
|
||||
return $monthsSinceLastSent >= 4;
|
||||
case FREQUENCY_SIX_MONTHS:
|
||||
return $monthsSinceLastSent >= 6;
|
||||
case FREQUENCY_ANNUALLY:
|
||||
@ -100,6 +102,9 @@ trait HasRecurrence
|
||||
case FREQUENCY_THREE_MONTHS:
|
||||
$rule = 'FREQ=MONTHLY;INTERVAL=3;';
|
||||
break;
|
||||
case FREQUENCY_FOUR_MONTHS:
|
||||
$rule = 'FREQ=MONTHLY;INTERVAL=4;';
|
||||
break;
|
||||
case FREQUENCY_SIX_MONTHS:
|
||||
$rule = 'FREQ=MONTHLY;INTERVAL=6;';
|
||||
break;
|
||||
|
@ -12,6 +12,29 @@ trait PresentsInvoice
|
||||
if ($this->invoice_fields) {
|
||||
$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);
|
||||
} else {
|
||||
return $this->getDefaultInvoiceFields();
|
||||
@ -54,6 +77,26 @@ trait PresentsInvoice
|
||||
'account.city_state_postal',
|
||||
'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) {
|
||||
@ -136,6 +179,26 @@ trait PresentsInvoice
|
||||
'account.custom_value2',
|
||||
'.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);
|
||||
@ -264,6 +327,10 @@ trait PresentsInvoice
|
||||
'invoice_due_date',
|
||||
'quote_due_date',
|
||||
'service',
|
||||
'product_key',
|
||||
'unit_cost',
|
||||
'custom_value1',
|
||||
'custom_value2',
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
@ -289,6 +356,8 @@ trait PresentsInvoice
|
||||
'client.custom_value2' => 'custom_client_label2',
|
||||
'contact.custom_value1' => 'custom_contact_label1',
|
||||
'contact.custom_value2' => 'custom_contact_label2',
|
||||
'product.custom_value1' => 'custom_invoice_item_label1',
|
||||
'product.custom_value2' => 'custom_invoice_item_label2',
|
||||
] as $field => $property) {
|
||||
$data[$field] = e($this->$property) ?: trans('texts.custom_field');
|
||||
}
|
||||
@ -307,4 +376,10 @@ trait PresentsInvoice
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function hideQuantity() {
|
||||
$fields = $this->getInvoiceFields();
|
||||
|
||||
return ! isset($fields['product_fields']['product.quantity']);
|
||||
}
|
||||
}
|
||||
|
@ -155,9 +155,15 @@ trait SendsEmails
|
||||
{
|
||||
for ($i = 1; $i <= 3; $i++) {
|
||||
if ($date = $this->getReminderDate($i, $filterEnabled)) {
|
||||
$field = $this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';
|
||||
if ($invoice->$field == $date) {
|
||||
return "reminder{$i}";
|
||||
if ($this->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE) {
|
||||
if (($invoice->partial && $invoice->partial_due_date == $date)
|
||||
|| $invoice->due_date == $date) {
|
||||
return "reminder{$i}";
|
||||
}
|
||||
} else {
|
||||
if ($invoice->invoice_date == $date) {
|
||||
return "reminder{$i}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,15 @@ class User extends Authenticatable
|
||||
*
|
||||
* @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
|
||||
|
50
app/Ninja/DNS/Cloudflare.php
Normal file
50
app/Ninja/DNS/Cloudflare.php
Normal 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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -15,8 +15,17 @@ class ActivityDatatable extends EntityDatatable
|
||||
'activities.id',
|
||||
function ($model) {
|
||||
$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;
|
||||
$str .= sprintf(' <i class="fa fa-globe" style="cursor:pointer" title="%s" onclick="openUrl(\'%s\', \'IP Lookup\')"></i>', $model->ip, $ipLookUpLink);
|
||||
}
|
||||
|
@ -66,7 +66,11 @@ class InvoiceDatatable extends EntityDatatable
|
||||
[
|
||||
$entityType == ENTITY_INVOICE ? 'due_date' : 'valid_until',
|
||||
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)
|
||||
{
|
||||
$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);
|
||||
|
||||
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
|
||||
|
@ -69,7 +69,7 @@ class PaymentDatatable extends EntityDatatable
|
||||
} elseif ($model->email) {
|
||||
return $model->email;
|
||||
} elseif ($model->payment_type) {
|
||||
return trans('texts.payment_type_' . $model->payment_type);
|
||||
return trans('texts.payment_type_' . strtolower($model->payment_type));
|
||||
}
|
||||
} elseif ($model->last4) {
|
||||
if ($model->bank_name) {
|
||||
|
@ -30,7 +30,7 @@ class ProductDatatable extends EntityDatatable
|
||||
[
|
||||
'cost',
|
||||
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");
|
||||
},
|
||||
],
|
||||
[
|
||||
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);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,12 @@ class ProjectDatatable extends EntityDatatable
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
'task_rate',
|
||||
function ($model) {
|
||||
return floatval($model->task_rate) ? Utils::roundSignificant($model->task_rate) : '';
|
||||
}
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -259,10 +259,21 @@ class BaseTransformer extends TransformerAbstract
|
||||
{
|
||||
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
|
||||
$invoiceNumber = strtolower($invoiceNumber);
|
||||
|
||||
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
|
||||
*
|
||||
|
@ -27,6 +27,7 @@ class PaymentTransformer extends BaseTransformer
|
||||
'payment_date_sql' => $this->getDate($data, 'payment_date'),
|
||||
'client_id' => $this->getInvoiceClientId($data->invoice_num),
|
||||
'invoice_id' => $this->getInvoiceId($data->invoice_num),
|
||||
'invoice_public_id' => $this->getInvoicePublicId($data->invoice_num),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ class AuthorizeNetAIMPaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
$data = parent::paymentDetails();
|
||||
$data['solutionId'] = $this->accountGateway->getConfigField('testMode') ? 'AAA100303' : 'AAA172036';
|
||||
$data['invoiceNumber'] = $this->invoice()->invoice_number;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
@ -318,7 +318,11 @@ class BasePaymentDriver
|
||||
|
||||
// parse the transaction reference
|
||||
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 {
|
||||
$ref = $response->getTransactionReference();
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class GoCardlessV2RedirectPaymentDriver extends BasePaymentDriver
|
||||
public function gatewayTypes()
|
||||
{
|
||||
$types = [
|
||||
GATEWAY_TYPE_BANK_TRANSFER,
|
||||
GATEWAY_TYPE_GOCARDLESS,
|
||||
GATEWAY_TYPE_TOKEN,
|
||||
];
|
||||
|
||||
@ -112,6 +112,10 @@ class GoCardlessV2RedirectPaymentDriver extends BasePaymentDriver
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($payment->is_deleted || $payment->invoice->is_deleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($action == 'failed' || $action == 'charged_back') {
|
||||
if (! $payment->isFailed()) {
|
||||
$payment->markFailed($event['details']['description']);
|
||||
|
@ -8,6 +8,14 @@ class PayFastPaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
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)
|
||||
{
|
||||
parent::completeOffsitePurchase([
|
||||
|
@ -43,6 +43,13 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
} elseif ($sofortEnabled) {
|
||||
$types[] = GATEWAY_TYPE_SOFORT;
|
||||
}
|
||||
|
||||
if ($gateway->getSepaEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_SEPA;
|
||||
}
|
||||
if ($gateway->getBitcoinEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_BITCOIN;
|
||||
}
|
||||
if ($gateway->getAlipayEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_ALIPAY;
|
||||
}
|
||||
@ -84,7 +91,7 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
|
||||
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)
|
||||
@ -240,13 +247,16 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
$isBank = $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod);
|
||||
$isAlipay = $this->isGatewayType(GATEWAY_TYPE_ALIPAY, $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;
|
||||
if ($isAlipay) {
|
||||
$payment->payment_type_id = PAYMENT_TYPE_ALIPAY;
|
||||
} elseif ($isSofort) {
|
||||
$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);
|
||||
$invoiceNumber = $this->invoice()->invoice_number;
|
||||
$currency = $this->client()->getCurrencyCode();
|
||||
$email = $this->contact()->email;
|
||||
$gatewayType = GatewayType::getAliasFromId($this->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 : '');
|
||||
@ -361,6 +372,12 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
throw new Exception('Alipay is not enabled');
|
||||
}
|
||||
$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 {
|
||||
if (! $this->accountGateway->getSofortEnabled()) {
|
||||
throw new Exception('Sofort is not enabled');
|
||||
@ -376,7 +393,18 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
$this->invitation->transaction_reference = $response['id'];
|
||||
$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 {
|
||||
throw new Exception($response);
|
||||
}
|
||||
@ -474,6 +502,10 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($payment->is_deleted || $payment->invoice->is_deleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($eventType == 'charge.failed') {
|
||||
if (! $payment->isFailed()) {
|
||||
$payment->markFailed($source['failure_message']);
|
||||
|
@ -276,6 +276,10 @@ class WePayPaymentDriver extends BasePaymentDriver
|
||||
throw new Exception('Unknown payment');
|
||||
}
|
||||
|
||||
if ($payment->is_deleted || $payment->invoice->is_deleted) {
|
||||
throw new Exception('Payment is deleted');
|
||||
}
|
||||
|
||||
$wepay = Utils::setupWePay($accountGateway);
|
||||
$checkout = $wepay->request('checkout', [
|
||||
'checkout_id' => intval($objectId),
|
||||
|
@ -52,6 +52,18 @@ class AccountPresenter extends Presenter
|
||||
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
|
||||
*/
|
||||
|
@ -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 string
|
||||
*/
|
||||
public function taskRate()
|
||||
{
|
||||
if ($this->entity->task_rate) {
|
||||
return Utils::roundSignificant($this->entity->task_rate);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class EntityPresenter extends Presenter
|
||||
$entity = $this->entity;
|
||||
$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)
|
||||
|
@ -67,11 +67,14 @@ class InvoicePresenter extends EntityPresenter
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$date = Carbon::parse($this->entity->due_date);
|
||||
$date = Carbon::parse($dueDate);
|
||||
|
||||
if ($date->isFuture()) {
|
||||
return 0;
|
||||
@ -155,6 +158,11 @@ class InvoicePresenter extends EntityPresenter
|
||||
return Utils::fromSqlDate($this->entity->due_date);
|
||||
}
|
||||
|
||||
public function partial_due_date()
|
||||
{
|
||||
return Utils::fromSqlDate($this->entity->partial_due_date);
|
||||
}
|
||||
|
||||
public function frequency()
|
||||
{
|
||||
$frequency = $this->entity->frequency ? $this->entity->frequency->name : '';
|
||||
@ -248,7 +256,7 @@ class InvoicePresenter extends EntityPresenter
|
||||
}
|
||||
|
||||
if ($invoice->onlyHasTasks()) {
|
||||
$actions[] = ['url' => 'javascript:onAddItemClick()', 'label' => trans('texts.add_item')];
|
||||
$actions[] = ['url' => 'javascript:onAddItemClick()', 'label' => trans('texts.add_product')];
|
||||
}
|
||||
|
||||
if ($invoice->canBePaid()) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Ninja\Presenters;
|
||||
|
||||
use DropdownButton;
|
||||
use App\Libraries\Skype\HeroCard;
|
||||
|
||||
class ProductPresenter extends EntityPresenter
|
||||
@ -22,4 +23,25 @@ class ProductPresenter extends EntityPresenter
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class AgingReport extends AbstractReport
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
|
||||
$invoice->present()->invoice_date,
|
||||
$invoice->present()->due_date,
|
||||
$invoice->present()->partial_due_date ?: $invoice->present()->due_date,
|
||||
$invoice->present()->age,
|
||||
$account->formatMoney($invoice->amount, $client),
|
||||
$account->formatMoney($invoice->balance, $client),
|
||||
|
74
app/Ninja/Reports/DocumentReport.php
Normal file
74
app/Ninja/Reports/DocumentReport.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Barracuda\ArchiveStream\Archive;
|
||||
use App\Models\Expense;
|
||||
use Auth;
|
||||
use Utils;
|
||||
@ -19,6 +20,12 @@ class ExpenseReport extends AbstractReport
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$exportFormat = $this->options['export_format'];
|
||||
$with = ['client.contacts', 'vendor'];
|
||||
|
||||
if ($exportFormat == 'zip') {
|
||||
$with[] = ['documents'];
|
||||
}
|
||||
|
||||
$expenses = Expense::scope()
|
||||
->orderBy('expense_date', 'desc')
|
||||
@ -27,6 +34,19 @@ class ExpenseReport extends AbstractReport
|
||||
->where('expense_date', '>=', $this->startDate)
|
||||
->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) {
|
||||
$amount = $expense->amountWithTax();
|
||||
|
||||
|
@ -4,6 +4,7 @@ namespace App\Ninja\Reports;
|
||||
|
||||
use App\Models\Client;
|
||||
use Auth;
|
||||
use Barracuda\ArchiveStream\Archive;
|
||||
|
||||
class InvoiceReport extends AbstractReport
|
||||
{
|
||||
@ -22,6 +23,7 @@ class InvoiceReport extends AbstractReport
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$status = $this->options['invoice_status'];
|
||||
$exportFormat = $this->options['export_format'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
@ -44,6 +46,21 @@ class InvoiceReport extends AbstractReport
|
||||
}, '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 ($client->invoices as $invoice) {
|
||||
$payments = count($invoice->payments) ? $invoice->payments : [false];
|
||||
|
@ -4,6 +4,7 @@ namespace App\Ninja\Reports;
|
||||
|
||||
use App\Models\Client;
|
||||
use Auth;
|
||||
use Barracuda\ArchiveStream\Archive;
|
||||
|
||||
class QuoteReport extends AbstractReport
|
||||
{
|
||||
@ -19,6 +20,7 @@ class QuoteReport extends AbstractReport
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$status = $this->options['invoice_status'];
|
||||
$exportFormat = $this->options['export_format'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
@ -35,6 +37,21 @@ class QuoteReport extends AbstractReport
|
||||
->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 ($client->invoices as $invoice) {
|
||||
$this->data[] = [
|
||||
@ -46,7 +63,7 @@ class QuoteReport extends AbstractReport
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,10 +58,30 @@ class AccountRepository
|
||||
$account->account_key = strtolower(str_random(RANDOM_KEY_LENGTH));
|
||||
$account->company_id = $company->id;
|
||||
|
||||
if ($locale = Session::get(SESSION_LOCALE)) {
|
||||
if ($language = Language::whereLocale($locale)->first()) {
|
||||
$account->language_id = $language->id;
|
||||
}
|
||||
// Set default language/currency based on IP
|
||||
$data = unserialize(file_get_contents('http://www.geoplugin.net/php.gp?ip=' . $account->ip));
|
||||
$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();
|
||||
|
@ -309,13 +309,13 @@ class DashboardRepository
|
||||
->where('invoices.deleted_at', '=', null)
|
||||
->where('invoices.is_public', '=', 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) {
|
||||
$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')
|
||||
->take(50)
|
||||
->get();
|
||||
@ -337,8 +337,7 @@ class DashboardRepository
|
||||
->where('invoices.is_public', '=', true)
|
||||
->where('contacts.is_primary', '=', true)
|
||||
->where(function($query) {
|
||||
$query->where('invoices.due_date', '>=', date('Y-m-d'))
|
||||
->orWhereNull('invoices.due_date');
|
||||
$query->where(DB::raw("coalesce(invoices.partial_due_date, invoices.due_date)"), '>=', date('Y-m-d'));
|
||||
})
|
||||
->orderBy('invoices.due_date', 'asc');
|
||||
|
||||
@ -347,7 +346,7 @@ class DashboardRepository
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -74,9 +74,10 @@ class InvoiceRepository extends BaseRepository
|
||||
'invoices.balance',
|
||||
'invoices.invoice_date',
|
||||
'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.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 due_date"),
|
||||
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 invoice_status_name',
|
||||
'contacts.first_name',
|
||||
@ -338,7 +339,7 @@ class InvoiceRepository extends BaseRepository
|
||||
} elseif (Invoice::calcIsOverdue($model->balance, $model->due_date)) {
|
||||
$class = 'danger';
|
||||
if ($entityType == ENTITY_INVOICE) {
|
||||
$label = trans('texts.overdue');
|
||||
$label = trans('texts.past_due');
|
||||
} else {
|
||||
$label = trans('texts.expired');
|
||||
}
|
||||
@ -389,7 +390,7 @@ class InvoiceRepository extends BaseRepository
|
||||
$invoice->custom_taxes2 = $account->custom_invoice_taxes2 ?: false;
|
||||
|
||||
// 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();
|
||||
$invoice->due_date = $account->defaultDueDate($client);
|
||||
}
|
||||
@ -402,6 +403,8 @@ class InvoiceRepository extends BaseRepository
|
||||
|
||||
if ($invoice->is_deleted) {
|
||||
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)) {
|
||||
@ -445,6 +448,7 @@ class InvoiceRepository extends BaseRepository
|
||||
if (isset($data['is_amount_discount'])) {
|
||||
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
|
||||
}
|
||||
|
||||
if (isset($data['invoice_date_sql'])) {
|
||||
$invoice->invoice_date = $data['invoice_date_sql'];
|
||||
} elseif (isset($data['invoice_date'])) {
|
||||
@ -499,8 +503,13 @@ class InvoiceRepository extends BaseRepository
|
||||
$invoice->terms = '';
|
||||
}
|
||||
|
||||
$invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (! $publicId && $account->invoice_footer ? $account->invoice_footer : '');
|
||||
$invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : '';
|
||||
if (isset($data['invoice_footer']) && trim($data['invoice_footer'])) {
|
||||
$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
|
||||
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));
|
||||
}
|
||||
|
||||
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->save();
|
||||
|
||||
@ -881,7 +898,7 @@ class InvoiceRepository extends BaseRepository
|
||||
if ($account->invoice_terms) {
|
||||
$clone->terms = $account->invoice_terms;
|
||||
}
|
||||
if ($account->auto_convert_quote) {
|
||||
if (! auth()->check()) {
|
||||
$clone->is_public = true;
|
||||
$clone->invoice_status_id = INVOICE_STATUS_SENT;
|
||||
}
|
||||
@ -1137,8 +1154,11 @@ class InvoiceRepository extends BaseRepository
|
||||
|
||||
for ($i = 1; $i <= 3; $i++) {
|
||||
if ($date = $account->getReminderDate($i, $filterEnabled)) {
|
||||
$field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';
|
||||
$dates[] = "$field = '$date'";
|
||||
if ($account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE) {
|
||||
$dates[] = "(due_date = '$date' OR partial_due_date = '$date')";
|
||||
} else {
|
||||
$dates[] = "invoice_date = '$date'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ class ProjectRepository extends BaseRepository
|
||||
'projects.public_id',
|
||||
'projects.user_id',
|
||||
'projects.deleted_at',
|
||||
'projects.task_rate',
|
||||
'projects.is_deleted',
|
||||
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',
|
||||
|
@ -274,6 +274,7 @@ class AccountTransformer extends EntityTransformer
|
||||
'reset_counter_date' => $account->reset_counter_date,
|
||||
'custom_contact_label1' => $account->custom_contact_label1,
|
||||
'custom_contact_label2' => $account->custom_contact_label2,
|
||||
'task_rate' => (float) $account->task_rate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ class ClientTransformer extends EntityTransformer
|
||||
* @SWG\Property(property="vat_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="task_rate", type="number", format="float", example=10)
|
||||
*/
|
||||
protected $defaultIncludes = [
|
||||
'contacts',
|
||||
@ -135,6 +136,7 @@ class ClientTransformer extends EntityTransformer
|
||||
'custom_value2' => $client->custom_value2,
|
||||
'invoice_number_counter' => (int) $client->invoice_number_counter,
|
||||
'quote_number_counter' => (int) $client->quote_number_counter,
|
||||
'task_rate' => (float) $client->task_rate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,18 @@ namespace App\Ninja\Transformers;
|
||||
use App\Models\Credit;
|
||||
|
||||
/**
|
||||
* Class CreditTransformer.
|
||||
* @SWG\Definition(definition="Credit", required={"client_id"}, @SWG\Xml(name="Credit"))
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
@ -119,6 +119,7 @@ class InvoiceTransformer extends EntityTransformer
|
||||
'is_amount_discount' => (bool) $invoice->is_amount_discount,
|
||||
'invoice_footer' => $invoice->invoice_footer,
|
||||
'partial' => (float) $invoice->partial,
|
||||
'partial_due_date' => $invoice->partial_due_date,
|
||||
'has_tasks' => (bool) $invoice->has_tasks,
|
||||
'auto_bill' => (bool) $invoice->auto_bill,
|
||||
'custom_value1' => (float) $invoice->custom_value1,
|
||||
|
@ -16,6 +16,7 @@ class ProjectTransformer extends EntityTransformer
|
||||
* @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="is_deleted", type="boolean", example=false, readOnly=true)
|
||||
* @SWG\Property(property="task_rate", type="number", format="float", example=10)
|
||||
*/
|
||||
public function transform(Project $project)
|
||||
{
|
||||
@ -26,6 +27,7 @@ class ProjectTransformer extends EntityTransformer
|
||||
'updated_at' => $this->getTimestamp($project->updated_at),
|
||||
'archived_at' => $this->getTimestamp($project->deleted_at),
|
||||
'is_deleted' => (bool) $project->is_deleted,
|
||||
'task_rate' => (float) $project->task_rate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -200,6 +200,11 @@ class EventServiceProvider extends ServiceProvider
|
||||
|
||||
'Illuminate\Queue\Events\JobExceptionOccurred' => [
|
||||
'App\Listeners\InvoiceListener@jobFailed'
|
||||
],
|
||||
|
||||
//DNS
|
||||
'App\Events\SubdomainWasUpdated' => [
|
||||
'App\Listeners\DNSListener@addDNSRecord'
|
||||
]
|
||||
|
||||
/*
|
||||
|
@ -83,10 +83,15 @@ class AuthService
|
||||
}
|
||||
} else {
|
||||
LookupUser::setServerByField('oauth_user_key', $providerId . '-' . $oauthUserId);
|
||||
|
||||
\Log::info("Find user: $providerId, $oauthUserId");
|
||||
if ($user = $this->accountRepo->findUserByOauth($providerId, $oauthUserId)) {
|
||||
Auth::login($user, true);
|
||||
event(new UserLoggedIn());
|
||||
if ($user->google_2fa_secret) {
|
||||
session(['2fa:user:id' => $user->id]);
|
||||
return redirect('/validate_two_factor/' . $user->account->account_key);
|
||||
} else {
|
||||
Auth::login($user);
|
||||
event(new UserLoggedIn());
|
||||
}
|
||||
} else {
|
||||
Session::flash('error', trans('texts.invalid_credentials'));
|
||||
|
||||
|
@ -443,7 +443,9 @@ class ImportService
|
||||
// update the entity maps
|
||||
if ($entityType != ENTITY_CUSTOMER) {
|
||||
$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
|
||||
@ -929,6 +931,7 @@ class ImportService
|
||||
private function addInvoiceToMaps(Invoice $invoice)
|
||||
{
|
||||
if ($number = strtolower(trim($invoice->invoice_number))) {
|
||||
$this->maps['invoices'][$number] = $invoice;
|
||||
$this->maps['invoice'][$number] = $invoice->id;
|
||||
$this->maps['invoice_client'][$number] = $invoice->client_id;
|
||||
$this->maps['invoice_ids'][$invoice->public_id] = $invoice->id;
|
||||
|
@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Models\Account;
|
||||
use App\Models\Activity;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Ninja\Datatables\PaymentDatatable;
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ class TemplateService
|
||||
'$emailSignature' => $account->getEmailFooter(),
|
||||
'$client' => $client->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),
|
||||
'$contact' => $contact->getDisplayName(),
|
||||
'$firstName' => $contact->first_name,
|
||||
|
@ -38,7 +38,8 @@
|
||||
"card": "^2.1.1",
|
||||
"fullcalendar": "^3.5.1",
|
||||
"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": {
|
||||
"jquery": "~1.11"
|
||||
|
@ -17,11 +17,13 @@
|
||||
"ext-gd": "*",
|
||||
"ext-gmp": "*",
|
||||
"Dwolla/omnipay-dwolla": "dev-master",
|
||||
"abdala/omnipay-pagseguro": "0.2",
|
||||
"agmscode/omnipay-agms": "~1.0",
|
||||
"alfaproject/omnipay-skrill": "dev-master",
|
||||
"anahkiasen/former": "4.0.*@dev",
|
||||
"andreas22/omnipay-fasapay": "1.*",
|
||||
"asgrim/ofxparser": "^1.1",
|
||||
"bacon/bacon-qr-code": "^1.0",
|
||||
"barracudanetworks/archivestream-php": "^1.0",
|
||||
"barryvdh/laravel-cors": "^0.9.1",
|
||||
"barryvdh/laravel-debugbar": "~2.2",
|
||||
@ -70,6 +72,7 @@
|
||||
"mpdf/mpdf": "6.1.3",
|
||||
"nwidart/laravel-modules": "^1.14",
|
||||
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
|
||||
"omnipay/authorizenet": "dev-solution-id as 2.5.0",
|
||||
"omnipay/bitpay": "dev-master",
|
||||
"omnipay/braintree": "~2.0@dev",
|
||||
"omnipay/gocardless": "dev-master",
|
||||
@ -77,6 +80,7 @@
|
||||
"omnipay/omnipay": "~2.3",
|
||||
"omnipay/stripe": "dev-master",
|
||||
"patricktalmadge/bootstrapper": "5.5.x",
|
||||
"pragmarx/google2fa-laravel": "^0.1.2",
|
||||
"predis/predis": "^1.1",
|
||||
"simshaun/recurr": "dev-master",
|
||||
"softcommerce/omnipay-paytrace": "~1.0",
|
||||
@ -86,10 +90,8 @@
|
||||
"webpatser/laravel-countries": "dev-master",
|
||||
"websight/l5-google-cloud-storage": "dev-master",
|
||||
"wepay/php-sdk": "^0.2",
|
||||
"wildbit/laravel-postmark-provider": "3.0",
|
||||
"abdala/omnipay-pagseguro": "0.2",
|
||||
"omnipay/authorizenet": "dev-solution-id as 2.5.0"
|
||||
},
|
||||
"wildbit/laravel-postmark-provider": "3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeception/c3": "~2.0",
|
||||
"codeception/codeception": "2.3.3",
|
||||
|
551
composer.lock
generated
551
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -158,6 +158,7 @@ return [
|
||||
Codedge\Updater\UpdaterServiceProvider::class,
|
||||
Nwidart\Modules\LaravelModulesServiceProvider::class,
|
||||
Barryvdh\Cors\ServiceProvider::class,
|
||||
PragmaRX\Google2FALaravel\ServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
@ -171,6 +172,7 @@ return [
|
||||
|
||||
'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
|
||||
'Davibennun\LaravelPushNotification\LaravelPushNotificationServiceProvider',
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@ -266,6 +268,8 @@ return [
|
||||
'DateUtils' => App\Libraries\DateUtils::class,
|
||||
'HTMLUtils' => App\Libraries\HTMLUtils::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
Loading…
x
Reference in New Issue
Block a user