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 # prevent MySQL went away error
- mysql -u root -e 'SET @@GLOBAL.wait_timeout=28800;' - mysql -u root -e 'SET @@GLOBAL.wait_timeout=28800;'
# copy configuration files # copy configuration files
- cp .env.example .env - cp .env.travis .env
- cp tests/_bootstrap.php.default tests/_bootstrap.php - cp tests/_bootstrap.php.default tests/_bootstrap.php
- php artisan key:generate --no-interaction - 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 NINJA_DEV=true' .env
- sed -i '$a TRAVIS=true' .env
# create the database and user # 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 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 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 # migrate and seed the database
- php artisan migrate --no-interaction - php artisan migrate --database=db-ninja-0 --seed --no-interaction
- php artisan db:seed --no-interaction # default seed - 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 # Start webserver on ninja.dev:8000
- php artisan serve --host=ninja.dev --port=8000 & # '&' allows to run in background - php artisan serve --host=ninja.dev --port=8000 & # '&' allows to run in background
# Start PhantomJS # Start PhantomJS
@ -69,6 +69,7 @@ before_script:
- curl -L http://ninja.dev:8000/update - curl -L http://ninja.dev:8000/update
- php artisan ninja:create-test-data 4 true - php artisan ninja:create-test-data 4 true
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed - 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: script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php
@ -92,6 +93,10 @@ script:
after_script: after_script:
- php artisan ninja:check-data --no-interaction - php artisan ninja:check-data --no-interaction
- cat .env - 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 accounts;' ninja
- mysql -u root -e 'select * from users;' ninja - mysql -u root -e 'select * from users;' ninja
- mysql -u root -e 'select * from account_gateways;' 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 payments;' ninja
- mysql -u root -e 'select * from credits;' ninja - mysql -u root -e 'select * from credits;' ninja
- mysql -u root -e 'select * from expenses;' 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-error.log
- cat storage/logs/laravel-info.log - cat storage/logs/laravel-info.log
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr) - 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 App\Services\PaymentService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Carbon; use Carbon;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class ChargeRenewalInvoices. * Class ChargeRenewalInvoices.
@ -60,6 +61,10 @@ class ChargeRenewalInvoices extends Command
{ {
$this->info(date('Y-m-d').' ChargeRenewalInvoices...'); $this->info(date('Y-m-d').' ChargeRenewalInvoices...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$ninjaAccount = $this->accountRepo->getNinjaAccount(); $ninjaAccount = $this->accountRepo->getNinjaAccount();
$invoices = Invoice::whereAccountId($ninjaAccount->id) $invoices = Invoice::whereAccountId($ninjaAccount->id)
->whereDueDate(date('Y-m-d')) ->whereDueDate(date('Y-m-d'))
@ -120,6 +125,8 @@ class ChargeRenewalInvoices extends Command
*/ */
protected function getOptions() 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...'); $this->logMessage(date('Y-m-d') . ' Running CheckData...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
if (! $this->option('client_id')) { if (! $this->option('client_id')) {
$this->checkBlankInvoiceHistory(); $this->checkBlankInvoiceHistory();
$this->checkPaidToDate(); $this->checkPaidToDate();
@ -72,6 +76,9 @@ class CheckData extends Command
$this->checkBalances(); $this->checkBalances();
$this->checkContacts(); $this->checkContacts();
// TODO Enable once user_account companies have been merged
//$this->checkUserAccounts();
if (! $this->option('client_id')) { if (! $this->option('client_id')) {
$this->checkInvitations(); $this->checkInvitations();
$this->checkFailedJobs(); $this->checkFailedJobs();
@ -83,10 +90,10 @@ class CheckData extends Command
$this->info($this->log); $this->info($this->log);
if ($errorEmail) { if ($errorEmail) {
Mail::raw($this->log, function ($message) use ($errorEmail) { Mail::raw($this->log, function ($message) use ($errorEmail, $database) {
$message->to($errorEmail) $message->to($errorEmail)
->from(CONTACT_EMAIL) ->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) { } elseif (! $this->isValid) {
throw new Exception('Check data failed!!'); throw new Exception('Check data failed!!');
@ -98,8 +105,86 @@ class CheckData extends Command
$this->log .= $str . "\n"; $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() 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') $clients = DB::table('clients')
->leftJoin('contacts', function($join) { ->leftJoin('contacts', function($join) {
$join->on('contacts.client_id', '=', 'clients.id') $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') $clients = DB::table('clients')
->leftJoin('contacts', function($join) { ->leftJoin('contacts', function($join) {
$join->on('contacts.client_id', '=', 'clients.id') $join->on('contacts.client_id', '=', 'clients.id')
@ -351,7 +437,7 @@ class CheckData extends Command
$clients->where('clients.id', '=', $this->option('client_id')); $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') ->orderBy('accounts.company_id', 'DESC')
->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]); ->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]);
$this->logMessage(count($clients) . ' clients with incorrect balance/activities'); $this->logMessage(count($clients) . ' clients with incorrect balance/activities');
@ -543,6 +629,7 @@ class CheckData extends Command
return [ return [
['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null], ['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null],
['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null], ['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
]; ];
} }
} }

View File

@ -25,7 +25,7 @@ class CreateTestData extends Command
/** /**
* @var string * @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 * @var
@ -68,12 +68,17 @@ class CreateTestData extends Command
public function fire() public function fire()
{ {
if (Utils::isNinjaProd()) { if (Utils::isNinjaProd()) {
$this->info('Unable to run in production');
return false; return false;
} }
$this->info(date('Y-m-d').' Running CreateTestData...'); $this->info(date('Y-m-d').' Running CreateTestData...');
$this->count = $this->argument('count'); $this->count = $this->argument('count');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
if (filter_var($this->argument('create_account'), FILTER_VALIDATE_BOOLEAN)) { if (filter_var($this->argument('create_account'), FILTER_VALIDATE_BOOLEAN)) {
$this->info('Creating new account...'); $this->info('Creating new account...');
$account = $this->accountRepo->create( $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 DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class PruneData. * Class PruneData.
@ -24,30 +25,40 @@ class PruneData extends Command
{ {
$this->info(date('Y-m-d').' Running PruneData...'); $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, // 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 // hansn't logged in within the past 6 months and isn't linked to another account
$sql = 'select a.id $sql = 'select c.id
from (select id, last_login from accounts) a from companies c
left join users u on u.account_id = a.id and u.public_id = 0 left join accounts a on a.company_id = c.id
left join invoices i on i.account_id = a.id left join clients cl on cl.account_id = a.id
left join user_accounts ua1 on ua1.user_id1 = u.id left join tasks t on t.account_id = a.id
left join user_accounts ua2 on ua2.user_id2 = u.id left join expenses e on e.account_id = a.id
left join user_accounts ua3 on ua3.user_id3 = u.id left join users u on u.account_id = a.id and u.registered = 1
left join user_accounts ua4 on ua4.user_id4 = u.id where c.created_at < DATE_SUB(now(), INTERVAL 6 MONTH)
left join user_accounts ua5 on ua5.user_id5 = u.id and c.trial_started is null
where u.registered = 0 and c.plan is null
and a.last_login < DATE_SUB(now(), INTERVAL 6 MONTH) group by c.id
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) having count(cl.id) = 0
group by a.id and count(t.id) = 0
having count(i.id) = 0'; and count(e.id) = 0
and count(u.id) = 0';
$results = DB::select($sql); $results = DB::select($sql);
foreach ($results as $result) { foreach ($results as $result) {
$this->info("Deleting {$result->id}"); $this->info("Deleting company: {$result->id}");
DB::table('accounts') try {
DB::table('companies')
->where('id', '=', $result->id) ->where('id', '=', $result->id)
->delete(); ->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'); $this->info('Done');
@ -66,6 +77,8 @@ class PruneData extends Command
*/ */
protected function getOptions() 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 App\Models\Document;
use DateTime; use DateTime;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class RemoveOrphanedDocuments. * Class RemoveOrphanedDocuments.
@ -24,6 +25,10 @@ class RemoveOrphanedDocuments extends Command
{ {
$this->info(date('Y-m-d').' Running RemoveOrphanedDocuments...'); $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')]) $documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', [new DateTime('-1 hour')])
->get(); ->get();
@ -49,6 +54,8 @@ class RemoveOrphanedDocuments extends Command
*/ */
protected function getOptions() 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 Illuminate\Console\Command;
use Utils; use Utils;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class ResetData. * Class ResetData.
@ -28,8 +29,24 @@ class ResetData extends Command
return; return;
} }
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
Artisan::call('migrate:reset'); Artisan::call('migrate:reset');
Artisan::call('migrate'); Artisan::call('migrate');
Artisan::call('db:seed'); 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 App\Services\PaymentService;
use DateTime; use DateTime;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class SendRecurringInvoices. * Class SendRecurringInvoices.
@ -61,6 +62,10 @@ class SendRecurringInvoices extends Command
$this->info(date('Y-m-d H:i:s') . ' Running SendRecurringInvoices...'); $this->info(date('Y-m-d H:i:s') . ' Running SendRecurringInvoices...');
$today = new DateTime(); $today = new DateTime();
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// check for counter resets // check for counter resets
$accounts = Account::where('reset_counter_frequency_id', '>', 0) $accounts = Account::where('reset_counter_frequency_id', '>', 0)
->orderBy('id', 'asc') ->orderBy('id', 'asc')
@ -130,6 +135,8 @@ class SendRecurringInvoices extends Command
*/ */
protected function getOptions() 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\AccountRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class SendReminders. * Class SendReminders.
@ -58,6 +59,10 @@ class SendReminders extends Command
{ {
$this->info(date('Y-m-d') . ' Running SendReminders...'); $this->info(date('Y-m-d') . ' Running SendReminders...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$accounts = $this->accountRepo->findWithReminders(); $accounts = $this->accountRepo->findWithReminders();
$this->info(count($accounts) . ' accounts found'); $this->info(count($accounts) . ' accounts found');
@ -82,10 +87,10 @@ class SendReminders extends Command
$this->info('Done'); $this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) { if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail) { \Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail) $message->to($errorEmail)
->from(CONTACT_EMAIL) ->from(CONTACT_EMAIL)
->subject('SendReminders: Finished successfully'); ->subject("SendReminders [{$database}]: Finished successfully");
}); });
} }
} }
@ -103,6 +108,8 @@ class SendReminders extends Command
*/ */
protected function getOptions() 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\AccountRepository;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Utils; use Utils;
use Symfony\Component\Console\Input\InputOption;
/** /**
* Class SendRenewalInvoices. * Class SendRenewalInvoices.
@ -51,6 +52,10 @@ class SendRenewalInvoices extends Command
{ {
$this->info(date('Y-m-d').' Running SendRenewalInvoices...'); $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 // get all accounts with plans expiring in 10 days
$companies = Company::whereRaw("datediff(plan_expires, curdate()) = 10 and (plan = 'pro' or plan = 'enterprise')") $companies = Company::whereRaw("datediff(plan_expires, curdate()) = 10 and (plan = 'pro' or plan = 'enterprise')")
->orderBy('id') ->orderBy('id')
@ -102,10 +107,10 @@ class SendRenewalInvoices extends Command
$this->info('Done'); $this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) { if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail) { \Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail) $message->to($errorEmail)
->from(CONTACT_EMAIL) ->from(CONTACT_EMAIL)
->subject('SendRenewalInvoices: Finished successfully'); ->subject("SendRenewalInvoices [{$database}]: Finished successfully");
}); });
} }
} }
@ -123,6 +128,8 @@ class SendRenewalInvoices extends Command
*/ */
protected function getOptions() 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\SendRenewalInvoices',
'App\Console\Commands\ChargeRenewalInvoices', 'App\Console\Commands\ChargeRenewalInvoices',
'App\Console\Commands\SendReminders', 'App\Console\Commands\SendReminders',
'App\Console\Commands\GenerateResources',
'App\Console\Commands\TestOFX', 'App\Console\Commands\TestOFX',
'App\Console\Commands\MakeModule', 'App\Console\Commands\MakeModule',
'App\Console\Commands\MakeClass', '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_REFERRAL_CODE', 'referralCode');
define('SESSION_LEFT_SIDEBAR', 'showLeftSidebar'); define('SESSION_LEFT_SIDEBAR', 'showLeftSidebar');
define('SESSION_RIGHT_SIDEBAR', 'showRightSidebar'); define('SESSION_RIGHT_SIDEBAR', 'showRightSidebar');
define('SESSION_DB_SERVER', 'dbServer');
define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE');
define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME');
@ -290,8 +291,8 @@ if (! defined('APP_NAME')) {
define('EVENT_DELETE_INVOICE', 9); define('EVENT_DELETE_INVOICE', 9);
define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN');
define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); define('NINJA_ACCOUNT_KEY', env('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'));
define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); define('NINJA_ACCOUNT_EMAIL', env('NINJA_ACCOUNT_EMAIL', 'contact@invoiceninja.com'));
define('NINJA_LICENSE_ACCOUNT_KEY', 'AsFmBAeLXF0IKf7tmi0eiyZfmWW9hxMT'); define('NINJA_LICENSE_ACCOUNT_KEY', 'AsFmBAeLXF0IKf7tmi0eiyZfmWW9hxMT');
define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); define('NINJA_GATEWAY_ID', GATEWAY_STRIPE);
define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG'); 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_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest')); define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01'); define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '3.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_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@ -335,6 +336,10 @@ if (! defined('APP_NAME')) {
define('BLANK_IMAGE', 'data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); 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', 4);
define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design
define('PRODUCT_ONE_CLICK_INSTALL', 1); define('PRODUCT_ONE_CLICK_INSTALL', 1);

View File

@ -39,6 +39,10 @@ class AccountApiController extends BaseAPIController
public function register(RegisterRequest $request) 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); $account = $this->accountRepo->create($request->first_name, $request->last_name, $request->email, $request->password);
$user = $account->users()->first(); $user = $account->users()->first();
@ -192,7 +196,7 @@ class AccountApiController extends BaseAPIController
$oAuth = new OAuth(); $oAuth = new OAuth();
$user = $oAuth->getProvider($provider)->getTokenResponse($token); $user = $oAuth->getProvider($provider)->getTokenResponse($token);
if($user) { if ($user) {
Auth::login($user); Auth::login($user);
return $this->processLogin($request); return $this->processLogin($request);
} }

View File

@ -97,25 +97,6 @@ class AccountController extends BaseController
$this->paymentService = $paymentService; $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 * @return \Illuminate\Http\RedirectResponse
*/ */
@ -975,7 +956,22 @@ class AccountController extends BaseController
$account->page_size = Input::get('page_size'); $account->page_size = Input::get('page_size');
$labels = []; $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}"); $labels[$field] = Input::get("labels_{$field}");
} }
$account->invoice_labels = json_encode($labels); $account->invoice_labels = json_encode($labels);
@ -1104,6 +1100,14 @@ class AccountController extends BaseController
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = Auth::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']; $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);
@ -1114,8 +1118,8 @@ class AccountController extends BaseController
} else { } else {
$user->first_name = trim(Input::get('first_name')); $user->first_name = trim(Input::get('first_name'));
$user->last_name = trim(Input::get('last_name')); $user->last_name = trim(Input::get('last_name'));
$user->username = trim(Input::get('email')); $user->username = $email;
$user->email = trim(strtolower(Input::get('email'))); $user->email = $email;
$user->phone = trim(Input::get('phone')); $user->phone = trim(Input::get('phone'));
if (! Auth::user()->is_admin) { if (! Auth::user()->is_admin) {
@ -1212,8 +1216,15 @@ class AccountController extends BaseController
*/ */
public function checkEmail() public function checkEmail()
{ {
$email = User::withTrashed()->where('email', '=', Input::get('email')) $email = trim(strtolower(Input::get('email')));
->where('id', '<>', Auth::user()->registered ? 0 : Auth::user()->id) $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(); ->first();
if ($email) { if ($email) {
@ -1253,6 +1264,10 @@ class AccountController extends BaseController
$email = trim(strtolower(Input::get('new_email'))); $email = trim(strtolower(Input::get('new_email')));
$password = trim(Input::get('new_password')); $password = trim(Input::get('new_password'));
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return '';
}
if ($user->registered) { if ($user->registered) {
$newAccount = $this->accountRepo->create($firstName, $lastName, $email, $password, $account->company); $newAccount = $this->accountRepo->create($firstName, $lastName, $email, $password, $account->company);
$newUser = $newAccount->users()->first(); $newUser = $newAccount->users()->first();

View File

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

View File

@ -51,8 +51,8 @@ class PasswordController extends Controller
$data = [ $data = [
'clientauth' => true, 'clientauth' => true,
]; ];
$contactKey = session('contact_key');
if (!$contactKey) { if (! session('contact_key')) {
return \Redirect::to('/client/sessionexpired'); return \Redirect::to('/client/sessionexpired');
} }
@ -104,7 +104,7 @@ class PasswordController extends Controller
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function showResetForm(Request $request, $key = null, $token = null) public function showResetForm(Request $request, $token = null)
{ {
if (is_null($token)) { if (is_null($token)) {
return $this->getEmail(); return $this->getEmail();
@ -115,24 +115,9 @@ class PasswordController extends Controller
'clientauth' => true, 'clientauth' => true,
); );
if ($key) { if (! session('contact_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'); return \Redirect::to('/client/sessionexpired');
} }
}
return view('clientauth.reset')->with($data); return view('clientauth.reset')->with($data);
} }
@ -148,9 +133,9 @@ class PasswordController extends Controller
* *
* @return \Illuminate\Http\Response * @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\Models\License;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use App\Libraries\CurlUtils;
use Auth; use Auth;
use Cache; use Cache;
use CreditCard; use CreditCard;
@ -290,4 +291,31 @@ class NinjaController extends BaseController
return RESULT_SUCCESS; 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'; $rules['email'] = 'required|email|unique:users,email,'.$user->id.',id';
} else { } else {
$user = false;
$rules['email'] = 'required|email|unique:users'; $rules['email'] = 'required|email|unique:users';
} }
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) { 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) { if ($userPublicId) {

View File

@ -29,6 +29,7 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'lookup' => 'App\Http\Middleware\DatabaseLookup',
'auth' => 'App\Http\Middleware\Authenticate', 'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth', 'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'permissions.required' => 'App\Http\Middleware\PermissionsRequired', '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', 'hook/email_bounced',
'reseller_stats', 'reseller_stats',
'payment_hook/*', 'payment_hook/*',
'buy_now/*', 'buy_now*',
'hook/bot/*', 'hook/bot/*',
]; ];

View File

@ -38,7 +38,7 @@ class SaveClientPortalSettings extends Request
$input = $this->all(); $input = $this->all();
if ($this->client_view_css && Utils::isNinja()) { 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()) { if (Utils::isNinja()) {

View File

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

View File

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

View File

@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Jobs\Job; use App\Jobs\Job;
use App\Models\Document; use App\Models\Document;
use App\Models\LookupAccount;
use Auth; use Auth;
use DB; use DB;
use Exception; use Exception;
@ -55,7 +56,18 @@ class PurgeAccountData extends Job
$account->invoice_number_counter = 1; $account->invoice_number_counter = 1;
$account->quote_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(); $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; protected $userId;
/**
* @var string
*/
protected $server;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -52,6 +57,7 @@ class SendInvoiceEmail extends Job implements ShouldQueue
$this->userId = $userId; $this->userId = $userId;
$this->reminder = $reminder; $this->reminder = $reminder;
$this->template = $template; $this->template = $template;
$this->server = config('database.default');
} }
/** /**

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ use HTMLPurifier_Config;
class HTMLUtils class HTMLUtils
{ {
public static function sanitize($css) public static function sanitizeCSS($css)
{ {
// Allow referencing the body element // Allow referencing the body element
$css = preg_replace('/(?<![a-z0-9\-\_\#\.])body(?![a-z0-9\-\_])/i', '.body', $css); $css = preg_replace('/(?<![a-z0-9\-\_\#\.])body(?![a-z0-9\-\_])/i', '.body', $css);
@ -36,4 +36,12 @@ class HTMLUtils
// Get the first style block // Get the first style block
return count($css) ? $css[0] : ''; 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) public static function getNewsFeedResponse($userType = false)
{ {
if (! $userType) { if (! $userType) {
@ -398,6 +393,7 @@ class Utils
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'ip' => Request::getClientIp(), 'ip' => Request::getClientIp(),
'count' => Session::get('error_count', 0), 'count' => Session::get('error_count', 0),
'is_console' => App::runningInConsole() ? 'yes' : 'no',
]; ];
if ($info) { if ($info) {

View File

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

View File

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

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\LookupAccountToken;
/** /**
* Class AccountToken. * Class AccountToken.
@ -39,3 +40,17 @@ class AccountToken extends EntityModel
return $this->belongsTo('App\Models\User')->withTrashed(); 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'; protected $presenter = 'App\Ninja\Presenters\CompanyPresenter';
/**
* @var array
*/
protected $fillable = [
'plan',
'plan_term',
'plan_price',
'plan_paid',
'plan_started',
'plan_expires',
];
/** /**
* @var array * @var array
*/ */
@ -174,3 +186,17 @@ class Company extends Eloquent
return false; 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\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\LookupContact;
/** /**
* Class Contact. * Class Contact.
@ -165,3 +166,17 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
return "{$url}/client/dashboard/{$this->contact_key}"; 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 Carbon;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Utils; use Utils;
use App\Models\LookupInvitation;
/** /**
* Class Invitation. * 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)); 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; $dueDay = $lastDayOfMonth;
} }
if ($currentDay >= $dueDay) { if ($currentDay > $dueDay) {
// Wait until next month // Wait until next month
// We don't need to handle the December->January wraparaound, since PHP handles month 13 as January of next year // We don't need to handle the December->January wraparaound, since PHP handles month 13 as January of next year
$dueMonth++; $dueMonth++;
@ -1511,6 +1511,11 @@ class Invoice extends EntityModel implements BalanceAffecting
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->get(); ->get();
} }
public function getDueDateLabel()
{
return $this->isQuote() ? 'valid_until' : 'due_date';
}
} }
Invoice::creating(function ($invoice) { 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); $prefix = $this->getNumberPrefix($entityType);
$counterOffset = 0; $counterOffset = 0;
$check = false; $check = false;
$lastNumber = false;
if ($entityType == ENTITY_CLIENT && ! $this->clientNumbersEnabled()) { if ($entityType == ENTITY_CLIENT && ! $this->clientNumbersEnabled()) {
return ''; return '';
@ -50,6 +51,13 @@ trait GeneratesNumbers
} }
$counter++; $counter++;
$counterOffset++; $counterOffset++;
// prevent getting stuck in a loop
if ($number == $lastNumber) {
return '';
}
$lastNumber = $number;
} while ($check); } while ($check);
// update the counter to be caught up // update the counter to be caught up
@ -194,15 +202,17 @@ trait GeneratesNumbers
'{$clientCounter}', '{$clientCounter}',
]; ];
$client = $invoice->client;
$clientCounter = ($invoice->isQuote() && ! $this->share_counter) ? $client->quote_number_counter : $client->invoice_number_counter;
$replace = [ $replace = [
$invoice->client->custom_value1, $client->custom_value1,
$invoice->client->custom_value2, $client->custom_value2,
$invoice->client->id_number, $client->id_number,
$invoice->client->custom_value1, // backwards compatibility $client->custom_value1, // backwards compatibility
$invoice->client->custom_value2, $client->custom_value2,
$invoice->client->id_number, $client->id_number,
str_pad($invoice->client->invoice_number_counter, $this->invoice_number_padding, '0', STR_PAD_LEFT), str_pad($clientCounter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
str_pad($invoice->client->quote_number_counter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
]; ];
return str_replace($search, $replace, $pattern); return str_replace($search, $replace, $pattern);

View File

@ -162,6 +162,28 @@ trait PresentsInvoice
return $fields; 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 * @return array
*/ */
@ -239,6 +261,8 @@ trait PresentsInvoice
'work_phone', 'work_phone',
'invoice_total', 'invoice_total',
'outstanding', 'outstanding',
'invoice_due_date',
'quote_due_date',
]; ];
foreach ($fields as $field) { foreach ($fields as $field) {

View File

@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
use Session; use Session;
use App\Models\LookupUser;
/** /**
* Class User. * 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::updating(function ($user) {
User::onUpdatingUser($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::updated(function ($user) {
User::onUpdatedUser($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(); $emailSettings = new AccountEmailSettings();
$account->account_email_settings()->save($emailSettings); $account->account_email_settings()->save($emailSettings);
$random = strtolower(str_random(RANDOM_KEY_LENGTH));
$user = new User(); $user = new User();
$user->registered = true; $user->registered = true;
$user->confirmed = true; $user->confirmed = true;
$user->email = 'contact@invoiceninja.com'; $user->email = NINJA_ACCOUNT_EMAIL;
$user->password = $random; $user->username = NINJA_ACCOUNT_EMAIL;
$user->username = $random; $user->password = strtolower(str_random(RANDOM_KEY_LENGTH));
$user->first_name = 'Invoice'; $user->first_name = 'Invoice';
$user->last_name = 'Ninja'; $user->last_name = 'Ninja';
$user->notify_sent = true; $user->notify_sent = true;
@ -393,7 +392,6 @@ class AccountRepository
$client = Client::whereAccountId($ninjaAccount->id) $client = Client::whereAccountId($ninjaAccount->id)
->wherePublicId($account->id) ->wherePublicId($account->id)
->first(); ->first();
$clientExists = $client ? true : false;
if (! $client) { if (! $client) {
$client = new Client(); $client = new Client();
@ -401,30 +399,21 @@ class AccountRepository
$client->account_id = $ninjaAccount->id; $client->account_id = $ninjaAccount->id;
$client->user_id = $ninjaUser->id; $client->user_id = $ninjaUser->id;
$client->currency_id = 1; $client->currency_id = 1;
}
foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) { foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) {
$client->$field = $account->$field; $client->$field = $account->$field;
} }
$client->save(); $client->save();
if ($clientExists) {
$contact = $client->getPrimaryContact();
} else {
$contact = new Contact(); $contact = new Contact();
$contact->user_id = $ninjaUser->id; $contact->user_id = $ninjaUser->id;
$contact->account_id = $ninjaAccount->id; $contact->account_id = $ninjaAccount->id;
$contact->public_id = $account->id; $contact->public_id = $account->id;
$contact->contact_key = strtolower(str_random(RANDOM_KEY_LENGTH));
$contact->is_primary = true; $contact->is_primary = true;
}
$user = $account->getPrimaryUser();
foreach (['first_name', 'last_name', 'email', 'phone'] as $field) { foreach (['first_name', 'last_name', 'email', 'phone'] as $field) {
$contact->$field = $user->$field; $contact->$field = $account->users()->first()->$field;
} }
$client->contacts()->save($contact); $client->contacts()->save($contact);
}
return $client; return $client;
} }
@ -450,12 +439,16 @@ class AccountRepository
if (! $user->registered) { if (! $user->registered) {
$rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id']; $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
$validator = Validator::make(['email' => $email], $rules); $validator = Validator::make(['email' => $email], $rules);
if ($validator->fails()) { if ($validator->fails()) {
$messages = $validator->messages(); $messages = $validator->messages();
return $messages->first('email'); return $messages->first('email');
} }
if (! \App\Models\LookupUser::validateEmail($email, $user)) {
return trans('texts.email_taken');
}
$user->email = $email; $user->email = $email;
$user->first_name = $firstName; $user->first_name = $firstName;
$user->last_name = $lastName; $user->last_name = $lastName;

View File

@ -15,12 +15,7 @@ class NinjaRepository
} }
$company = $account->company; $company = $account->company;
$company->plan = ! empty($data['plan']) && $data['plan'] != PLAN_FREE ? $data['plan'] : null; $company->fill($data);
$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->save(); $company->save();
} }
} }

View File

@ -134,6 +134,10 @@ class TaskRepository extends BaseRepository
$timeLog = []; $timeLog = [];
} }
if(isset($data['client_id'])) {
$task->client_id = Client::getPrivateId($data['client_id']);
}
array_multisort($timeLog); array_multisort($timeLog);
if (isset($data['action'])) { if (isset($data['action'])) {
@ -146,6 +150,8 @@ class TaskRepository extends BaseRepository
} elseif ($data['action'] == 'stop' && $task->is_running) { } elseif ($data['action'] == 'stop' && $task->is_running) {
$timeLog[count($timeLog) - 1][1] = time(); $timeLog[count($timeLog) - 1][1] = time();
$task->is_running = false; $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 URL;
use Utils; use Utils;
use Validator; use Validator;
use Queue;
use Illuminate\Queue\Events\JobProcessing;
/** /**
* Class AppServiceProvider. * Class AppServiceProvider.
@ -21,6 +23,15 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot() 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) { Form::macro('image_data', function ($image, $contents = false) {
if (! $contents) { if (! $contents) {
$contents = file_get_contents($image); $contents = file_get_contents($image);

View File

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

View File

@ -46,12 +46,7 @@ return [
'connections' => [ 'connections' => [
'sqlite' => [ // single database setup
'driver' => 'sqlite',
'database' => storage_path().'/database.sqlite',
'prefix' => '',
],
'mysql' => [ 'mysql' => [
'driver' => 'mysql', 'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'), 'host' => env('DB_HOST', 'localhost'),
@ -65,24 +60,44 @@ return [
'engine' => 'InnoDB', 'engine' => 'InnoDB',
], ],
'pgsql' => [ // multi-database setup
'driver' => 'pgsql', 'db-ninja-0' => [
'host' => env('DB_HOST', 'localhost'), 'driver' => 'mysql',
'database' => env('DB_DATABASE', 'forge'), 'host' => env('DB_HOST', env('DB_HOST0', 'localhost')),
'username' => env('DB_USERNAME', 'forge'), 'database' => env('DB_DATABASE0', env('DB_DATABASE', 'forge')),
'password' => env('DB_PASSWORD', ''), 'username' => env('DB_USERNAME0', env('DB_USERNAME', 'forge')),
'password' => env('DB_PASSWORD0', env('DB_PASSWORD', '')),
'charset' => 'utf8', 'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '', 'prefix' => '',
'schema' => 'public', 'strict' => env('DB_STRICT', false),
'engine' => 'InnoDB',
], ],
'sqlsrv' => [ 'db-ninja-1' => [
'driver' => 'sqlsrv', 'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'), 'host' => env('DB_HOST', env('DB_HOST1', 'localhost')),
'database' => env('DB_DATABASE', 'forge'), 'database' => env('DB_DATABASE1', env('DB_DATABASE', 'forge')),
'username' => env('DB_USERNAME', 'forge'), 'username' => env('DB_USERNAME1', env('DB_USERNAME', 'forge')),
'password' => env('DB_PASSWORD', ''), 'password' => env('DB_PASSWORD1', env('DB_PASSWORD', '')),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '', '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' => [ 'database' => [
'connection' => env('QUEUE_DATABASE', 'mysql'),
'driver' => 'database', 'driver' => 'database',
'table' => 'jobs', 'table' => 'jobs',
'queue' => 'default', 'queue' => 'default',
@ -86,7 +87,8 @@ return [
*/ */
'failed' => [ 'failed' => [
'database' => 'mysql', 'table' => 'failed_jobs', 'database' => env('QUEUE_DATABASE', 'mysql'),
'table' => 'failed_jobs',
], ],
]; ];

View File

@ -22,6 +22,8 @@ class AddCustomContactFields extends Migration
$table->string('custom_value2')->nullable(); $table->string('custom_value2')->nullable();
}); });
// This may fail if the foreign key doesn't exist
try {
Schema::table('payment_methods', function ($table) { Schema::table('payment_methods', function ($table) {
$table->unsignedInteger('account_gateway_token_id')->nullable()->change(); $table->unsignedInteger('account_gateway_token_id')->nullable()->change();
$table->dropForeign('payment_methods_account_gateway_token_id_foreign'); $table->dropForeign('payment_methods_account_gateway_token_id_foreign');
@ -38,6 +40,9 @@ class AddCustomContactFields extends Migration
Schema::table('payments', function ($table) { Schema::table('payments', function ($table) {
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade'); $table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
}); });
} catch (Exception $e) {
// do nothing
}
Schema::table('expenses', function($table) { Schema::table('expenses', function($table) {
$table->unsignedInteger('payment_type_id')->nullable(); $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' => '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' => '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' => '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) { foreach ($currencies as $currency) {

View File

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

View File

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

View File

@ -85,6 +85,7 @@ class UserTableSeeder extends Seeder
'email' => env('TEST_EMAIL', TEST_USERNAME), 'email' => env('TEST_EMAIL', TEST_USERNAME),
'is_primary' => true, 'is_primary' => true,
'send_invoice' => true, 'send_invoice' => true,
'contact_key' => strtolower(str_random(RANDOM_KEY_LENGTH)),
]); ]);
Product::create([ 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. # The short X.Y version.
version = u'3.3' version = u'3.3'
# The full version, including alpha/beta/rc tags. # 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 # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # 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. 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. .. 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++) { for (var i=0; i < fields.length; i++) {
var field = fields[i]; var field = fields[i];
var value = NINJA.renderClientOrAccountField(invoice, field); var value = NINJA.renderField(invoice, field);
if (value) { if (value) {
data.push(value); data.push(value);
} }
@ -734,7 +734,7 @@ NINJA.accountAddress = function(invoice) {
for (var i=0; i < fields.length; i++) { for (var i=0; i < fields.length; i++) {
var field = fields[i]; var field = fields[i];
var value = NINJA.renderClientOrAccountField(invoice, field); var value = NINJA.renderField(invoice, field);
if (value) { if (value) {
data.push(value); data.push(value);
} }
@ -743,90 +743,6 @@ NINJA.accountAddress = function(invoice) {
return NINJA.prepareDataList(data, 'accountAddress'); 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) { NINJA.invoiceDetails = function(invoice) {
var account = invoice.account; var account = invoice.account;
@ -848,7 +764,7 @@ NINJA.invoiceDetails = function(invoice) {
for (var i=0; i < fields.length; i++) { for (var i=0; i < fields.length; i++) {
var field = fields[i]; var field = fields[i];
var value = NINJA.renderInvoiceField(invoice, field); var value = NINJA.renderField(invoice, field, true);
if (value) { if (value) {
data.push(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; var client = invoice.client;
if (!client) { if (!client) {
return false; return false;
@ -867,92 +783,173 @@ NINJA.renderClientOrAccountField = function(invoice, field) {
var contact = client.contacts[0]; var contact = client.contacts[0];
var clientName = client.name || (contact.first_name || contact.last_name ? (contact.first_name + ' ' + contact.last_name) : contact.email); 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') { if (field == 'client.client_name') {
return {text:clientName || ' ', style: ['clientName']}; value = clientName || ' ';
} else if (field == 'client.contact_name') { } 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') { } else if (field == 'client.id_number') {
return {text:client.id_number}; value = client.id_number;
} else if (field == 'client.vat_number') { } else if (field == 'client.vat_number') {
return {text:client.vat_number}; value = client.vat_number;
} else if (field == 'client.address1') { } else if (field == 'client.address1') {
return {text:client.address1}; value = client.address1;
} else if (field == 'client.address2') { } else if (field == 'client.address2') {
return {text:client.address2}; value = client.address2;
} else if (field == 'client.city_state_postal') { } else if (field == 'client.city_state_postal') {
var cityStatePostal = ''; var cityStatePostal = '';
if (client.city || client.state || client.postal_code) { if (client.city || client.state || client.postal_code) {
var swap = client.country && client.country.swap_postal_code; var swap = client.country && client.country.swap_postal_code;
cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap); cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap);
} }
return {text:cityStatePostal}; value = cityStatePostal;
} else if (field == 'client.postal_city_state') { } else if (field == 'client.postal_city_state') {
var postalCityState = ''; var postalCityState = '';
if (client.city || client.state || client.postal_code) { if (client.city || client.state || client.postal_code) {
postalCityState = formatAddress(client.city, client.state, client.postal_code, true); postalCityState = formatAddress(client.city, client.state, client.postal_code, true);
} }
return {text:postalCityState}; value = postalCityState;
} else if (field == 'client.country') { } else if (field == 'client.country') {
return {text:client.country ? client.country.name : ''}; value = client.country ? client.country.name : '';
} else if (field == 'client.email') { } else if (field == 'client.email') {
var clientEmail = contact.email == clientName ? '' : contact.email; value = contact.email == clientName ? '' : contact.email;
return {text:clientEmail};
} else if (field == 'client.phone') { } else if (field == 'client.phone') {
return {text:contact.phone}; value = contact.phone;
} else if (field == 'client.custom_value1') { } 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) {
} else if (field == 'client.custom_value2') { label = account.custom_client_label1;
return {text: account.custom_client_label2 && client.custom_value2 ? account.custom_client_label2 + ' ' + client.custom_value2 : false}; value = client.custom_value1;
} else if (field == 'contact.custom_value1') {
return {text:contact.custom_value1};
} else if (field == 'contact.custom_value2') {
return {text:contact.custom_value2};
} }
} else if (field == 'client.custom_value2') {
if (field == 'account.company_name') { if (account.custom_client_label2 && client.custom_value2) {
return {text:account.name, style: ['accountName']}; label = account.custom_client_label2;
value = client.custom_value2;
}
} else if (field == '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') {
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') { } else if (field == 'account.id_number') {
return {text:account.id_number, style: ['idNumber']}; value = account.id_number;
} else if (field == 'account.vat_number') { } else if (field == 'account.vat_number') {
return {text:account.vat_number, style: ['vatNumber']}; value = account.vat_number;
} else if (field == 'account.website') { } else if (field == 'account.website') {
return {text:account.website, style: ['website']}; value = account.website;
} else if (field == 'account.email') { } else if (field == 'account.email') {
return {text:account.work_email, style: ['email']}; value = account.work_email;
} else if (field == 'account.phone') { } else if (field == 'account.phone') {
return {text:account.work_phone, style: ['phone']}; value = account.work_phone;
} else if (field == 'account.address1') { } else if (field == 'account.address1') {
return {text: account.address1}; value = account.address1;
} else if (field == 'account.address2') { } else if (field == 'account.address2') {
return {text: account.address2}; value = account.address2;
} else if (field == 'account.city_state_postal') { } else if (field == 'account.city_state_postal') {
var cityStatePostal = ''; var cityStatePostal = '';
if (account.city || account.state || account.postal_code) { if (account.city || account.state || account.postal_code) {
var swap = account.country && account.country.swap_postal_code; var swap = account.country && account.country.swap_postal_code;
cityStatePostal = formatAddress(account.city, account.state, account.postal_code, swap); cityStatePostal = formatAddress(account.city, account.state, account.postal_code, swap);
} }
return {text: cityStatePostal}; value = cityStatePostal;
} else if (field == 'account.postal_city_state') { } else if (field == 'account.postal_city_state') {
var postalCityState = ''; var postalCityState = '';
if (account.city || account.state || account.postal_code) { if (account.city || account.state || account.postal_code) {
postalCityState = formatAddress(account.city, account.state, account.postal_code, true); postalCityState = formatAddress(account.city, account.state, account.postal_code, true);
} }
return {text: postalCityState}; value = postalCityState;
} else if (field == 'account.country') { } 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') { } else if (field == 'account.custom_value1') {
if (invoice.features.invoice_settings) { if (invoice.account.custom_label1 && invoice.account.custom_value1) {
return invoice.account.custom_label1 && invoice.account.custom_value1 ? {text: invoice.account.custom_label1 + ' ' + invoice.account.custom_value1} : false; label = invoice.account.custom_label1;
value = invoice.account.custom_value1;
} }
} else if (field == 'account.custom_value2') { } else if (field == 'account.custom_value2') {
if (invoice.features.invoice_settings) { if (invoice.account.custom_label2 && invoice.account.custom_value2) {
return invoice.account.custom_label2 && invoice.account.custom_value2 ? {text: invoice.account.custom_label2 + ' ' + invoice.account.custom_value2} : false; 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') { } else if (field == '.blank') {
return {text: ' '}; value = ' ';
} }
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; return false;
}
} }
NINJA.clientDetails = function(invoice) { NINJA.clientDetails = function(invoice) {
@ -979,7 +976,7 @@ NINJA.clientDetails = function(invoice) {
for (var i=0; i < fields.length; i++) { for (var i=0; i < fields.length; i++) {
var field = fields[i]; var field = fields[i];
var value = NINJA.renderClientOrAccountField(invoice, field); var value = NINJA.renderField(invoice, field);
if (value) { if (value) {
data.push(value); data.push(value);
} }
@ -999,6 +996,9 @@ NINJA.getSecondaryColor = function(defaultColor) {
// remove blanks and add section style to all elements // remove blanks and add section style to all elements
NINJA.prepareDataList = function(oldData, section) { NINJA.prepareDataList = function(oldData, section) {
var newData = []; var newData = [];
if (! oldData.length) {
oldData.push({text:' '});
}
for (var i=0; i<oldData.length; i++) { for (var i=0; i<oldData.length; i++) {
var item = NINJA.processItem(oldData[i], section); var item = NINJA.processItem(oldData[i], section);
if (item.text || item.stack) { if (item.text || item.stack) {
@ -1028,6 +1028,9 @@ NINJA.prepareDataTable = function(oldData, section) {
NINJA.prepareDataPairs = function(oldData, section) { NINJA.prepareDataPairs = function(oldData, section) {
var newData = []; var newData = [];
if (! oldData.length) {
oldData.push([{text:' '}, {text:' '}]);
}
for (var i=0; i<oldData.length; i++) { for (var i=0; i<oldData.length; i++) {
var row = oldData[i]; var row = oldData[i];
var isBlank = false; var isBlank = false;

View File

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

View File

@ -1712,6 +1712,7 @@ $LANG = array(
'lang_Swedish' => 'Swedish', 'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2494,6 +2495,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin', 'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'FAQ', 'knowledge_base' => 'FAQ',
'partial' => 'Partial/Deposit', 'partial' => 'Teilzahlung/Anzahlung',
'partial_remaining' => ':partial von :balance', 'partial_remaining' => ':partial von :balance',
'more_fields' => 'Weitere Felder', 'more_fields' => 'Weitere Felder',
'less_fields' => 'Weniger 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_send_date' => 'On send date',
'on_due_date' => 'On due 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.', 'warn_change_auto_bill' => 'Due to NACHA rules, changes to this invoice may prevent ACH auto bill.',
'bank_account' => 'Bankkonto', '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_Swedish' => 'Schwedisch',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'Englisch (UK)', 'lang_English - United Kingdom' => 'Englisch (UK)',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Wöchentlich', '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', 'changes_take_effect_immediately' => 'Anmerkung: Änderungen treten sofort in Kraft',
'wepay_account_description' => 'Zahlungsanbieter für Invoice Ninja', '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.', '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', 'limit_import_rows' => 'Daten müssen in Stapeln von :count Zeilen oder weniger importiert werden',
'error_title' => 'Etwas lief falsch', 'error_title' => 'Etwas lief falsch',
'error_contact_text' => 'Wenn Sie Hilfe benötigen, schreiben Sie uns bitte eine E-Mail an :mailaddress', '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', 'renew_license' => 'Verlängere die Lizenz',
'iphone_app_message' => 'Berücksichtigen Sie unser :link herunterzuladen', 'iphone_app_message' => 'Berücksichtigen Sie unser :link herunterzuladen',
'iphone_app' => 'iPhone-App', 'iphone_app' => 'iPhone-App',
'android_app' => 'Android app', 'android_app' => 'Android App',
'logged_in' => 'Eingeloggt', 'logged_in' => 'Eingeloggt',
'switch_to_primary' => 'Wechseln Sie zu Ihrem Primärunternehmen (:name), um Ihren Plan zu managen.', 'switch_to_primary' => 'Wechseln Sie zu Ihrem Primärunternehmen (:name), um Ihren Plan zu managen.',
'inclusive' => 'Inklusive', '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', 'pro_plan_reports' => ':link to enable reports by joining the Pro Plan',
'mark_ready' => 'Als bereit markieren', 'mark_ready' => 'Als bereit markieren',
'limits' => 'Limits', 'limits' => 'Grenzwerte',
'fees' => 'Gebühren', 'fees' => 'Gebühren',
'fee' => 'Gebühr', 'fee' => 'Gebühr',
'set_limits_fees' => 'Set :gateway_type Limits/Fees', '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', 'location' => 'Ort',
'line_item' => 'Posten', 'line_item' => 'Posten',
'surcharge' => 'Gebühr', 'surcharge' => 'Gebühr',
'location_first_surcharge' => 'Enabled - First surcharge', 'location_first_surcharge' => 'Aktiviert - Erste Mahngebühr',
'location_second_surcharge' => 'Enabled - Second surcharge', 'location_second_surcharge' => 'Aktiviert - Zweite Mahngebühr',
'location_line_item' => 'Aktiv - Posten', 'location_line_item' => 'Aktiv - Posten',
'online_payment_surcharge' => 'Online Payment Surcharge', 'online_payment_surcharge' => 'Online Payment Surcharge',
'gateway_fees' => 'Gateway Fees', 'gateway_fees' => 'Zugangsgebühren',
'fees_disabled' => 'Gebühren sind deaktiviert', 'fees_disabled' => 'Gebühren sind deaktiviert',
'gateway_fees_help' => 'Automatically add an online payment surcharge/discount.', 'gateway_fees_help' => 'Automatically add an online payment surcharge/discount.',
'gateway' => 'Provider', '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', '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.', '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', '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_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.', 'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed', '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.', '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', 'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total', 'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable', 'mark_billable' => 'zur Verrechnung kennzeichnen',
'billed' => 'Billed', 'billed' => 'Verrechnet',
'company_variables' => 'Company Variables', 'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables', 'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables', 'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables', 'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables', 'custom_variables' => 'Benutzerdefinierte Variablen',
'invalid_file' => 'Invalid file type', 'invalid_file' => 'Ungültiger Dateityp',
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Σουηδικά',
'lang_Albanian' => 'Αλβανικά', 'lang_Albanian' => 'Αλβανικά',
'lang_English - United Kingdom' => 'Αγγλικά - Ηνωμένο Βασίλειο', 'lang_English - United Kingdom' => 'Αγγλικά - Ηνωμένο Βασίλειο',
'lang_Slovenian' => 'Σλοβένικά',
// Frequencies // Frequencies
'freq_weekly' => 'Εβδομάδα', 'freq_weekly' => 'Εβδομάδα',
@ -2491,7 +2492,8 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'invalid_file' => 'Μη έγκυρος τύπος αρχείου', 'invalid_file' => 'Μη έγκυρος τύπος αρχείου',
'add_documents_to_invoice' => 'Προσθέστε έγγραφα στο τιμολόγιο', 'add_documents_to_invoice' => 'Προσθέστε έγγραφα στο τιμολόγιο',
'mark_expense_paid' => 'Σήμανση ως εξοφλημένο', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2485,6 +2486,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Sueco',
'lang_Albanian' => 'Albanés', 'lang_Albanian' => 'Albanés',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', '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', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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', 'private_notes' => 'Note personnelle',
'invoice' => 'Facture', 'invoice' => 'Facture',
'client' => 'Client', 'client' => 'Client',
'invoice_date' => 'Date de la facture', 'invoice_date' => 'Date de facture',
'due_date' => 'Date d\'échéance', 'due_date' => 'Date d\'échéance',
'invoice_number' => 'Numéro de facture', 'invoice_number' => 'Numéro de facture',
'invoice_number_short' => 'Facture #', 'invoice_number_short' => 'Facture #',
@ -266,7 +266,7 @@ $LANG = array(
'working' => 'En cours', 'working' => 'En cours',
'success' => 'Succès', '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.', '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', 'password' => 'Mot de passe',
'pro_plan_product' => 'Plan Pro', '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.', '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_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 ?', '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' => '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', 'go_back' => 'Retour',
'data_visualizations' => 'Visualisation des données', 'data_visualizations' => 'Visualisation des données',
'sample_data' => 'Données fictives présentées', 'sample_data' => 'Données fictives présentées',
@ -433,7 +433,7 @@ $LANG = array(
'reset_all' => 'Réinitialiser', 'reset_all' => 'Réinitialiser',
'approve' => 'Accepter', 'approve' => 'Accepter',
'token_billing_type_id' => 'Jeton de paiement', '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_1' => 'Désactiver',
'token_billing_2' => 'Opt-in - Case à cocher affichée mais non sélectionnée', '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', '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', 'valid_until' => 'Valide jusqu\'au',
'reset_terms' => 'Ràz conditions', 'reset_terms' => 'Ràz conditions',
'reset_footer' => 'Ràz pied de facture', 'reset_footer' => 'Ràz pied de facture',
'invoice_sent' => ':count invoice sent', 'invoice_sent' => ':count facture envoyée',
'invoices_sent' => ':count invoices sent', 'invoices_sent' => ':count factures envoyées',
'status_draft' => 'Brouillon', 'status_draft' => 'Brouillon',
'status_sent' => 'Envoyée', 'status_sent' => 'Envoyée',
'status_viewed' => 'Vue', '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', 'disable' => 'Désactiver',
'invoice_quote_number' => 'Numéro des devis & factures', 'invoice_quote_number' => 'Numéro des devis & factures',
'invoice_charges' => 'Majoration de facture', 'invoice_charges' => 'Majoration de facture',
'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', 'notification_invoice_bounced' => 'Impossible d\'envoyer la facture :invoice à :contact.',
'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', 'notification_invoice_bounced_subject' => 'Impossible d\'envoyer la facture :invoice',
'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', 'notification_quote_bounced' => 'Impossible d\'envoyer le devis :invoice à :contact.',
'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', 'notification_quote_bounced_subject' => 'Impossible d\'envoyer le devis :invoice',
'custom_invoice_link' => 'Personnaliser le lien de la facture', 'custom_invoice_link' => 'Personnaliser le lien de la facture',
'total_invoiced' => 'Total facturé', 'total_invoiced' => 'Total facturé',
'open_balance' => 'Open Balance', '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', 'basic_settings' => 'Paramètres généraux',
'pro' => 'Pro', 'pro' => 'Pro',
'gateways' => 'Passerelles de paiement', 'gateways' => 'Passerelles de paiement',
'next_send_on' => 'Envoi suivant: :date', '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', 'general_settings' => 'Réglages généraux',
'customize' => 'Personnaliser', 'customize' => 'Personnaliser',
'oneclick_login_help' => 'Connectez un compte pour vous connecter sans votre mot de passe', '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', 'archived_tax_rate' => 'Taux de taxe archivé avec succès',
'default_tax_rate_id' => 'Taux de taxe par défaut', 'default_tax_rate_id' => 'Taux de taxe par défaut',
'tax_rate' => 'Taux de taxe', 'tax_rate' => 'Taux de taxe',
'recurring_hour' => 'Recurring Hour', 'recurring_hour' => 'Heure récurrente',
'pattern' => 'Pattern', 'pattern' => 'Pattern',
'pattern_help_title' => 'Pattern Help', 'pattern_help_title' => 'Aide Pattern',
'pattern_help_1' => 'Create custom numbers by specifying a pattern', 'pattern_help_1' => 'Créer un numéro personnalisé en précisant un modèle personnalisé',
'pattern_help_2' => 'Variables disponibles:', 'pattern_help_2' => 'Variables disponibles:',
'pattern_help_3' => 'Par exemple, :example sera converti en :value', 'pattern_help_3' => 'Par exemple, :example sera converti en :value',
'see_options' => 'Voir les options', '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_7' => ':contact a lu la facture :invoice',
'activity_8' => ':user a archivé la facture :invoice', 'activity_8' => ':user a archivé la facture :invoice',
'activity_9' => ':user a supprimé 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_11' => ':user a mis à jour le moyen de paiement :payment',
'activity_12' => ':user a archivé 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', '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', 'user' => 'Utilisateur',
'country' => 'Pays', 'country' => 'Pays',
'include' => 'Inclure', '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_freshbooks' => 'Importer depuis FreshBooks',
'import_data' => 'Importer des données', 'import_data' => 'Importer des données',
'source' => 'Source', 'source' => 'Source',
@ -826,17 +826,17 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'disabled' => 'Désactivé', 'disabled' => 'Désactivé',
'show_archived_users' => 'Afficher les utilisateurs archivés', 'show_archived_users' => 'Afficher les utilisateurs archivés',
'notes' => 'Notes', 'notes' => 'Notes',
'invoice_will_create' => 'client will be created', 'invoice_will_create' => 'Le client sera créé',
'invoices_will_create' => 'invoices will be created', 'invoices_will_create' => 'Les factures seront créées',
'failed_to_import' => 'The following records failed to import, they either already exist or are missing required fields.', 'failed_to_import' => 'L\'import des enregistrements suivants à échoué, ils sont soit existants soit il manque des champs requis.',
'publishable_key' => 'Clé publique', 'publishable_key' => 'Clé publique',
'secret_key' => 'Clé secrète', 'secret_key' => 'Clé secrète',
'missing_publishable_key' => 'Saisissez votre clé publique Stripe pour un processus de commande amélioré', 'missing_publishable_key' => 'Saisissez votre clé publique Stripe pour un processus de commande amélioré',
'email_design' => 'Email Design', 'email_design' => 'Email Design',
'due_by' => 'A échéanche du :date', 'due_by' => 'A échéanche du :date',
'enable_email_markup' => 'Enable Markup', '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.', 'enable_email_markup_help' => 'Rendez le règlement de vos clients plus facile en ajoutant les markup schema.org à vos emails.',
'template_help_title' => 'Templates Help', 'template_help_title' => 'Aide Modèles',
'template_help_1' => 'Variable disponibles :', 'template_help_1' => 'Variable disponibles :',
'email_design_id' => 'Style de mail', 'email_design_id' => 'Style de mail',
'email_design_help' => 'Rendez vos courriels plus professionnels avec des mises en page en HTML.', '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', 'dark' => 'Sombre',
'industry_help' => 'Utilisé dans le but de fournir des statistiques la taille et le secteur de l\'entreprise.', '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.', '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', 'website_help' => 'Affiche la facture dans un iFrame sur votre site web',
'invoice_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the invoice number.', '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' => 'Specify a prefix or use a custom pattern to dynamically set the quote number.', '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' => 'Add a text input to the client create/edit page and display the label and value on the PDF.', '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' => 'Add a label and value to the company details section of the 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' => 'Add a text input to the invoice create/edit page and display the label and value on the 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' => 'Add a text input to the invoice create/edit page and include the charge in the invoice subtotals.', '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.', 'token_expired' => 'Validation jeton expiré. Veuillez réessayer.',
'invoice_link' => 'Lien vers la facture', 'invoice_link' => 'Lien vers la facture',
'button_confirmation_message' => 'Cliquez pour confirmer votre adresse e-mail.', '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', 'days_after' => 'jours après le',
'field_due_date' => 'date d\'échéance', 'field_due_date' => 'date d\'échéance',
'field_invoice_date' => 'Date de la facture', 'field_invoice_date' => 'Date de la facture',
'schedule' => 'Schedule', 'schedule' => 'Planification',
'email_designs' => 'Email Designs', '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', 'white_label_purchase_link' => 'Acheter une licence en marque blanche',
'expense' => 'Dépense', 'expense' => 'Dépense',
'expenses' => 'Dépenses', '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', 'edit_vendor' => 'Éditer le fournisseur',
'archive_vendor' => 'Archiver ce fournisseur', 'archive_vendor' => 'Archiver ce fournisseur',
'delete_vendor' => 'Supprimer ce fournisseur', 'delete_vendor' => 'Supprimer ce fournisseur',
'view_vendor' => 'View Vendor', 'view_vendor' => 'Voir Vendeur',
'deleted_expense' => 'Successfully deleted expense', 'deleted_expense' => 'Effacement de la dépense avec succès',
'archived_expense' => 'Successfully archived expense', 'archived_expense' => 'Archivage de la dépense avec succès',
'deleted_expenses' => 'Successfully deleted expenses', 'deleted_expenses' => 'Effacement des dépenses avec succès',
'archived_expenses' => 'Successfully archived expenses', 'archived_expenses' => 'Archivage des dépenses avec succès',
'expense_amount' => 'Montant de la dépense', '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_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', 'public_notes' => 'Note publique',
'invoice_amount' => 'Montant de la facture', 'invoice_amount' => 'Montant de la facture',
'exchange_rate' => 'Taux de change', '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', 'archive_expense' => 'Archiver le dépense',
'delete_expense' => 'supprimer la dépense', 'delete_expense' => 'supprimer la dépense',
'view_expense_num' => 'Dépense # :expense', 'view_expense_num' => 'Dépense # :expense',
'updated_expense' => 'Successfully updated expense', 'updated_expense' => 'Mise à jour de la dépense avec succès',
'created_expense' => 'Successfully created expense', 'created_expense' => 'Création de la dépense avec succès',
'enter_expense' => 'Nouvelle dépense', 'enter_expense' => 'Nouvelle dépense',
'view' => 'Voir', 'view' => 'Voir',
'restore_expense' => 'Restorer la dépense', 'restore_expense' => 'Restorer la dépense',
'invoice_expense' => 'Invoice Expense', 'invoice_expense' => 'Facturer la dépense',
'expense_error_multiple_clients' => 'The expenses can\'t belong to different clients', 'expense_error_multiple_clients' => 'La dépense ne peut pas être attribuée à plusieurs clients',
'expense_error_invoiced' => 'Expense has already been invoiced', 'expense_error_invoiced' => 'La dépense à déjà été facturée',
'convert_currency' => 'Convert currency', 'convert_currency' => 'Convertir la devise',
'num_days' => 'Nombre de jours', 'num_days' => 'Nombre de jours',
'create_payment_term' => 'Créer une condition de paiement', 'create_payment_term' => 'Créer une condition de paiement',
'edit_payment_terms' => 'Éditer 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', 'thursday' => 'Jeudi',
'friday' => 'Vendredi', 'friday' => 'Vendredi',
'saturday' => 'Samedi', 'saturday' => 'Samedi',
'header_font_id' => 'Header Font', 'header_font_id' => 'Police de l\'en-tête',
'body_font_id' => 'Body Font', 'body_font_id' => 'Police du corps',
'color_font_help' => 'Note: the primary color and fonts are also used in the client portal and custom email designs.', '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', 'live_preview' => 'Aperçu',
'invalid_mail_config' => 'Impossible d\'envoyer le mail, veuillez vérifier que les paramètres de messagerie sont corrects.', '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', '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é', 'archived_bank_account' => 'Compte bancaire archivé',
'created_bank_account' => 'Compte bancaire créé', 'created_bank_account' => 'Compte bancaire créé',
'validate_bank_account' => 'Valider le compte bancaire', '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.', 'bank_password_warning' => 'Attention: votre mot de passe peut être transmis en clair, pensez à activer HTTPS.',
'username' => 'Nom d\'utilisateur', 'username' => 'Nom d\'utilisateur',
'account_number' => 'N° de compte', 'account_number' => 'N° de compte',
'account_name' => 'Nom du 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é', 'status_approved' => 'Approuvé',
'quote_settings' => 'Paramètres des devis', 'quote_settings' => 'Paramètres des devis',
'auto_convert_quote' => 'Convertir automatiquement les 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', 'validate' => 'Valider',
'info' => 'Info', 'info' => 'Info',
'imported_expenses' => 'Successfully created :count_vendors vendor(s) and :count_expenses expense(s)', '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.', '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' => 'The expenses can\'t have different currencies.', 'expense_error_multiple_currencies' => 'Les dépenses ne peuvent avoir plusieurs devises.',
'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.', 'expense_error_mismatch_currencies' => 'La devise du clients n\'est pas la même que celle de la dépense.',
'trello_roadmap' => 'Trello Roadmap', 'trello_roadmap' => 'Trello Roadmap',
'header_footer' => 'En-tête/Pied de page', 'header_footer' => 'En-tête/Pied de page',
'first_page' => 'première 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.', 'enable_https' => 'Nous vous recommandons fortement d\'activer le HTTPS si vous acceptez les paiements en ligne.',
'quote_issued_to' => 'Devis à l\'attention de', 'quote_issued_to' => 'Devis à l\'attention de',
'show_currency_code' => 'Code de la devise', 'show_currency_code' => 'Code de la devise',
'trial_message' => 'Your account will receive a free two week trial of our pro plan.', 'trial_message' => 'Votre compte va être crédité d\'un essai gratuit de 2 semaines de notre Plan pro.',
'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.', '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_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_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é', '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', '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>', '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.', '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 pour supprimer le logo Invoice Ninja en souscrivant au Plan Pro',
'pro_plan_remove_logo_link' => 'Cliquez ici', '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', 'list_credits' => 'List Credits',
'tax_name' => 'Nom de la taxe', 'tax_name' => 'Nom de la taxe',
'report_settings' => 'Report Settings', 'report_settings' => 'Report Settings',
'search_hotkey' => 'shortcut is /', 'search_hotkey' => 'la racine est /',
'new_user' => 'Nouvel utilisateur', 'new_user' => 'Nouvel utilisateur',
'new_product' => 'Nouvel article', 'new_product' => 'Nouvel article',
'new_tax_rate' => 'Nouveau taux de taxe', 'new_tax_rate' => 'Nouveau taux de taxe',
'invoiced_amount' => 'Montant de la facture', 'invoiced_amount' => 'Montant de la facture',
'invoice_item_fields' => 'Invoice Item Fields', '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.', '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' => 'Recurring Number', 'recurring_invoice_number' => 'Numéro récurrent',
'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.', '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 // Client Passwords
'enable_portal_password' => 'Protéger les factures avec un mot de passe', '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' => '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é', 'expired' => 'Expiré',
'invalid_card_number' => 'Le numéro de carte bancaire est invalide.', 'invalid_card_number' => 'Le numéro de carte bancaire est invalide.',
'invalid_expiry' => 'La date d\'expiration est invalide.', 'invalid_expiry' => 'La date d\'expiration est invalide.',
'invalid_cvv' => 'Le code de sécurité est incorrect.', 'invalid_cvv' => 'Le code de sécurité est incorrect.',
'cost' => 'Coût', '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 // User Permissions
'owner' => 'Propriétaire', '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.', 'user_edit_all' => 'Modifier tous les clients, les factures, etc.',
'gateway_help_20' => ':link pour vous inscrire à Sage Pay.', 'gateway_help_20' => ':link pour vous inscrire à Sage Pay.',
'gateway_help_21' => ':link pour vous inscrire à Sage Pay.', 'gateway_help_21' => ':link pour vous inscrire à Sage Pay.',
'partial_due' => 'Partial Due', 'partial_due' => 'Solde partiel',
'restore_vendor' => 'Restorer le fournisseur', 'restore_vendor' => 'Restaurer le fournisseur',
'restored_vendor' => 'Fournisseur restoré', 'restored_vendor' => 'Fournisseur restauré',
'restored_expense' => 'Dépense restorée', 'restored_expense' => 'Dépense restaurée',
'permissions' => 'Permissions', 'permissions' => 'Permissions',
'create_all_help' => 'Autoriser l\'utilisateur à créer et éditer tous les enregistrements', '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', 'view_all_help' => 'Autoriser l\'utilisateur à voir les enregistrement qu\'il n\'a pas créé',
'edit_all_help' => 'Allow user to modify records they didn\'t create', 'edit_all_help' => 'Autoriser l\'utilisateur à modifier les enregistrement qu\'il n\'a pas créé',
'view_payment' => 'Voir le paiement', 'view_payment' => 'Voir le paiement',
'january' => 'Janvier', 'january' => 'Janvier',
@ -1085,90 +1085,90 @@ Si vous avez besoin d\'aide à ce sujet, vous pouvez publier une question sur no
'december' => 'Décembre', 'december' => 'Décembre',
// Documents // Documents
'documents_header' => 'Documents:', 'documents_header' => 'Documents :',
'email_documents_header' => 'Documents:', 'email_documents_header' => 'Documents :',
'email_documents_example_1' => 'Widgets Receipt.pdf', 'email_documents_example_1' => 'Widgets Receipt.pdf',
'email_documents_example_2' => 'Final Deliverable.zip', 'email_documents_example_2' => 'Final Deliverable.zip',
'invoice_documents' => 'Documents', 'invoice_documents' => 'Documents',
'expense_documents' => 'Documents attachés', 'expense_documents' => 'Documents attachés',
'invoice_embed_documents' => 'Embed Documents', 'invoice_embed_documents' => 'Documents intégrés',
'invoice_embed_documents_help' => 'Include attached images in the invoice.', 'invoice_embed_documents_help' => 'Inclure l\'image attachée dans la facture.',
'document_email_attachment' => 'Attach Documents', 'document_email_attachment' => 'Attacher les Documents',
'download_documents' => 'Download Documents (:size)', 'download_documents' => 'Télécharger les Documents (:size)',
'documents_from_expenses' => 'From Expenses:', 'documents_from_expenses' => 'Des dépenses :',
'dropzone_default_message' => 'Drop files or click to upload', 'dropzone_default_message' => 'Glisser le fichier ou cliquer pour envoyer',
'dropzone_fallback_message' => 'Your browser does not support drag\'n\'drop file uploads.', '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_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_file_too_big' => 'Fichier trop gros ({{filesize}}Mo). Max filesize: {{maxFilesize}}Mo.',
'dropzone_invalid_file_type' => 'You can\'t upload files of this type.', 'dropzone_invalid_file_type' => 'Vous ne pouvez pas envoyer de fichiers de ce type.',
'dropzone_response_error' => 'Server responded with {{statusCode}} code.', 'dropzone_response_error' => 'Le serveur a répondu avec le code {{statusCode}}.',
'dropzone_cancel_upload' => 'Cancel upload', 'dropzone_cancel_upload' => 'Annuler l\'envoi',
'dropzone_cancel_upload_confirmation' => 'Are you sure you want to cancel this upload?', 'dropzone_cancel_upload_confirmation' => 'Etes-vous sûr de vouloir annuler cet envoi ?',
'dropzone_remove_file' => 'Remove file', 'dropzone_remove_file' => 'Supprimer le fichier',
'documents' => 'Documents', 'documents' => 'Documents',
'document_date' => 'Document Date', 'document_date' => 'Date de Document',
'document_size' => 'Size', 'document_size' => 'Taille',
'enable_client_portal' => 'Tableau de bord', 'enable_client_portal' => 'Tableau de bord',
'enable_client_portal_help' => 'Afficher / masquer le tableau de bord sur le portail client.', 'enable_client_portal_help' => 'Afficher / masquer le tableau de bord sur le portail client.',
'enable_client_portal_dashboard' => 'Dashboard', '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 // Plans
'account_management' => 'Account Management', 'account_management' => 'Gestion des comptes',
'plan_status' => 'Plan Status', 'plan_status' => 'Status du Plan',
'plan_upgrade' => 'Upgrade', 'plan_upgrade' => 'Upgrade',
'plan_change' => 'Change Plan', 'plan_change' => 'Change Plan',
'pending_change_to' => 'Changes To', 'pending_change_to' => 'Changer vers',
'plan_changes_to' => ':plan on :date', 'plan_changes_to' => ':plan au :date',
'plan_term_changes_to' => ':plan (:term) on :date', 'plan_term_changes_to' => ':plan (:term) le :date',
'cancel_plan_change' => 'Cancel Change', 'cancel_plan_change' => 'Annuler la modification',
'plan' => 'Plan', 'plan' => 'Plan',
'expires' => 'Expires', 'expires' => 'Expire',
'renews' => 'Renews', 'renews' => 'Renouvellement',
'plan_expired' => ':plan Plan Expired', 'plan_expired' => ':plan Plan Expiré',
'trial_expired' => ':plan Plan Trial Ended', 'trial_expired' => ':plan Essai du Plan terminé',
'never' => 'Never', 'never' => 'Jamais',
'plan_free' => 'Free', 'plan_free' => 'Gratuit',
'plan_pro' => 'Pro', 'plan_pro' => 'Pro',
'plan_enterprise' => 'Enterprise', 'plan_enterprise' => 'Entreprise',
'plan_white_label' => 'Self Hosted (White labeled)', 'plan_white_label' => 'Auto hébergé (Marque blanche)',
'plan_free_self_hosted' => 'Self Hosted (Free)', 'plan_free_self_hosted' => 'Auto hébergé (Gratuit)',
'plan_trial' => 'Trial', 'plan_trial' => 'Essai',
'plan_term' => 'Term', 'plan_term' => 'Terme',
'plan_term_monthly' => 'Monthly', 'plan_term_monthly' => 'Mensuel',
'plan_term_yearly' => 'Yearly', 'plan_term_yearly' => 'Annuel',
'plan_term_month' => 'Month', 'plan_term_month' => 'Mois',
'plan_term_year' => 'Year', 'plan_term_year' => 'An',
'plan_price_monthly' => '$:price/Month', 'plan_price_monthly' => '$:price/Mois',
'plan_price_yearly' => '$:price/Year', 'plan_price_yearly' => '$:price/An',
'updated_plan' => 'Updated plan settings', 'updated_plan' => 'Updated plan settings',
'plan_paid' => 'Term Started', 'plan_paid' => 'Term Started',
'plan_started' => 'Plan Started', 'plan_started' => 'Début du Plan',
'plan_expires' => 'Plan Expires', 'plan_expires' => 'Fin du Plan',
'white_label_button' => 'Marque blanche', 'white_label_button' => 'Marque blanche',
'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_year_description' => 'Engagement d\'un an dans le Plan Invoice Ninja Pro.',
'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_month_description' => 'Engagement d\'un mois dans le Plan Invoice Ninja Pro.',
'enterprise_plan_product' => 'Plan Enterprise', 'enterprise_plan_product' => 'Plan Entreprise',
'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', 'enterprise_plan_year_description' => 'Engagement d\'un an dans le Plan Invoice Ninja Entreprise.',
'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', 'enterprise_plan_month_description' => 'Engagement d\'un mois dans le Plan Invoice Ninja Entreprise.',
'plan_credit_product' => 'Credit', 'plan_credit_product' => 'Crédit',
'plan_credit_description' => 'Credit for unused time', 'plan_credit_description' => 'Crédit pour temps inutilisé',
'plan_pending_monthly' => 'Will switch to monthly on :date', 'plan_pending_monthly' => 'Basculera en mensuel le :date',
'plan_refunded' => 'A refund has been issued.', 'plan_refunded' => 'Un remboursement a été émis.',
'live_preview' => 'Aperçu', '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', 'live_preview_disabled' => 'Live preview has been disabled to support selected font',
'invoice_number_padding' => 'Padding', 'invoice_number_padding' => 'Remplissage',
'preview' => 'Preview', 'preview' => 'Prévisualisation',
'list_vendors' => 'List Vendors', '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.', '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 // Payment updates
@ -1705,7 +1705,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'lang_Spanish - Spain' => 'Espagnol - Espagne', 'lang_Spanish - Spain' => 'Espagnol - Espagne',
'lang_Swedish' => 'Suédois', 'lang_Swedish' => 'Suédois',
'lang_Albanian' => 'Albanais', 'lang_Albanian' => 'Albanais',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'Anglais - Royaume Uni',
'lang_Slovenian' => 'Slovène',
// Frequencies // Frequencies
'freq_weekly' => 'Hebdomadaire', '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', 'changes_take_effect_immediately' => 'Note: Les changements s\'appliquent immédiatement',
'wepay_account_description' => 'Passerelle de paiement pour Invoice Ninja', '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.', '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.', '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_title' => 'Il y a eu une erreur',
'error_contact_text' => 'Si vous avez besoin d\'aide, veuillez nous contacter à :mailaddress', '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', '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_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_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>', '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', 'list_products' => 'Afficher les produits',
'include_item_taxes_inline' => 'Inclure une <b>ligne de taxes dans le total de la ligne', '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)', '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', 'warning' => 'Avertissement',
'self-update' => 'Mettre à jour', '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', '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.', '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_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' => '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.', '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', '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', 'renew_license' => 'Renouveler la licence',
'iphone_app_message' => 'Avez-vous penser télécharger notre :link', 'iphone_app_message' => 'Avez-vous penser télécharger notre :link',
'iphone_app' => 'App iPhone', 'iphone_app' => 'App iPhone',
'android_app' => 'Android app', 'android_app' => 'App Android',
'logged_in' => 'Connecté', 'logged_in' => 'Connecté',
'switch_to_primary' => 'Veuillez basculer vers votre entreprise initiale (:name) pour gérer votre plan d\'abonnement.', 'switch_to_primary' => 'Veuillez basculer vers votre entreprise initiale (:name) pour gérer votre plan d\'abonnement.',
'inclusive' => 'Inclusif', 'inclusive' => 'Inclusif',
@ -2312,7 +2313,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'domain' => 'Domaine', 'domain' => 'Domaine',
'domain_help' => 'Utilisé dans le portail du client et lors de l\'envoi de courriels', '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', 'domain_help_website' => 'Utilisé lors de l\'envoi de courriels',
'preview' => 'Preview', 'preview' => 'Prévisualisation',
'import_invoices' => 'Importer des factures', 'import_invoices' => 'Importer des factures',
'new_report' => 'Nouveau rapport', 'new_report' => 'Nouveau rapport',
'edit_report' => 'Editer le 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 // New Client Portal styling
'invoice_from' => 'Factures de:', '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', 'full_name' => 'Nom complet',
'month_year' => 'MOIS/ANNEE', 'month_year' => 'MOIS/ANNEE',
'valid_thru' => 'Valide\nthru', 'valid_thru' => 'Valide\nthru',
@ -2398,94 +2399,95 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'fees' => 'Frais', 'fees' => 'Frais',
'fee' => 'Frais', 'fee' => 'Frais',
'set_limits_fees' => 'Définir les limites/frais de :gateway_type', '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.', '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.', 'discount_sample' => 'La réduction pour une facture de :amount serait de :total.',
'no_fees' => 'Aucun frais', '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.', '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', 'percent' => 'Pourcent',
'location' => 'Location', 'location' => 'Localisation',
'line_item' => 'Line Item', 'line_item' => 'Ligne d\'article',
'surcharge' => 'Majoration', 'surcharge' => 'Majoration',
'location_first_surcharge' => 'Activé - Première majoration', 'location_first_surcharge' => 'Activé - Première majoration',
'location_second_surcharge' => 'Activé - Seconde 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', 'online_payment_surcharge' => 'Majoration de paiement en ligne',
'gateway_fees' => 'Frais de la passerelle', 'gateway_fees' => 'Frais de la passerelle',
'fees_disabled' => 'Les frais sont désactivés', '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' => '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.', 'fees_surcharge_help' => 'Personnaliser la majoration :link.',
'label_and_taxes' => 'label and taxes', 'label_and_taxes' => 'Libellé et taxes',
'billable' => 'Facturable', 'billable' => 'Facturable',
'logo_warning_too_large' => 'Le fichier image est trop grand', '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_fileinfo' => 'Attention : Pour supporter les gifs, l\'extension PHP fileinfo doit être activée.',
'logo_warning_invalid' => 'There was a problem reading the image file, please try a different format.', '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.', 'error_refresh_page' => 'Un erreur est survenue, merci de rafraichir la page et essayer à nouveau',
'data' => 'Data', 'data' => 'Données',
'imported_settings' => 'Successfully imported settings', 'imported_settings' => 'Paramètres importés avec succès',
'lang_Greek' => 'Grec', 'lang_Greek' => 'Grec',
'reset_counter' => 'Reset Counter', 'reset_counter' => 'Remettre le compteur à zéro',
'next_reset' => 'Next Reset', 'next_reset' => 'Prochaine remise à zéro',
'reset_counter_help' => 'Automatically reset the invoice and quote counters.', 'reset_counter_help' => 'Remettre automatiquement à zéro les compteurs de facture et de devis.',
'auto_bill_failed' => 'Auto-billing for invoice :invoice_number failed', 'auto_bill_failed' => 'La facturation automatique de :invoice_number a échouée.',
'online_payment_discount' => 'Online Payment Discount', 'online_payment_discount' => 'Remise de paiement en ligne',
'created_new_company' => 'Successfully created new company', 'created_new_company' => 'La nouvelle entreprise a été créé',
'fees_disabled_for_gateway' => 'Fees are disabled for this gateway.', 'fees_disabled_for_gateway' => 'Les frais sont désactivés pour cette passerelle.',
'logout_and_delete' => 'Log Out/Delete Account', 'logout_and_delete' => 'Déconnexion/Suppression du compte',
'tax_rate_type_help' => 'Inclusive taxes adjust the line item cost when selected.', 'tax_rate_type_help' => 'Les taxes incluses ajustent le prix de la ligne d\'article lorsque sélectionnée.',
'invoice_footer_help' => 'Use $pageNumber and $pageCount to display the page information.', 'invoice_footer_help' => 'Utilisez $pageNumber et $pageCount pour afficher les informations de la page.',
'credit_note' => 'Credit Note', 'credit_note' => 'Note de crédit',
'credit_issued_to' => 'Credit issued to', 'credit_issued_to' => 'Crédit accordé à',
'credit_to' => 'Credit to', 'credit_to' => 'Crédit pour ',
'your_credit' => 'Your Credit', 'your_credit' => 'Votre crédit',
'credit_number' => 'Credit Number', 'credit_number' => 'Numéro de crédit',
'create_credit_note' => 'Create Credit Note', 'create_credit_note' => 'Créer une note de crédit',
'menu' => 'Menu', 'menu' => 'Menu',
'error_incorrect_gateway_ids' => 'Error: The gateways table has incorrect ids.', 'error_incorrect_gateway_ids' => 'Erreur : La table de passerelle a des ID incorrectes.',
'purge_data' => 'Purge Data', 'purge_data' => 'Purger les données',
'delete_data' => 'Delete Data', 'delete_data' => 'Effacer les données',
'purge_data_help' => 'Permanently delete all data in the account, keeping the account and settings.', 'purge_data_help' => 'Supprime toutes les données du compte, ne conserve que les comptes et les réglages',
'cancel_account_help' => 'Permanently delete the account along with all data and setting.', 'cancel_account_help' => 'Supprime le compte ainsi que les données, les comptes et les réglages.',
'purge_successful' => 'Successfully purged account data', 'purge_successful' => 'Les données du comptes ont été supprimées avec succès',
'forbidden' => 'Forbidden', 'forbidden' => 'Interdit',
'purge_data_message' => 'Warning: This will permanently erase your data, there is no undo.', 'purge_data_message' => 'Attention : Cette action va supprimer vos données et est irréversible',
'contact_phone' => 'Contact Phone', 'contact_phone' => 'Téléphone du contact',
'contact_email' => 'Contact Email', 'contact_email' => 'Email du contact',
'reply_to_email' => 'Reply-To Email', 'reply_to_email' => 'Adresse de réponse',
'reply_to_email_help' => 'Specify the reply-to address for client emails.', 'reply_to_email_help' => 'Spécifier une adresse courriel de réponse',
'bcc_email_help' => 'Privately include this address with client emails.', 'bcc_email_help' => 'Inclut de façon privée cette adresse avec les courriels du client.',
'import_complete' => 'Your import has successfully completed.', 'import_complete' => 'L\'importation s\'est réalisée avec succès.',
'confirm_account_to_import' => 'Please confirm your account to import data.', 'confirm_account_to_import' => 'Confirmer votre compte pour l\'importation des données.',
'import_started' => 'Your import has started, we\'ll send you an email once it completes.', 'import_started' => 'L\'importation est en cours. Vous recevrez un courriel lorsqu\'elle sera terminée.',
'listening' => 'Listening...', 'listening' => 'A l\'écoute...',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"', 'microphone_help' => 'Dire "nouvelle facture pour [client]" ou "montre-moi les paiements archivés pour [client]"',
'voice_commands' => 'Voice Commands', 'voice_commands' => 'Commandes vocales',
'sample_commands' => 'Sample commands', 'sample_commands' => 'Exemples de commandes',
'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.', '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', 'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products', 'archived_products' => ':count produits archivés',
'recommend_on' => 'We recommend <b>enabling</b> this setting.', 'recommend_on' => 'Nous recommandons d\'<b>activer</b> ce réglage.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.', 'recommend_off' => 'Nous recommandons de<b>désactiver</b> ce réglage.',
'notes_auto_billed' => 'Auto-billed', 'notes_auto_billed' => 'Auto-facturation',
'surcharge_label' => 'Surcharge Label', 'surcharge_label' => 'Majoration',
'contact_fields' => 'Contact Fields', 'contact_fields' => 'Champs de contact',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.', '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' => 'Showing :start to :end of :total entries', 'datatable_info' => 'Affichage :start sur :end de :total entrées',
'credit_total' => 'Credit Total', 'credit_total' => 'Total Crédit',
'mark_billable' => 'Mark billable', 'mark_billable' => 'Marquer facturable',
'billed' => 'Billed', 'billed' => 'Facturé',
'company_variables' => 'Company Variables', 'company_variables' => 'Variables de la compagnie',
'client_variables' => 'Client Variables', 'client_variables' => 'Variables du client',
'invoice_variables' => 'Invoice Variables', 'invoice_variables' => 'Variables de facture',
'navigation_variables' => 'Navigation Variables', 'navigation_variables' => 'Variables de navigation',
'custom_variables' => 'Custom Variables', 'custom_variables' => 'Variables personnalisées',
'invalid_file' => 'Invalid file type', 'invalid_file' => 'Type de fichier invalide',
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Ajouter un document à la facture',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Marquer payé',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Suédois',
'lang_Albanian' => 'Albanien', 'lang_Albanian' => 'Albanien',
'lang_English - United Kingdom' => 'Anglais - Royaume Uni', 'lang_English - United Kingdom' => 'Anglais - Royaume Uni',
'lang_Slovenian' => 'Slovénien',
// Frequencies // Frequencies
'freq_weekly' => 'Hebdomadaire', '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', 'invalid_file' => 'Type de fichier invalide',
'add_documents_to_invoice' => 'Ajouter un document à la facture', 'add_documents_to_invoice' => 'Ajouter un document à la facture',
'mark_expense_paid' => 'Marquer comme payé', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2485,6 +2486,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', 'freq_weekly' => 'Weekly',
@ -2492,6 +2493,7 @@ $LANG = array(
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Zweeds',
'lang_Albanian' => 'Albanees', 'lang_Albanian' => 'Albanees',
'lang_English - United Kingdom' => 'Engels - Verenigd Koninkrijk', 'lang_English - United Kingdom' => 'Engels - Verenigd Koninkrijk',
'lang_Slovenian' => 'Sloveens',
// Frequencies // Frequencies
'freq_weekly' => 'Wekelijks', '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.', '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', 'datatable_info' => ':start tot :end van :totaal items worden getoond',
'credit_total' => 'Totaal krediet', 'credit_total' => 'Totaal krediet',
'mark_billable' => 'Mark billable', 'mark_billable' => 'Markeer factureerbaar',
'billed' => 'Gefactureerd', 'billed' => 'Gefactureerd',
'company_variables' => 'Company Variables', 'company_variables' => 'Bedrijfsvariabelen',
'client_variables' => 'Client Variables', 'client_variables' => 'Klant variabelen',
'invoice_variables' => 'Invoice Variables', 'invoice_variables' => 'Factuur variabelen',
'navigation_variables' => 'Navigation Variables', 'navigation_variables' => 'Navigatie variabelen',
'custom_variables' => 'Custom Variables', 'custom_variables' => 'Aangepaste variabelen',
'invalid_file' => 'Invalid file type', 'invalid_file' => 'Ongeldig bestandstype',
'add_documents_to_invoice' => 'Add documents to invoice', 'add_documents_to_invoice' => 'Voeg documenten toe aan factuur',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Markeer betaald',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Szwedzki',
'lang_Albanian' => 'Albański', 'lang_Albanian' => 'Albański',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Co tydzień', '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', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian', 'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom', 'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
// Frequencies // Frequencies
'freq_weekly' => 'Weekly', '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', 'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid', 'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.', '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