1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Merge branch 'release-3.3.1'

This commit is contained in:
Hillel Coren 2017-05-09 15:14:02 +03:00
commit 5f74f5ca3d
134 changed files with 7404 additions and 866 deletions

23
.env.travis Normal file
View File

@ -0,0 +1,23 @@
APP_ENV=development
APP_DEBUG=true
APP_URL=http://ninja.dev
APP_KEY=SomeRandomStringSomeRandomString
APP_CIPHER=AES-256-CBC
APP_LOCALE=en
MULTI_DB_ENABLED=true
MULTI_DB_CACHE_ENABLED=true
DB_TYPE=db-ninja-1
DB_STRICT=false
DB_HOST=localhost
DB_USERNAME=ninja
DB_PASSWORD=ninja
DB_DATABASE0=ninja0
DB_DATABASE1=ninja
DB_DATABASE2=ninja2
MAIL_DRIVER=log
TRAVIS=true
API_SECRET=password

View File

@ -44,21 +44,21 @@ before_script:
# prevent MySQL went away error
- mysql -u root -e 'SET @@GLOBAL.wait_timeout=28800;'
# copy configuration files
- cp .env.example .env
- cp .env.travis .env
- cp tests/_bootstrap.php.default tests/_bootstrap.php
- php artisan key:generate --no-interaction
- sed -i 's/APP_ENV=production/APP_ENV=development/g' .env
- sed -i 's/APP_DEBUG=false/APP_DEBUG=true/g' .env
- sed -i 's/MAIL_DRIVER=smtp/MAIL_DRIVER=log/g' .env
- sed -i 's/PHANTOMJS_CLOUD_KEY/#PHANTOMJS_CLOUD_KEY/g' .env
- sed -i '$a NINJA_DEV=true' .env
- sed -i '$a TRAVIS=true' .env
# create the database and user
- mysql -u root -e "create database IF NOT EXISTS ninja0;"
- mysql -u root -e "create database IF NOT EXISTS ninja;"
- mysql -u root -e "create database IF NOT EXISTS ninja2;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON ninja0.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON ninja.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON ninja2.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;"
# migrate and seed the database
- php artisan migrate --no-interaction
- php artisan db:seed --no-interaction # default seed
- php artisan migrate --database=db-ninja-0 --seed --no-interaction
- php artisan migrate --database=db-ninja-1 --seed --no-interaction
- php artisan migrate --database=db-ninja-2 --seed --no-interaction
# Start webserver on ninja.dev:8000
- php artisan serve --host=ninja.dev --port=8000 & # '&' allows to run in background
# Start PhantomJS
@ -69,6 +69,7 @@ before_script:
- curl -L http://ninja.dev:8000/update
- php artisan ninja:create-test-data 4 true
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
- sed -i 's/DB_TYPE=db-ninja-1/DB_TYPE=db-ninja-2/g' .env
script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php
@ -92,6 +93,10 @@ script:
after_script:
- php artisan ninja:check-data --no-interaction
- cat .env
- mysql -u root -e 'select * from lookup_companies;' ninja0
- mysql -u root -e 'select * from lookup_accounts;' ninja0
- mysql -u root -e 'select * from lookup_contacts;' ninja0
- mysql -u root -e 'select * from lookup_invitations;' ninja0
- mysql -u root -e 'select * from accounts;' ninja
- mysql -u root -e 'select * from users;' ninja
- mysql -u root -e 'select * from account_gateways;' ninja
@ -103,6 +108,7 @@ after_script:
- mysql -u root -e 'select * from payments;' ninja
- mysql -u root -e 'select * from credits;' ninja
- mysql -u root -e 'select * from expenses;' ninja
- mysql -u root -e 'select * from accounts;' ninja
- cat storage/logs/laravel-error.log
- cat storage/logs/laravel-info.log
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr)

View File

@ -9,6 +9,7 @@ use App\Ninja\Repositories\AccountRepository;
use App\Services\PaymentService;
use Illuminate\Console\Command;
use Carbon;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ChargeRenewalInvoices.
@ -60,6 +61,10 @@ class ChargeRenewalInvoices extends Command
{
$this->info(date('Y-m-d').' ChargeRenewalInvoices...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$ninjaAccount = $this->accountRepo->getNinjaAccount();
$invoices = Invoice::whereAccountId($ninjaAccount->id)
->whereDueDate(date('Y-m-d'))
@ -120,6 +125,8 @@ class ChargeRenewalInvoices extends Command
*/
protected function getOptions()
{
return [];
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -64,6 +64,10 @@ class CheckData extends Command
{
$this->logMessage(date('Y-m-d') . ' Running CheckData...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
if (! $this->option('client_id')) {
$this->checkBlankInvoiceHistory();
$this->checkPaidToDate();
@ -72,6 +76,9 @@ class CheckData extends Command
$this->checkBalances();
$this->checkContacts();
// TODO Enable once user_account companies have been merged
//$this->checkUserAccounts();
if (! $this->option('client_id')) {
$this->checkInvitations();
$this->checkFailedJobs();
@ -83,10 +90,10 @@ class CheckData extends Command
$this->info($this->log);
if ($errorEmail) {
Mail::raw($this->log, function ($message) use ($errorEmail) {
Mail::raw($this->log, function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject('Check-Data: ' . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
->subject("Check-Data [{$database}]: " . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
});
} elseif (! $this->isValid) {
throw new Exception('Check data failed!!');
@ -98,8 +105,86 @@ class CheckData extends Command
$this->log .= $str . "\n";
}
private function checkUserAccounts()
{
$userAccounts = DB::table('user_accounts')
->leftJoin('users as u1', 'u1.id', '=', 'user_accounts.user_id1')
->leftJoin('accounts as a1', 'a1.id', '=', 'u1.account_id')
->leftJoin('users as u2', 'u2.id', '=', 'user_accounts.user_id2')
->leftJoin('accounts as a2', 'a2.id', '=', 'u2.account_id')
->leftJoin('users as u3', 'u3.id', '=', 'user_accounts.user_id3')
->leftJoin('accounts as a3', 'a3.id', '=', 'u3.account_id')
->leftJoin('users as u4', 'u4.id', '=', 'user_accounts.user_id4')
->leftJoin('accounts as a4', 'a4.id', '=', 'u4.account_id')
->leftJoin('users as u5', 'u5.id', '=', 'user_accounts.user_id5')
->leftJoin('accounts as a5', 'a5.id', '=', 'u5.account_id')
->get([
'user_accounts.id',
'a1.company_id as a1_company_id',
'a2.company_id as a2_company_id',
'a3.company_id as a3_company_id',
'a4.company_id as a4_company_id',
'a5.company_id as a5_company_id',
]);
$countInvalid = 0;
foreach ($userAccounts as $userAccount) {
$ids = [];
if ($companyId1 = $userAccount->a1_company_id) {
$ids[$companyId1] = true;
}
if ($companyId2 = $userAccount->a2_company_id) {
$ids[$companyId2] = true;
}
if ($companyId3 = $userAccount->a3_company_id) {
$ids[$companyId3] = true;
}
if ($companyId4 = $userAccount->a4_company_id) {
$ids[$companyId4] = true;
}
if ($companyId5 = $userAccount->a5_company_id) {
$ids[$companyId5] = true;
}
if (count($ids) > 1) {
$this->info('user_account: ' . $userAccount->id);
$countInvalid++;
}
}
if ($countInvalid > 0) {
$this->logMessage($countInvalid . ' user accounts with multiple companies');
$this->isValid = false;
}
}
private function checkContacts()
{
// check for contacts with the contact_key value set
$contacts = DB::table('contacts')
->whereNull('contact_key')
->orderBy('id')
->get(['id']);
$this->logMessage(count($contacts) . ' contacts without a contact_key');
if (count($contacts) > 0) {
$this->isValid = false;
}
if ($this->option('fix') == 'true') {
foreach ($contacts as $contact) {
DB::table('contacts')
->where('id', $contact->id)
->whereNull('contact_key')
->update([
'contact_key' => strtolower(str_random(RANDOM_KEY_LENGTH)),
]);
}
}
// check for missing contacts
$clients = DB::table('clients')
->leftJoin('contacts', function($join) {
$join->on('contacts.client_id', '=', 'clients.id')
@ -133,6 +218,7 @@ class CheckData extends Command
}
}
// check for more than one primary contact
$clients = DB::table('clients')
->leftJoin('contacts', function($join) {
$join->on('contacts.client_id', '=', 'clients.id')
@ -351,7 +437,7 @@ class CheckData extends Command
$clients->where('clients.id', '=', $this->option('client_id'));
}
$clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at')
$clients = $clients->groupBy('clients.id', 'clients.balance')
->orderBy('accounts.company_id', 'DESC')
->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]);
$this->logMessage(count($clients) . ' clients with incorrect balance/activities');
@ -543,6 +629,7 @@ class CheckData extends Command
return [
['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null],
['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -25,7 +25,7 @@ class CreateTestData extends Command
/**
* @var string
*/
protected $signature = 'ninja:create-test-data {count=1} {create_account=false}';
protected $signature = 'ninja:create-test-data {count=1} {create_account=false} {--database}';
/**
* @var
@ -68,12 +68,17 @@ class CreateTestData extends Command
public function fire()
{
if (Utils::isNinjaProd()) {
$this->info('Unable to run in production');
return false;
}
$this->info(date('Y-m-d').' Running CreateTestData...');
$this->count = $this->argument('count');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
if (filter_var($this->argument('create_account'), FILTER_VALIDATE_BOOLEAN)) {
$this->info('Creating new account...');
$account = $this->accountRepo->create(

View File

@ -1,63 +0,0 @@
<?php
namespace App\Console\Commands;
use File;
use Illuminate\Console\Command;
/**
* Class GenerateResources.
*/
class GenerateResources extends Command
{
/**
* @var string
*/
protected $name = 'ninja:generate-resources';
/**
* @var string
*/
protected $description = 'Generate Resouces';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the command.
*
* @return void
*/
public function fire()
{
$texts = File::getRequire(base_path() . '/resources/lang/en/texts.php');
foreach ($texts as $key => $value) {
if (is_array($value)) {
echo $key;
} else {
echo "$key => $value\n";
}
}
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [];
}
}

View File

@ -0,0 +1,293 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use DB;
use Mail;
use Exception;
use App\Models\DbServer;
use App\Models\LookupCompany;
use App\Models\LookupAccount;
use App\Models\LookupUser;
use App\Models\LookupContact;
use App\Models\LookupAccountToken;
use App\Models\LookupInvitation;
class InitLookup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:init-lookup {--truncate=} {--validate=} {--company_id=} {--page_size=100} {--database=db-ninja-1}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Initialize lookup tables';
protected $log = '';
protected $isValid = true;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->logMessage('Running InitLookup...');
config(['database.default' => DB_NINJA_LOOKUP]);
$database = $this->option('database');
$dbServer = DbServer::whereName($database)->first();
if ($this->option('truncate')) {
$this->truncateTables();
$this->logMessage('Truncated');
} else {
config(['database.default' => $this->option('database')]);
$count = DB::table('companies')
->where('id', '>=', $this->option('company_id') ?: 1)
->count();
for ($i=0; $i<$count; $i += (int) $this->option('page_size')) {
$this->initCompanies($dbServer->id, $i);
}
}
$this->info($this->log);
$this->info('Valid: ' . ($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
if ($this->option('validate')) {
if ($errorEmail = env('ERROR_EMAIL')) {
Mail::raw($this->log, function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("Check-Lookups [{$database}]: " . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
});
} elseif (! $this->isValid) {
throw new Exception('Check lookups failed!!');
}
}
}
private function initCompanies($dbServerId, $offset = 0)
{
$data = [];
config(['database.default' => $this->option('database')]);
$companies = DB::table('companies')
->offset($offset)
->limit((int) $this->option('page_size'))
->orderBy('id')
->where('id', '>=', $this->option('company_id') ?: 1)
->get(['id']);
foreach ($companies as $company) {
$data[$company->id] = $this->parseCompany($company->id);
}
config(['database.default' => DB_NINJA_LOOKUP]);
foreach ($data as $companyId => $company) {
if ($this->option('validate')) {
$lookupCompany = LookupCompany::whereDbServerId($dbServerId)->whereCompanyId($companyId)->first();
if (! $lookupCompany) {
$this->logError("LookupCompany - dbServerId: {$dbServerId}, companyId: {$companyId} | Not found!");
continue;
}
} else {
$lookupCompany = LookupCompany::create([
'db_server_id' => $dbServerId,
'company_id' => $companyId,
]);
}
foreach ($company as $accountKey => $account) {
if ($this->option('validate')) {
$lookupAccount = LookupAccount::whereLookupCompanyId($lookupCompany->id)->whereAccountKey($accountKey)->first();
if (! $lookupAccount) {
$this->logError("LookupAccount - lookupCompanyId: {$lookupCompany->id}, accountKey {$accountKey} | Not found!");
continue;
}
} else {
$lookupAccount = LookupAccount::create([
'lookup_company_id' => $lookupCompany->id,
'account_key' => $accountKey
]);
}
foreach ($account['users'] as $user) {
if ($this->option('validate')) {
$lookupUser = LookupUser::whereLookupAccountId($lookupAccount->id)->whereUserId($user['user_id'])->first();
if (! $lookupUser) {
$this->logError("LookupUser - lookupAccountId: {$lookupAccount->id}, userId: {$user['user_id']} | Not found!");
continue;
}
} else {
LookupUser::create([
'lookup_account_id' => $lookupAccount->id,
'email' => $user['email'] ?: null,
'user_id' => $user['user_id'],
]);
}
}
foreach ($account['contacts'] as $contact) {
if ($this->option('validate')) {
$lookupContact = LookupContact::whereLookupAccountId($lookupAccount->id)->whereContactKey($contact['contact_key'])->first();
if (! $lookupContact) {
$this->logError("LookupContact - lookupAccountId: {$lookupAccount->id}, contactKey: {$contact['contact_key']} | Not found!");
continue;
}
} else {
LookupContact::create([
'lookup_account_id' => $lookupAccount->id,
'contact_key' => $contact['contact_key'],
]);
}
}
foreach ($account['invitations'] as $invitation) {
if ($this->option('validate')) {
$lookupInvitation = LookupInvitation::whereLookupAccountId($lookupAccount->id)->whereInvitationKey($invitation['invitation_key'])->first();
if (! $lookupInvitation) {
$this->logError("LookupInvitation - lookupAccountId: {$lookupAccount->id}, invitationKey: {$invitation['invitation_key']} | Not found!");
continue;
}
} else {
LookupInvitation::create([
'lookup_account_id' => $lookupAccount->id,
'invitation_key' => $invitation['invitation_key'],
'message_id' => $invitation['message_id'] ?: null,
]);
}
}
foreach ($account['tokens'] as $token) {
if ($this->option('validate')) {
$lookupToken = LookupAccountToken::whereLookupAccountId($lookupAccount->id)->whereToken($token['token'])->first();
if (! $lookupToken) {
$this->logError("LookupAccountToken - lookupAccountId: {$lookupAccount->id}, token: {$token['token']} | Not found!");
continue;
}
} else {
LookupAccountToken::create([
'lookup_account_id' => $lookupAccount->id,
'token' => $token['token'],
]);
}
}
}
}
}
private function parseCompany($companyId)
{
$data = [];
config(['database.default' => $this->option('database')]);
$accounts = DB::table('accounts')->whereCompanyId($companyId)->orderBy('id')->get(['id', 'account_key']);
foreach ($accounts as $account) {
$data[$account->account_key] = $this->parseAccount($account->id);
}
return $data;
}
private function parseAccount($accountId)
{
$data = [
'users' => [],
'contacts' => [],
'invitations' => [],
'tokens' => [],
];
$users = DB::table('users')->whereAccountId($accountId)->orderBy('id')->get(['email', 'id']);
foreach ($users as $user) {
$data['users'][] = [
'email' => $user->email,
'user_id' => $user->id,
];
}
$contacts = DB::table('contacts')->whereAccountId($accountId)->orderBy('id')->get(['contact_key']);
foreach ($contacts as $contact) {
$data['contacts'][] = [
'contact_key' => $contact->contact_key,
];
}
$invitations = DB::table('invitations')->whereAccountId($accountId)->orderBy('id')->get(['invitation_key', 'message_id']);
foreach ($invitations as $invitation) {
$data['invitations'][] = [
'invitation_key' => $invitation->invitation_key,
'message_id' => $invitation->message_id,
];
}
$tokens = DB::table('account_tokens')->whereAccountId($accountId)->orderBy('id')->get(['token']);
foreach ($tokens as $token) {
$data['tokens'][] = [
'token' => $token->token,
];
}
return $data;
}
private function logMessage($str)
{
$this->log .= date('Y-m-d h:i:s') . ' ' . $str . "\n";
}
private function logError($str)
{
$this->isValid = false;
$this->logMessage($str);
}
private function truncateTables()
{
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::statement('truncate lookup_companies');
DB::statement('truncate lookup_accounts');
DB::statement('truncate lookup_users');
DB::statement('truncate lookup_contacts');
DB::statement('truncate lookup_invitations');
DB::statement('truncate lookup_account_tokens');
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}
protected function getOptions()
{
return [
['truncate', null, InputOption::VALUE_OPTIONAL, 'Truncate', null],
['company_id', null, InputOption::VALUE_OPTIONAL, 'Company Id', null],
['page_size', null, InputOption::VALUE_OPTIONAL, 'Page Size', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
['validate', null, InputOption::VALUE_OPTIONAL, 'Validate', null],
];
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace InvoiceNinja\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
/**
* Class Inspire.
*/
class Inspire extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use DB;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class PruneData.
@ -14,7 +15,7 @@ class PruneData extends Command
* @var string
*/
protected $name = 'ninja:prune-data';
/**
* @var string
*/
@ -24,32 +25,42 @@ class PruneData extends Command
{
$this->info(date('Y-m-d').' Running PruneData...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// delete accounts who never registered, didn't create any invoices,
// hansn't logged in within the past 6 months and isn't linked to another account
$sql = 'select a.id
from (select id, last_login from accounts) a
left join users u on u.account_id = a.id and u.public_id = 0
left join invoices i on i.account_id = a.id
left join user_accounts ua1 on ua1.user_id1 = u.id
left join user_accounts ua2 on ua2.user_id2 = u.id
left join user_accounts ua3 on ua3.user_id3 = u.id
left join user_accounts ua4 on ua4.user_id4 = u.id
left join user_accounts ua5 on ua5.user_id5 = u.id
where u.registered = 0
and a.last_login < DATE_SUB(now(), INTERVAL 6 MONTH)
and (ua1.id is null and ua2.id is null and ua3.id is null and ua4.id is null and ua5.id is null)
group by a.id
having count(i.id) = 0';
$sql = 'select c.id
from companies c
left join accounts a on a.company_id = c.id
left join clients cl on cl.account_id = a.id
left join tasks t on t.account_id = a.id
left join expenses e on e.account_id = a.id
left join users u on u.account_id = a.id and u.registered = 1
where c.created_at < DATE_SUB(now(), INTERVAL 6 MONTH)
and c.trial_started is null
and c.plan is null
group by c.id
having count(cl.id) = 0
and count(t.id) = 0
and count(e.id) = 0
and count(u.id) = 0';
$results = DB::select($sql);
foreach ($results as $result) {
$this->info("Deleting {$result->id}");
DB::table('accounts')
->where('id', '=', $result->id)
->delete();
$this->info("Deleting company: {$result->id}");
try {
DB::table('companies')
->where('id', '=', $result->id)
->delete();
} catch (\Illuminate\Database\QueryException $e) {
// most likely because a user_account record exists which doesn't cascade delete
$this->info("Unable to delete companyId: {$result->id}");
}
}
$this->info('Done');
}
@ -66,6 +77,8 @@ class PruneData extends Command
*/
protected function getOptions()
{
return [];
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -5,6 +5,7 @@ namespace App\Console\Commands;
use App\Models\Document;
use DateTime;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class RemoveOrphanedDocuments.
@ -19,14 +20,18 @@ class RemoveOrphanedDocuments extends Command
* @var string
*/
protected $description = 'Removes old documents not associated with an expense or invoice';
public function fire()
{
$this->info(date('Y-m-d').' Running RemoveOrphanedDocuments...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', [new DateTime('-1 hour')])
->get();
$this->info(count($documents).' orphaned document(s) found');
foreach ($documents as $document) {
@ -49,6 +54,8 @@ class RemoveOrphanedDocuments extends Command
*/
protected function getOptions()
{
return [];
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Utils;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ResetData.
@ -14,7 +15,7 @@ class ResetData extends Command
* @var string
*/
protected $name = 'ninja:reset-data';
/**
* @var string
*/
@ -28,8 +29,24 @@ class ResetData extends Command
return;
}
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
Artisan::call('migrate:reset');
Artisan::call('migrate');
Artisan::call('db:seed');
}
/**
* @return array
*/
protected function getOptions()
{
return [
['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null],
['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use Carbon\Carbon;
use Illuminate\Console\Command;
class ResetInvoiceSchemaCounter extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:reset-invoice-schema-counter
{account? : The ID of the account}
{--force : Force setting the counter back to "1", regardless if the year changed}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset the invoice schema counter at the turn of the year.';
/**
* @var Invoice
*/
protected $invoice;
/**
* Create a new command instance.
*
* @param Invoice $invoice
*/
public function __construct(Invoice $invoice)
{
parent::__construct();
$this->invoice = $invoice;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$force = $this->option('force');
$account = $this->argument('account');
$accounts = null;
if ($account) {
$accounts = Account::find($account)->get();
} else {
$accounts = Account::all();
}
$latestInvoice = $this->invoice->latest()->first();
$invoiceYear = Carbon::parse($latestInvoice->created_at)->year;
if (Carbon::now()->year > $invoiceYear || $force) {
$accounts->transform(function ($a) {
/* @var Account $a */
$a->invoice_number_counter = 1;
$a->update();
});
$this->info('The counter has been resetted successfully for '.$accounts->count().' account(s).');
}
}
}

View File

@ -9,6 +9,7 @@ use App\Ninja\Repositories\InvoiceRepository;
use App\Services\PaymentService;
use DateTime;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class SendRecurringInvoices.
@ -61,6 +62,10 @@ class SendRecurringInvoices extends Command
$this->info(date('Y-m-d H:i:s') . ' Running SendRecurringInvoices...');
$today = new DateTime();
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// check for counter resets
$accounts = Account::where('reset_counter_frequency_id', '>', 0)
->orderBy('id', 'asc')
@ -130,6 +135,8 @@ class SendRecurringInvoices extends Command
*/
protected function getOptions()
{
return [];
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -7,6 +7,7 @@ use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\InvoiceRepository;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class SendReminders.
@ -58,6 +59,10 @@ class SendReminders extends Command
{
$this->info(date('Y-m-d') . ' Running SendReminders...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$accounts = $this->accountRepo->findWithReminders();
$this->info(count($accounts) . ' accounts found');
@ -82,10 +87,10 @@ class SendReminders extends Command
$this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject('SendReminders: Finished successfully');
->subject("SendReminders [{$database}]: Finished successfully");
});
}
}
@ -103,6 +108,8 @@ class SendReminders extends Command
*/
protected function getOptions()
{
return [];
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -7,6 +7,7 @@ use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\AccountRepository;
use Illuminate\Console\Command;
use Utils;
use Symfony\Component\Console\Input\InputOption;
/**
* Class SendRenewalInvoices.
@ -51,6 +52,10 @@ class SendRenewalInvoices extends Command
{
$this->info(date('Y-m-d').' Running SendRenewalInvoices...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// get all accounts with plans expiring in 10 days
$companies = Company::whereRaw("datediff(plan_expires, curdate()) = 10 and (plan = 'pro' or plan = 'enterprise')")
->orderBy('id')
@ -102,10 +107,10 @@ class SendRenewalInvoices extends Command
$this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject('SendRenewalInvoices: Finished successfully');
->subject("SendRenewalInvoices [{$database}]: Finished successfully");
});
}
}
@ -123,6 +128,8 @@ class SendRenewalInvoices extends Command
*/
protected function getOptions()
{
return [];
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View File

@ -24,10 +24,10 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\SendRenewalInvoices',
'App\Console\Commands\ChargeRenewalInvoices',
'App\Console\Commands\SendReminders',
'App\Console\Commands\GenerateResources',
'App\Console\Commands\TestOFX',
'App\Console\Commands\MakeModule',
'App\Console\Commands\MakeClass',
'App\Console\Commands\InitLookup',
];
/**

View File

@ -229,6 +229,7 @@ if (! defined('APP_NAME')) {
define('SESSION_REFERRAL_CODE', 'referralCode');
define('SESSION_LEFT_SIDEBAR', 'showLeftSidebar');
define('SESSION_RIGHT_SIDEBAR', 'showRightSidebar');
define('SESSION_DB_SERVER', 'dbServer');
define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE');
define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME');
@ -290,8 +291,8 @@ if (! defined('APP_NAME')) {
define('EVENT_DELETE_INVOICE', 9);
define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN');
define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID');
define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h');
define('NINJA_ACCOUNT_KEY', env('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'));
define('NINJA_ACCOUNT_EMAIL', env('NINJA_ACCOUNT_EMAIL', 'contact@invoiceninja.com'));
define('NINJA_LICENSE_ACCOUNT_KEY', 'AsFmBAeLXF0IKf7tmi0eiyZfmWW9hxMT');
define('NINJA_GATEWAY_ID', GATEWAY_STRIPE);
define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG');
@ -299,7 +300,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.3.0' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '3.3.1' . 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'));
@ -335,6 +336,10 @@ if (! defined('APP_NAME')) {
define('BLANK_IMAGE', 'data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=');
define('DB_NINJA_LOOKUP', 'db-ninja-0');
define('DB_NINJA_1', 'db-ninja-1');
define('DB_NINJA_2', 'db-ninja-2');
define('COUNT_FREE_DESIGNS', 4);
define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design
define('PRODUCT_ONE_CLICK_INSTALL', 1);

View File

@ -39,6 +39,10 @@ class AccountApiController extends BaseAPIController
public function register(RegisterRequest $request)
{
if (! \App\Models\LookupUser::validateEmail($request->email)) {
return $this->errorResponse(['message' => trans('texts.email_taken')], 500);
}
$account = $this->accountRepo->create($request->first_name, $request->last_name, $request->email, $request->password);
$user = $account->users()->first();
@ -192,7 +196,7 @@ class AccountApiController extends BaseAPIController
$oAuth = new OAuth();
$user = $oAuth->getProvider($provider)->getTokenResponse($token);
if($user) {
if ($user) {
Auth::login($user);
return $this->processLogin($request);
}

View File

@ -97,25 +97,6 @@ class AccountController extends BaseController
$this->paymentService = $paymentService;
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function demo()
{
$demoAccountId = Utils::getDemoAccountId();
if (! $demoAccountId) {
return Redirect::to('/');
}
$account = Account::find($demoAccountId);
$user = $account->users()->first();
Auth::login($user, true);
return Redirect::to('invoices/create');
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
@ -975,7 +956,22 @@ class AccountController extends BaseController
$account->page_size = Input::get('page_size');
$labels = [];
foreach (['item', 'description', 'unit_cost', 'quantity', 'line_total', 'terms', 'balance_due', 'partial_due', 'subtotal', 'paid_to_date', 'discount', 'tax'] as $field) {
foreach ([
'item',
'description',
'unit_cost',
'quantity',
'line_total',
'terms',
'balance_due',
'partial_due',
'subtotal',
'paid_to_date',
'discount',
'tax',
'po_number',
'due_date',
] as $field) {
$labels[$field] = Input::get("labels_{$field}");
}
$account->invoice_labels = json_encode($labels);
@ -1104,6 +1100,14 @@ class AccountController extends BaseController
{
/** @var \App\Models\User $user */
$user = Auth::user();
$email = trim(strtolower(Input::get('email')));
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return Redirect::to('settings/' . ACCOUNT_USER_DETAILS)
->withError(trans('texts.email_taken'))
->withInput();
}
$rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
$validator = Validator::make(Input::all(), $rules);
@ -1114,8 +1118,8 @@ class AccountController extends BaseController
} else {
$user->first_name = trim(Input::get('first_name'));
$user->last_name = trim(Input::get('last_name'));
$user->username = trim(Input::get('email'));
$user->email = trim(strtolower(Input::get('email')));
$user->username = $email;
$user->email = $email;
$user->phone = trim(Input::get('phone'));
if (! Auth::user()->is_admin) {
@ -1212,8 +1216,15 @@ class AccountController extends BaseController
*/
public function checkEmail()
{
$email = User::withTrashed()->where('email', '=', Input::get('email'))
->where('id', '<>', Auth::user()->registered ? 0 : Auth::user()->id)
$email = trim(strtolower(Input::get('email')));
$user = Auth::user();
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return 'taken';
}
$email = User::withTrashed()->where('email', '=', $email)
->where('id', '<>', $user->registered ? 0 : $user->id)
->first();
if ($email) {
@ -1253,6 +1264,10 @@ class AccountController extends BaseController
$email = trim(strtolower(Input::get('new_email')));
$password = trim(Input::get('new_password'));
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return '';
}
if ($user->registered) {
$newAccount = $this->accountRepo->create($firstName, $lastName, $email, $password, $account->company);
$newUser = $newAccount->users()->first();

View File

@ -351,9 +351,10 @@ class AppController extends BaseController
{
try {
Artisan::call('ninja:check-data');
Artisan::call('ninja:init-lookup', ['--validate' => true]);
return RESULT_SUCCESS;
} catch (Exception $exception) {
return RESULT_FAILURE;
return $exception->getMessage() ?: RESULT_FAILURE;
}
}

View File

@ -51,8 +51,8 @@ class PasswordController extends Controller
$data = [
'clientauth' => true,
];
$contactKey = session('contact_key');
if (!$contactKey) {
if (! session('contact_key')) {
return \Redirect::to('/client/sessionexpired');
}
@ -104,7 +104,7 @@ class PasswordController extends Controller
*
* @return \Illuminate\Http\Response
*/
public function showResetForm(Request $request, $key = null, $token = null)
public function showResetForm(Request $request, $token = null)
{
if (is_null($token)) {
return $this->getEmail();
@ -115,23 +115,8 @@ class PasswordController extends Controller
'clientauth' => true,
);
if ($key) {
$contact = Contact::where('contact_key', '=', $key)->first();
if ($contact && ! $contact->is_deleted) {
$account = $contact->account;
$data['contact_key'] = $contact->contact_key;
} else {
// Maybe it's an invitation key
$invitation = Invitation::where('invitation_key', '=', $key)->first();
if ($invitation && ! $invitation->is_deleted) {
$account = $invitation->account;
$data['contact_key'] = $invitation->contact->contact_key;
}
}
if ( empty($account)) {
return \Redirect::to('/client/sessionexpired');
}
if (! session('contact_key')) {
return \Redirect::to('/client/sessionexpired');
}
return view('clientauth.reset')->with($data);
@ -148,9 +133,9 @@ class PasswordController extends Controller
*
* @return \Illuminate\Http\Response
*/
public function getReset(Request $request, $key = null, $token = null)
public function getReset(Request $request, $token = null)
{
return $this->showResetForm($request, $key, $token);
return $this->showResetForm($request, $token);
}
/**

View File

@ -7,6 +7,7 @@ use App\Models\Country;
use App\Models\License;
use App\Ninja\Mailers\ContactMailer;
use App\Ninja\Repositories\AccountRepository;
use App\Libraries\CurlUtils;
use Auth;
use Cache;
use CreditCard;
@ -290,4 +291,31 @@ class NinjaController extends BaseController
return RESULT_SUCCESS;
}
public function purchaseWhiteLabel()
{
if (Utils::isNinja()) {
return redirect('/');
}
$user = Auth::user();
$url = NINJA_APP_URL . '/buy_now';
$contactKey = $user->primaryAccount()->account_key;
$data = [
'account_key' => NINJA_LICENSE_ACCOUNT_KEY,
'contact_key' => $contactKey,
'product_id' => PRODUCT_WHITE_LABEL,
'first_name' => Auth::user()->first_name,
'last_name' => Auth::user()->last_name,
'email' => Auth::user()->email,
'return_link' => true,
];
if ($url = CurlUtils::post($url, $data)) {
return redirect($url);
} else {
return redirect()->back()->withError(trans('texts.error_refresh_page'));
}
}
}

View File

@ -170,13 +170,22 @@ class UserController extends BaseController
$rules['email'] = 'required|email|unique:users,email,'.$user->id.',id';
} else {
$user = false;
$rules['email'] = 'required|email|unique:users';
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
return Redirect::to($userPublicId ? 'users/edit' : 'users/create')->withInput()->withErrors($validator);
return Redirect::to($userPublicId ? 'users/edit' : 'users/create')
->withErrors($validator)
->withInput();
}
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return Redirect::to($userPublicId ? 'users/edit' : 'users/create')
->withError(trans('texts.email_taken'))
->withInput();
}
if ($userPublicId) {

View File

@ -29,6 +29,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'lookup' => 'App\Http\Middleware\DatabaseLookup',
'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'permissions.required' => 'App\Http\Middleware\PermissionsRequired',

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Closure;
use App\Models\LookupAccount;
use App\Models\LookupContact;
use App\Models\LookupInvitation;
use App\Models\LookupAccountToken;
use App\Models\LookupUser;
class DatabaseLookup
{
public function handle(Request $request, Closure $next, $guard = 'user')
{
if (! env('MULTI_DB_ENABLED')) {
return $next($request);
}
if ($guard == 'user') {
if ($server = session(SESSION_DB_SERVER)) {
config(['database.default' => $server]);
} elseif ($email = $request->email) {
LookupUser::setServerByField('email', $email);
} elseif ($code = $request->confirmation_code) {
LookupUser::setServerByField('confirmation_code', $code);
}
} elseif ($guard == 'api') {
if ($token = $request->header('X-Ninja-Token')) {
LookupAccountToken::setServerByField('token', $token);
} elseif ($email = $request->email) {
LookupUser::setServerByField('email', $email);
}
} elseif ($guard == 'contact') {
if ($key = request()->invitation_key) {
LookupInvitation::setServerByField('invitation_key', $key);
} elseif ($key = request()->contact_key ?: session('contact_key')) {
LookupContact::setServerByField('contact_key', $key);
}
} elseif ($guard == 'postmark') {
LookupInvitation::setServerByField('message_id', request()->MessageID);
} elseif ($guard == 'account') {
if ($key = request()->account_key) {
LookupAccount::setServerByField('account_key', $key);
}
} elseif ($guard == 'license') {
config(['database.default' => DB_NINJA_1]);
}
return $next($request);
}
}

View File

@ -21,7 +21,7 @@ class VerifyCsrfToken extends BaseVerifier
'hook/email_bounced',
'reseller_stats',
'payment_hook/*',
'buy_now/*',
'buy_now*',
'hook/bot/*',
];

View File

@ -38,7 +38,7 @@ class SaveClientPortalSettings extends Request
$input = $this->all();
if ($this->client_view_css && Utils::isNinja()) {
$input['client_view_css'] = HTMLUtils::sanitize($this->client_view_css);
$input['client_view_css'] = HTMLUtils::sanitizeCSS($this->client_view_css);
}
if (Utils::isNinja()) {
@ -53,7 +53,7 @@ class SaveClientPortalSettings extends Request
$input['subdomain'] = null;
}
}
$this->replace($input);
return $this->all();

View File

@ -25,7 +25,7 @@ Route::get('/keep_alive', 'HomeController@keepAlive');
Route::post('/get_started', 'AccountController@getStarted');
// Client visible pages
Route::group(['middleware' => 'auth:client'], function () {
Route::group(['middleware' => ['lookup:contact', 'auth:client']], function () {
Route::get('view/{invitation_key}', 'ClientPortalController@view');
Route::get('download/{invitation_key}', 'ClientPortalController@download');
Route::put('sign/{invitation_key}', 'ClientPortalController@sign');
@ -62,51 +62,59 @@ Route::group(['middleware' => 'auth:client'], function () {
Route::get('api/client.activity', ['as' => 'api.client.activity', 'uses' => 'ClientPortalController@activityDatatable']);
});
Route::get('license', 'NinjaController@show_license_payment');
Route::post('license', 'NinjaController@do_license_payment');
Route::get('claim_license', 'NinjaController@claim_license');
Route::post('signup/validate', 'AccountController@checkEmail');
Route::post('signup/submit', 'AccountController@submitSignup');
Route::get('/auth/{provider}', 'Auth\AuthController@authLogin');
Route::get('/auth_unlink', 'Auth\AuthController@authUnlink');
Route::group(['middleware' => 'lookup:license'], function () {
Route::get('license', 'NinjaController@show_license_payment');
Route::post('license', 'NinjaController@do_license_payment');
Route::get('claim_license', 'NinjaController@claim_license');
});
Route::group(['middleware' => 'cors'], function () {
Route::match(['GET', 'POST', 'OPTIONS'], '/buy_now/{gateway_type?}', 'OnlinePaymentController@handleBuyNow');
});
Route::post('/hook/email_bounced', 'AppController@emailBounced');
Route::post('/hook/email_opened', 'AppController@emailOpened');
Route::post('/hook/bot/{platform?}', 'BotController@handleMessage');
Route::post('/payment_hook/{accountKey}/{gatewayId}', 'OnlinePaymentController@handlePaymentWebhook');
Route::group(['middleware' => 'lookup:postmark'], function () {
Route::post('/hook/email_bounced', 'AppController@emailBounced');
Route::post('/hook/email_opened', 'AppController@emailOpened');
});
Route::group(['middleware' => 'lookup:account'], function () {
Route::post('/payment_hook/{account_key}/{gateway_id}', 'OnlinePaymentController@handlePaymentWebhook');
});
//Route::post('/hook/bot/{platform?}', 'BotController@handleMessage');
// Laravel auth routes
Route::get('/signup', ['as' => 'signup', 'uses' => 'Auth\AuthController@getRegister']);
Route::post('/signup', ['as' => 'signup', 'uses' => 'Auth\AuthController@postRegister']);
Route::get('/login', ['as' => 'login', 'uses' => 'Auth\AuthController@getLoginWrapper']);
Route::post('/login', ['as' => 'login', 'uses' => 'Auth\AuthController@postLoginWrapper']);
Route::get('/logout', ['as' => 'logout', 'uses' => 'Auth\AuthController@getLogoutWrapper']);
Route::get('/recover_password', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@getEmail']);
Route::post('/recover_password', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@postEmail']);
Route::get('/password/reset/{token}', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@getReset']);
Route::post('/password/reset', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@postReset']);
Route::get('/user/confirm/{code}', 'UserController@confirm');
Route::get('/auth/{provider}', 'Auth\AuthController@authLogin');
Route::group(['middleware' => ['lookup:user']], function () {
Route::get('/user/confirm/{confirmation_code}', 'UserController@confirm');
Route::post('/login', ['as' => 'login', 'uses' => 'Auth\AuthController@postLoginWrapper']);
Route::post('/recover_password', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@postEmail']);
Route::post('/password/reset', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@postReset']);
});
// Client auth
Route::get('/client/login', ['as' => 'login', 'uses' => 'ClientAuth\AuthController@getLogin']);
Route::post('/client/login', ['as' => 'login', 'uses' => 'ClientAuth\AuthController@postLogin']);
Route::get('/client/logout', ['as' => 'logout', 'uses' => 'ClientAuth\AuthController@getLogout']);
Route::get('/client/sessionexpired', ['as' => 'logout', 'uses' => 'ClientAuth\AuthController@getSessionExpired']);
Route::get('/client/recover_password', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getEmail']);
Route::post('/client/recover_password', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postEmail']);
Route::get('/client/password/reset/{invitation_key}/{token}', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getReset']);
Route::post('/client/password/reset', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postReset']);
Route::get('/client/password/reset/{token}', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getReset']);
Route::group(['middleware' => ['lookup:contact']], function () {
Route::post('/client/login', ['as' => 'login', 'uses' => 'ClientAuth\AuthController@postLogin']);
Route::post('/client/recover_password', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postEmail']);
Route::post('/client/password/reset', ['as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postReset']);
});
if (Utils::isNinja()) {
Route::post('/signup/register', 'AccountController@doRegister');
Route::get('/news_feed/{user_type}/{version}/', 'HomeController@newsFeed');
Route::get('/demo', 'AccountController@demo');
}
if (Utils::isReseller()) {
@ -117,7 +125,7 @@ if (Utils::isTravis()) {
Route::get('/check_data', 'AppController@checkData');
}
Route::group(['middleware' => 'auth:user'], function () {
Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
Route::get('dashboard', 'DashboardController@index');
Route::get('dashboard_chart_data/{group_by}/{start_date}/{end_date}/{currency_id}/{include_expenses}', 'DashboardController@chartData');
Route::get('set_entity_filter/{entity_type}/{filter?}', 'AccountController@setEntityFilter');
@ -129,6 +137,10 @@ Route::group(['middleware' => 'auth:user'], function () {
Route::post('contact_us', 'HomeController@contactUs');
Route::post('handle_command', 'BotController@handleCommand');
Route::post('signup/validate', 'AccountController@checkEmail');
Route::post('signup/submit', 'AccountController@submitSignup');
Route::get('auth_unlink', 'Auth\AuthController@authUnlink');
Route::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails');
Route::post('settings/payment_gateway_limits', 'AccountGatewayController@savePaymentGatewayLimits');
@ -223,14 +235,16 @@ Route::group(['middleware' => 'auth:user'], function () {
Route::post('bluevine/signup', 'BlueVineController@signup');
Route::get('bluevine/hide_message', 'BlueVineController@hideMessage');
Route::get('bluevine/completed', 'BlueVineController@handleCompleted');
Route::get('white_label/hide_message', 'NinjaController@hideWhiteLabelMessage');
Route::get('white_label/purchase', 'NinjaController@purchaseWhiteLabel');
Route::get('reports', 'ReportController@showReports');
Route::post('reports', 'ReportController@showReports');
});
Route::group([
'middleware' => ['auth:user', 'permissions.required'],
'middleware' => ['lookup:user', 'auth:user', 'permissions.required'],
'permissions' => 'admin',
], function () {
Route::get('api/users', 'UserController@getDatatable');
@ -295,12 +309,12 @@ Route::group([
Route::get('self-update/download', 'SelfUpdateController@download');
});
Route::group(['middleware' => 'auth:user'], function () {
Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
Route::get('settings/{section?}', 'AccountController@showSection');
});
// Route groups for API
Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function () {
Route::group(['middleware' => ['lookup:api', 'api'], 'prefix' => 'api/v1'], function () {
Route::get('ping', 'AccountApiController@ping');
Route::post('login', 'AccountApiController@login');
Route::post('oauth_login', 'AccountApiController@oauthLogin');

View File

@ -34,6 +34,11 @@ class ImportData extends Job implements ShouldQueue
*/
protected $settings;
/**
* @var string
*/
protected $server;
/**
* Create a new job instance.
*
@ -45,6 +50,7 @@ class ImportData extends Job implements ShouldQueue
$this->user = $user;
$this->type = $type;
$this->settings = $settings;
$this->server = config('database.default');
}
/**

View File

@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Jobs\Job;
use App\Models\Document;
use App\Models\LookupAccount;
use Auth;
use DB;
use Exception;
@ -55,7 +56,18 @@ class PurgeAccountData extends Job
$account->invoice_number_counter = 1;
$account->quote_number_counter = 1;
$account->client_number_counter = 1;
$account->client_number_counter = $account->client_number_counter > 0 ? 1 : 0;
$account->save();
if (env('MULTI_DB_ENABLED')) {
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupAccount = LookupAccount::whereAccountKey($account->account_key)->firstOrFail();
DB::table('lookup_contacts')->where('lookup_account_id', '=', $lookupAccount->id)->delete();
DB::table('lookup_invitations')->where('lookup_account_id', '=', $lookupAccount->id)->delete();
config(['database.default' => $current]);
}
}
}

View File

@ -38,6 +38,11 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/
protected $userId;
/**
* @var string
*/
protected $server;
/**
* Create a new job instance.
*
@ -52,6 +57,7 @@ class SendInvoiceEmail extends Job implements ShouldQueue
$this->userId = $userId;
$this->reminder = $reminder;
$this->template = $template;
$this->server = config('database.default');
}
/**

View File

@ -40,6 +40,11 @@ class SendNotificationEmail extends Job implements ShouldQueue
*/
protected $notes;
/**
* @var string
*/
protected $server;
/**
* Create a new job instance.
@ -58,6 +63,7 @@ class SendNotificationEmail extends Job implements ShouldQueue
$this->type = $type;
$this->payment = $payment;
$this->notes = $notes;
$this->server = config('database.default');
}
/**

View File

@ -20,6 +20,11 @@ class SendPaymentEmail extends Job implements ShouldQueue
*/
protected $payment;
/**
* @var string
*/
protected $server;
/**
* Create a new job instance.
@ -28,6 +33,7 @@ class SendPaymentEmail extends Job implements ShouldQueue
public function __construct($payment)
{
$this->payment = $payment;
$this->server = config('database.default');
}
/**

View File

@ -25,6 +25,11 @@ class SendPushNotification extends Job implements ShouldQueue
*/
protected $type;
/**
* @var string
*/
protected $server;
/**
* Create a new job instance.
@ -35,6 +40,7 @@ class SendPushNotification extends Job implements ShouldQueue
{
$this->invoice = $invoice;
$this->type = $type;
$this->server = config('database.default');
}
/**

View File

@ -7,7 +7,7 @@ use HTMLPurifier_Config;
class HTMLUtils
{
public static function sanitize($css)
public static function sanitizeCSS($css)
{
// Allow referencing the body element
$css = preg_replace('/(?<![a-z0-9\-\_\#\.])body(?![a-z0-9\-\_])/i', '.body', $css);
@ -36,4 +36,12 @@ class HTMLUtils
// Get the first style block
return count($css) ? $css[0] : '';
}
public static function sanitizeHTML($html)
{
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
return $purifier->purify($html);
}
}

View File

@ -251,11 +251,6 @@ class Utils
}
}
public static function getDemoAccountId()
{
return isset($_ENV[DEMO_ACCOUNT_ID]) ? $_ENV[DEMO_ACCOUNT_ID] : false;
}
public static function getNewsFeedResponse($userType = false)
{
if (! $userType) {
@ -398,6 +393,7 @@ class Utils
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'ip' => Request::getClientIp(),
'count' => Session::get('error_count', 0),
'is_console' => App::runningInConsole() ? 'yes' : 'no',
];
if ($info) {

View File

@ -153,6 +153,7 @@ class InvoiceListener
public function jobFailed(JobExceptionOccurred $exception)
{
/*
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw(print_r($exception->data, true), function ($message) use ($errorEmail) {
$message->to($errorEmail)
@ -160,6 +161,7 @@ class InvoiceListener
->subject('Job failed');
});
}
*/
Utils::logError($exception->exception);
}

View File

@ -4,6 +4,7 @@ namespace App\Models;
use App;
use App\Events\UserSettingsChanged;
use App\Models\LookupAccount;
use App\Models\Traits\GeneratesNumbers;
use App\Models\Traits\PresentsInvoice;
use App\Models\Traits\SendsEmails;
@ -1655,6 +1656,11 @@ class Account extends Eloquent
}
}
Account::creating(function ($account)
{
LookupAccount::createAccount($account->account_key, $account->company_id);
});
Account::updated(function ($account) {
// prevent firing event if the invoice/quote counter was changed
// TODO: remove once counters are moved to separate table
@ -1665,3 +1671,10 @@ Account::updated(function ($account) {
Event::fire(new UserSettingsChanged());
});
Account::deleted(function ($account)
{
LookupAccount::deleteWhere([
'account_key' => $account->account_key
]);
});

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\LookupAccountToken;
/**
* Class AccountToken.
@ -39,3 +40,17 @@ class AccountToken extends EntityModel
return $this->belongsTo('App\Models\User')->withTrashed();
}
}
AccountToken::creating(function ($token)
{
LookupAccountToken::createNew($token->account->account_key, [
'token' => $token->token,
]);
});
AccountToken::deleted(function ($token)
{
LookupAccountToken::deleteWhere([
'token' => $token->token
]);
});

View File

@ -21,6 +21,18 @@ class Company extends Eloquent
*/
protected $presenter = 'App\Ninja\Presenters\CompanyPresenter';
/**
* @var array
*/
protected $fillable = [
'plan',
'plan_term',
'plan_price',
'plan_paid',
'plan_started',
'plan_expires',
];
/**
* @var array
*/
@ -174,3 +186,17 @@ class Company extends Eloquent
return false;
}
}
Company::deleted(function ($company)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$server = \App\Models\DbServer::whereName(config('database.default'))->firstOrFail();
LookupCompany::deleteWhere([
'company_id' => $company->id,
'db_server_id' => $server->id,
]);
});

View File

@ -8,6 +8,7 @@ use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\LookupContact;
/**
* Class Contact.
@ -165,3 +166,17 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
return "{$url}/client/dashboard/{$this->contact_key}";
}
}
Contact::creating(function ($contact)
{
LookupContact::createNew($contact->account->account_key, [
'contact_key' => $contact->contact_key,
]);
});
Contact::deleted(function ($contact)
{
LookupContact::deleteWhere([
'contact_key' => $contact->contact_key,
]);
});

24
app/Models/DbServer.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class DbServer extends Eloquent
{
/**
* @var bool
*/
public $timestamps = false;
/**
* @var array
*/
protected $fillable = [
'name',
];
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
use Carbon;
use Illuminate\Database\Eloquent\SoftDeletes;
use Utils;
use App\Models\LookupInvitation;
/**
* Class Invitation.
@ -162,3 +163,17 @@ class Invitation extends EntityModel
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date));
}
}
Invitation::creating(function ($invitation)
{
LookupInvitation::createNew($invitation->account->account_key, [
'invitation_key' => $invitation->invitation_key,
]);
});
Invitation::deleted(function ($invitation)
{
LookupInvitation::deleteWhere([
'invitation_key' => $invitation->invitation_key,
]);
});

View File

@ -1034,7 +1034,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$dueDay = $lastDayOfMonth;
}
if ($currentDay >= $dueDay) {
if ($currentDay > $dueDay) {
// Wait until next month
// We don't need to handle the December->January wraparaound, since PHP handles month 13 as January of next year
$dueMonth++;
@ -1511,6 +1511,11 @@ class Invoice extends EntityModel implements BalanceAffecting
->orderBy('id', 'desc')
->get();
}
public function getDueDateLabel()
{
return $this->isQuote() ? 'valid_until' : 'due_date';
}
}
Invoice::creating(function ($invoice) {

View File

@ -0,0 +1,52 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class LookupAccount extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'lookup_company_id',
'account_key',
];
public function lookupCompany()
{
return $this->belongsTo('App\Models\LookupCompany');
}
public static function createAccount($accountKey, $companyId)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$server = DbServer::whereName($current)->firstOrFail();
$lookupCompany = LookupCompany::whereDbServerId($server->id)
->whereCompanyId($companyId)->first();
if (! $lookupCompany) {
$lookupCompany = LookupCompany::create([
'db_server_id' => $server->id,
'company_id' => $companyId,
]);
}
LookupAccount::create([
'lookup_company_id' => $lookupCompany->id,
'account_key' => $accountKey,
]);
static::setDbServer($current);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class LookupAccountToken extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'lookup_account_id',
'token',
];
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class LookupCompany extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'db_server_id',
'company_id',
];
public function dbServer()
{
return $this->belongsTo('App\Models\DbServer');
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class LookupContact extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'lookup_account_id',
'contact_key',
];
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class ExpenseCategory.
*/
class LookupInvitation extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'lookup_account_id',
'invitation_key',
'message_id',
];
}

114
app/Models/LookupModel.php Normal file
View File

@ -0,0 +1,114 @@
<?php
namespace App\Models;
use Eloquent;
use Cache;
/**
* Class ExpenseCategory.
*/
class LookupModel extends Eloquent
{
/**
* @var bool
*/
public $timestamps = false;
public function lookupAccount()
{
return $this->belongsTo('App\Models\LookupAccount');
}
public static function createNew($accountKey, $data)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupAccount = LookupAccount::whereAccountKey($accountKey)->first();
if ($lookupAccount) {
$data['lookup_account_id'] = $lookupAccount->id;
} else {
abort('Lookup account not found for ' . $accountKey);
}
static::create($data);
config(['database.default' => $current]);
}
public static function deleteWhere($where)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
static::where($where)->delete();
config(['database.default' => $current]);
}
public static function setServerByField($field, $value)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$className = get_called_class();
$className = str_replace('Lookup', '', $className);
$key = sprintf('server:%s:%s:%s', $className, $field, $value);
$isUser = $className == 'App\Models\User';
// check if we've cached this lookup
if (env('MULTI_DB_CACHE_ENABLED') && $server = Cache::get($key)) {
static::setDbServer($server, $isUser);
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
if ($value && $lookupModel = static::where($field, '=', $value)->first()) {
$entity = new $className();
$server = $lookupModel->getDbServer();
static::setDbServer($server, $isUser);
// check entity is found on the server
if (! $entity::where($field, '=', $value)->first()) {
abort("Looked up {$className} not found: {$field} => {$value}");
}
Cache::put($key, $server, 120);
} else {
config(['database.default' => $current]);
}
}
protected static function setDbServer($server, $isUser = false)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
config(['database.default' => $server]);
if ($isUser) {
session([SESSION_DB_SERVER => $server]);
}
}
public function getDbServer()
{
return $this->lookupAccount->lookupCompany->dbServer->name;
}
}

68
app/Models/LookupUser.php Normal file
View File

@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Eloquent;
use App\Models\User;
/**
* Class ExpenseCategory.
*/
class LookupUser extends LookupModel
{
/**
* @var array
*/
protected $fillable = [
'lookup_account_id',
'email',
'user_id',
];
public static function updateUser($accountKey, $userId, $email, $confirmationCode)
{
if (! env('MULTI_DB_ENABLED')) {
return;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupAccount = LookupAccount::whereAccountKey($accountKey)
->firstOrFail();
$lookupUser = LookupUser::whereLookupAccountId($lookupAccount->id)
->whereUserId($userId)
->firstOrFail();
$lookupUser->email = $email;
$lookupUser->confirmation_code = $confirmationCode;
$lookupUser->save();
config(['database.default' => $current]);
}
public static function validateEmail($email, $user = false)
{
if (! env('MULTI_DB_ENABLED')) {
return true;
}
$current = config('database.default');
config(['database.default' => DB_NINJA_LOOKUP]);
$lookupUser = LookupUser::whereEmail($email)->first();
if ($user) {
$lookupAccount = LookupAccount::whereAccountKey($user->account->account_key)->firstOrFail();
$isValid = ! $lookupUser || ($lookupUser->lookup_account_id == $lookupAccount->id && $lookupUser->user_id == $user->id);
} else {
$isValid = ! $lookupUser;
}
config(['database.default' => $current]);
return $isValid;
}
}

View File

@ -26,6 +26,7 @@ trait GeneratesNumbers
$prefix = $this->getNumberPrefix($entityType);
$counterOffset = 0;
$check = false;
$lastNumber = false;
if ($entityType == ENTITY_CLIENT && ! $this->clientNumbersEnabled()) {
return '';
@ -50,6 +51,13 @@ trait GeneratesNumbers
}
$counter++;
$counterOffset++;
// prevent getting stuck in a loop
if ($number == $lastNumber) {
return '';
}
$lastNumber = $number;
} while ($check);
// update the counter to be caught up
@ -194,15 +202,17 @@ trait GeneratesNumbers
'{$clientCounter}',
];
$client = $invoice->client;
$clientCounter = ($invoice->isQuote() && ! $this->share_counter) ? $client->quote_number_counter : $client->invoice_number_counter;
$replace = [
$invoice->client->custom_value1,
$invoice->client->custom_value2,
$invoice->client->id_number,
$invoice->client->custom_value1, // backwards compatibility
$invoice->client->custom_value2,
$invoice->client->id_number,
str_pad($invoice->client->invoice_number_counter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
str_pad($invoice->client->quote_number_counter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
$client->custom_value1,
$client->custom_value2,
$client->id_number,
$client->custom_value1, // backwards compatibility
$client->custom_value2,
$client->id_number,
str_pad($clientCounter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
];
return str_replace($search, $replace, $pattern);

View File

@ -162,6 +162,28 @@ trait PresentsInvoice
return $fields;
}
public function hasCustomLabel($field)
{
$custom = (array) json_decode($this->invoice_labels);
return isset($custom[$field]) && $custom[$field];
}
public function getLabel($field, $override = false)
{
$custom = (array) json_decode($this->invoice_labels);
if (isset($custom[$field]) && $custom[$field]) {
return $custom[$field];
} else {
if ($override) {
$field = $override;
}
return $this->isEnglish() ? uctrans("texts.$field") : trans("texts.$field");
}
}
/**
* @return array
*/
@ -239,6 +261,8 @@ trait PresentsInvoice
'work_phone',
'invoice_total',
'outstanding',
'invoice_due_date',
'quote_due_date',
];
foreach ($fields as $field) {

View File

@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laracasts\Presenter\PresentableTrait;
use Session;
use App\Models\LookupUser;
/**
* Class User.
@ -412,10 +413,34 @@ class User extends Authenticatable
}
}
User::created(function ($user)
{
LookupUser::createNew($user->account->account_key, [
'email' => $user->email,
'user_id' => $user->id,
]);
});
User::updating(function ($user) {
User::onUpdatingUser($user);
$dirty = $user->getDirty();
if (isset($dirty['email']) || isset($dirty['confirmation_code'])) {
LookupUser::updateUser($user->account->account_key, $user->id, $user->email, $user->confirmation_code);
}
});
User::updated(function ($user) {
User::onUpdatedUser($user);
});
User::deleted(function ($user)
{
if (! $user->email) {
return;
}
LookupUser::deleteWhere([
'email' => $user->email
]);
});

View File

@ -359,13 +359,12 @@ class AccountRepository
$emailSettings = new AccountEmailSettings();
$account->account_email_settings()->save($emailSettings);
$random = strtolower(str_random(RANDOM_KEY_LENGTH));
$user = new User();
$user->registered = true;
$user->confirmed = true;
$user->email = 'contact@invoiceninja.com';
$user->password = $random;
$user->username = $random;
$user->email = NINJA_ACCOUNT_EMAIL;
$user->username = NINJA_ACCOUNT_EMAIL;
$user->password = strtolower(str_random(RANDOM_KEY_LENGTH));
$user->first_name = 'Invoice';
$user->last_name = 'Ninja';
$user->notify_sent = true;
@ -393,7 +392,6 @@ class AccountRepository
$client = Client::whereAccountId($ninjaAccount->id)
->wherePublicId($account->id)
->first();
$clientExists = $client ? true : false;
if (! $client) {
$client = new Client();
@ -401,31 +399,22 @@ class AccountRepository
$client->account_id = $ninjaAccount->id;
$client->user_id = $ninjaUser->id;
$client->currency_id = 1;
}
foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) {
$client->$field = $account->$field;
}
$client->save();
if ($clientExists) {
$contact = $client->getPrimaryContact();
} else {
foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) {
$client->$field = $account->$field;
}
$client->save();
$contact = new Contact();
$contact->user_id = $ninjaUser->id;
$contact->account_id = $ninjaAccount->id;
$contact->public_id = $account->id;
$contact->contact_key = strtolower(str_random(RANDOM_KEY_LENGTH));
$contact->is_primary = true;
foreach (['first_name', 'last_name', 'email', 'phone'] as $field) {
$contact->$field = $account->users()->first()->$field;
}
$client->contacts()->save($contact);
}
$user = $account->getPrimaryUser();
foreach (['first_name', 'last_name', 'email', 'phone'] as $field) {
$contact->$field = $user->$field;
}
$client->contacts()->save($contact);
return $client;
}
@ -450,12 +439,16 @@ class AccountRepository
if (! $user->registered) {
$rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
$validator = Validator::make(['email' => $email], $rules);
if ($validator->fails()) {
$messages = $validator->messages();
return $messages->first('email');
}
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return trans('texts.email_taken');
}
$user->email = $email;
$user->first_name = $firstName;
$user->last_name = $lastName;

View File

@ -15,12 +15,7 @@ class NinjaRepository
}
$company = $account->company;
$company->plan = ! empty($data['plan']) && $data['plan'] != PLAN_FREE ? $data['plan'] : null;
$company->plan_term = ! empty($data['plan_term']) ? $data['plan_term'] : null;
$company->plan_paid = ! empty($data['plan_paid']) ? $data['plan_paid'] : null;
$company->plan_started = ! empty($data['plan_started']) ? $data['plan_started'] : null;
$company->plan_expires = ! empty($data['plan_expires']) ? $data['plan_expires'] : null;
$company->fill($data);
$company->save();
}
}

View File

@ -134,6 +134,10 @@ class TaskRepository extends BaseRepository
$timeLog = [];
}
if(isset($data['client_id'])) {
$task->client_id = Client::getPrivateId($data['client_id']);
}
array_multisort($timeLog);
if (isset($data['action'])) {
@ -146,6 +150,8 @@ class TaskRepository extends BaseRepository
} elseif ($data['action'] == 'stop' && $task->is_running) {
$timeLog[count($timeLog) - 1][1] = time();
$task->is_running = false;
} elseif ($data['action'] == 'offline'){
$task->is_running = $data['is_running'] ? 1 : 0;
}
}

View File

@ -8,6 +8,8 @@ use Request;
use URL;
use Utils;
use Validator;
use Queue;
use Illuminate\Queue\Events\JobProcessing;
/**
* Class AppServiceProvider.
@ -21,6 +23,15 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
// support selecting job database
Queue::before(function (JobProcessing $event) {
$body = $event->job->getRawBody();
preg_match('/db-ninja-[\d+]/', $body, $matches);
if (count($matches)) {
config(['database.default' => $matches[0]]);
}
});
Form::macro('image_data', function ($image, $contents = false) {
if (! $contents) {
$contents = file_get_contents($image);

View File

@ -4,6 +4,7 @@ namespace App\Services;
use App\Events\UserLoggedIn;
use App\Ninja\Repositories\AccountRepository;
use App\Models\LookupUser;
use Auth;
use Input;
use Session;
@ -59,13 +60,13 @@ class AuthService
$socialiteUser = Socialite::driver($provider)->user();
$providerId = self::getProviderId($provider);
$email = $socialiteUser->email;
$oauthUserId = $socialiteUser->id;
$name = Utils::splitName($socialiteUser->name);
if (Auth::check()) {
$user = Auth::user();
$isRegistered = $user->registered;
$email = $socialiteUser->email;
$oauthUserId = $socialiteUser->id;
$name = Utils::splitName($socialiteUser->name);
$result = $this->accountRepo->updateUserFromOauth($user, $name[0], $name[1], $email, $providerId, $oauthUserId);
if ($result === true) {
@ -81,6 +82,8 @@ class AuthService
Session::flash('error', $result);
}
} else {
LookupUser::setServerByField('email', $email);
if ($user = $this->accountRepo->findUserByOauth($providerId, $socialiteUser->id)) {
Auth::login($user, true);
event(new UserLoggedIn());

View File

@ -46,12 +46,7 @@ return [
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => storage_path().'/database.sqlite',
'prefix' => '',
],
// single database setup
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
@ -65,24 +60,44 @@ return [
'engine' => 'InnoDB',
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
// multi-database setup
'db-ninja-0' => [
'driver' => 'mysql',
'host' => env('DB_HOST', env('DB_HOST0', 'localhost')),
'database' => env('DB_DATABASE0', env('DB_DATABASE', 'forge')),
'username' => env('DB_USERNAME0', env('DB_USERNAME', 'forge')),
'password' => env('DB_PASSWORD0', env('DB_PASSWORD', '')),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => env('DB_STRICT', false),
'engine' => 'InnoDB',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'prefix' => '',
'db-ninja-1' => [
'driver' => 'mysql',
'host' => env('DB_HOST', env('DB_HOST1', 'localhost')),
'database' => env('DB_DATABASE1', env('DB_DATABASE', 'forge')),
'username' => env('DB_USERNAME1', env('DB_USERNAME', 'forge')),
'password' => env('DB_PASSWORD1', env('DB_PASSWORD', '')),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => env('DB_STRICT', false),
'engine' => 'InnoDB',
],
'db-ninja-2' => [
'driver' => 'mysql',
'host' => env('DB_HOST', env('DB_HOST2', 'localhost')),
'database' => env('DB_DATABASE2', env('DB_DATABASE', 'forge')),
'username' => env('DB_USERNAME2', env('DB_USERNAME', 'forge')),
'password' => env('DB_PASSWORD2', env('DB_PASSWORD', '')),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => env('DB_STRICT', false),
'engine' => 'InnoDB',
],
],

View File

@ -36,6 +36,7 @@ return [
],
'database' => [
'connection' => env('QUEUE_DATABASE', 'mysql'),
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
@ -86,7 +87,8 @@ return [
*/
'failed' => [
'database' => 'mysql', 'table' => 'failed_jobs',
'database' => env('QUEUE_DATABASE', 'mysql'),
'table' => 'failed_jobs',
],
];

View File

@ -22,22 +22,27 @@ class AddCustomContactFields extends Migration
$table->string('custom_value2')->nullable();
});
Schema::table('payment_methods', function ($table) {
$table->unsignedInteger('account_gateway_token_id')->nullable()->change();
$table->dropForeign('payment_methods_account_gateway_token_id_foreign');
});
// This may fail if the foreign key doesn't exist
try {
Schema::table('payment_methods', function ($table) {
$table->unsignedInteger('account_gateway_token_id')->nullable()->change();
$table->dropForeign('payment_methods_account_gateway_token_id_foreign');
});
Schema::table('payment_methods', function ($table) {
$table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens')->onDelete('cascade');
});
Schema::table('payment_methods', function ($table) {
$table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens')->onDelete('cascade');
});
Schema::table('payments', function ($table) {
$table->dropForeign('payments_payment_method_id_foreign');
});
Schema::table('payments', function ($table) {
$table->dropForeign('payments_payment_method_id_foreign');
});
Schema::table('payments', function ($table) {
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
});
Schema::table('payments', function ($table) {
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
});
} catch (Exception $e) {
// do nothing
}
Schema::table('expenses', function($table) {
$table->unsignedInteger('payment_type_id')->nullable();

View File

@ -0,0 +1,71 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMultipleDatabaseSupport extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('lookup_companies', function ($table) {
$table->unsignedInteger('company_id')->index();
});
Schema::table('lookup_companies', function ($table) {
$table->unique(['db_server_id', 'company_id']);
});
Schema::table('lookup_accounts', function ($table) {
$table->string('account_key')->change()->unique();
});
Schema::table('lookup_users', function ($table) {
$table->string('email')->change()->nullable()->unique();
$table->string('confirmation_code')->nullable()->unique();
$table->unsignedInteger('user_id')->index();
});
Schema::table('lookup_users', function ($table) {
$table->unique(['lookup_account_id', 'user_id']);
});
Schema::table('lookup_contacts', function ($table) {
$table->string('contact_key')->change()->unique();
});
Schema::table('lookup_invitations', function ($table) {
$table->string('invitation_key')->change()->unique();
$table->string('message_id')->change()->nullable()->unique();
});
Schema::table('lookup_tokens', function ($table) {
$table->string('token')->change()->unique();
});
Schema::rename('lookup_tokens', 'lookup_account_tokens');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('lookup_companies', function ($table) {
$table->dropColumn('company_id');
});
Schema::table('lookup_users', function ($table) {
$table->dropColumn('confirmation_code');
});
Schema::rename('lookup_account_tokens', 'lookup_tokens');
}
}

View File

@ -73,6 +73,7 @@ class CurrenciesSeeder extends Seeder
['name' => 'Dominican Peso', 'code' => 'DOP', 'symbol' => 'RD$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['name' => 'Chilean Peso', 'code' => 'CLP', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
['name' => 'Icelandic Króna', 'code' => 'ISK', 'symbol' => 'kr', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ',', 'swap_currency_symbol' => true],
['name' => 'Papua New Guinean Kina', 'code' => 'PGK', 'symbol' => 'K', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
];
foreach ($currencies as $currency) {

View File

@ -29,5 +29,6 @@ class DatabaseSeeder extends Seeder
$this->call('LanguageSeeder');
$this->call('IndustrySeeder');
$this->call('FrequencySeeder');
$this->call('DbServerSeeder');
}
}

View File

@ -0,0 +1,26 @@
<?php
use App\Models\DbServer;
class DbServerSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$servers = [
['name' => 'db-ninja-1'],
['name' => 'db-ninja-2'],
];
foreach ($servers as $server) {
$record = DbServer::where('name', '=', $server['name'])->first();
if ($record) {
// do nothing
} else {
DbServer::create($server);
}
}
}
}

View File

@ -16,7 +16,7 @@ class LanguageSeeder extends Seeder
['name' => 'Italian', 'locale' => 'it'],
['name' => 'German', 'locale' => 'de'],
['name' => 'French', 'locale' => 'fr'],
['name' => 'Brazilian Portuguese', 'locale' => 'pt_BR'],
['name' => 'Portuguese - Brazilian', 'locale' => 'pt_BR'],
['name' => 'Dutch', 'locale' => 'nl'],
['name' => 'Spanish', 'locale' => 'es'],
['name' => 'Norwegian', 'locale' => 'nb_NO'],
@ -32,6 +32,8 @@ class LanguageSeeder extends Seeder
['name' => 'Albanian', 'locale' => 'sq'],
['name' => 'Greek', 'locale' => 'el'],
['name' => 'English - United Kingdom', 'locale' => 'en_UK'],
['name' => 'Portuguese - Portugal', 'locale' => 'pt_PT'],
['name' => 'Slovenian', 'locale' => 'sl'],
];
foreach ($languages as $language) {

View File

@ -25,7 +25,8 @@ class UpdateSeeder extends Seeder
$this->call('LanguageSeeder');
$this->call('IndustrySeeder');
$this->call('FrequencySeeder');
$this->call('DbServerSeeder');
Cache::flush();
}
}

View File

@ -85,6 +85,7 @@ class UserTableSeeder extends Seeder
'email' => env('TEST_EMAIL', TEST_USERNAME),
'is_primary' => true,
'send_invoice' => true,
'contact_key' => strtolower(str_random(RANDOM_KEY_LENGTH)),
]);
Product::create([

File diff suppressed because one or more lines are too long

View File

@ -59,7 +59,7 @@ author = u'Invoice Ninja'
# The short X.Y version.
version = u'3.3'
# The full version, including alpha/beta/rc tags.
release = u'3.3.0'
release = u'3.3.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -29,7 +29,7 @@ Step 1: Download the code
You can either download the zip file below or checkout the code from our GitHub repository. The zip includes all third party libraries whereas using GitHub requires you to use Composer to install the dependencies.
https://download.invoiceninja.com/ninja-v3.2.1.zip
https://download.invoiceninja.com/ninja-v3.3.0.zip
.. Note:: All Pro and Enterprise features from our hosted app are included in both the zip file and the GitHub repository. We offer a $20 per year white-label license to remove our branding.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -706,7 +706,7 @@ NINJA.accountDetails = function(invoice) {
for (var i=0; i < fields.length; i++) {
var field = fields[i];
var value = NINJA.renderClientOrAccountField(invoice, field);
var value = NINJA.renderField(invoice, field);
if (value) {
data.push(value);
}
@ -734,7 +734,7 @@ NINJA.accountAddress = function(invoice) {
for (var i=0; i < fields.length; i++) {
var field = fields[i];
var value = NINJA.renderClientOrAccountField(invoice, field);
var value = NINJA.renderField(invoice, field);
if (value) {
data.push(value);
}
@ -743,90 +743,6 @@ NINJA.accountAddress = function(invoice) {
return NINJA.prepareDataList(data, 'accountAddress');
}
NINJA.renderInvoiceField = function(invoice, field) {
var account = invoice.account;
var client = invoice.client;
if (field == 'invoice.invoice_number') {
if (invoice.is_statement) {
return false;
} else {
return [
{text: (invoice.is_quote ? invoiceLabels.quote_number : invoice.balance_amount < 0 ? invoiceLabels.credit_number : invoiceLabels.invoice_number), style: ['invoiceNumberLabel']},
{text: invoice.invoice_number, style: ['invoiceNumber']}
];
}
} else if (field == 'invoice.po_number') {
return [
{text: invoiceLabels.po_number},
{text: invoice.po_number}
];
} else if (field == 'invoice.invoice_date') {
return [
{text: (invoice.is_statement ? invoiceLabels.statement_date : invoice.is_quote ? invoiceLabels.quote_date : invoice.balance_amount < 0 ? invoiceLabels.credit_date : invoiceLabels.invoice_date)},
{text: invoice.invoice_date}
];
} else if (field == 'invoice.due_date') {
return [
{text: (invoice.is_quote ? invoiceLabels.valid_until : invoiceLabels.due_date)},
{text: invoice.is_recurring ? false : invoice.due_date}
];
} else if (field == 'invoice.custom_text_value1') {
if (invoice.custom_text_value1 && account.custom_invoice_text_label1) {
return [
{text: invoice.account.custom_invoice_text_label1},
{text: invoice.is_recurring ? processVariables(invoice.custom_text_value1) : invoice.custom_text_value1}
];
} else {
return false;
}
} else if (field == 'invoice.custom_text_value2') {
if (invoice.custom_text_value2 && account.custom_invoice_text_label2) {
return [
{text: invoice.account.custom_invoice_text_label2},
{text: invoice.is_recurring ? processVariables(invoice.custom_text_value2) : invoice.custom_text_value2}
];
} else {
return false;
}
} else if (field == 'invoice.balance_due') {
return [
{text: invoice.is_quote || invoice.balance_amount < 0 ? invoiceLabels.total : invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']},
{text: formatMoneyInvoice(invoice.total_amount, invoice), style: ['invoiceDetailBalanceDue']}
];
} else if (field == invoice.partial_due) {
if (NINJA.parseFloat(invoice.partial)) {
return [
{text: invoiceLabels.partial_due, style: ['invoiceDetailBalanceDueLabel']},
{text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['invoiceDetailBalanceDue']}
];
} else {
return false;
}
} else if (field == 'invoice.invoice_total') {
if (invoice.is_statement || invoice.is_quote || invoice.balance_amount < 0) {
return false;
} else {
return [
{text: invoiceLabels.invoice_total, style: ['invoiceTotalLabel']},
{text: formatMoneyInvoice(invoice.amount, invoice), style: ['invoiceTotal']}
];
}
} else if (field == 'invoice.outstanding') {
if (invoice.is_statement || invoice.is_quote) {
return false;
} else {
return [
{text: invoiceLabels.outstanding, style: ['invoiceOutstandingLabel']},
{text: formatMoneyInvoice(client.balance, invoice), style: ['outstanding']}
];
}
} else if (field == '.blank') {
return [{text: ' '}, {text: ' '}];
}
}
NINJA.invoiceDetails = function(invoice) {
var account = invoice.account;
@ -848,7 +764,7 @@ NINJA.invoiceDetails = function(invoice) {
for (var i=0; i < fields.length; i++) {
var field = fields[i];
var value = NINJA.renderInvoiceField(invoice, field);
var value = NINJA.renderField(invoice, field, true);
if (value) {
data.push(value);
}
@ -858,7 +774,7 @@ NINJA.invoiceDetails = function(invoice) {
}
NINJA.renderClientOrAccountField = function(invoice, field) {
NINJA.renderField = function(invoice, field, twoColumn) {
var client = invoice.client;
if (!client) {
return false;
@ -867,92 +783,173 @@ NINJA.renderClientOrAccountField = function(invoice, field) {
var contact = client.contacts[0];
var clientName = client.name || (contact.first_name || contact.last_name ? (contact.first_name + ' ' + contact.last_name) : contact.email);
var label = false;
var value = false;
if (field == 'client.client_name') {
return {text:clientName || ' ', style: ['clientName']};
value = clientName || ' ';
} else if (field == 'client.contact_name') {
return (contact.first_name || contact.last_name) ? {text:contact.first_name + ' ' + contact.last_name} : false;
value = (contact.first_name || contact.last_name) ? contact.first_name + ' ' + contact.last_name : false;
} else if (field == 'client.id_number') {
return {text:client.id_number};
value = client.id_number;
} else if (field == 'client.vat_number') {
return {text:client.vat_number};
value = client.vat_number;
} else if (field == 'client.address1') {
return {text:client.address1};
value = client.address1;
} else if (field == 'client.address2') {
return {text:client.address2};
value = client.address2;
} else if (field == 'client.city_state_postal') {
var cityStatePostal = '';
if (client.city || client.state || client.postal_code) {
var swap = client.country && client.country.swap_postal_code;
cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap);
}
return {text:cityStatePostal};
value = cityStatePostal;
} else if (field == 'client.postal_city_state') {
var postalCityState = '';
if (client.city || client.state || client.postal_code) {
postalCityState = formatAddress(client.city, client.state, client.postal_code, true);
}
return {text:postalCityState};
value = postalCityState;
} else if (field == 'client.country') {
return {text:client.country ? client.country.name : ''};
value = client.country ? client.country.name : '';
} else if (field == 'client.email') {
var clientEmail = contact.email == clientName ? '' : contact.email;
return {text:clientEmail};
value = contact.email == clientName ? '' : contact.email;
} else if (field == 'client.phone') {
return {text:contact.phone};
value = contact.phone;
} else if (field == 'client.custom_value1') {
return {text: account.custom_client_label1 && client.custom_value1 ? account.custom_client_label1 + ' ' + client.custom_value1 : false};
if (account.custom_client_label1 && client.custom_value1) {
label = account.custom_client_label1;
value = client.custom_value1;
}
} else if (field == 'client.custom_value2') {
return {text: account.custom_client_label2 && client.custom_value2 ? account.custom_client_label2 + ' ' + client.custom_value2 : false};
if (account.custom_client_label2 && client.custom_value2) {
label = account.custom_client_label2;
value = client.custom_value2;
}
} else if (field == 'contact.custom_value1') {
return {text:contact.custom_value1};
if (account.custom_contact_label1 && contact.custom_value1) {
label = account.custom_contact_label1;
value = contact.custom_value1;
}
} else if (field == 'contact.custom_value2') {
return {text:contact.custom_value2};
}
if (field == 'account.company_name') {
return {text:account.name, style: ['accountName']};
if (account.custom_contact_label2 && contact.custom_value2) {
label = account.custom_contact_label2;
value = contact.custom_value2;
}
} else if (field == 'account.company_name') {
value = account.name;
} else if (field == 'account.id_number') {
return {text:account.id_number, style: ['idNumber']};
value = account.id_number;
} else if (field == 'account.vat_number') {
return {text:account.vat_number, style: ['vatNumber']};
value = account.vat_number;
} else if (field == 'account.website') {
return {text:account.website, style: ['website']};
value = account.website;
} else if (field == 'account.email') {
return {text:account.work_email, style: ['email']};
value = account.work_email;
} else if (field == 'account.phone') {
return {text:account.work_phone, style: ['phone']};
value = account.work_phone;
} else if (field == 'account.address1') {
return {text: account.address1};
value = account.address1;
} else if (field == 'account.address2') {
return {text: account.address2};
value = account.address2;
} else if (field == 'account.city_state_postal') {
var cityStatePostal = '';
if (account.city || account.state || account.postal_code) {
var swap = account.country && account.country.swap_postal_code;
cityStatePostal = formatAddress(account.city, account.state, account.postal_code, swap);
}
return {text: cityStatePostal};
value = cityStatePostal;
} else if (field == 'account.postal_city_state') {
var postalCityState = '';
if (account.city || account.state || account.postal_code) {
postalCityState = formatAddress(account.city, account.state, account.postal_code, true);
}
return {text: postalCityState};
value = postalCityState;
} else if (field == 'account.country') {
return account.country ? {text: account.country.name} : false;
value = account.country ? account.country.name : false;
} else if (field == 'account.custom_value1') {
if (invoice.features.invoice_settings) {
return invoice.account.custom_label1 && invoice.account.custom_value1 ? {text: invoice.account.custom_label1 + ' ' + invoice.account.custom_value1} : false;
if (invoice.account.custom_label1 && invoice.account.custom_value1) {
label = invoice.account.custom_label1;
value = invoice.account.custom_value1;
}
} else if (field == 'account.custom_value2') {
if (invoice.features.invoice_settings) {
return invoice.account.custom_label2 && invoice.account.custom_value2 ? {text: invoice.account.custom_label2 + ' ' + invoice.account.custom_value2} : false;
if (invoice.account.custom_label2 && invoice.account.custom_value2) {
label = invoice.account.custom_label2;
value = invoice.account.custom_value2;
}
} else if (field == 'invoice.invoice_number') {
if (! invoice.is_statement) {
label = invoice.is_quote ? invoiceLabels.quote_number : invoice.balance_amount < 0 ? invoiceLabels.credit_number : invoiceLabels.invoice_number;
value = invoice.invoice_number;
}
} else if (field == 'invoice.po_number') {
value = invoice.po_number;
} else if (field == 'invoice.invoice_date') {
label = invoice.is_statement ? invoiceLabels.statement_date : invoice.is_quote ? invoiceLabels.quote_date : invoice.balance_amount < 0 ? invoiceLabels.credit_date : invoiceLabels.invoice_date;
value = invoice.invoice_date;
} else if (field == 'invoice.due_date') {
label = invoice.is_quote ? invoiceLabels.valid_until : invoiceLabels.due_date;
value = invoice.is_recurring ? false : invoice.due_date;
} else if (field == 'invoice.custom_text_value1') {
if (invoice.custom_text_value1 && account.custom_invoice_text_label1) {
label = invoice.account.custom_invoice_text_label1;
value = invoice.is_recurring ? processVariables(invoice.custom_text_value1) : invoice.custom_text_value1;
}
} else if (field == 'invoice.custom_text_value2') {
if (invoice.custom_text_value2 && account.custom_invoice_text_label2) {
label = invoice.account.custom_invoice_text_label2;
value = invoice.is_recurring ? processVariables(invoice.custom_text_value2) : invoice.custom_text_value2;
}
} else if (field == 'invoice.balance_due') {
label = invoice.is_quote || invoice.balance_amount < 0 ? invoiceLabels.total : invoiceLabels.balance_due;
value = formatMoneyInvoice(invoice.total_amount, invoice);
} else if (field == invoice.partial_due) {
if (NINJA.parseFloat(invoice.partial)) {
label = invoiceLabels.partial_due;
value = formatMoneyInvoice(invoice.balance_amount, invoice);
}
} else if (field == 'invoice.invoice_total') {
if (invoice.is_statement || invoice.is_quote || invoice.balance_amount < 0) {
// hide field
} else {
value = formatMoneyInvoice(invoice.amount, invoice);
}
} else if (field == 'invoice.outstanding') {
if (invoice.is_statement || invoice.is_quote) {
// hide field
} else {
value = formatMoneyInvoice(client.balance, invoice);
}
} else if (field == '.blank') {
return {text: ' '};
value = ' ';
}
return false;
if (value) {
var shortField = false;
var parts = field.split('.');
if (parts.length >= 2) {
var shortField = parts[1];
}
var style = snakeToCamel(shortField == 'company_name' ? 'account_name' : shortField); // backwards compatibility
if (twoColumn) {
// try to automatically determine the label
if (! label && label != 'Blank') {
if (invoiceLabels[shortField]) {
label = invoiceLabels[shortField];
}
}
return [{text: label, style: [style + 'Label']}, {text: value, style: [style]}];
} else {
// if the label is set prepend it to the value
if (label) {
value = label + ': ' + value;
}
return {text:value, style: [style]};
}
} else {
return false;
}
}
NINJA.clientDetails = function(invoice) {
@ -979,7 +976,7 @@ NINJA.clientDetails = function(invoice) {
for (var i=0; i < fields.length; i++) {
var field = fields[i];
var value = NINJA.renderClientOrAccountField(invoice, field);
var value = NINJA.renderField(invoice, field);
if (value) {
data.push(value);
}
@ -999,6 +996,9 @@ NINJA.getSecondaryColor = function(defaultColor) {
// remove blanks and add section style to all elements
NINJA.prepareDataList = function(oldData, section) {
var newData = [];
if (! oldData.length) {
oldData.push({text:' '});
}
for (var i=0; i<oldData.length; i++) {
var item = NINJA.processItem(oldData[i], section);
if (item.text || item.stack) {
@ -1028,6 +1028,9 @@ NINJA.prepareDataTable = function(oldData, section) {
NINJA.prepareDataPairs = function(oldData, section) {
var newData = [];
if (! oldData.length) {
oldData.push([{text:' '}, {text:' '}]);
}
for (var i=0; i<oldData.length; i++) {
var row = oldData[i];
var isBlank = false;

View File

@ -453,8 +453,8 @@ function comboboxHighlighter(item) {
result = result.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
return match ? '<strong>' + match + '</strong>' : query;
});
result = result.replace(new RegExp("\n", 'g'), '<br/>');
return result;
result = stripHtmlTags(result);
return result.replace(new RegExp("\n", 'g'), '<br/>');
}
function comboboxMatcher(item) {

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1714,6 +1714,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2494,6 +2495,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -507,7 +507,7 @@ $LANG = array(
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'FAQ',
'partial' => 'Partial/Deposit',
'partial' => 'Teilzahlung/Anzahlung',
'partial_remaining' => ':partial von :balance',
'more_fields' => 'Weitere Felder',
'less_fields' => 'Weniger Felder',
@ -1347,7 +1347,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'on_send_date' => 'On send date',
'on_due_date' => 'On due date',
'auto_bill_ach_date_help' => 'ACH will always auto bill on the due date.',
'auto_bill_ach_date_help' => 'Am Tag der Fälligkeit wird ACH immer automatisch verrechnet.',
'warn_change_auto_bill' => 'Due to NACHA rules, changes to this invoice may prevent ACH auto bill.',
'bank_account' => 'Bankkonto',
@ -1712,6 +1712,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'lang_Swedish' => 'Schwedisch',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'Englisch (UK)',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Wöchentlich',
@ -2058,7 +2059,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'changes_take_effect_immediately' => 'Anmerkung: Änderungen treten sofort in Kraft',
'wepay_account_description' => 'Zahlungsanbieter für Invoice Ninja',
'payment_error_code' => 'Bei der Bearbeitung Ihrer Zahlung [:code] gab es einen Fehler. Bitte versuchen Sie es später erneut.',
'standard_fees_apply' => 'Fee: 2.9%/1.2% [Credit Card/Bank Transfer] + $0.30 per successful charge.',
'standard_fees_apply' => 'Standardgebühren werden erhoben: 2,9% + 0,25€ pro erfolgreicher Belastung bei nicht-europäischen Kreditkarten und 1,4% + 0,25€ bei europäischen Kreditkarten.',
'limit_import_rows' => 'Daten müssen in Stapeln von :count Zeilen oder weniger importiert werden',
'error_title' => 'Etwas lief falsch',
'error_contact_text' => 'Wenn Sie Hilfe benötigen, schreiben Sie uns bitte eine E-Mail an :mailaddress',
@ -2294,7 +2295,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'renew_license' => 'Verlängere die Lizenz',
'iphone_app_message' => 'Berücksichtigen Sie unser :link herunterzuladen',
'iphone_app' => 'iPhone-App',
'android_app' => 'Android app',
'android_app' => 'Android App',
'logged_in' => 'Eingeloggt',
'switch_to_primary' => 'Wechseln Sie zu Ihrem Primärunternehmen (:name), um Ihren Plan zu managen.',
'inclusive' => 'Inklusive',
@ -2400,7 +2401,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'pro_plan_reports' => ':link to enable reports by joining the Pro Plan',
'mark_ready' => 'Als bereit markieren',
'limits' => 'Limits',
'limits' => 'Grenzwerte',
'fees' => 'Gebühren',
'fee' => 'Gebühr',
'set_limits_fees' => 'Set :gateway_type Limits/Fees',
@ -2413,11 +2414,11 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'location' => 'Ort',
'line_item' => 'Posten',
'surcharge' => 'Gebühr',
'location_first_surcharge' => 'Enabled - First surcharge',
'location_second_surcharge' => 'Enabled - Second surcharge',
'location_first_surcharge' => 'Aktiviert - Erste Mahngebühr',
'location_second_surcharge' => 'Aktiviert - Zweite Mahngebühr',
'location_line_item' => 'Aktiv - Posten',
'online_payment_surcharge' => 'Online Payment Surcharge',
'gateway_fees' => 'Gateway Fees',
'gateway_fees' => 'Zugangsgebühren',
'fees_disabled' => 'Gebühren sind deaktiviert',
'gateway_fees_help' => 'Automatically add an online payment surcharge/discount.',
'gateway' => 'Provider',
@ -2472,7 +2473,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'sample_commands' => 'Beispiele für Sprachbefehle',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'archived_products' => 'Archivierung erfolgreich :Produktzähler',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
@ -2481,17 +2482,18 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'mark_billable' => 'zur Verrechnung kennzeichnen',
'billed' => 'Verrechnet',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'custom_variables' => 'Benutzerdefinierte Variablen',
'invalid_file' => 'Ungültiger Dateityp',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1712,6 +1712,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'lang_Swedish' => 'Σουηδικά',
'lang_Albanian' => 'Αλβανικά',
'lang_English - United Kingdom' => 'Αγγλικά - Ηνωμένο Βασίλειο',
'lang_Slovenian' => 'Σλοβένικά',
// Frequencies
'freq_weekly' => 'Εβδομάδα',
@ -2491,7 +2492,8 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'invalid_file' => 'Μη έγκυρος τύπος αρχείου',
'add_documents_to_invoice' => 'Προσθέστε έγγραφα στο τιμολόγιο',
'mark_expense_paid' => 'Σήμανση ως εξοφλημένο',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'white_label_license_error' => 'Αδυναμία επικύρωσης της άδειας, ελέγξτε το αρχείο storage/logs/laravel-error.log για περισσότερες λεπτομέρειες.',
'plan_price' => 'Τιμή Πλάνου'
);

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1705,6 +1705,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2485,6 +2486,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1708,6 +1708,7 @@ Atención! tu password puede estar transmitida como texto plano, considera habil
'lang_Swedish' => 'Sueco',
'lang_Albanian' => 'Albanés',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2488,6 +2489,7 @@ Atención! tu password puede estar transmitida como texto plano, considera habil
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -26,7 +26,7 @@ $LANG = array(
'private_notes' => 'Note personnelle',
'invoice' => 'Facture',
'client' => 'Client',
'invoice_date' => 'Date de la facture',
'invoice_date' => 'Date de facture',
'due_date' => 'Date d\'échéance',
'invoice_number' => 'Numéro de facture',
'invoice_number_short' => 'Facture #',
@ -266,7 +266,7 @@ $LANG = array(
'working' => 'En cours',
'success' => 'Succès',
'success_message' => 'Inscription réussie. Veuillez cliquer sur le lien dans le courriel de confirmation de compte pour vérifier votre adresse courriel.',
'erase_data' => 'Your account is not registered, this will permanently erase your data.',
'erase_data' => 'Votre compte n\'est pas enregistré, cela va supprimer définitivement vos données',
'password' => 'Mot de passe',
'pro_plan_product' => 'Plan Pro',
'pro_plan_success' => 'Merci pour votre inscription ! Une fois la facture réglée, votre adhésion au Plan Pro commencera.',
@ -363,7 +363,7 @@ $LANG = array(
'confirm_email_quote' => 'Voulez-vous vraiment envoyer ce devis par courriel ?',
'confirm_recurring_email_invoice' => 'Les factures récurrentes sont activées, voulez-vous vraiment envoyer cette facture par courriel ?',
'cancel_account' => 'Supprimer le compte',
'cancel_account_message' => 'Warning: This will permanently delete your account, there is no undo.',
'cancel_account_message' => 'Attention : Cela va supprimer définitivement votre compte, il n\'y a pas d\'annulation possible',
'go_back' => 'Retour',
'data_visualizations' => 'Visualisation des données',
'sample_data' => 'Données fictives présentées',
@ -433,7 +433,7 @@ $LANG = array(
'reset_all' => 'Réinitialiser',
'approve' => 'Accepter',
'token_billing_type_id' => 'Jeton de paiement',
'token_billing_help' => 'Store payment details with WePay, Stripe or Braintree.',
'token_billing_help' => 'Stocke les détails de paiement avec WePay, Stripe ou Braintree ',
'token_billing_1' => 'Désactiver',
'token_billing_2' => 'Opt-in - Case à cocher affichée mais non sélectionnée',
'token_billing_3' => 'Opt-out - Case à cocher affichée et sélectionnée',
@ -657,8 +657,8 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'valid_until' => 'Valide jusqu\'au',
'reset_terms' => 'Ràz conditions',
'reset_footer' => 'Ràz pied de facture',
'invoice_sent' => ':count invoice sent',
'invoices_sent' => ':count invoices sent',
'invoice_sent' => ':count facture envoyée',
'invoices_sent' => ':count factures envoyées',
'status_draft' => 'Brouillon',
'status_sent' => 'Envoyée',
'status_viewed' => 'Vue',
@ -696,19 +696,19 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'disable' => 'Désactiver',
'invoice_quote_number' => 'Numéro des devis & factures',
'invoice_charges' => 'Majoration de facture',
'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.',
'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice',
'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.',
'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice',
'notification_invoice_bounced' => 'Impossible d\'envoyer la facture :invoice à :contact.',
'notification_invoice_bounced_subject' => 'Impossible d\'envoyer la facture :invoice',
'notification_quote_bounced' => 'Impossible d\'envoyer le devis :invoice à :contact.',
'notification_quote_bounced_subject' => 'Impossible d\'envoyer le devis :invoice',
'custom_invoice_link' => 'Personnaliser le lien de la facture',
'total_invoiced' => 'Total facturé',
'open_balance' => 'Open Balance',
'verify_email' => 'Please visit the link in the account confirmation email to verify your email address.',
'verify_email' => 'Cliquez sur le lien dans le mail de confirmation de compte pour valider votre adresse email.',
'basic_settings' => 'Paramètres généraux',
'pro' => 'Pro',
'gateways' => 'Passerelles de paiement',
'next_send_on' => 'Envoi suivant: :date',
'no_longer_running' => 'This invoice is not scheduled to run',
'no_longer_running' => 'La facturation n\'est pas planifiée pour être lancée',
'general_settings' => 'Réglages généraux',
'customize' => 'Personnaliser',
'oneclick_login_help' => 'Connectez un compte pour vous connecter sans votre mot de passe',
@ -723,10 +723,10 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'archived_tax_rate' => 'Taux de taxe archivé avec succès',
'default_tax_rate_id' => 'Taux de taxe par défaut',
'tax_rate' => 'Taux de taxe',
'recurring_hour' => 'Recurring Hour',
'recurring_hour' => 'Heure récurrente',
'pattern' => 'Pattern',
'pattern_help_title' => 'Pattern Help',
'pattern_help_1' => 'Create custom numbers by specifying a pattern',
'pattern_help_title' => 'Aide Pattern',
'pattern_help_1' => 'Créer un numéro personnalisé en précisant un modèle personnalisé',
'pattern_help_2' => 'Variables disponibles:',
'pattern_help_3' => 'Par exemple, :example sera converti en :value',
'see_options' => 'Voir les options',
@ -742,7 +742,7 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'activity_7' => ':contact a lu la facture :invoice',
'activity_8' => ':user a archivé la facture :invoice',
'activity_9' => ':user a supprimé la facture :invoice',
'activity_10' => ':contact entered payment :payment for :invoice',
'activity_10' => ':contact a saisi un paiement de :payment pour :invoice',
'activity_11' => ':user a mis à jour le moyen de paiement :payment',
'activity_12' => ':user a archivé le moyen de paiement :payment',
'activity_13' => ':user a supprimé le moyen de paiement :payment',
@ -811,7 +811,7 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'user' => 'Utilisateur',
'country' => 'Pays',
'include' => 'Inclure',
'logo_too_large' => 'Your logo is :size, for better PDF performance we suggest uploading an image file less than 200KB',
'logo_too_large' => 'Votre logo fait :size, pour de meilleures performance PDF nous vous suggérons d\'envoyer une image de moins de 200Ko',
'import_freshbooks' => 'Importer depuis FreshBooks',
'import_data' => 'Importer des données',
'source' => 'Source',
@ -826,17 +826,17 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'disabled' => 'Désactivé',
'show_archived_users' => 'Afficher les utilisateurs archivés',
'notes' => 'Notes',
'invoice_will_create' => 'client will be created',
'invoices_will_create' => 'invoices will be created',
'failed_to_import' => 'The following records failed to import, they either already exist or are missing required fields.',
'invoice_will_create' => 'Le client sera créé',
'invoices_will_create' => 'Les factures seront créées',
'failed_to_import' => 'L\'import des enregistrements suivants à échoué, ils sont soit existants soit il manque des champs requis.',
'publishable_key' => 'Clé publique',
'secret_key' => 'Clé secrète',
'missing_publishable_key' => 'Saisissez votre clé publique Stripe pour un processus de commande amélioré',
'email_design' => 'Email Design',
'due_by' => 'A échéanche du :date',
'enable_email_markup' => 'Enable Markup',
'enable_email_markup_help' => 'Make it easier for your clients to pay you by adding schema.org markup to your emails.',
'template_help_title' => 'Templates Help',
'enable_email_markup_help' => 'Rendez le règlement de vos clients plus facile en ajoutant les markup schema.org à vos emails.',
'template_help_title' => 'Aide Modèles',
'template_help_1' => 'Variable disponibles :',
'email_design_id' => 'Style de mail',
'email_design_help' => 'Rendez vos courriels plus professionnels avec des mises en page en HTML.',
@ -845,13 +845,13 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'dark' => 'Sombre',
'industry_help' => 'Utilisé dans le but de fournir des statistiques la taille et le secteur de l\'entreprise.',
'subdomain_help' => 'Définissez un sous-domaine ou affichez la facture sur votre propre site web.',
'website_help' => 'Display the invoice in an iFrame on your own website',
'invoice_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the invoice number.',
'quote_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the quote number.',
'custom_client_fields_helps' => 'Add a text input to the client create/edit page and display the label and value on the PDF.',
'custom_account_fields_helps' => 'Add a label and value to the company details section of the PDF.',
'custom_invoice_fields_helps' => 'Add a text input to the invoice create/edit page and display the label and value on the PDF.',
'custom_invoice_charges_helps' => 'Add a text input to the invoice create/edit page and include the charge in the invoice subtotals.',
'website_help' => 'Affiche la facture dans un iFrame sur votre site web',
'invoice_number_help' => 'Spécifier un préfixe ou utiliser un modèle personnalisé pour la création du numéro de facture.',
'quote_number_help' => 'Spécifier un préfixe ou utiliser un modèle personnalisé pour la création du numéro de devis.',
'custom_client_fields_helps' => 'Ajouter une entrée de texte à la page de création/édition du client et afficher le label et la valeur sur le PDF.',
'custom_account_fields_helps' => 'Ajouter un label et une valeur aux détails de la société dur le PDF.',
'custom_invoice_fields_helps' => 'Ajouter une entrée de texte lors de la création d\'une facture et afficher le label et la valeur sur le PDF.',
'custom_invoice_charges_helps' => 'Ajouter une entrée de texte à la page de création/édition de devis et inclure le supplément au sous-toal de la facture.',
'token_expired' => 'Validation jeton expiré. Veuillez réessayer.',
'invoice_link' => 'Lien vers la facture',
'button_confirmation_message' => 'Cliquez pour confirmer votre adresse e-mail.',
@ -864,9 +864,9 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'days_after' => 'jours après le',
'field_due_date' => 'date d\'échéance',
'field_invoice_date' => 'Date de la facture',
'schedule' => 'Schedule',
'schedule' => 'Planification',
'email_designs' => 'Email Designs',
'assigned_when_sent' => 'Assigned when sent',
'assigned_when_sent' => 'Affecté lors de l\'envoi',
'white_label_purchase_link' => 'Acheter une licence en marque blanche',
'expense' => 'Dépense',
'expenses' => 'Dépenses',
@ -879,15 +879,15 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'edit_vendor' => 'Éditer le fournisseur',
'archive_vendor' => 'Archiver ce fournisseur',
'delete_vendor' => 'Supprimer ce fournisseur',
'view_vendor' => 'View Vendor',
'deleted_expense' => 'Successfully deleted expense',
'archived_expense' => 'Successfully archived expense',
'deleted_expenses' => 'Successfully deleted expenses',
'archived_expenses' => 'Successfully archived expenses',
'view_vendor' => 'Voir Vendeur',
'deleted_expense' => 'Effacement de la dépense avec succès',
'archived_expense' => 'Archivage de la dépense avec succès',
'deleted_expenses' => 'Effacement des dépenses avec succès',
'archived_expenses' => 'Archivage des dépenses avec succès',
'expense_amount' => 'Montant de la dépense',
'expense_balance' => 'Expense Balance',
'expense_balance' => 'Balance de la dépnse',
'expense_date' => 'Date de la dépense',
'expense_should_be_invoiced' => 'Should this expense be invoiced?',
'expense_should_be_invoiced' => 'Cette dépense doit elle être facturée ?',
'public_notes' => 'Note publique',
'invoice_amount' => 'Montant de la facture',
'exchange_rate' => 'Taux de change',
@ -899,15 +899,15 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'archive_expense' => 'Archiver le dépense',
'delete_expense' => 'supprimer la dépense',
'view_expense_num' => 'Dépense # :expense',
'updated_expense' => 'Successfully updated expense',
'created_expense' => 'Successfully created expense',
'updated_expense' => 'Mise à jour de la dépense avec succès',
'created_expense' => 'Création de la dépense avec succès',
'enter_expense' => 'Nouvelle dépense',
'view' => 'Voir',
'restore_expense' => 'Restorer la dépense',
'invoice_expense' => 'Invoice Expense',
'expense_error_multiple_clients' => 'The expenses can\'t belong to different clients',
'expense_error_invoiced' => 'Expense has already been invoiced',
'convert_currency' => 'Convert currency',
'invoice_expense' => 'Facturer la dépense',
'expense_error_multiple_clients' => 'La dépense ne peut pas être attribuée à plusieurs clients',
'expense_error_invoiced' => 'La dépense à déjà été facturée',
'convert_currency' => 'Convertir la devise',
'num_days' => 'Nombre de jours',
'create_payment_term' => 'Créer une condition de paiement',
'edit_payment_terms' => 'Éditer condition de paiement',
@ -940,9 +940,9 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'thursday' => 'Jeudi',
'friday' => 'Vendredi',
'saturday' => 'Samedi',
'header_font_id' => 'Header Font',
'body_font_id' => 'Body Font',
'color_font_help' => 'Note: the primary color and fonts are also used in the client portal and custom email designs.',
'header_font_id' => 'Police de l\'en-tête',
'body_font_id' => 'Police du corps',
'color_font_help' => 'Note : la couleur et la police primaires sont également utilisées dans le portail client et le design des emails.',
'live_preview' => 'Aperçu',
'invalid_mail_config' => 'Impossible d\'envoyer le mail, veuillez vérifier que les paramètres de messagerie sont corrects.',
'invoice_message_button' => 'Pour visionner votre facture de :amount, cliquer sur le lien ci-dessous',
@ -961,12 +961,12 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'archived_bank_account' => 'Compte bancaire archivé',
'created_bank_account' => 'Compte bancaire créé',
'validate_bank_account' => 'Valider le compte bancaire',
'bank_password_help' => 'Note: your password is transmitted securely and never stored on our servers.',
'bank_password_help' => 'Note : votre mot de passe est transmis de manière sécurisée et jamais stocké sur nos serveurs',
'bank_password_warning' => 'Attention: votre mot de passe peut être transmis en clair, pensez à activer HTTPS.',
'username' => 'Nom d\'utilisateur',
'account_number' => 'N° de compte',
'account_name' => 'Nom du compte',
'bank_account_error' => 'Failed to retreive account details, please check your credentials.',
'bank_account_error' => 'Echec de récupération des détails du compte, merci de vérifier vos identifiants',
'status_approved' => 'Approuvé',
'quote_settings' => 'Paramètres des devis',
'auto_convert_quote' => 'Convertir automatiquement les devis',
@ -974,9 +974,9 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'validate' => 'Valider',
'info' => 'Info',
'imported_expenses' => 'Successfully created :count_vendors vendor(s) and :count_expenses expense(s)',
'iframe_url_help3' => 'Note: if you plan on accepting credit cards details we strongly recommend enabling HTTPS on your site.',
'expense_error_multiple_currencies' => 'The expenses can\'t have different currencies.',
'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.',
'iframe_url_help3' => 'Note : si vous prévoyez d\'accepter les cartes de crédit, nous vous recommandons d\'activer HTTPS sur votre site.',
'expense_error_multiple_currencies' => 'Les dépenses ne peuvent avoir plusieurs devises.',
'expense_error_mismatch_currencies' => 'La devise du clients n\'est pas la même que celle de la dépense.',
'trello_roadmap' => 'Trello Roadmap',
'header_footer' => 'En-tête/Pied de page',
'first_page' => 'première page',
@ -988,11 +988,11 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'enable_https' => 'Nous vous recommandons fortement d\'activer le HTTPS si vous acceptez les paiements en ligne.',
'quote_issued_to' => 'Devis à l\'attention de',
'show_currency_code' => 'Code de la devise',
'trial_message' => 'Your account will receive a free two week trial of our pro plan.',
'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.',
'trial_message' => 'Votre compte va être crédité d\'un essai gratuit de 2 semaines de notre Plan pro.',
'trial_footer' => 'Il reste :count jours à votre essai gratuit, :link pour mettre à jour maintenant',
'trial_footer_last_day' => 'Ceci est le dernier jour de votre essai gratuit, :link pour mettre à niveau maintenant.',
'trial_call_to_action' => 'Commencer l\'essai gratuit',
'trial_success' => 'Successfully enabled two week free pro plan trial',
'trial_success' => 'Crédit d\'un essai gratuit de 2 semaines de notre Plan pro avec succès',
'overdue' => 'Impayé',
@ -1003,7 +1003,7 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'more_designs_self_host_header' => 'Obtenez 6 modèles de factures additionnels pour seulement $:price',
'old_browser' => 'Merci d\'utiliser un <a href=":link" target="_blank">navigateur plus récent</a>',
'white_label_custom_css' => ':link for $:price to enable custom styling and help support our project.',
'bank_accounts_help' => 'Connect a bank account to automatically import expenses and create vendors. Supports American Express and <a href=":link" target="_blank">400+ US banks.</a>',
'bank_accounts_help' => 'Liez un compte bancaire pour importer automatiquement les dépenses et créer les fournisseurs. Supporte American Express et <a href=":link" target="_blank">400+ banques US.</a>',
'pro_plan_remove_logo' => ':link pour supprimer le logo Invoice Ninja en souscrivant au Plan Pro',
'pro_plan_remove_logo_link' => 'Cliquez ici',
@ -1028,29 +1028,29 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'list_credits' => 'List Credits',
'tax_name' => 'Nom de la taxe',
'report_settings' => 'Report Settings',
'search_hotkey' => 'shortcut is /',
'search_hotkey' => 'la racine est /',
'new_user' => 'Nouvel utilisateur',
'new_product' => 'Nouvel article',
'new_tax_rate' => 'Nouveau taux de taxe',
'invoiced_amount' => 'Montant de la facture',
'invoice_item_fields' => 'Invoice Item Fields',
'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.',
'recurring_invoice_number' => 'Recurring Number',
'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.',
'custom_invoice_item_fields_help' => 'Ajouter un champs lors de la création d\'un article de facture et afficher le label et la valeur sur le PDF.',
'recurring_invoice_number' => 'Numéro récurrent',
'recurring_invoice_number_prefix_help' => 'Spécifier un préfix à ajouter au numéro de la facture pour les factures récurrentes. La valeur par défaut est \'R\'.',
// Client Passwords
'enable_portal_password' => 'Protéger les factures avec un mot de passe',
'enable_portal_password_help' => 'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.',
'enable_portal_password_help' => 'Autoriser la création d\'un mot de passe pour chaque contact. Si un mot de passe est créé, le contact devra rentrer un mot de passe avant de voir les factures.',
'send_portal_password' => 'Générer un mot de passe automatiquement',
'send_portal_password_help' => 'If no password is set, one will be generated and sent with the first invoice.',
'send_portal_password_help' => 'Si aucun mot de passe n\'est créé, un sera généré et envoyé avec la première facture.',
'expired' => 'Expiré',
'invalid_card_number' => 'Le numéro de carte bancaire est invalide.',
'invalid_expiry' => 'La date d\'expiration est invalide.',
'invalid_cvv' => 'Le code de sécurité est incorrect.',
'cost' => 'Coût',
'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.',
'create_invoice_for_sample' => 'Note : créez votre première facture pour voir la prévisualisation ici.',
// User Permissions
'owner' => 'Propriétaire',
@ -1061,14 +1061,14 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'user_edit_all' => 'Modifier tous les clients, les factures, etc.',
'gateway_help_20' => ':link pour vous inscrire à Sage Pay.',
'gateway_help_21' => ':link pour vous inscrire à Sage Pay.',
'partial_due' => 'Partial Due',
'restore_vendor' => 'Restorer le fournisseur',
'restored_vendor' => 'Fournisseur restoré',
'restored_expense' => 'Dépense restorée',
'partial_due' => 'Solde partiel',
'restore_vendor' => 'Restaurer le fournisseur',
'restored_vendor' => 'Fournisseur restauré',
'restored_expense' => 'Dépense restaurée',
'permissions' => 'Permissions',
'create_all_help' => 'Autoriser l\'utilisateur à créer et éditer tous les enregistrements',
'view_all_help' => 'Allow user to view records they didn\'t create',
'edit_all_help' => 'Allow user to modify records they didn\'t create',
'view_all_help' => 'Autoriser l\'utilisateur à voir les enregistrement qu\'il n\'a pas créé',
'edit_all_help' => 'Autoriser l\'utilisateur à modifier les enregistrement qu\'il n\'a pas créé',
'view_payment' => 'Voir le paiement',
'january' => 'Janvier',
@ -1085,90 +1085,90 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'december' => 'Décembre',
// Documents
'documents_header' => 'Documents:',
'email_documents_header' => 'Documents:',
'documents_header' => 'Documents :',
'email_documents_header' => 'Documents :',
'email_documents_example_1' => 'Widgets Receipt.pdf',
'email_documents_example_2' => 'Final Deliverable.zip',
'invoice_documents' => 'Documents',
'expense_documents' => 'Documents attachés',
'invoice_embed_documents' => 'Embed Documents',
'invoice_embed_documents_help' => 'Include attached images in the invoice.',
'document_email_attachment' => 'Attach Documents',
'download_documents' => 'Download Documents (:size)',
'documents_from_expenses' => 'From Expenses:',
'dropzone_default_message' => 'Drop files or click to upload',
'dropzone_fallback_message' => 'Your browser does not support drag\'n\'drop file uploads.',
'invoice_embed_documents' => 'Documents intégrés',
'invoice_embed_documents_help' => 'Inclure l\'image attachée dans la facture.',
'document_email_attachment' => 'Attacher les Documents',
'download_documents' => 'Télécharger les Documents (:size)',
'documents_from_expenses' => 'Des dépenses :',
'dropzone_default_message' => 'Glisser le fichier ou cliquer pour envoyer',
'dropzone_fallback_message' => 'Votre navigateur ne supporte pas le drag\'n\'drop de fichier pour envoyer.',
'dropzone_fallback_text' => 'Please use the fallback form below to upload your files like in the olden days.',
'dropzone_file_too_big' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.',
'dropzone_invalid_file_type' => 'You can\'t upload files of this type.',
'dropzone_response_error' => 'Server responded with {{statusCode}} code.',
'dropzone_cancel_upload' => 'Cancel upload',
'dropzone_cancel_upload_confirmation' => 'Are you sure you want to cancel this upload?',
'dropzone_remove_file' => 'Remove file',
'dropzone_file_too_big' => 'Fichier trop gros ({{filesize}}Mo). Max filesize: {{maxFilesize}}Mo.',
'dropzone_invalid_file_type' => 'Vous ne pouvez pas envoyer de fichiers de ce type.',
'dropzone_response_error' => 'Le serveur a répondu avec le code {{statusCode}}.',
'dropzone_cancel_upload' => 'Annuler l\'envoi',
'dropzone_cancel_upload_confirmation' => 'Etes-vous sûr de vouloir annuler cet envoi ?',
'dropzone_remove_file' => 'Supprimer le fichier',
'documents' => 'Documents',
'document_date' => 'Document Date',
'document_size' => 'Size',
'document_date' => 'Date de Document',
'document_size' => 'Taille',
'enable_client_portal' => 'Tableau de bord',
'enable_client_portal_help' => 'Afficher / masquer le tableau de bord sur le portail client.',
'enable_client_portal_dashboard' => 'Dashboard',
'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.',
'enable_client_portal_dashboard_help' => 'Voir/cacher la page de dashboard dans le portail client.',
// Plans
'account_management' => 'Account Management',
'plan_status' => 'Plan Status',
'account_management' => 'Gestion des comptes',
'plan_status' => 'Status du Plan',
'plan_upgrade' => 'Upgrade',
'plan_change' => 'Change Plan',
'pending_change_to' => 'Changes To',
'plan_changes_to' => ':plan on :date',
'plan_term_changes_to' => ':plan (:term) on :date',
'cancel_plan_change' => 'Cancel Change',
'pending_change_to' => 'Changer vers',
'plan_changes_to' => ':plan au :date',
'plan_term_changes_to' => ':plan (:term) le :date',
'cancel_plan_change' => 'Annuler la modification',
'plan' => 'Plan',
'expires' => 'Expires',
'renews' => 'Renews',
'plan_expired' => ':plan Plan Expired',
'trial_expired' => ':plan Plan Trial Ended',
'never' => 'Never',
'plan_free' => 'Free',
'expires' => 'Expire',
'renews' => 'Renouvellement',
'plan_expired' => ':plan Plan Expiré',
'trial_expired' => ':plan Essai du Plan terminé',
'never' => 'Jamais',
'plan_free' => 'Gratuit',
'plan_pro' => 'Pro',
'plan_enterprise' => 'Enterprise',
'plan_white_label' => 'Self Hosted (White labeled)',
'plan_free_self_hosted' => 'Self Hosted (Free)',
'plan_trial' => 'Trial',
'plan_term' => 'Term',
'plan_term_monthly' => 'Monthly',
'plan_term_yearly' => 'Yearly',
'plan_term_month' => 'Month',
'plan_term_year' => 'Year',
'plan_price_monthly' => '$:price/Month',
'plan_price_yearly' => '$:price/Year',
'plan_enterprise' => 'Entreprise',
'plan_white_label' => 'Auto hébergé (Marque blanche)',
'plan_free_self_hosted' => 'Auto hébergé (Gratuit)',
'plan_trial' => 'Essai',
'plan_term' => 'Terme',
'plan_term_monthly' => 'Mensuel',
'plan_term_yearly' => 'Annuel',
'plan_term_month' => 'Mois',
'plan_term_year' => 'An',
'plan_price_monthly' => '$:price/Mois',
'plan_price_yearly' => '$:price/An',
'updated_plan' => 'Updated plan settings',
'plan_paid' => 'Term Started',
'plan_started' => 'Plan Started',
'plan_expires' => 'Plan Expires',
'plan_started' => 'Début du Plan',
'plan_expires' => 'Fin du Plan',
'white_label_button' => 'Marque blanche',
'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.',
'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.',
'enterprise_plan_product' => 'Plan Enterprise',
'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.',
'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.',
'plan_credit_product' => 'Credit',
'plan_credit_description' => 'Credit for unused time',
'plan_pending_monthly' => 'Will switch to monthly on :date',
'plan_refunded' => 'A refund has been issued.',
'pro_plan_year_description' => 'Engagement d\'un an dans le Plan Invoice Ninja Pro.',
'pro_plan_month_description' => 'Engagement d\'un mois dans le Plan Invoice Ninja Pro.',
'enterprise_plan_product' => 'Plan Entreprise',
'enterprise_plan_year_description' => 'Engagement d\'un an dans le Plan Invoice Ninja Entreprise.',
'enterprise_plan_month_description' => 'Engagement d\'un mois dans le Plan Invoice Ninja Entreprise.',
'plan_credit_product' => 'Crédit',
'plan_credit_description' => 'Crédit pour temps inutilisé',
'plan_pending_monthly' => 'Basculera en mensuel le :date',
'plan_refunded' => 'Un remboursement a été émis.',
'live_preview' => 'Aperçu',
'page_size' => 'Page Size',
'page_size' => 'Taille de Page',
'live_preview_disabled' => 'Live preview has been disabled to support selected font',
'invoice_number_padding' => 'Padding',
'preview' => 'Preview',
'invoice_number_padding' => 'Remplissage',
'preview' => 'Prévisualisation',
'list_vendors' => 'List Vendors',
'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.',
'add_users_not_supported' => 'Passez au Plan Enterprise pour ajouter des utilisateurs supplémentaires à votre compte.',
'enterprise_plan_features' => 'Le plan entreprise ajoute le support pour de multiples utilisateurs ainsi que l\'ajout de pièces jointes, :link pour voir la liste complète des fonctionnalités.',
'return_to_app' => 'Return to app',
'return_to_app' => 'Retourner à l\'app',
// Payment updates
@ -1705,7 +1705,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'lang_Spanish - Spain' => 'Espagnol - Espagne',
'lang_Swedish' => 'Suédois',
'lang_Albanian' => 'Albanais',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_English - United Kingdom' => 'Anglais - Royaume Uni',
'lang_Slovenian' => 'Slovène',
// Frequencies
'freq_weekly' => 'Hebdomadaire',
@ -2052,7 +2053,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'changes_take_effect_immediately' => 'Note: Les changements s\'appliquent immédiatement',
'wepay_account_description' => 'Passerelle de paiement pour Invoice Ninja',
'payment_error_code' => 'Il y a eu une erreur lors du traitement de paiement [:code]. Veuillez réessayer plus tard.',
'standard_fees_apply' => 'Fee: 2.9%/1.2% [Credit Card/Bank Transfer] + $0.30 per successful charge.',
'standard_fees_apply' => 'Taux : 2.9%/1.2% [Carte de Crédit/Transfert Bancaire] + $0.30 par paiement réussit.',
'limit_import_rows' => 'Les données nécessitent d\'être importées en lots de :count rangées ou moins.',
'error_title' => 'Il y a eu une erreur',
'error_contact_text' => 'Si vous avez besoin d\'aide, veuillez nous contacter à :mailaddress',
@ -2079,13 +2080,13 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'invalid_code' => 'Le code n\'est pas valide',
'security_code_email_subject' => 'Code de sécurité pour le Bot de Invoice Ninja',
'security_code_email_line1' => 'Ceci est votre code de sécurité pour le Bot de Invoice Ninja.',
'security_code_email_line2' => 'Note: il expirera dans 10 minutes.',
'security_code_email_line2' => 'Note : il expirera dans 10 minutes.',
'bot_help_message' => 'Je supporte actuellement:<br/>• Créer\mettre à jour\envoyer une facture<br/>• Lister les produits<br/>Par exemple:<br/><i>Facturer 2 billets à Simon, définir la date d\'échéance au prochain jeudi et l\'escompte à 10 %</i>',
'list_products' => 'Afficher les produits',
'include_item_taxes_inline' => 'Inclure une <b>ligne de taxes dans le total de la ligne',
'created_quotes' => ':count offre(s) ont été créée(s)',
'limited_gateways' => 'Note: Nous supportons une passerelle de carte de crédit par entreprise',
'limited_gateways' => 'Note : Nous supportons une passerelle de carte de crédit par entreprise',
'warning' => 'Avertissement',
'self-update' => 'Mettre à jour',
@ -2251,7 +2252,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'edit_credit' => 'Éditer le crédit',
'live_preview_help' => 'Afficher une prévisualisation actualisée sur la page d\'une facture.<br/>Désactiver cette fonctionnalité pour améliorer les performances pendant l\'édition des factures.',
'force_pdfjs_help' => 'Remplacer le lecteur PDF intégré dans :chrome_link et dans :firefox_link.<br/>Activez cette fonctionnalité si votre navigateur télécharge automatiquement les fichiers PDF.',
'force_pdfjs' => 'Prevent Download',
'force_pdfjs' => 'Empêcher le téléchargement',
'redirect_url' => 'URL de redirection',
'redirect_url_help' => 'Indiquez si vous le souhaitez une URL à laquelle vous vouler rediriger après l\'entrée d\'un paiement.',
'save_draft' => 'Sauvegarder le brouillon',
@ -2288,7 +2289,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'renew_license' => 'Renouveler la licence',
'iphone_app_message' => 'Avez-vous penser télécharger notre :link',
'iphone_app' => 'App iPhone',
'android_app' => 'Android app',
'android_app' => 'App Android',
'logged_in' => 'Connecté',
'switch_to_primary' => 'Veuillez basculer vers votre entreprise initiale (:name) pour gérer votre plan d\'abonnement.',
'inclusive' => 'Inclusif',
@ -2312,7 +2313,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'domain' => 'Domaine',
'domain_help' => 'Utilisé dans le portail du client et lors de l\'envoi de courriels',
'domain_help_website' => 'Utilisé lors de l\'envoi de courriels',
'preview' => 'Preview',
'preview' => 'Prévisualisation',
'import_invoices' => 'Importer des factures',
'new_report' => 'Nouveau rapport',
'edit_report' => 'Editer le rapport',
@ -2365,7 +2366,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
// New Client Portal styling
'invoice_from' => 'Factures de:',
'email_alias_message' => 'We require each company to have a unique email address.<br/>Consider using an alias. ie, email+label@example.com',
'email_alias_message' => 'Chaque société doit avoir une adresse email unique.<br/>Envisagez d\'utiliser un alias. ie, email+label@example.com',
'full_name' => 'Nom complet',
'month_year' => 'MOIS/ANNEE',
'valid_thru' => 'Valide\nthru',
@ -2398,94 +2399,95 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'fees' => 'Frais',
'fee' => 'Frais',
'set_limits_fees' => 'Définir les limites/frais de :gateway_type',
'fees_tax_help' => 'Enable line item taxes to set the fee tax rates.',
'fees_tax_help' => 'Activer les taxes par article pour définir les taux de taxes.',
'fees_sample' => 'Le frais pour une facture de :amount serait de :total.',
'discount_sample' => 'La réduction pour une facture de :amount serait de :total.',
'no_fees' => 'Aucun frais',
'gateway_fees_disclaimer' => 'Attention: tous les pays/passerelles de paiement n\'autorisent pas l\'ajout de frais. Consultez les conditions d\'utilisation de votre passerelle de paiement.',
'percent' => 'Pourcent',
'location' => 'Location',
'line_item' => 'Line Item',
'location' => 'Localisation',
'line_item' => 'Ligne d\'article',
'surcharge' => 'Majoration',
'location_first_surcharge' => 'Activé - Première majoration',
'location_second_surcharge' => 'Activé - Seconde majoration',
'location_line_item' => 'Enabled - Line item',
'location_line_item' => 'Activer - Ligne d\'article',
'online_payment_surcharge' => 'Majoration de paiement en ligne',
'gateway_fees' => 'Frais de la passerelle',
'fees_disabled' => 'Les frais sont désactivés',
'gateway_fees_help' => 'Automatically add an online payment surcharge/discount.',
'gateway_fees_help' => 'Ajoute automatiquement une surcharge/remise de paiement en ligne.',
'gateway' => 'Passerelle',
'gateway_fee_change_warning' => 'If there are unpaid invoices with fees they need to be updated manually.',
'gateway_fee_change_warning' => 'S\'il existe des factures impayées avec des frais, elles doivent être mises à jour manuellement.',
'fees_surcharge_help' => 'Personnaliser la majoration :link.',
'label_and_taxes' => 'label and taxes',
'label_and_taxes' => 'Libellé et taxes',
'billable' => 'Facturable',
'logo_warning_too_large' => 'Le fichier image est trop grand',
'logo_warning_fileinfo' => 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.',
'logo_warning_invalid' => 'There was a problem reading the image file, please try a different format.',
'logo_warning_fileinfo' => 'Attention : Pour supporter les gifs, l\'extension PHP fileinfo doit être activée.',
'logo_warning_invalid' => 'il y a eu un problème lors de la lecture du fichier image, merci d\'essayer un autre format.',
'error_refresh_page' => 'An error occurred, please refresh the page and try again.',
'data' => 'Data',
'imported_settings' => 'Successfully imported settings',
'error_refresh_page' => 'Un erreur est survenue, merci de rafraichir la page et essayer à nouveau',
'data' => 'Données',
'imported_settings' => 'Paramètres importés avec succès',
'lang_Greek' => 'Grec',
'reset_counter' => 'Reset Counter',
'next_reset' => 'Next Reset',
'reset_counter_help' => 'Automatically reset the invoice and quote counters.',
'auto_bill_failed' => 'Auto-billing for invoice :invoice_number failed',
'online_payment_discount' => 'Online Payment Discount',
'created_new_company' => 'Successfully created new company',
'fees_disabled_for_gateway' => 'Fees are disabled for this gateway.',
'logout_and_delete' => 'Log Out/Delete Account',
'tax_rate_type_help' => 'Inclusive taxes adjust the line item cost when selected.',
'invoice_footer_help' => 'Use $pageNumber and $pageCount to display the page information.',
'credit_note' => 'Credit Note',
'credit_issued_to' => 'Credit issued to',
'credit_to' => 'Credit to',
'your_credit' => 'Your Credit',
'credit_number' => 'Credit Number',
'create_credit_note' => 'Create Credit Note',
'reset_counter' => 'Remettre le compteur à zéro',
'next_reset' => 'Prochaine remise à zéro',
'reset_counter_help' => 'Remettre automatiquement à zéro les compteurs de facture et de devis.',
'auto_bill_failed' => 'La facturation automatique de :invoice_number a échouée.',
'online_payment_discount' => 'Remise de paiement en ligne',
'created_new_company' => 'La nouvelle entreprise a été créé',
'fees_disabled_for_gateway' => 'Les frais sont désactivés pour cette passerelle.',
'logout_and_delete' => 'Déconnexion/Suppression du compte',
'tax_rate_type_help' => 'Les taxes incluses ajustent le prix de la ligne d\'article lorsque sélectionnée.',
'invoice_footer_help' => 'Utilisez $pageNumber et $pageCount pour afficher les informations de la page.',
'credit_note' => 'Note de crédit',
'credit_issued_to' => 'Crédit accordé à',
'credit_to' => 'Crédit pour ',
'your_credit' => 'Votre crédit',
'credit_number' => 'Numéro de crédit',
'create_credit_note' => 'Créer une note de crédit',
'menu' => 'Menu',
'error_incorrect_gateway_ids' => 'Error: The gateways table has incorrect ids.',
'purge_data' => 'Purge Data',
'delete_data' => 'Delete Data',
'purge_data_help' => 'Permanently delete all data in the account, keeping the account and settings.',
'cancel_account_help' => 'Permanently delete the account along with all data and setting.',
'purge_successful' => 'Successfully purged account data',
'forbidden' => 'Forbidden',
'purge_data_message' => 'Warning: This will permanently erase your data, there is no undo.',
'contact_phone' => 'Contact Phone',
'contact_email' => 'Contact Email',
'reply_to_email' => 'Reply-To Email',
'reply_to_email_help' => 'Specify the reply-to address for client emails.',
'bcc_email_help' => 'Privately include this address with client emails.',
'import_complete' => 'Your import has successfully completed.',
'confirm_account_to_import' => 'Please confirm your account to import data.',
'import_started' => 'Your import has started, we\'ll send you an email once it completes.',
'listening' => 'Listening...',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'error_incorrect_gateway_ids' => 'Erreur : La table de passerelle a des ID incorrectes.',
'purge_data' => 'Purger les données',
'delete_data' => 'Effacer les données',
'purge_data_help' => 'Supprime toutes les données du compte, ne conserve que les comptes et les réglages',
'cancel_account_help' => 'Supprime le compte ainsi que les données, les comptes et les réglages.',
'purge_successful' => 'Les données du comptes ont été supprimées avec succès',
'forbidden' => 'Interdit',
'purge_data_message' => 'Attention : Cette action va supprimer vos données et est irréversible',
'contact_phone' => 'Téléphone du contact',
'contact_email' => 'Email du contact',
'reply_to_email' => 'Adresse de réponse',
'reply_to_email_help' => 'Spécifier une adresse courriel de réponse',
'bcc_email_help' => 'Inclut de façon privée cette adresse avec les courriels du client.',
'import_complete' => 'L\'importation s\'est réalisée avec succès.',
'confirm_account_to_import' => 'Confirmer votre compte pour l\'importation des données.',
'import_started' => 'L\'importation est en cours. Vous recevrez un courriel lorsqu\'elle sera terminée.',
'listening' => 'A l\'écoute...',
'microphone_help' => 'Dire "nouvelle facture pour [client]" ou "montre-moi les paiements archivés pour [client]"',
'voice_commands' => 'Commandes vocales',
'sample_commands' => 'Exemples de commandes',
'voice_commands_feedback' => 'Nous travaillons activement à l\'amélioration de cette fonctionnalité. Si vous souhaitez l\'ajout d\'une commande sépcifique, veuillez nous contacter par courriel à :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
'surcharge_label' => 'Surcharge Label',
'contact_fields' => 'Contact Fields',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'archived_products' => ':count produits archivés',
'recommend_on' => 'Nous recommandons d\'<b>activer</b> ce réglage.',
'recommend_off' => 'Nous recommandons de<b>désactiver</b> ce réglage.',
'notes_auto_billed' => 'Auto-facturation',
'surcharge_label' => 'Majoration',
'contact_fields' => 'Champs de contact',
'custom_contact_fields_help' => 'Ajoute un champ lors de la création d\'un contact et affiche l\'intitulé et sa valeur dans le fichier PDF.',
'datatable_info' => 'Affichage :start sur :end de :total entrées',
'credit_total' => 'Total Crédit',
'mark_billable' => 'Marquer facturable',
'billed' => 'Facturé',
'company_variables' => 'Variables de la compagnie',
'client_variables' => 'Variables du client',
'invoice_variables' => 'Variables de facture',
'navigation_variables' => 'Variables de navigation',
'custom_variables' => 'Variables personnalisées',
'invalid_file' => 'Type de fichier invalide',
'add_documents_to_invoice' => 'Ajouter un document à la facture',
'mark_expense_paid' => 'Marquer payé',
'white_label_license_error' => 'Validation de licence échouée, vérifier storage/logs/laravel-error.log pour plus de détails.',
'plan_price' => 'Prix du Plan'
);

View File

@ -1703,6 +1703,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'lang_Swedish' => 'Suédois',
'lang_Albanian' => 'Albanien',
'lang_English - United Kingdom' => 'Anglais - Royaume Uni',
'lang_Slovenian' => 'Slovénien',
// Frequencies
'freq_weekly' => 'Hebdomadaire',
@ -2483,7 +2484,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'invalid_file' => 'Type de fichier invalide',
'add_documents_to_invoice' => 'Ajouter un document à la facture',
'mark_expense_paid' => 'Marquer comme payé',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'white_label_license_error' => 'La validation de la licence n\'a pas fonctionné. Veuillez consulter storage/logs/laravel-error.log pour plus d\'information.',
'plan_price' => 'Tarification'
);

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1705,6 +1705,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2485,6 +2486,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1704,6 +1704,7 @@ Kom terug naar deze betalingsmethode pagina zodra u de bedragen heeft ontvangen
'lang_Swedish' => 'Zweeds',
'lang_Albanian' => 'Albanees',
'lang_English - United Kingdom' => 'Engels - Verenigd Koninkrijk',
'lang_Slovenian' => 'Sloveens',
// Frequencies
'freq_weekly' => 'Wekelijks',
@ -2473,17 +2474,18 @@ Kom terug naar deze betalingsmethode pagina zodra u de bedragen heeft ontvangen
'custom_contact_fields_help' => 'Voeg een veld toe bij het creeren van een contact en toon het label en de waarde op de PDF.',
'datatable_info' => ':start tot :end van :totaal items worden getoond',
'credit_total' => 'Totaal krediet',
'mark_billable' => 'Mark billable',
'mark_billable' => 'Markeer factureerbaar',
'billed' => 'Gefactureerd',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'company_variables' => 'Bedrijfsvariabelen',
'client_variables' => 'Klant variabelen',
'invoice_variables' => 'Factuur variabelen',
'navigation_variables' => 'Navigatie variabelen',
'custom_variables' => 'Aangepaste variabelen',
'invalid_file' => 'Ongeldig bestandstype',
'add_documents_to_invoice' => 'Voeg documenten toe aan factuur',
'mark_expense_paid' => 'Markeer betaald',
'white_label_license_error' => 'Validatie van de licentie mislukt, controleer storage/logs/laravel-error.log voor meer informatie.',
'plan_price' => 'Plan prijs'
);

View File

@ -1712,6 +1712,7 @@ Gdy przelewy zostaną zaksięgowane na Twoim koncie, wróć do tej strony i klik
'lang_Swedish' => 'Szwedzki',
'lang_Albanian' => 'Albański',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Co tydzień',
@ -2492,6 +2493,7 @@ Gdy przelewy zostaną zaksięgowane na Twoim koncie, wróć do tej strony i klik
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -1703,6 +1703,7 @@ Quando tiver os valores dos depósitos, volte a esta pagina e complete a verific
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2483,6 +2484,7 @@ Quando tiver os valores dos depósitos, volte a esta pagina e complete a verific
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price'
);

View File

@ -0,0 +1,20 @@
<?php
return array(
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Anterior',
'next' => 'Próximo &raquo;',
);

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
"password" => "A palavra-passe deve conter pelo menos seis caracteres e combinar com a confirmação.",
"user" => "Utilizador não encontrado.",
"token" => "Token inválido.",
"sent" => "Link para reposição da palavra-passe enviado por email!",
"reset" => "Palavra-passe reposta!",
];

View File

@ -0,0 +1,24 @@
<?php
return array(
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
"password" => "As palavras-passe devem conter no mínimo seis caracteres e devem ser iguais.",
"user" => "Não foi encontrado um utilizador com o endereço de email indicado.",
"token" => "Este token de redefinição de palavra-passe é inválido.",
"sent" => "Lembrete de palavra-passe enviado!",
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
<?php
return array(
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| such as the size rules. Feel free to tweak each of these messages.
|
*/
"accepted" => ":attribute deve ser aceite.",
"active_url" => ":attribute não é um URL válido.",
"after" => ":attribute deve ser uma data maior que :date.",
"alpha" => ":attribute deve conter apenas letras.",
"alpha_dash" => ":attribute pode conter apenas letras, número e hífens",
"alpha_num" => ":attribute pode conter apenas letras e números.",
"array" => ":attribute deve ser uma lista.",
"before" => ":attribute deve ser uma data anterior a :date.",
"between" => array(
"numeric" => ":attribute deve estar entre :min - :max.",
"file" => ":attribute deve estar entre :min - :max kilobytes.",
"string" => ":attribute deve estar entre :min - :max caracteres.",
"array" => ":attribute deve conter entre :min - :max itens.",
),
"confirmed" => ":attribute confirmação não corresponde.",
"date" => ":attribute não é uma data válida.",
"date_format" => ":attribute não satisfaz o formato :format.",
"different" => ":attribute e :other devem ser diferentes.",
"digits" => ":attribute deve conter :digits dígitos.",
"digits_between" => ":attribute deve conter entre :min e :max dígitos.",
"email" => ":attribute está em um formato inválido.",
"exists" => "A opção selecionada :attribute é inválida.",
"image" => ":attribute deve ser uma imagem.",
"in" => "A opção selecionada :attribute é inválida.",
"integer" => ":attribute deve ser um número inteiro.",
"ip" => ":attribute deve ser um endereço IP válido.",
"max" => array(
"numeric" => ":attribute não pode ser maior que :max.",
"file" => ":attribute não pode ser maior que :max kilobytes.",
"string" => ":attribute não pode ser maior que :max caracteres.",
"array" => ":attribute não pode conter mais que :max itens.",
),
"mimes" => ":attribute deve ser um arquivo do tipo: :values.",
"min" => array(
"numeric" => ":attribute não deve ser menor que :min.",
"file" => ":attribute deve ter no mínimo :min kilobytes.",
"string" => ":attribute deve conter no mínimo :min caracteres.",
"array" => ":attribute deve conter ao menos :min itens.",
),
"not_in" => "A opção selecionada :attribute é inválida.",
"numeric" => ":attribute deve ser um número.",
"regex" => ":attribute está em um formato inválido.",
"required" => ":attribute é um campo obrigatório.",
"required_if" => ":attribute é necessário quando :other é :value.",
"required_with" => ":attribute é obrigatório quando :values está presente.",
"required_without" => ":attribute é obrigatório quando :values não está presente.",
"same" => ":attribute e :other devem corresponder.",
"size" => array(
"numeric" => ":attribute deve ter :size.",
"file" => ":attribute deve ter :size kilobytes.",
"string" => ":attribute deve conter :size caracteres.",
"array" => ":attribute deve conter :size itens.",
),
"unique" => ":attribute já está sendo utilizado.",
"url" => ":attribute está num formato inválido.",
"positive" => ":attribute deve ser maior que zero.",
"has_credit" => "O cliente não possui crédito suficiente.",
"notmasked" => "Os valores são mascarados",
"less_than" => ':attribute deve ser menor que :value',
"has_counter" => 'O valor deve conter {$counter}',
"valid_contacts" => "Todos os contatos devem conter um email ou nome",
"valid_invoice_items" => "Esta fatura excedeu o número máximo de itens",
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => array(),
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => array(),
);

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

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