1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Merge branch 'release-3.3.0'

This commit is contained in:
Hillel Coren 2017-04-30 17:24:45 +03:00
commit be4878db43
160 changed files with 5920 additions and 900 deletions

View File

@ -8,6 +8,7 @@ use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\AccountRepository;
use App\Services\PaymentService;
use Illuminate\Console\Command;
use Carbon;
/**
* Class ChargeRenewalInvoices.
@ -83,6 +84,11 @@ class ChargeRenewalInvoices extends Command
continue;
}
if (Carbon::parse($company->plan_expires)->isFuture()) {
$this->info('Skipping invoice ' . $invoice->invoice_number . ' [plan not expired]');
continue;
}
$this->info("Charging invoice {$invoice->invoice_number}");
if (! $this->paymentService->autoBillInvoice($invoice)) {
$this->info('Failed to auto-bill, emailing invoice');

View File

@ -9,6 +9,8 @@ use Illuminate\Console\Command;
use Mail;
use Symfony\Component\Console\Input\InputOption;
use Utils;
use App\Models\Contact;
use App\Models\Invitation;
/*
@ -63,13 +65,15 @@ class CheckData extends Command
$this->logMessage(date('Y-m-d') . ' Running CheckData...');
if (! $this->option('client_id')) {
$this->checkPaidToDate();
$this->checkBlankInvoiceHistory();
$this->checkPaidToDate();
}
$this->checkBalances();
$this->checkContacts();
if (! $this->option('client_id')) {
$this->checkInvitations();
$this->checkFailedJobs();
$this->checkAccountData();
}
@ -94,6 +98,62 @@ class CheckData extends Command
$this->log .= $str . "\n";
}
private function checkContacts()
{
$clients = DB::table('clients')
->leftJoin('contacts', function($join) {
$join->on('contacts.client_id', '=', 'clients.id')
->whereNull('contacts.deleted_at');
})
->groupBy('clients.id', 'clients.user_id', 'clients.account_id')
->havingRaw('count(contacts.id) = 0');
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
}
$clients = $clients->get(['clients.id', 'clients.user_id', 'clients.account_id']);
$this->logMessage(count($clients) . ' clients without any contacts');
if (count($clients) > 0) {
$this->isValid = false;
}
if ($this->option('fix') == 'true') {
foreach ($clients as $client) {
$contact = new Contact();
$contact->account_id = $client->account_id;
$contact->user_id = $client->user_id;
$contact->client_id = $client->id;
$contact->is_primary = true;
$contact->send_invoice = true;
$contact->contact_key = strtolower(str_random(RANDOM_KEY_LENGTH));
$contact->public_id = Contact::whereAccountId($client->account_id)->withTrashed()->max('public_id') + 1;
$contact->save();
}
}
$clients = DB::table('clients')
->leftJoin('contacts', function($join) {
$join->on('contacts.client_id', '=', 'clients.id')
->where('contacts.is_primary', '=', true)
->whereNull('contacts.deleted_at');
})
->groupBy('clients.id')
->havingRaw('count(contacts.id) != 1');
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
}
$clients = $clients->get(['clients.id', DB::raw('count(contacts.id)')]);
$this->logMessage(count($clients) . ' clients without a single primary contact');
if (count($clients) > 0) {
$this->isValid = false;
}
}
private function checkFailedJobs()
{
$count = DB::table('failed_jobs')->count();
@ -120,6 +180,34 @@ class CheckData extends Command
$this->logMessage($count . ' activities with blank invoice backup');
}
private function checkInvitations()
{
$invoices = DB::table('invoices')
->leftJoin('invitations', 'invitations.invoice_id', '=', 'invoices.id')
->groupBy('invoices.id', 'invoices.user_id', 'invoices.account_id', 'invoices.client_id')
->havingRaw('count(invitations.id) = 0')
->get(['invoices.id', 'invoices.user_id', 'invoices.account_id', 'invoices.client_id']);
$this->logMessage(count($invoices) . ' invoices without any invitations');
if (count($invoices) > 0) {
$this->isValid = false;
}
if ($this->option('fix') == 'true') {
foreach ($invoices as $invoice) {
$invitation = new Invitation();
$invitation->account_id = $invoice->account_id;
$invitation->user_id = $invoice->user_id;
$invitation->invoice_id = $invoice->id;
$invitation->contact_id = Contact::whereClientId($invoice->client_id)->whereIsPrimary(true)->first()->id;
$invitation->invitation_key = strtolower(str_random(RANDOM_KEY_LENGTH));
$invitation->public_id = Invitation::whereAccountId($invoice->account_id)->withTrashed()->max('public_id') + 1;
$invitation->save();
}
}
}
private function checkAccountData()
{
$tables = [
@ -159,6 +247,7 @@ class CheckData extends Command
],
'products' => [
ENTITY_USER,
ENTITY_TAX_RATE,
],
'vendors' => [
ENTITY_USER,
@ -173,14 +262,28 @@ class CheckData extends Command
ENTITY_USER,
ENTITY_CLIENT,
],
'accounts' => [
ENTITY_TAX_RATE,
]
];
foreach ($tables as $table => $entityTypes) {
foreach ($entityTypes as $entityType) {
$tableName = Utils::pluralizeEntityType($entityType);
if ($entityType == ENTITY_TAX_RATE) {
$field = 'default_' . $entityType;
} else {
$field = $entityType;
}
if ($table == 'accounts') {
$accountId = 'id';
} else {
$accountId = 'account_id';
}
$records = DB::table($table)
->join($tableName, "{$tableName}.id", '=', "{$table}.{$entityType}_id")
->where("{$table}.account_id", '!=', DB::raw("{$tableName}.account_id"))
->join($tableName, "{$tableName}.id", '=', "{$table}.{$field}_id")
->where("{$table}.{$accountId}", '!=', DB::raw("{$tableName}.account_id"))
->get(["{$table}.id"]);
if (count($records)) {

View File

@ -0,0 +1,223 @@
<?php
namespace App\Console\Commands;
use Utils;
use stdClass;
use App\Models\Account;
use Faker\Factory;
use Illuminate\Console\Command;
/**
* Class CreateLuisData.
*/
class CreateLuisData extends Command
{
/**
* @var string
*/
protected $description = 'Create LUIS Data';
/**
* @var string
*/
protected $signature = 'ninja:create-luis-data {faker_field=name}';
/**
* CreateLuisData constructor.
*
*/
public function __construct()
{
parent::__construct();
$this->faker = Factory::create();
}
/**
* @return bool
*/
public function fire()
{
$this->fakerField = $this->argument('faker_field');
$intents = [];
$entityTypes = [
ENTITY_INVOICE,
ENTITY_QUOTE,
ENTITY_CLIENT,
ENTITY_CREDIT,
ENTITY_EXPENSE,
ENTITY_PAYMENT,
ENTITY_PRODUCT,
ENTITY_RECURRING_INVOICE,
ENTITY_TASK,
ENTITY_VENDOR,
];
foreach ($entityTypes as $entityType) {
$intents = array_merge($intents, $this->createIntents($entityType));
}
$intents = array_merge($intents, $this->getNavigateToIntents($entityType));
$this->info(json_encode($intents));
}
private function createIntents($entityType)
{
$intents = [];
$intents = array_merge($intents, $this->getCreateEntityIntents($entityType));
$intents = array_merge($intents, $this->getFindEntityIntents($entityType));
$intents = array_merge($intents, $this->getListEntityIntents($entityType));
return $intents;
}
private function getCreateEntityIntents($entityType)
{
$intents = [];
$phrases = [
"create new {$entityType}",
"new {$entityType}",
"make a {$entityType}",
];
foreach ($phrases as $phrase) {
$intents[] = $this->createIntent('CreateEntity', $phrase, [
$entityType => 'EntityType',
]);
if ($entityType != ENTITY_CLIENT) {
$client = $this->faker->{$this->fakerField};
$phrase .= " for {$client}";
$intents[] = $this->createIntent('CreateEntity', $phrase, [
$entityType => 'EntityType',
$client => 'Name',
]);
}
}
return $intents;
}
private function getFindEntityIntents($entityType)
{
$intents = [];
if (in_array($entityType, [ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_QUOTE])) {
$name = $entityType === ENTITY_CLIENT ? $this->faker->{$this->fakerField} : $this->faker->randomNumber(4);
$intents[] = $this->createIntent('FindEntity', "find {$entityType} {$name}", [
$entityType => 'EntityType',
$name => 'Name',
]);
if ($entityType === ENTITY_CLIENT) {
$name = $this->faker->{$this->fakerField};
$intents[] = $this->createIntent('FindEntity', "find {$name}", [
$name => 'Name',
]);
}
}
return $intents;
}
private function getListEntityIntents($entityType)
{
$intents = [];
$entityTypePlural = Utils::pluralizeEntityType($entityType);
$intents[] = $this->createIntent('ListEntity', "show me {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
]);
$intents[] = $this->createIntent('ListEntity', "list {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
]);
$intents[] = $this->createIntent('ListEntity', "show me active {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
'active' => 'Filter',
]);
$intents[] = $this->createIntent('ListEntity', "list archived and deleted {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
'archived' => 'Filter',
'deleted' => 'Filter',
]);
if ($entityType != ENTITY_CLIENT) {
$client = $this->faker->{$this->fakerField};
$intents[] = $this->createIntent('ListEntity', "list {$entityTypePlural} for {$client}", [
$entityTypePlural => 'EntityType',
$client => 'Name',
]);
$intents[] = $this->createIntent('ListEntity', "show me {$client}'s {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
$client . '\'s' => 'Name',
]);
$intents[] = $this->createIntent('ListEntity', "show me {$client}'s active {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
$client . '\'s' => 'Name',
'active' => 'Filter',
]);
}
return $intents;
}
private function getNavigateToIntents($entityType)
{
$intents = [];
$locations = array_merge(Account::$basicSettings, Account::$advancedSettings);
foreach ($locations as $location) {
$location = str_replace('_', ' ', $location);
$intents[] = $this->createIntent('NavigateTo', "go to {$location}", [
$location => 'Location',
]);
$intents[] = $this->createIntent('NavigateTo', "show me {$location}", [
$location => 'Location',
]);
}
return $intents;
}
private function createIntent($name, $text, $entities)
{
$intent = new stdClass();
$intent->intent = $name;
$intent->text = $text;
$intent->entities = [];
foreach ($entities as $value => $entity) {
$startPos = strpos($text, (string)$value);
if (! $startPos) {
dd("Failed to find {$value} in {$text}");
}
$entityClass = new stdClass();
$entityClass->entity = $entity;
$entityClass->startPos = $startPos;
$entityClass->endPos = $entityClass->startPos + strlen($value) - 1;
$intent->entities[] = $entityClass;
}
return $intent;
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [];
}
}

View File

@ -18,7 +18,6 @@ use Utils;
*/
class CreateTestData extends Command
{
//protected $name = 'ninja:create-test-data';
/**
* @var string
*/
@ -126,6 +125,7 @@ class CreateTestData extends Command
{
for ($i = 0; $i < $this->count; $i++) {
$data = [
'is_public' => true,
'client_id' => $client->id,
'invoice_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
'due_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),

View File

@ -20,6 +20,7 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\CheckData',
'App\Console\Commands\PruneData',
'App\Console\Commands\CreateTestData',
'App\Console\Commands\CreateLuisData',
'App\Console\Commands\SendRenewalInvoices',
'App\Console\Commands\ChargeRenewalInvoices',
'App\Console\Commands\SendReminders',

View File

@ -200,8 +200,11 @@ if (! defined('APP_NAME')) {
define('TASK_STATUS_PAID', 4);
define('EXPENSE_STATUS_LOGGED', 1);
define('EXPENSE_STATUS_INVOICED', 2);
define('EXPENSE_STATUS_PAID', 3);
define('EXPENSE_STATUS_PENDING', 2);
define('EXPENSE_STATUS_INVOICED', 3);
define('EXPENSE_STATUS_BILLED', 4);
define('EXPENSE_STATUS_PAID', 5);
define('EXPENSE_STATUS_UNPAID', 6);
define('CUSTOM_DESIGN', 11);
@ -296,7 +299,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '3.2.1' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '3.3.0' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@ -306,6 +309,7 @@ if (! defined('APP_NAME')) {
define('NINJA_CONTACT_URL', env('NINJA_CONTACT_URL', 'https://www.invoiceninja.com/contact/'));
define('NINJA_FROM_EMAIL', env('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'));
define('NINJA_IOS_APP_URL', 'https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1220337560&mt=8');
define('NINJA_ANDROID_APP_URL', 'https://play.google.com/store/apps/details?id=com.invoiceninja.invoiceninja');
define('RELEASES_URL', env('RELEASES_URL', 'https://trello.com/b/63BbiVVe/invoice-ninja'));
define('ZAPIER_URL', env('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja'));
define('OUTDATE_BROWSER_URL', env('OUTDATE_BROWSER_URL', 'http://browsehappy.com/'));
@ -317,6 +321,7 @@ if (! defined('APP_NAME')) {
define('OFX_HOME_URL', env('OFX_HOME_URL', 'http://www.ofxhome.com/index.php/home/directory/all'));
define('GOOGLE_ANALYITCS_URL', env('GOOGLE_ANALYITCS_URL', 'https://www.google-analytics.com/collect'));
define('TRANSIFEX_URL', env('TRANSIFEX_URL', 'https://www.transifex.com/invoice-ninja/invoice-ninja'));
define('IP_LOOKUP_URL', env('IP_LOOKUP_URL', 'http://whatismyipaddress.com/ip/'));
define('CHROME_PDF_HELP_URL', 'https://support.google.com/chrome/answer/6213030?hl=en');
define('FIREFOX_PDF_HELP_URL', 'https://support.mozilla.org/en-US/kb/view-pdf-files-firefox');
@ -441,7 +446,7 @@ if (! defined('APP_NAME')) {
define('CURRENCY_DECORATOR_NONE', 'none');
define('RESELLER_REVENUE_SHARE', 'A');
define('RESELLER_LIMITED_USERS', 'B');
define('RESELLER_ACCOUNT_COUNT', 'B');
define('AUTO_BILL_OFF', 1);
define('AUTO_BILL_OPT_IN', 2);

View File

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

View File

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

View File

@ -17,13 +17,19 @@ class InvoiceWasEmailed extends Event
*/
public $invoice;
/**
* @var string
*/
public $notes;
/**
* Create a new event instance.
*
* @param Invoice $invoice
*/
public function __construct(Invoice $invoice)
public function __construct(Invoice $invoice, $notes)
{
$this->invoice = $invoice;
$this->notes = $notes;
}
}

View File

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

View File

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

View File

@ -12,13 +12,19 @@ class QuoteWasEmailed extends Event
use SerializesModels;
public $quote;
/**
* @var string
*/
public $notes;
/**
* Create a new event instance.
*
* @param $quote
*/
public function __construct($quote)
public function __construct($quote, $notes)
{
$this->quote = $quote;
$this->notes = $notes;
}
}

View File

@ -78,7 +78,7 @@ class AccountApiController extends BaseAPIController
$updatedAt = $request->updated_at ? date('Y-m-d H:i:s', $request->updated_at) : false;
$transformer = new AccountTransformer(null, $request->serializer);
$account->load(array_merge($transformer->getDefaultIncludes(), ['projects.client']));
$account->load(array_merge($transformer->getDefaultIncludes(), ['projects.client', 'products.default_tax_rate']));
$account = $this->createItem($account, $transformer, 'account');
return $this->response($account);

View File

@ -546,6 +546,7 @@ class AccountController extends BaseController
$client->postal_code = '10000';
$client->work_phone = '(212) 555-0000';
$client->work_email = 'sample@example.com';
$client->balance = 100;
$invoice->invoice_number = '0000';
$invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d'));
@ -892,6 +893,8 @@ class AccountController extends BaseController
$account->custom_value2 = trim(Input::get('custom_value2'));
$account->custom_client_label1 = trim(Input::get('custom_client_label1'));
$account->custom_client_label2 = trim(Input::get('custom_client_label2'));
$account->custom_contact_label1 = trim(Input::get('custom_contact_label1'));
$account->custom_contact_label2 = trim(Input::get('custom_contact_label2'));
$account->custom_invoice_label1 = trim(Input::get('custom_invoice_label1'));
$account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2'));
$account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false;
@ -1016,13 +1019,9 @@ class AccountController extends BaseController
/* Logo image file */
if ($uploaded = Input::file('logo')) {
$path = Input::file('logo')->getRealPath();
$disk = $account->getLogoDisk();
if ($account->hasLogo() && ! Utils::isNinjaProd()) {
$disk->delete($account->logo);
}
$extension = strtolower($uploaded->getClientOriginalExtension());
if (empty(Document::$types[$extension]) && ! empty(Document::$extraExtensions[$extension])) {
$documentType = Document::$extraExtensions[$extension];
} else {
@ -1038,7 +1037,7 @@ class AccountController extends BaseController
$size = filesize($filePath);
if ($size / 1000 > MAX_DOCUMENT_SIZE) {
Session::flash('warning', trans('texts.logo_warning_too_large'));
Session::flash('error', trans('texts.logo_warning_too_large'));
} else {
if ($documentType != 'gif') {
$account->logo = $account->account_key.'.'.$documentType;
@ -1055,24 +1054,25 @@ class AccountController extends BaseController
$image->interlace(false);
$imageStr = (string) $image->encode($documentType);
$disk->put($account->logo, $imageStr);
$account->logo_size = strlen($imageStr);
} else {
$stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($account->logo, $stream, ['mimetype' => $documentTypeData['mime']]);
fclose($stream);
if (Utils::isInterlaced($filePath)) {
$account->clearLogo();
Session::flash('error', trans('texts.logo_warning_invalid'));
} else {
$stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($account->logo, $stream, ['mimetype' => $documentTypeData['mime']]);
fclose($stream);
}
}
} catch (Exception $exception) {
Session::flash('warning', trans('texts.logo_warning_invalid'));
$account->clearLogo();
Session::flash('error', trans('texts.logo_warning_invalid'));
}
} else {
if (extension_loaded('fileinfo')) {
$image = Image::make($path);
$image->resize(200, 120, function ($constraint) {
$constraint->aspectRatio();
});
$account->logo = $account->account_key.'.png';
$image = Image::make($path);
$image = Image::canvas($image->width(), $image->height(), '#FFFFFF')->insert($image);
$imageStr = (string) $image->encode('png');
$disk->put($account->logo, $imageStr);
@ -1081,7 +1081,7 @@ class AccountController extends BaseController
$account->logo_width = $image->width();
$account->logo_height = $image->height();
} else {
Session::flash('warning', trans('texts.logo_warning_fileinfo'));
Session::flash('error', trans('texts.logo_warning_fileinfo'));
}
}
}

View File

@ -274,21 +274,21 @@ class AccountGatewayController extends BaseController
}
$plaidClientId = trim(Input::get('plaid_client_id'));
if ($plaidClientId = str_replace('*', '', $plaidClientId)) {
if (! $plaidClientId || $plaidClientId = str_replace('*', '', $plaidClientId)) {
$config->plaidClientId = $plaidClientId;
} elseif ($oldConfig && property_exists($oldConfig, 'plaidClientId')) {
$config->plaidClientId = $oldConfig->plaidClientId;
}
$plaidSecret = trim(Input::get('plaid_secret'));
if ($plaidSecret = str_replace('*', '', $plaidSecret)) {
if (! $plaidSecret || $plaidSecret = str_replace('*', '', $plaidSecret)) {
$config->plaidSecret = $plaidSecret;
} elseif ($oldConfig && property_exists($oldConfig, 'plaidSecret')) {
$config->plaidSecret = $oldConfig->plaidSecret;
}
$plaidPublicKey = trim(Input::get('plaid_public_key'));
if ($plaidPublicKey = str_replace('*', '', $plaidPublicKey)) {
if (! $plaidPublicKey || $plaidPublicKey = str_replace('*', '', $plaidPublicKey)) {
$config->plaidPublicKey = $plaidPublicKey;
} elseif ($oldConfig && property_exists($oldConfig, 'plaidPublicKey')) {
$config->plaidPublicKey = $oldConfig->plaidPublicKey;

View File

@ -83,10 +83,11 @@ class AppController extends BaseController
$_ENV['APP_ENV'] = 'production';
$_ENV['APP_DEBUG'] = $app['debug'];
$_ENV['REQUIRE_HTTPS'] = $app['https'];
$_ENV['APP_LOCALE'] = 'en';
$_ENV['APP_URL'] = $app['url'];
$_ENV['APP_KEY'] = $app['key'];
$_ENV['APP_CIPHER'] = env('APP_CIPHER', 'AES-256-CBC');
$_ENV['REQUIRE_HTTPS'] = $app['https'];
$_ENV['DB_TYPE'] = $dbType;
$_ENV['DB_HOST'] = $database['type']['host'];
$_ENV['DB_DATABASE'] = $database['type']['database'];

View File

@ -118,7 +118,17 @@ class AuthController extends Controller
public function getLoginWrapper()
{
if (! Utils::isNinja() && ! User::count()) {
return redirect()->to('invoice_now');
return redirect()->to('/setup');
}
if (Utils::isNinja() && ! Utils::isTravis()) {
// make sure the user is on SITE_URL/login to ensure OAuth works
$requestURL = request()->url();
$loginURL = SITE_URL . '/login';
$subdomain = Utils::getSubdomain(request()->url());
if ($requestURL != $loginURL && ! strstr($subdomain, 'webapp-')) {
return redirect()->to($loginURL);
}
}
return self::getLogin();

View File

@ -95,6 +95,9 @@ class ClientController extends BaseController
if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_QUOTE)) {
$actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)];
}
if ($user->can('create', ENTITY_RECURRING_INVOICE)) {
$actionLinks[] = ['label' => trans('texts.new_recurring_invoice'), 'url' => URL::to('/recurring_invoices/create/'.$client->public_id)];
}
if (! empty($actionLinks)) {
$actionLinks[] = \DropdownButton::DIVIDER;

View File

@ -651,7 +651,9 @@ class ClientPortalController extends BaseController
$documents = $invoice->documents;
foreach ($invoice->expenses as $expense) {
$documents = $documents->merge($expense->documents);
if ($expense->invoice_documents) {
$documents = $documents->merge($expense->documents);
}
}
$documents = $documents->sortBy('size');
@ -740,7 +742,7 @@ class ClientPortalController extends BaseController
$document = Document::scope($publicId, $invitation->account_id)->firstOrFail();
$authorized = false;
if ($document->expense && $document->expense->client_id == $invitation->invoice->client_id) {
if ($document->expense && $document->expense->invoice_documents && $document->expense->client_id == $invitation->invoice->client_id) {
$authorized = true;
} elseif ($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id) {
$authorized = true;

View File

@ -98,8 +98,6 @@ class ExpenseController extends BaseController
{
$expense = $request->entity();
$expense->expense_date = Utils::fromSqlDate($expense->expense_date);
$actions = [];
if ($expense->invoice) {
$actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')];

View File

@ -37,7 +37,7 @@ class ExportController extends BaseController
// set the filename based on the entity types selected
if ($request->include == 'all') {
$fileName = "invoice-ninja-{$date}";
$fileName = "{$date}-invoiceninja";
} else {
$fields = $request->all();
$fields = array_filter(array_map(function ($key) {
@ -47,7 +47,7 @@ class ExportController extends BaseController
return null;
}
}, array_keys($fields), $fields));
$fileName = 'invoice-ninja-' . implode('-', $fields) . "-{$date}";
$fileName = $date. '-invoiceninja-' . implode('-', $fields);
}
if ($format === 'JSON') {

View File

@ -34,16 +34,26 @@ class ImportController extends BaseController
$fileName = $entityType;
if ($request->hasFile($fileName)) {
$file = $request->file($fileName);
$destinationPath = storage_path() . '/import';
$destinationPath = env('FILE_IMPORT_PATH') ?: storage_path() . '/import';
$extension = $file->getClientOriginalExtension();
if (! in_array($extension, ['csv', 'xls', 'xlsx', 'json'])) {
continue;
if ($source === IMPORT_CSV) {
if ($extension != 'csv') {
return redirect()->to('/settings/' . ACCOUNT_IMPORT_EXPORT)->withError(trans('texts.invalid_file'));
}
} elseif ($source === IMPORT_JSON) {
if ($extension != 'json') {
return redirect()->to('/settings/' . ACCOUNT_IMPORT_EXPORT)->withError(trans('texts.invalid_file'));
}
} else {
if (! in_array($extension, ['csv', 'xls', 'xlsx', 'json'])) {
return redirect()->to('/settings/' . ACCOUNT_IMPORT_EXPORT)->withError(trans('texts.invalid_file'));
}
}
$newFileName = sprintf('%s_%s_%s.%s', Auth::user()->account_id, $timestamp, $fileName, $extension);
$file->move($destinationPath, $newFileName);
$files[$entityType] = $newFileName;
$files[$entityType] = $destinationPath . '/' . $newFileName;
}
}
@ -102,6 +112,7 @@ class ImportController extends BaseController
$map = Input::get('map');
$headers = Input::get('headers');
$timestamp = Input::get('timestamp');
if (config('queue.default') === 'sync') {
$results = $this->importService->importCSV($map, $headers, $timestamp);
$message = $this->importService->presentResults($results);

View File

@ -164,8 +164,9 @@ class InvoiceController extends BaseController
foreach ($invoice->invitations as $invitation) {
foreach ($client->contacts as $contact) {
if ($invitation->contact_id == $contact->id) {
$hasPassword = $account->isClientPortalPasswordEnabled() && $contact->password;
$contact->email_error = $invitation->email_error;
$contact->invitation_link = $invitation->getLink();
$contact->invitation_link = $invitation->getLink('view', $hasPassword, $hasPassword);
$contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false;
$contact->invitation_openend = $invitation->opened_date && $invitation->opened_date != '0000-00-00 00:00:00' ? $invitation->opened_date : false;
$contact->invitation_status = $contact->email_error ? false : $invitation->getStatus();
@ -313,7 +314,7 @@ class InvoiceController extends BaseController
'recurringHelp' => $recurringHelp,
'recurringDueDateHelp' => $recurringDueDateHelp,
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
'tasks' => Session::get('tasks') ? Session::get('tasks') : null,
'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null,
'expenses' => Session::get('expenses') ? Expense::scope(Session::get('expenses'))->with('documents', 'expense_category')->get() : [],
];
@ -404,7 +405,8 @@ class InvoiceController extends BaseController
if ($invoice->is_recurring) {
$response = $this->emailRecurringInvoice($invoice);
} else {
$this->dispatch(new SendInvoiceEmail($invoice, $reminder, $template));
$userId = Auth::user()->id;
$this->dispatch(new SendInvoiceEmail($invoice, $userId, $reminder, $template));
$response = true;
}
@ -436,7 +438,8 @@ class InvoiceController extends BaseController
if ($invoice->isPaid()) {
return true;
} else {
$this->dispatch(new SendInvoiceEmail($invoice));
$userId = Auth::user()->id;
$this->dispatch(new SendInvoiceEmail($invoice, $userId));
return true;
}
}

View File

@ -334,12 +334,14 @@ class OnlinePaymentController extends BaseController
'custom_value1' => Input::get('custom_client1'),
'custom_value2' => Input::get('custom_client2'),
];
if (request()->currency_code) {
$data['currency_code'] = request()->currency_code;
}
$client = $clientRepo->save($data, $client);
}
$data = [
'client_id' => $client->id,
'is_public' => true,
'is_recurring' => filter_var(Input::get('is_recurring'), FILTER_VALIDATE_BOOLEAN),
'frequency_id' => Input::get('frequency_id'),
'auto_bill_id' => Input::get('auto_bill_id'),

View File

@ -0,0 +1,223 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateProjectRequest;
use App\Http\Requests\ProjectRequest;
use App\Http\Requests\UpdateProjectRequest;
use App\Models\Project;
use App\Ninja\Repositories\ProjectRepository;
use App\Services\ProjectService;
use Auth;
use Input;
use Session;
use View;
/**
* Class ProjectApiController
* @package App\Http\Controllers
*/
class ProjectApiController extends BaseAPIController
{
/**
* @var ProjectRepository
*/
protected $projectRepo;
/**
* @var ProjectService
*/
protected $projectService;
/**
* @var string
*/
protected $entityType = ENTITY_PROJECT;
/**
* ProjectApiController constructor.
* @param ProjectRepository $projectRepo
* @param ProjectService $projectService
*/
public function __construct(ProjectRepository $projectRepo, ProjectService $projectService)
{
parent::__construct();
$this->projectRepo = $projectRepo;
$this->projectService = $projectService;
}
/**
* @SWG\Get(
* path="/projects",
* summary="List projects",
* operationId="listProjects",
* tags={"project"},
* @SWG\Response(
* response=200,
* description="A list of projects",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Project"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$projects = Project::scope()
->withTrashed()
->orderBy('created_at', 'desc');
return $this->listResponse($projects);
}
/**
* @SWG\Get(
* path="/projects/{project_id}",
* summary="Retrieve a project",
* operationId="getProject",
* tags={"project"},
* @SWG\Parameter(
* in="path",
* name="project_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Project"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(ProjectRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/projects",
* summary="Create a project",
* operationId="createProject",
* tags={"project"},
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/project")
* ),
* @SWG\Response(
* response=200,
* description="New project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function store(CreateProjectRequest $request)
{
$project = $this->projectService->save($request->input());
return $this->itemResponse($project);
}
/**
* @SWG\Put(
* path="/projects/{project_id}",
* summary="Update a project",
* operationId="updateProject",
* tags={"project"},
* @SWG\Parameter(
* in="path",
* name="project_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="project",
* @SWG\Schema(ref="#/definitions/project")
* ),
* @SWG\Response(
* response=200,
* description="Updated project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*
* @param mixed $publicId
*/
public function update(UpdateProjectRequest $request, $publicId)
{
if ($request->action) {
return $this->handleAction($request);
}
$data = $request->input();
$data['public_id'] = $publicId;
$project = $this->projectService->save($request->input(), $request->entity());
return $this->itemResponse($project);
}
/**
* @SWG\Delete(
* path="/projects/{project_id}",
* summary="Delete a project",
* operationId="deleteProject",
* tags={"project"},
* @SWG\Parameter(
* in="path",
* name="project_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted project",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/project"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*
*/
public function destroy(UpdateProjectRequest $request)
{
$project = $request->entity();
$this->projectRepo->delete($project);
return $this->itemResponse($project);
}
}

View File

@ -98,7 +98,7 @@ class QuoteController extends BaseController
return [
'entityType' => ENTITY_QUOTE,
'account' => $account,
'products' => Product::scope()->orderBy('id')->get(['product_key', 'notes', 'cost', 'qty']),
'products' => Product::scope()->with('default_tax_rate')->orderBy('product_key')->get(),
'taxRateOptions' => $account->present()->taxRateOptions,
'defaultTax' => $account->default_tax_rate,
'countries' => Cache::get('countries'),

View File

@ -27,7 +27,7 @@ class ReportController extends BaseController
->with(['clients.invoices.invoice_items', 'clients.contacts'])
->first();
$account = $account->hideFieldsForViz();
$clients = $account->clients->toJson();
$clients = $account->clients;
} elseif (file_exists($fileName)) {
$clients = file_get_contents($fileName);
$message = trans('texts.sample_data');
@ -129,11 +129,14 @@ class ReportController extends BaseController
}
$output = fopen('php://output', 'w') or Utils::fatalError();
$reportType = trans("texts.{$reportType}s");
$date = date('Y-m-d');
$columns = array_map(function($key, $val) {
return is_array($val) ? $key : $val;
}, array_keys($columns), $columns);
header('Content-Type:application/csv');
header("Content-Disposition:attachment;filename={$date}_Ninja_{$reportType}.csv");
header("Content-Disposition:attachment;filename={$date}-invoiceninja-{$reportType}-report.csv");
Utils::exportData($output, $data, Utils::trans($columns));

View File

@ -152,7 +152,6 @@ class VendorController extends BaseController
'data' => Input::old('data'),
'account' => Auth::user()->account,
'currencies' => Cache::get('currencies'),
'countries' => Cache::get('countries'),
];
}

View File

@ -135,33 +135,20 @@ class StartupCheck
$url = (Utils::isNinjaDev() ? SITE_URL : NINJA_APP_URL) . "/claim_license?license_key={$licenseKey}&product_id={$productId}&get_date=true";
$data = trim(CurlUtils::get($url));
if ($productId == PRODUCT_INVOICE_DESIGNS) {
if ($data = json_decode($data)) {
foreach ($data as $item) {
$design = new InvoiceDesign();
$design->id = $item->id;
$design->name = $item->name;
$design->pdfmake = $item->pdfmake;
$design->save();
}
if ($data == RESULT_FAILURE) {
Session::flash('error', trans('texts.invalid_white_label_license'));
} elseif ($data) {
$company = Auth::user()->account->company;
$company->plan_term = PLAN_TERM_YEARLY;
$company->plan_paid = $data;
$date = max(date_create($data), date_create($company->plan_expires));
$company->plan_expires = $date->modify('+1 year')->format('Y-m-d');
$company->plan = PLAN_WHITE_LABEL;
$company->save();
Cache::forget('invoiceDesigns');
Session::flash('message', trans('texts.bought_designs'));
}
} elseif ($productId == PRODUCT_WHITE_LABEL) {
if ($data && $data != RESULT_FAILURE) {
$company = Auth::user()->account->company;
$company->plan_term = PLAN_TERM_YEARLY;
$company->plan_paid = $data;
$date = max(date_create($data), date_create($company->plan_expires));
$company->plan_expires = $date->modify('+1 year')->format('Y-m-d');
$company->plan = PLAN_WHITE_LABEL;
$company->save();
Session::flash('message', trans('texts.bought_white_label'));
} else {
Session::flash('error', trans('texts.invalid_white_label_license'));
}
Session::flash('message', trans('texts.bought_white_label'));
} else {
Session::flash('error', trans('texts.white_label_license_error'));
}
}
}

View File

@ -2,8 +2,8 @@
namespace App\Http\ViewComposers;
use App\Models\Contact;
use DB;
use App\Models\Contact;
use Illuminate\View\View;
/**
@ -37,6 +37,7 @@ class ClientPortalHeaderComposer
}
$client = $contact->client;
$account = $contact->account;
$hasDocuments = DB::table('invoices')
->where('invoices.client_id', '=', $client->id)
@ -44,8 +45,18 @@ class ClientPortalHeaderComposer
->join('documents', 'documents.invoice_id', '=', 'invoices.id')
->count();
$hasPaymentMethods = false;
if ($account->getTokenGatewayId() && ! $account->enable_client_portal_dashboard) {
$hasPaymentMethods = DB::table('payment_methods')
->where('contacts.client_id', '=', $client->id)
->whereNull('payment_methods.deleted_at')
->join('contacts', 'contacts.id', '=', 'payment_methods.contact_id')
->count();
}
$view->with('hasQuotes', $client->publicQuotes->count());
$view->with('hasCredits', $client->creditsWithBalance->count());
$view->with('hasDocuments', $hasDocuments);
$view->with('hasPaymentMethods', $hasPaymentMethods);
}
}

View File

@ -319,6 +319,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function () {
Route::post('email_invoice', 'InvoiceApiController@emailInvoice');
Route::get('user_accounts', 'AccountApiController@getUserAccounts');
Route::resource('products', 'ProductApiController');
Route::resource('projects', 'ProjectApiController');
Route::resource('tax_rates', 'TaxRateApiController');
Route::resource('users', 'UserApiController');
Route::resource('expenses', 'ExpenseApiController');

View File

@ -8,6 +8,7 @@ use Illuminate\Queue\SerializesModels;
use Monolog\Logger;
use App\Services\ImportService;
use App\Ninja\Mailers\UserMailer;
use App\Models\User;
use Auth;
use App;
@ -39,7 +40,7 @@ class ImportData extends Job implements ShouldQueue
* @param mixed $files
* @param mixed $settings
*/
public function __construct($user, $type, $settings)
public function __construct(User $user, $type, $settings)
{
$this->user = $user;
$this->type = $type;

View File

@ -8,6 +8,8 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Monolog\Logger;
use Auth;
use App;
/**
* Class SendInvoiceEmail.
@ -31,6 +33,11 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/
protected $template;
/**
* @var int
*/
protected $userId;
/**
* Create a new job instance.
*
@ -39,9 +46,10 @@ class SendInvoiceEmail extends Job implements ShouldQueue
* @param bool $reminder
* @param mixed $pdfString
*/
public function __construct(Invoice $invoice, $reminder = false, $template = false)
public function __construct(Invoice $invoice, $userId = false, $reminder = false, $template = false)
{
$this->invoice = $invoice;
$this->userId = $userId;
$this->reminder = $reminder;
$this->template = $template;
}
@ -53,7 +61,16 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/
public function handle(ContactMailer $mailer)
{
// send email as user
if (App::runningInConsole() && $this->userId) {
Auth::onceUsingId($this->userId);
}
$mailer->sendInvoice($this->invoice, $this->reminder, $this->template);
if (App::runningInConsole() && $this->userId) {
Auth::logout();
}
}
/*

View File

@ -35,6 +35,11 @@ class SendNotificationEmail extends Job implements ShouldQueue
*/
protected $payment;
/**
* @var string
*/
protected $notes;
/**
* Create a new job instance.
@ -46,12 +51,13 @@ class SendNotificationEmail extends Job implements ShouldQueue
* @param mixed $type
* @param mixed $payment
*/
public function __construct($user, $invoice, $type, $payment)
public function __construct($user, $invoice, $type, $payment, $notes)
{
$this->user = $user;
$this->invoice = $invoice;
$this->type = $type;
$this->payment = $payment;
$this->notes = $notes;
}
/**
@ -61,6 +67,6 @@ class SendNotificationEmail extends Job implements ShouldQueue
*/
public function handle(UserMailer $userMailer)
{
$userMailer->sendNotification($this->user, $this->invoice, $this->type, $this->payment);
$userMailer->sendNotification($this->user, $this->invoice, $this->type, $this->payment, $this->notes);
}
}

View File

@ -162,7 +162,7 @@ class HistoryUtils
$icon = '<i class="fa fa-users" style="width:32px"></i>';
if ($item->client_id) {
$link = url('/clients/' . $item->client_id);
$name = $item->client_name;
$name = e($item->client_name);
$buttonLink = url('/invoices/create/' . $item->client_id);
$button = '<a type="button" class="btn btn-primary btn-sm pull-right" href="' . $buttonLink . '">

View File

@ -969,10 +969,11 @@ class Utils
return $str;
}
public static function getSubdomainPlaceholder()
public static function getSubdomain($url)
{
$parts = parse_url(SITE_URL);
$parts = parse_url($url);
$subdomain = '';
if (isset($parts['host'])) {
$host = explode('.', $parts['host']);
if (count($host) > 2) {
@ -983,6 +984,11 @@ class Utils
return $subdomain;
}
public static function getSubdomainPlaceholder()
{
return static::getSubdomain(SITE_URL);
}
public static function getDomainPlaceholder()
{
$parts = parse_url(SITE_URL);
@ -1234,4 +1240,13 @@ class Utils
{
return strlen($string) > $length ? rtrim(substr($string, 0, $length)) . '...' : $string;
}
// http://stackoverflow.com/a/14238078/497368
public static function isInterlaced($filename)
{
$handle = fopen($filename, 'r');
$contents = fread($handle, 32);
fclose($handle);
return( ord($contents[28]) != 0 );
}
}

View File

@ -392,7 +392,9 @@ class ActivityListener
$event->payment,
ACTIVITY_TYPE_CREATE_PAYMENT,
$event->payment->amount * -1,
$event->payment->amount
$event->payment->amount,
false,
\App::runningInConsole() ? 'auto_billed' : ''
);
}

View File

@ -56,8 +56,10 @@ class HandleUserLoggedIn
$account->loadLocalizationSettings();
if (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') || strpos($_SERVER['HTTP_USER_AGENT'], 'iPad')) {
if (strstr($_SERVER['HTTP_USER_AGENT'], 'iPhone') || strstr($_SERVER['HTTP_USER_AGENT'], 'iPad')) {
Session::flash('warning', trans('texts.iphone_app_message', ['link' => link_to(NINJA_IOS_APP_URL, trans('texts.iphone_app'))]));
} elseif (strstr($_SERVER['HTTP_USER_AGENT'], 'Android')) {
Session::flash('warning', trans('texts.iphone_app_message', ['link' => link_to(NINJA_ANDROID_APP_URL, trans('texts.android_app'))]));
}
// if they're using Stripe make sure they're using Stripe.js

View File

@ -46,13 +46,13 @@ class NotificationListener
* @param $type
* @param null $payment
*/
private function sendEmails($invoice, $type, $payment = null)
private function sendEmails($invoice, $type, $payment = null, $notes = false)
{
foreach ($invoice->account->users as $user)
{
if ($user->{"notify_{$type}"})
{
$this->userMailer->sendNotification($user, $invoice, $type, $payment);
dispatch(new SendNotificationEmail($user, $invoice, $type, $payment, $notes));
}
}
}
@ -62,7 +62,7 @@ class NotificationListener
*/
public function emailedInvoice(InvoiceWasEmailed $event)
{
$this->sendEmails($event->invoice, 'sent');
$this->sendEmails($event->invoice, 'sent', null, $event->notes);
$this->pushService->sendNotification($event->invoice, 'sent');
}
@ -71,7 +71,7 @@ class NotificationListener
*/
public function emailedQuote(QuoteWasEmailed $event)
{
$this->sendEmails($event->quote, 'sent');
$this->sendEmails($event->quote, 'sent', null, $event->notes);
$this->pushService->sendNotification($event->quote, 'sent');
}

View File

@ -5,13 +5,13 @@ namespace App\Listeners;
use App\Events\ClientWasCreated;
use App\Events\CreditWasCreated;
use App\Events\ExpenseWasCreated;
use App\Events\InvoiceWasCreated;
use App\Events\QuoteItemsWereCreated;
use App\Events\QuoteItemsWereUpdated;
use App\Events\InvoiceWasDeleted;
use App\Events\InvoiceWasUpdated;
use App\Events\PaymentWasCreated;
use App\Events\QuoteWasCreated;
use App\Events\InvoiceItemsWereCreated;
use App\Events\InvoiceItemsWereUpdated;
use App\Events\QuoteWasDeleted;
use App\Events\QuoteWasUpdated;
use App\Events\VendorWasCreated;
use App\Models\EntityModel;
use App\Ninja\Serializers\ArraySerializer;
@ -36,15 +36,6 @@ class SubscriptionListener
$this->checkSubscriptions(EVENT_CREATE_CLIENT, $event->client, $transformer);
}
/**
* @param QuoteWasCreated $event
*/
public function createdQuote(QuoteWasCreated $event)
{
$transformer = new InvoiceTransformer($event->quote->account);
$this->checkSubscriptions(EVENT_CREATE_QUOTE, $event->quote, $transformer, ENTITY_CLIENT);
}
/**
* @param PaymentWasCreated $event
*/
@ -54,15 +45,6 @@ class SubscriptionListener
$this->checkSubscriptions(EVENT_CREATE_PAYMENT, $event->payment, $transformer, [ENTITY_CLIENT, ENTITY_INVOICE]);
}
/**
* @param InvoiceWasCreated $event
*/
public function createdInvoice(InvoiceWasCreated $event)
{
$transformer = new InvoiceTransformer($event->invoice->account);
$this->checkSubscriptions(EVENT_CREATE_INVOICE, $event->invoice, $transformer, ENTITY_CLIENT);
}
/**
* @param CreditWasCreated $event
*/
@ -84,15 +66,42 @@ class SubscriptionListener
{
}
/**
* @param InvoiceWasCreated $event
*/
public function createdInvoice(InvoiceItemsWereCreated $event)
{
$transformer = new InvoiceTransformer($event->invoice->account);
$this->checkSubscriptions(EVENT_CREATE_INVOICE, $event->invoice, $transformer, ENTITY_CLIENT);
}
/**
* @param InvoiceWasUpdated $event
*/
public function updatedInvoice(InvoiceWasUpdated $event)
public function updatedInvoice(InvoiceItemsWereUpdated $event)
{
$transformer = new InvoiceTransformer($event->invoice->account);
$this->checkSubscriptions(EVENT_UPDATE_INVOICE, $event->invoice, $transformer, ENTITY_CLIENT);
}
/**
* @param QuoteWasCreated $event
*/
public function createdQuote(QuoteItemsWereCreated $event)
{
$transformer = new InvoiceTransformer($event->quote->account);
$this->checkSubscriptions(EVENT_CREATE_QUOTE, $event->quote, $transformer, ENTITY_CLIENT);
}
/**
* @param QuoteWasUpdated $event
*/
public function updatedQuote(QuoteItemsWereUpdated $event)
{
$transformer = new InvoiceTransformer($event->quote->account);
$this->checkSubscriptions(EVENT_UPDATE_QUOTE, $event->quote, $transformer, ENTITY_CLIENT);
}
/**
* @param InvoiceWasDeleted $event
*/
@ -102,15 +111,6 @@ class SubscriptionListener
$this->checkSubscriptions(EVENT_DELETE_INVOICE, $event->invoice, $transformer, ENTITY_CLIENT);
}
/**
* @param QuoteWasUpdated $event
*/
public function updatedQuote(QuoteWasUpdated $event)
{
$transformer = new InvoiceTransformer($event->quote->account);
$this->checkSubscriptions(EVENT_UPDATE_QUOTE, $event->quote, $transformer, ENTITY_CLIENT);
}
/**
* @param InvoiceWasDeleted $event
*/

View File

@ -7,13 +7,13 @@ use App\Events\UserSettingsChanged;
use App\Models\Traits\GeneratesNumbers;
use App\Models\Traits\PresentsInvoice;
use App\Models\Traits\SendsEmails;
use App\Models\Traits\HasLogo;
use Cache;
use Carbon;
use DateTime;
use Eloquent;
use Event;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Laracasts\Presenter\PresentableTrait;
use Session;
use Utils;
@ -28,6 +28,7 @@ class Account extends Eloquent
use PresentsInvoice;
use GeneratesNumbers;
use SendsEmails;
use HasLogo;
/**
* @var string
@ -161,6 +162,8 @@ class Account extends Eloquent
'payment_type_id',
'gateway_fee_enabled',
'reset_counter_date',
'custom_contact_label1',
'custom_contact_label2',
'domain_id',
];
@ -370,6 +373,14 @@ class Account extends Eloquent
return $this->belongsTo('App\Models\TaxRate');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function payment_type()
{
return $this->belongsTo('App\Models\PaymentType');
}
/**
* @return mixed
*/
@ -826,101 +837,6 @@ class Account extends Eloquent
return false;
}
/**
* @return bool
*/
public function hasLogo()
{
return ! empty($this->logo);
}
/**
* @return mixed
*/
public function getLogoDisk()
{
return Storage::disk(env('LOGO_FILESYSTEM', 'logos'));
}
protected function calculateLogoDetails()
{
$disk = $this->getLogoDisk();
if ($disk->exists($this->account_key.'.png')) {
$this->logo = $this->account_key.'.png';
} elseif ($disk->exists($this->account_key.'.jpg')) {
$this->logo = $this->account_key.'.jpg';
}
if (! empty($this->logo)) {
$image = imagecreatefromstring($disk->get($this->logo));
$this->logo_width = imagesx($image);
$this->logo_height = imagesy($image);
$this->logo_size = $disk->size($this->logo);
} else {
$this->logo = null;
}
$this->save();
}
/**
* @return null
*/
public function getLogoRaw()
{
if (! $this->hasLogo()) {
return null;
}
$disk = $this->getLogoDisk();
return $disk->get($this->logo);
}
/**
* @param bool $cachebuster
*
* @return null|string
*/
public function getLogoURL($cachebuster = false)
{
if (! $this->hasLogo()) {
return null;
}
$disk = $this->getLogoDisk();
$adapter = $disk->getAdapter();
if ($adapter instanceof \League\Flysystem\Adapter\Local) {
// Stored locally
$logoUrl = url('/logo/' . $this->logo);
if ($cachebuster) {
$logoUrl .= '?no_cache='.time();
}
return $logoUrl;
}
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk());
}
public function getLogoPath()
{
if (! $this->hasLogo()) {
return null;
}
$disk = $this->getLogoDisk();
$adapter = $disk->getAdapter();
if ($adapter instanceof \League\Flysystem\Adapter\Local) {
return $adapter->applyPathPrefix($this->logo);
} else {
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk());
}
}
/**
* @return mixed
*/
@ -948,30 +864,6 @@ class Account extends Eloquent
return null;
}
/**
* @return mixed|null
*/
public function getLogoWidth()
{
if (! $this->hasLogo()) {
return null;
}
return $this->logo_width;
}
/**
* @return mixed|null
*/
public function getLogoHeight()
{
if (! $this->hasLogo()) {
return null;
}
return $this->logo_height;
}
/**
* @param $entityType
* @param null $clientId
@ -1338,26 +1230,6 @@ class Account extends Eloquent
return Carbon::instance($date);
}
/**
* @return float|null
*/
public function getLogoSize()
{
if (! $this->hasLogo()) {
return null;
}
return round($this->logo_size / 1000);
}
/**
* @return bool
*/
public function isLogoTooLarge()
{
return $this->getLogoSize() > MAX_LOGO_FILE_SIZE;
}
/**
* @param $eventId
*
@ -1776,6 +1648,11 @@ class Account extends Eloquent
return $yearStart->format('Y-m-d');
}
public function isClientPortalPasswordEnabled()
{
return $this->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->enable_portal_password;
}
}
Account::updated(function ($account) {

View File

@ -294,7 +294,7 @@ class Client extends EntityModel
}
}
if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->account->enable_portal_password) {
if ($this->account->isClientPortalPasswordEnabled()) {
if (! empty($data['password']) && $data['password'] != '-%unchanged%-') {
$contact->password = bcrypt($data['password']);
} elseif (empty($data['password'])) {

View File

@ -37,6 +37,8 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
'email',
'phone',
'send_invoice',
'custom_value1',
'custom_value2',
];
/**

View File

@ -47,6 +47,10 @@ class Expense extends EntityModel
'tax_name1',
'tax_rate2',
'tax_name2',
'payment_date',
'payment_type_id',
'transaction_reference',
'invoice_documents',
];
public static function getImportColumns()
@ -129,6 +133,14 @@ class Expense extends EntityModel
return $this->hasMany('App\Models\Document')->orderBy('id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function payment_type()
{
return $this->belongsTo('App\Models\PaymentType');
}
/**
* @return mixed
*/
@ -175,6 +187,14 @@ class Expense extends EntityModel
return $this->invoice_currency_id != $this->expense_currency_id || $this->exchange_rate != 1;
}
/**
* @return bool
*/
public function isPaid()
{
return $this->payment_date || $this->payment_type_id;
}
/**
* @return float
*/
@ -221,19 +241,23 @@ class Expense extends EntityModel
{
$statuses = [];
$statuses[EXPENSE_STATUS_LOGGED] = trans('texts.logged');
$statuses[EXPENSE_STATUS_PENDING] = trans('texts.pending');
$statuses[EXPENSE_STATUS_INVOICED] = trans('texts.invoiced');
$statuses[EXPENSE_STATUS_BILLED] = trans('texts.billed');
$statuses[EXPENSE_STATUS_PAID] = trans('texts.paid');
$statuses[EXPENSE_STATUS_UNPAID] = trans('texts.unpaid');
return $statuses;
}
public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance)
public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance, $paymentDate)
{
if ($invoiceId) {
if (floatval($balance) > 0) {
$label = 'invoiced';
} else {
$label = 'paid';
$label = 'billed';
}
} elseif ($shouldBeInvoiced) {
$label = 'pending';
@ -241,7 +265,13 @@ class Expense extends EntityModel
$label = 'logged';
}
return trans("texts.{$label}");
$label = trans("texts.{$label}");
if ($paymentDate) {
$label = trans('texts.paid') . ' | ' . $label;
}
return $label;
}
public static function calcStatusClass($shouldBeInvoiced, $invoiceId, $balance)
@ -270,7 +300,7 @@ class Expense extends EntityModel
{
$balance = $this->invoice ? $this->invoice->balance : 0;
return static::calcStatusLabel($this->should_be_invoiced, $this->invoice_id, $balance);
return static::calcStatusLabel($this->should_be_invoiced, $this->invoice_id, $balance, $this->payment_date);
}
}

View File

@ -66,7 +66,7 @@ class Invitation extends EntityModel
*
* @return string
*/
public function getLink($type = 'view', $forceOnsite = false)
public function getLink($type = 'view', $forceOnsite = false, $forcePlain = false)
{
if (! $this->account) {
$this->load('account');
@ -87,7 +87,7 @@ class Invitation extends EntityModel
if ($iframe_url && ! $forceOnsite) {
return "{$iframe_url}?{$this->invitation_key}";
} elseif ($this->account->subdomain) {
} elseif ($this->account->subdomain && ! $forcePlain) {
$url = Utils::replaceSubdomain($url, $account->subdomain);
}
}

View File

@ -93,54 +93,23 @@ class Invoice extends EntityModel implements BalanceAffecting
INVOICE_STATUS_PAID => 'success',
];
/**
* @var string
*/
public static $fieldInvoiceNumber = 'invoice_number';
/**
* @var string
*/
public static $fieldPONumber = 'po_number';
/**
* @var string
*/
public static $fieldInvoiceDate = 'invoice_date';
/**
* @var string
*/
public static $fieldDueDate = 'due_date';
/**
* @var string
*/
public static $fieldAmount = 'amount';
/**
* @var string
*/
public static $fieldPaid = 'paid';
/**
* @var string
*/
public static $fieldNotes = 'notes';
/**
* @var string
*/
public static $fieldTerms = 'terms';
/**
* @return array
*/
public static function getImportColumns()
{
return [
Client::$fieldName,
self::$fieldInvoiceNumber,
self::$fieldPONumber,
self::$fieldInvoiceDate,
self::$fieldDueDate,
self::$fieldAmount,
self::$fieldPaid,
self::$fieldNotes,
self::$fieldTerms,
'name',
'invoice_number',
'po_number',
'invoice_date',
'due_date',
'amount',
'paid',
'notes',
'terms',
'product',
'quantity',
];
}
@ -159,6 +128,8 @@ class Invoice extends EntityModel implements BalanceAffecting
'due date' => 'due_date',
'terms' => 'terms',
'notes' => 'notes',
'product|item' => 'product',
'quantity|qty' => 'quantity',
];
}
@ -930,6 +901,8 @@ class Invoice extends EntityModel implements BalanceAffecting
'last_name',
'email',
'phone',
'custom_value1',
'custom_value2',
]);
}
@ -1439,6 +1412,22 @@ class Invoice extends EntityModel implements BalanceAffecting
$taxes[$key]['paid'] += $paid;
}
/**
* @return int
*/
public function countDocuments()
{
$count = count($this->documents);
foreach ($this->expenses as $expense) {
if ($expense->invoice_documents) {
$count += count($expense->documents);
}
}
return $count;
}
/**
* @return bool
*/
@ -1457,7 +1446,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function hasExpenseDocuments()
{
foreach ($this->expenses as $expense) {
if (count($expense->documents)) {
if ($expense->invoice_documents && count($expense->documents)) {
return true;
}
}

View File

@ -29,6 +29,11 @@ class PaymentTerm extends EntityModel
return ENTITY_PAYMENT_TERM;
}
public function getNumDays()
{
return $this->num_days == -1 ? 0 : $this->num_days;
}
public static function getSelectOptions()
{
$terms = Cache::get('paymentTerms');
@ -37,6 +42,10 @@ class PaymentTerm extends EntityModel
$terms->push($term);
}
foreach ($terms as $term) {
$term->name = trans('texts.payment_terms_net') . ' ' . $term->getNumDays();
}
return $terms->sortBy('num_days');
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Models\Traits;
use Utils;
use Illuminate\Support\Facades\Storage;
/**
* Class HasLogo.
*/
trait HasLogo
{
/**
* @return bool
*/
public function hasLogo()
{
return ! empty($this->logo);
}
/**
* @return mixed
*/
public function getLogoDisk()
{
return Storage::disk(env('LOGO_FILESYSTEM', 'logos'));
}
protected function calculateLogoDetails()
{
$disk = $this->getLogoDisk();
if ($disk->exists($this->account_key.'.png')) {
$this->logo = $this->account_key.'.png';
} elseif ($disk->exists($this->account_key.'.jpg')) {
$this->logo = $this->account_key.'.jpg';
}
if (! empty($this->logo)) {
$image = imagecreatefromstring($disk->get($this->logo));
$this->logo_width = imagesx($image);
$this->logo_height = imagesy($image);
$this->logo_size = $disk->size($this->logo);
} else {
$this->logo = null;
}
$this->save();
}
/**
* @return null
*/
public function getLogoRaw()
{
if (! $this->hasLogo()) {
return null;
}
$disk = $this->getLogoDisk();
if (! $disk->exists($this->logo)) {
return null;
}
return $disk->get($this->logo);
}
/**
* @param bool $cachebuster
*
* @return null|string
*/
public function getLogoURL($cachebuster = false)
{
if (! $this->hasLogo()) {
return null;
}
$disk = $this->getLogoDisk();
$adapter = $disk->getAdapter();
if ($adapter instanceof \League\Flysystem\Adapter\Local) {
// Stored locally
$logoUrl = url('/logo/' . $this->logo);
if ($cachebuster) {
$logoUrl .= '?no_cache='.time();
}
return $logoUrl;
}
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk());
}
public function getLogoPath()
{
if (! $this->hasLogo()) {
return null;
}
$disk = $this->getLogoDisk();
$adapter = $disk->getAdapter();
if ($adapter instanceof \League\Flysystem\Adapter\Local) {
return $adapter->applyPathPrefix($this->logo);
} else {
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk());
}
}
/**
* @return mixed|null
*/
public function getLogoWidth()
{
if (! $this->hasLogo()) {
return null;
}
return $this->logo_width;
}
/**
* @return mixed|null
*/
public function getLogoHeight()
{
if (! $this->hasLogo()) {
return null;
}
return $this->logo_height;
}
/**
* @return float|null
*/
public function getLogoSize()
{
if (! $this->hasLogo()) {
return null;
}
return round($this->logo_size / 1000);
}
/**
* @return bool
*/
public function isLogoTooLarge()
{
return $this->getLogoSize() > MAX_LOGO_FILE_SIZE;
}
public function clearLogo()
{
$this->logo = '';
$this->logo_width = 0;
$this->logo_height = 0;
$this->logo_size = 0;
}
}

View File

@ -68,6 +68,12 @@ trait PresentsInvoice
if ($this->custom_client_label2) {
$fields[INVOICE_FIELDS_CLIENT][] = 'client.custom_value2';
}
if ($this->custom_contact_label1) {
$fields[INVOICE_FIELDS_CLIENT][] = 'contact.custom_value1';
}
if ($this->custom_contact_label2) {
$fields[INVOICE_FIELDS_CLIENT][] = 'contact.custom_value2';
}
if ($this->custom_label1) {
$fields['account_fields2'][] = 'account.custom_value1';
}
@ -86,8 +92,10 @@ trait PresentsInvoice
'invoice.po_number',
'invoice.invoice_date',
'invoice.due_date',
'invoice.invoice_total',
'invoice.balance_due',
'invoice.partial_due',
'invoice.outstanding',
'invoice.custom_text_value1',
'invoice.custom_text_value2',
'.blank',
@ -108,6 +116,8 @@ trait PresentsInvoice
'client.phone',
'client.custom_value1',
'client.custom_value2',
'contact.custom_value1',
'contact.custom_value2',
'.blank',
],
INVOICE_FIELDS_ACCOUNT => [
@ -227,6 +237,8 @@ trait PresentsInvoice
'credit_to',
'your_credit',
'work_phone',
'invoice_total',
'outstanding',
];
foreach ($fields as $field) {
@ -242,12 +254,14 @@ trait PresentsInvoice
}
foreach ([
'account.custom_value1' => 'custom_label1',
'account.custom_value2' => 'custom_label2',
'invoice.custom_text_value1' => 'custom_invoice_text_label1',
'invoice.custom_text_value2' => 'custom_invoice_text_label2',
'client.custom_value1' => 'custom_client_label1',
'client.custom_value2' => 'custom_client_label2',
'account.custom_value1' => 'custom_label1',
'account.custom_value2' => 'custom_label2',
'contact.custom_value1' => 'custom_contact_label1',
'contact.custom_value2' => 'custom_contact_label2',
] as $field => $property) {
$data[$field] = $this->$property ?: trans('texts.custom_field');
}

View File

@ -69,7 +69,7 @@ trait SendsEmails
$template .= "$message<p/>";
}
return $template . '$footer';
return $template . '$emailSignature';
}
/**

View File

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

View File

@ -90,7 +90,7 @@ class ExpenseDatatable extends EntityDatatable
[
'status',
function ($model) {
return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced, $model->balance);
return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced, $model->balance, $model->payment_date);
},
],
];
@ -129,9 +129,9 @@ class ExpenseDatatable extends EntityDatatable
];
}
private function getStatusLabel($invoiceId, $shouldBeInvoiced, $balance)
private function getStatusLabel($invoiceId, $shouldBeInvoiced, $balance, $paymentDate)
{
$label = Expense::calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance);
$label = Expense::calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance, $paymentDate);
$class = Expense::calcStatusClass($shouldBeInvoiced, $invoiceId, $balance);
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";

View File

@ -52,7 +52,7 @@ class PaymentDatatable extends EntityDatatable
[
'method',
function ($model) {
return ($model->payment_type && ! $model->last4) ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : '');
return ($model->payment_type && ! $model->last4) ? trans('texts.payment_type_' . $model->payment_type) : ($model->account_gateway_id ? $model->gateway_name : '');
},
],
[

View File

@ -37,10 +37,10 @@ class InvoiceTransformer extends BaseTransformer
'due_date_sql' => $this->getDate($data, 'due_date'),
'invoice_items' => [
[
'product_key' => '',
'product_key' => $this->getString($data, 'product'),
'notes' => $this->getString($data, 'notes'),
'cost' => $this->getFloat($data, 'amount'),
'qty' => 1,
'qty' => $this->getFloat($data, 'quantity') ?: 1,
],
],
];

View File

@ -29,7 +29,7 @@ class BaseIntent
$this->state = $state;
$this->data = $data;
// If they're viewing a client set it as the current state
if (! $this->hasField('Filter', 'all')) {
$url = url()->previous();
@ -237,7 +237,6 @@ class BaseIntent
foreach ($compositeEntity->children as $child) {
if ($child->type == 'Field') {
$field = $child->value;
;
} elseif ($child->type == 'Value') {
$value = $child->value;
}

View File

@ -104,9 +104,9 @@ class ContactMailer extends Mailer
if ($sent === true) {
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
event(new QuoteWasEmailed($invoice));
event(new QuoteWasEmailed($invoice, $reminder));
} else {
event(new InvoiceWasEmailed($invoice));
event(new InvoiceWasEmailed($invoice, $reminder));
}
}
@ -140,7 +140,7 @@ class ContactMailer extends Mailer
$account = $invoice->account;
$user = $invitation->user;
if ($invitation->user->trashed()) {
if ($user->trashed()) {
$user = $account->users()->orderBy('id')->first();
}
@ -166,7 +166,7 @@ class ContactMailer extends Mailer
$variables['autobill'] = $invoice->present()->autoBillEmailMessage();
}
if (empty($invitation->contact->password) && $account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $account->enable_portal_password && $account->send_portal_password) {
if (empty($invitation->contact->password) && $account->isClientPortalPasswordEnabled() && $account->send_portal_password) {
// The contact needs a password
$variables['password'] = $password = $this->generatePassword();
$invitation->contact->password = bcrypt($password);
@ -254,7 +254,7 @@ class ContactMailer extends Mailer
$invitation = $payment->invitation;
} else {
$user = $payment->user;
$contact = $client->contacts[0];
$contact = count($client->contacts) ? $client->contacts[0] : '';
$invitation = $payment->invoice->invitations[0];
}

View File

@ -48,7 +48,8 @@ class UserMailer extends Mailer
User $user,
Invoice $invoice,
$notificationType,
Payment $payment = null
Payment $payment = null,
$notes = false
) {
if (! $user->email || $user->cannot('view', $invoice)) {
return;
@ -81,6 +82,10 @@ class UserMailer extends Mailer
'client' => $client->getDisplayName(),
]);
if ($notes) {
$subject .= ' [' . trans('texts.notes_' . $notes) . ']';
}
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}

View File

@ -155,6 +155,8 @@ class AccountPresenter extends Presenter
$fields = [
'custom_client_label1' => 'custom_client1',
'custom_client_label2' => 'custom_client2',
'custom_contact_label1' => 'custom_contact1',
'custom_contact_label2' => 'custom_contact2',
'custom_invoice_text_label1' => 'custom_invoice1',
'custom_invoice_text_label2' => 'custom_invoice2',
'custom_invoice_item_label1' => 'custom_product1',

View File

@ -26,6 +26,14 @@ class ExpensePresenter extends EntityPresenter
return Utils::fromSqlDate($this->entity->expense_date);
}
/**
* @return \DateTime|string
*/
public function payment_date()
{
return Utils::fromSqlDate($this->entity->payment_date);
}
public function month()
{
return Carbon::parse($this->entity->payment_date)->format('Y m');

View File

@ -80,4 +80,39 @@ class AbstractReport
return $str;
}
// convert the date format to one supported by tablesorter
public function convertDateFormat()
{
$account = Auth::user()->account;
$format = $account->getMomentDateFormat();
$format = strtolower($format);
$format = str_replace('do', '', $format);
$orignalFormat = $format;
$format = preg_replace("/[^mdy]/", '', $format);
$lastLetter = false;
$reportParts = [];
$phpParts = [];
foreach (str_split($format) as $letter) {
if ($lastLetter && $letter == $lastLetter) {
continue;
}
$lastLetter = $letter;
if ($letter == 'm') {
$reportParts[] = 'mm';
$phpParts[] = 'm';
} elseif ($letter == 'd') {
$reportParts[] = 'dd';
$phpParts[] = 'd';
} elseif ($letter == 'y') {
$reportParts[] = 'yyyy';
$phpParts[] = 'Y';
}
}
return join('', $reportParts);
}
}

View File

@ -356,6 +356,9 @@ class AccountRepository
$account->company_id = $company->id;
$account->save();
$emailSettings = new AccountEmailSettings();
$account->account_email_settings()->save($emailSettings);
$random = strtolower(str_random(RANDOM_KEY_LENGTH));
$user = new User();
$user->registered = true;

View File

@ -90,6 +90,8 @@ class ActivityRepository
'activities.balance',
'activities.adjustment',
'activities.notes',
'activities.ip',
'activities.is_system',
'users.first_name as user_first_name',
'users.last_name as user_last_name',
'users.email as user_email',

View File

@ -60,6 +60,7 @@ class ClientRepository extends BaseRepository
if ($filter) {
$query->where(function ($query) use ($filter) {
$query->where('clients.name', 'like', '%'.$filter.'%')
->orWhere('clients.id_number', 'like', '%'.$filter.'%')
->orWhere('contacts.first_name', 'like', '%'.$filter.'%')
->orWhere('contacts.last_name', 'like', '%'.$filter.'%')
->orWhere('contacts.email', 'like', '%'.$filter.'%');

View File

@ -81,6 +81,7 @@ class ExpenseRepository extends BaseRepository
'expenses.user_id',
'expenses.tax_rate1',
'expenses.tax_rate2',
'expenses.payment_date',
'expense_categories.name as category',
'expense_categories.user_id as category_user_id',
'expense_categories.public_id as category_public_id',
@ -112,14 +113,24 @@ class ExpenseRepository extends BaseRepository
}
if (in_array(EXPENSE_STATUS_INVOICED, $statuses)) {
$query->orWhere('expenses.invoice_id', '>', 0);
if (! in_array(EXPENSE_STATUS_PAID, $statuses)) {
if (! in_array(EXPENSE_STATUS_BILLED, $statuses)) {
$query->where('invoices.balance', '>', 0);
}
}
if (in_array(EXPENSE_STATUS_PAID, $statuses)) {
if (in_array(EXPENSE_STATUS_BILLED, $statuses)) {
$query->orWhere('invoices.balance', '=', 0)
->where('expenses.invoice_id', '>', 0);
}
if (in_array(EXPENSE_STATUS_PAID, $statuses)) {
$query->orWhereNotNull('expenses.payment_date');
}
if (in_array(EXPENSE_STATUS_UNPAID, $statuses)) {
$query->orWhereNull('expenses.payment_date');
}
if (in_array(EXPENSE_STATUS_PENDING, $statuses)) {
$query->orWhere('expenses.should_be_invoiced', '=', 1)
->whereNull('expenses.invoice_id');
}
});
}
@ -161,6 +172,9 @@ class ExpenseRepository extends BaseRepository
if (isset($input['expense_date'])) {
$expense->expense_date = Utils::toSqlDate($input['expense_date']);
}
if (isset($input['payment_date'])) {
$expense->payment_date = Utils::toSqlDate($input['payment_date']);
}
$expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false;

View File

@ -2,10 +2,10 @@
namespace App\Ninja\Repositories;
use App\Events\InvoiceWasCreated;
use App\Events\InvoiceWasUpdated;
use App\Events\QuoteWasCreated;
use App\Events\QuoteWasUpdated;
use App\Events\QuoteItemsWereCreated;
use App\Events\QuoteItemsWereUpdated;
use App\Events\InvoiceItemsWereCreated;
use App\Events\InvoiceItemsWereUpdated;
use App\Jobs\SendInvoiceEmail;
use App\Models\Account;
use App\Models\Client;
@ -693,11 +693,13 @@ class InvoiceRepository extends BaseRepository
$invoice->invoice_items()->save($invoiceItem);
}
$invoice->load('invoice_items');
if (Auth::check()) {
$invoice = $this->saveInvitations($invoice);
}
//$this->dispachEvents($invoice);
$this->dispatchEvents($invoice);
return $invoice;
}
@ -708,6 +710,10 @@ class InvoiceRepository extends BaseRepository
$client->load('contacts');
$sendInvoiceIds = [];
if (! count($client->contacts)) {
return $invoice;
}
foreach ($client->contacts as $contact) {
if ($contact->send_invoice) {
$sendInvoiceIds[] = $contact->id;
@ -740,19 +746,19 @@ class InvoiceRepository extends BaseRepository
return $invoice;
}
private function dispachEvents($invoice)
private function dispatchEvents($invoice)
{
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
if ($invoice->wasRecentlyCreated) {
event(new QuoteWasCreated($invoice));
event(new QuoteItemsWereCreated($invoice));
} else {
event(new QuoteWasUpdated($invoice));
event(new QuoteItemsWereUpdated($invoice));
}
} else {
if ($invoice->wasRecentlyCreated) {
event(new InvoiceWasCreated($invoice));
event(new InvoiceItemsWereCreated($invoice));
} else {
event(new InvoiceWasUpdated($invoice));
event(new InvoiceItemsWereUpdated($invoice));
}
}
}
@ -1110,7 +1116,6 @@ class InvoiceRepository extends BaseRepository
if ($item['invoice_item_type_id'] == INVOICE_ITEM_TYPE_PENDING_GATEWAY_FEE) {
unset($data['invoice_items'][$key]);
$this->save($data, $invoice);
$invoice->load('invoice_items');
break;
}
}
@ -1147,7 +1152,6 @@ class InvoiceRepository extends BaseRepository
$data['invoice_items'][] = $item;
$this->save($data, $invoice);
$invoice->load('invoice_items');
}
public function findPhonetically($invoiceNumber)

View File

@ -7,6 +7,7 @@ use App\Models\Project;
use App\Models\Task;
use Auth;
use Session;
use DB;
class TaskRepository extends BaseRepository
{
@ -17,7 +18,7 @@ class TaskRepository extends BaseRepository
public function find($clientPublicId = null, $filter = null)
{
$query = \DB::table('tasks')
$query = DB::table('tasks')
->leftJoin('clients', 'tasks.client_id', '=', 'clients.id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'tasks.invoice_id')
@ -30,7 +31,7 @@ class TaskRepository extends BaseRepository
->where('contacts.deleted_at', '=', null)
->select(
'tasks.public_id',
\DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"),
DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"),
'clients.public_id as client_public_id',
'clients.user_id as client_user_id',
'contacts.first_name',
@ -49,7 +50,7 @@ class TaskRepository extends BaseRepository
'tasks.time_log',
'tasks.time_log as duration',
'tasks.created_at',
'tasks.created_at as date',
DB::raw("SUBSTRING(time_log, 3, 10) date"),
'tasks.user_id',
'projects.name as project',
'projects.public_id as project_public_id',

View File

@ -213,7 +213,7 @@ class AccountTransformer extends EntityTransformer
'num_days_reminder3' => $account->num_days_reminder3,
'custom_invoice_text_label1' => $account->custom_invoice_text_label1,
'custom_invoice_text_label2' => $account->custom_invoice_text_label2,
'default_tax_rate_id' => $account->default_tax_rate_id,
'default_tax_rate_id' => $account->default_tax_rate_id ? $account->default_tax_rate->public_id : 0,
'recurring_hour' => $account->recurring_hour,
'invoice_number_pattern' => $account->invoice_number_pattern,
'quote_number_pattern' => $account->quote_number_pattern,
@ -266,6 +266,8 @@ class AccountTransformer extends EntityTransformer
'payment_type_id' => (int) $account->payment_type_id,
'gateway_fee_enabled' => (bool) $account->gateway_fee_enabled,
'reset_counter_date' => $account->reset_counter_date,
'custom_contact_label1' => $account->custom_contact_label1,
'custom_contact_label2' => $account->custom_contact_label2,
];
}
}

View File

@ -26,6 +26,8 @@ class ContactTransformer extends EntityTransformer
* @SWG\Property(property="phone", type="string", example="(212) 555-1212")
* @SWG\Property(property="last_login", type="string", format="date-time", example="2016-01-01 12:10:00")
* @SWG\Property(property="send_invoice", type="boolean", example=false)
* @SWG\Property(property="custom_value1", type="string", example="Value")
* @SWG\Property(property="custom_value2", type="string", example="Value")
*/
public function transform(Contact $contact)
{
@ -40,6 +42,8 @@ class ContactTransformer extends EntityTransformer
'phone' => $contact->phone,
'last_login' => $contact->last_login,
'send_invoice' => (bool) $contact->send_invoice,
'custom_value1' => $contact->custom_value1,
'custom_value2' => $contact->custom_value2,
]);
}
}

View File

@ -27,7 +27,7 @@ class ProductTransformer extends EntityTransformer
'notes' => $product->notes,
'cost' => $product->cost,
'qty' => $product->qty,
'default_tax_rate_id' => $product->default_tax_rate_id,
'default_tax_rate_id' => $product->default_tax_rate_id ? $product->default_tax_rate->public_id : 0,
'updated_at' => $this->getTimestamp($product->updated_at),
'archived_at' => $this->getTimestamp($product->deleted_at),
]);

View File

@ -28,7 +28,7 @@ class AppServiceProvider extends ServiceProvider
$contents = $image;
}
return 'data:image/jpeg;base64,' . base64_encode($contents);
return $contents ? 'data:image/jpeg;base64,' . base64_encode($contents) : '';
});
Form::macro('nav_link', function ($url, $text) {

View File

@ -17,9 +17,12 @@ class ComposerServiceProvider extends ServiceProvider
[
'accounts.details',
'clients.edit',
'vendors.edit',
'payments.edit',
'invoices.edit',
'expenses.edit',
'accounts.localization',
'payments.credit_card',
],
'App\Http\ViewComposers\TranslationComposer'
);

View File

@ -32,12 +32,16 @@ class EventServiceProvider extends ServiceProvider
// Invoices
'App\Events\InvoiceWasCreated' => [
'App\Listeners\ActivityListener@createdInvoice',
'App\Listeners\SubscriptionListener@createdInvoice',
'App\Listeners\InvoiceListener@createdInvoice',
],
'App\Events\InvoiceWasUpdated' => [
'App\Listeners\ActivityListener@updatedInvoice',
'App\Listeners\InvoiceListener@updatedInvoice',
],
'App\Events\InvoiceItemsWereCreated' => [
'App\Listeners\SubscriptionListener@createdInvoice',
],
'App\Events\InvoiceItemsWereUpdated' => [
'App\Listeners\SubscriptionListener@updatedInvoice',
],
'App\Events\InvoiceWasArchived' => [
@ -66,10 +70,14 @@ class EventServiceProvider extends ServiceProvider
// Quotes
'App\Events\QuoteWasCreated' => [
'App\Listeners\ActivityListener@createdQuote',
'App\Listeners\SubscriptionListener@createdQuote',
],
'App\Events\QuoteWasUpdated' => [
'App\Listeners\ActivityListener@updatedQuote',
],
'App\Events\QuoteItemsWereCreated' => [
'App\Listeners\SubscriptionListener@createdQuote',
],
'App\Events\QuoteItemsWereUpdated' => [
'App\Listeners\SubscriptionListener@updatedQuote',
],
'App\Events\QuoteWasArchived' => [

View File

@ -24,6 +24,7 @@ use Auth;
use Cache;
use Excel;
use Exception;
use File;
use League\Fractal\Manager;
use parsecsv;
use Session;
@ -145,10 +146,9 @@ class ImportService
*
* @return array
*/
public function importJSON($file, $includeData, $includeSettings)
public function importJSON($fileName, $includeData, $includeSettings)
{
$this->initMaps();
$fileName = storage_path() . '/import/' . $file;
$this->checkForFile($fileName);
$file = file_get_contents($fileName);
$json = json_decode($file, true);
@ -229,7 +229,7 @@ class ImportService
}
}
@unlink($fileName);
File::delete($fileName);
return $this->results;
}
@ -278,7 +278,7 @@ class ImportService
*
* @return array
*/
private function execute($source, $entityType, $file)
private function execute($source, $entityType, $fileName)
{
$results = [
RESULT_SUCCESS => [],
@ -287,7 +287,6 @@ class ImportService
// Convert the data
$row_list = [];
$fileName = storage_path() . '/import/' . $file;
$this->checkForFile($fileName);
Excel::load($fileName, function ($reader) use ($source, $entityType, &$row_list, &$results) {
@ -321,7 +320,7 @@ class ImportService
}
}
@unlink($fileName);
File::delete($fileName);
return $results;
}
@ -590,7 +589,6 @@ class ImportService
{
require_once app_path().'/Includes/parsecsv.lib.php';
$fileName = storage_path() . '/import/' . $fileName;
$this->checkForFile($fileName);
$csv = new parseCSV();
@ -686,7 +684,8 @@ class ImportService
];
$source = IMPORT_CSV;
$fileName = sprintf('%s_%s_%s.csv', Auth::user()->account_id, $timestamp, $entityType);
$path = env('FILE_IMPORT_PATH') ?: storage_path() . '/import';
$fileName = sprintf('%s/%s_%s_%s.csv', $path, Auth::user()->account_id, $timestamp, $entityType);
$data = $this->getCsvData($fileName);
$this->checkData($entityType, count($data));
$this->initMaps();
@ -726,7 +725,7 @@ class ImportService
}
}
@unlink(storage_path() . '/import/' . $fileName);
File::delete($fileName);
return $results;
}
@ -868,7 +867,7 @@ class ImportService
$this->maps['client'][$name] = $client->id;
$this->maps['client_ids'][$client->public_id] = $client->id;
}
if ($name = strtolower(trim($client->contacts[0]->email))) {
if (count($client->contacts) && $name = strtolower(trim($client->contacts[0]->email))) {
$this->maps['client'][$name] = $client->id;
$this->maps['client_ids'][$client->public_id] = $client->id;
}

View File

@ -28,6 +28,7 @@ class TemplateService
$invitation = $data['invitation'];
$invoice = $invitation->invoice;
$contact = $invitation->contact;
$passwordHTML = isset($data['password']) ? '<p>'.trans('texts.password').': '.$data['password'].'<p>' : false;
$documentsHTML = '';
@ -46,12 +47,13 @@ class TemplateService
$variables = [
'$footer' => $account->getEmailFooter(),
'$emailSignature' => $account->getEmailFooter(),
'$client' => $client->getDisplayName(),
'$account' => $account->getDisplayName(),
'$dueDate' => $account->formatDate($invoice->due_date),
'$invoiceDate' => $account->formatDate($invoice->invoice_date),
'$contact' => $invitation->contact->getDisplayName(),
'$firstName' => $invitation->contact->first_name,
'$contact' => $contact->getDisplayName(),
'$firstName' => $contact->first_name,
'$amount' => $account->formatMoney($data['amount'], $client),
'$invoice' => $invoice->invoice_number,
'$quote' => $invoice->invoice_number,
@ -63,6 +65,8 @@ class TemplateService
'$paymentButton' => Form::emailPaymentButton($invitation->getLink('payment')).'$password',
'$customClient1' => $client->custom_value1,
'$customClient2' => $client->custom_value2,
'$customContact1' => $contact->custom_value1,
'$customContact2' => $contact->custom_value2,
'$customInvoice1' => $invoice->custom_text_value1,
'$customInvoice2' => $invoice->custom_text_value2,
'$documents' => $documentsHTML,

View File

@ -17,7 +17,7 @@
"ext-gmp": "*",
"ext-gd": "*",
"turbo124/laravel-push-notification": "2.*",
"omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525",
"omnipay/mollie": "3.*",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/gocardless": "dev-master",
"omnipay/stripe": "dev-master",

551
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddCustomContactFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function ($table) {
$table->string('custom_contact_label1')->nullable();
$table->string('custom_contact_label2')->nullable();
});
Schema::table('contacts', function ($table) {
$table->string('custom_value1')->nullable();
$table->string('custom_value2')->nullable();
});
Schema::table('payment_methods', function ($table) {
$table->unsignedInteger('account_gateway_token_id')->nullable()->change();
$table->dropForeign('payment_methods_account_gateway_token_id_foreign');
});
Schema::table('payment_methods', function ($table) {
$table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens')->onDelete('cascade');
});
Schema::table('payments', function ($table) {
$table->dropForeign('payments_payment_method_id_foreign');
});
Schema::table('payments', function ($table) {
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
});
Schema::table('expenses', function($table) {
$table->unsignedInteger('payment_type_id')->nullable();
$table->date('payment_date')->nullable();
$table->string('transaction_reference')->nullable();
$table->foreign('payment_type_id')->references('id')->on('payment_types');
$table->boolean('invoice_documents')->default(true);
});
// remove duplicate annual frequency
if (DB::table('frequencies')->count() == 9) {
DB::statement('update invoices set frequency_id = 8 where is_recurring = 1 and frequency_id = 9');
DB::statement('update accounts set reset_counter_frequency_id = 8 where reset_counter_frequency_id = 9');
DB::statement('update frequencies set name = "Annually" where id = 8');
DB::statement('delete from frequencies where id = 9');
}
Schema::create('db_servers', function ($table) {
$table->increments('id');
$table->string('name');
});
Schema::create('lookup_companies', function ($table) {
$table->increments('id');
$table->unsignedInteger('db_server_id');
$table->foreign('db_server_id')->references('id')->on('db_servers');
});
Schema::create('lookup_accounts', function ($table) {
$table->increments('id');
$table->unsignedInteger('lookup_company_id')->index();
$table->string('account_key');
$table->foreign('lookup_company_id')->references('id')->on('lookup_companies')->onDelete('cascade');
});
Schema::create('lookup_users', function ($table) {
$table->increments('id');
$table->unsignedInteger('lookup_account_id')->index();
$table->string('email');
$table->foreign('lookup_account_id')->references('id')->on('lookup_accounts')->onDelete('cascade');
});
Schema::create('lookup_contacts', function ($table) {
$table->increments('id');
$table->unsignedInteger('lookup_account_id')->index();
$table->string('contact_key');
$table->foreign('lookup_account_id')->references('id')->on('lookup_accounts')->onDelete('cascade');
});
Schema::create('lookup_invitations', function ($table) {
$table->increments('id');
$table->unsignedInteger('lookup_account_id')->index();
$table->string('invitation_key');
$table->string('message_id');
$table->foreign('lookup_account_id')->references('id')->on('lookup_accounts')->onDelete('cascade');
});
Schema::create('lookup_tokens', function ($table) {
$table->increments('id');
$table->unsignedInteger('lookup_account_id')->index();
$table->string('token');
$table->foreign('lookup_account_id')->references('id')->on('lookup_accounts')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('accounts', function ($table) {
$table->dropColumn('custom_contact_label1');
$table->dropColumn('custom_contact_label2');
});
Schema::table('contacts', function ($table) {
$table->dropColumn('custom_value1');
$table->dropColumn('custom_value2');
});
Schema::table('expenses', function($table) {
$table->dropColumn('payment_type_id');
$table->dropColumn('payment_date');
$table->dropColumn('transaction_reference');
$table->dropColumn('invoice_documents');
});
Schema::dropIfExists('db_servers');
Schema::dropIfExists('lookup_companies');
Schema::dropIfExists('lookup_accounts');
Schema::dropIfExists('lookup_users');
Schema::dropIfExists('lookup_contacts');
Schema::dropIfExists('lookup_invitations');
Schema::dropIfExists('lookup_tokens');
}
}

View File

@ -72,6 +72,7 @@ class CurrenciesSeeder extends Seeder
['name' => 'Taiwan New Dollar', 'code' => 'TWD', 'symbol' => 'NT$', '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' => 'Icelandic Króna', 'code' => 'ISK', 'symbol' => 'kr', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ',', 'swap_currency_symbol' => true],
];
foreach ($currencies as $currency) {

View File

@ -31,6 +31,7 @@ class LanguageSeeder extends Seeder
['name' => 'Croatian', 'locale' => 'hr'],
['name' => 'Albanian', 'locale' => 'sq'],
['name' => 'Greek', 'locale' => 'el'],
['name' => 'English - United Kingdom', 'locale' => 'en_UK'],
];
foreach ($languages as $language) {

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,9 @@ Invoice Ninja provides a REST based API, `click here <https://app.invoiceninja.c
To access the API you first need to create a token using the "Tokens” page under "Advanced Settings”.
- **Zapier** [hosted or self-host]: https://zapier.com/zapbook/invoice-ninja/
- **Integromat**: https://www.integromat.com/en/integrations/invoiceninja
- **PHP SDK**: https://github.com/invoiceninja/sdk-php
- **Zend Framework**: https://github.com/alexz707/InvoiceNinjaModule
.. NOTE:: Replace ninja.dev with https://app.invoiceninja.com to access a hosted account.
@ -73,7 +75,7 @@ Heres an example of creating a client. Note that email address is a property
.. code-block:: shell
curl -X POST ninja.dev/api/v1/clients -H "Content-Type:application/json"
-d '{"name":"Client","contact":{"email":"test@gmail.com"}}' -H "X-Ninja-Token: TOKEN"
-d '{"name":"Client","contact":{"email":"test@example.com"}}' -H "X-Ninja-Token: TOKEN"
You can also update a client by specifying a value for id. Next, heres an example of creating an invoice.
@ -87,10 +89,24 @@ If the product_key is set and matches an existing record the product fields will
Options
^^^^^^^
The following options are available when creating an invoice.
- ``email_invoice``: Email the invoice to the client.
- ``auto_bill``: Attempt to auto-bill the invoice using stored payment methods or credits.
- ``paid``: Create a payment for the defined amount.
Updating Data
"""""""""""""
.. NOTE:: When updating a client it's important to include the contact ids.
.. code-block:: shell
curl -X PUT ninja.dev/api/v1/clients/1 -H "Content-Type:application/json"
-d '{"name":"test", "contacts":[{"id": 1, "first_name": "test"}]}'
-H "X-Ninja-Token: TOKEN"
Emailing Invoices
"""""""""""""""""

View File

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

View File

@ -94,6 +94,19 @@ You need to create a Google Maps API key for the Javascript, Geocoding and Embed
You can disable the feature by adding ``GOOGLE_MAPS_ENABLED=false`` to the .env file.
Voice Commands
""""""""""""""
Supporting voice commands requires creating a `LUIS.ai <https://www.luis.ai/home/index>`_ app, once the app is created you can import this `model file <https://download.invoiceninja.com/luis.json>`_.
You'll also need to set the following values in the .env file.
.. code-block:: shell
SPEECH_ENABLED=true
MSBOT_LUIS_APP_ID=...
MSBOT_LUIS_SUBSCRIPTION_KEY=...
Using a Proxy
"""""""""""""

View File

@ -46,7 +46,7 @@ Want to find out everything there is to know about how to use your Invoice Ninja
install
configure
update
iphone_app
mobile_apps
api
developer_guide
custom_modules

View File

@ -29,7 +29,7 @@ Step 1: Download the code
You can either download the zip file below or checkout the code from our GitHub repository. The zip includes all third party libraries whereas using GitHub requires you to use Composer to install the dependencies.
https://download.invoiceninja.com/ninja-v3.2.0.zip
https://download.invoiceninja.com/ninja-v3.2.1.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.

View File

@ -1,14 +1,14 @@
iPhone Application
==================
Mobile Applications
===================
The Invoice Ninja iPhone application allows a user to connect to their self-hosted Invoice Ninja web application.
The Invoice Ninja iPhone and Android applications allows a user to connect to their self-hosted Invoice Ninja web application.
Connecting your iPhone to your self-hosted invoice ninja installation requires a couple of easy steps.
Connecting your to your self-hosted invoice ninja installation requires a couple of easy steps.
Web app configuration
Web App configuration
"""""""""""""""""""""
Firstly you'll need to add an additional field to your .env file which is located in the root directory of your self-hosted Invoice Ninja installation.
First, you'll need to add an additional field to your .env file which is located in the root directory of your self-hosted Invoice Ninja installation.
The additional field to add is API_SECRET, set this to your own defined alphanumeric string.
@ -17,10 +17,10 @@ The additional field to add is API_SECRET, set this to your own defined alphanum
Save your .env file and now open Invoice Ninja on your iPhone.
iPhone configuration
""""""""""""""""""""
Mobile App configuration
""""""""""""""""""""""""
Once you have completed the in-app purchase to unlock the iPhone to connect to your own server, you'll be presented with two fields.
Once you have completed the in-app purchase to unlock the mobile app to connect to your own server, you'll be presented with two fields.
The first is the Base URL of your self-hosted installation, ie http://ninja.yourapp.com
@ -30,7 +30,7 @@ The second field is the API_SECRET, enter in the API_SECRET you used in your .en
Click SAVE.
You should be able to login now from your iPhone!
You should now be able to login!
FAQ:
@ -40,9 +40,9 @@ Q: I get a HTTP 500 error.
A: Most likely you have not entered your API_SECRET in your .env file
Q: I get a HTTP 403 error when i attempt to login with the iPhone.
Q: I get a HTTP 403 error when i attempt to login with the iPhone or Android device.
A: Most likely your API_SECRET on the iPhone does not match that on your self-hosted installation.
A: Most likely your API_SECRET on the iPhone/Android device does not match that on your self-hosted installation.
Q: Do I need to create a token on the server?

View File

@ -16,6 +16,11 @@ If the auto-update fails you can manually run the update with the following comm
.. NOTE:: If you've downloaded the code from GitHub you also need to run ``composer install``
Version 3.2
"""""""""""
An import folder has been adding to storage/, you may need to run ``sudo chown -R www-data:www-data storage``
Version 2.6
"""""""""""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -424,6 +424,18 @@ ul.dropdown-menu,
overflow-x: hidden;
}
ul.typeahead li:first-child {
border-top: solid 1px #EEE;
}
ul.typeahead li {
border-bottom: solid 1px #EEE;
}
.combobox-container .active {
border-color: #EEE !important;
}
.panel-default,
canvas {
border: 1px solid;

View File

@ -746,6 +746,7 @@ NINJA.accountAddress = function(invoice) {
NINJA.renderInvoiceField = function(invoice, field) {
var account = invoice.account;
var client = invoice.client;
if (field == 'invoice.invoice_number') {
if (invoice.is_statement) {
@ -803,6 +804,24 @@ NINJA.renderInvoiceField = function(invoice, field) {
} 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: ' '}];
}
@ -884,6 +903,10 @@ NINJA.renderClientOrAccountField = function(invoice, field) {
return {text: account.custom_client_label1 && client.custom_value1 ? account.custom_client_label1 + ' ' + client.custom_value1 : false};
} else if (field == 'client.custom_value2') {
return {text: account.custom_client_label2 && client.custom_value2 ? account.custom_client_label2 + ' ' + client.custom_value2 : false};
} else if (field == 'contact.custom_value1') {
return {text:contact.custom_value1};
} else if (field == 'contact.custom_value2') {
return {text:contact.custom_value2};
}
if (field == 'account.company_name') {
@ -948,6 +971,8 @@ NINJA.clientDetails = function(invoice) {
'client.email',
'client.custom_value1',
'client.custom_value2',
'contact.custom_value1',
'contact.custom_value2',
];
}
var data = [];

View File

@ -447,6 +447,27 @@ if (window.ko) {
};
}
function comboboxHighlighter(item) {
var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
var result = item.replace(new RegExp('<br/>', 'g'), "\n");
result = result.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
return match ? '<strong>' + match + '</strong>' : query;
});
result = result.replace(new RegExp("\n", 'g'), '<br/>');
return result;
}
function comboboxMatcher(item) {
return ~stripHtmlTags(item).toLowerCase().indexOf(this.query.toLowerCase());
}
function stripHtmlTags(text) {
// http://stackoverflow.com/a/5002618/497368
var div = document.createElement("div");
div.innerHTML = text;
return div.textContent || div.innerText || '';
}
function getContactDisplayName(contact)
{
if (contact.first_name || contact.last_name) {
@ -456,6 +477,25 @@ function getContactDisplayName(contact)
}
}
function getContactDisplayNameWithEmail(contact)
{
var str = '';
if (contact.first_name || contact.last_name) {
str += $.trim((contact.first_name || '') + ' ' + (contact.last_name || ''));
}
if (contact.email) {
if (str) {
str += ' - ';
}
str += contact.email;
}
return $.trim(str);
}
function getClientDisplayName(client)
{
var contact = client.contacts ? client.contacts[0] : false;
@ -716,8 +756,8 @@ function calculateAmounts(invoice) {
if (invoice.tax_rate2 && parseFloat(invoice.tax_rate2)) {
taxRate2 = parseFloat(invoice.tax_rate2);
}
taxAmount1 = roundToTwo(total * (taxRate1/100));
taxAmount2 = roundToTwo(total * (taxRate2/100));
taxAmount1 = roundToTwo(total * taxRate1 / 100);
taxAmount2 = roundToTwo(total * taxRate2 / 100);
total = total + taxAmount1 + taxAmount2;
for (var key in taxes) {

View File

@ -1711,6 +1711,7 @@ $LANG = array(
'lang_Spanish - Spain' => 'Spanish - Spain',
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2256,7 +2257,7 @@ $LANG = array(
'edit_credit' => 'Edit Credit',
'live_preview_help' => 'Display a live PDF preview on the invoice page.<br/>Disable this to improve performance when editing invoices.',
'force_pdfjs_help' => 'Replace the built-in PDF viewer in :chrome_link and :firefox_link.<br/>Enable this if your browser is automatically downloading the PDF.',
'force_pdfjs' => 'PDF Viewer',
'force_pdfjs' => 'Prevent Download',
'redirect_url' => 'Redirect URL',
'redirect_url_help' => 'Optionally specify a URL to redirect to after a payment is entered.',
'save_draft' => 'Save Draft',
@ -2293,6 +2294,7 @@ $LANG = array(
'renew_license' => 'Renew License',
'iphone_app_message' => 'Consider downloading our :link',
'iphone_app' => 'iPhone app',
'android_app' => 'Android app',
'logged_in' => 'Logged In',
'switch_to_primary' => 'Switch to your primary company (:name) to manage your plan.',
'inclusive' => 'Inclusive',
@ -2465,11 +2467,31 @@ $LANG = array(
'confirm_account_to_import' => 'Please confirm your account to import data.',
'import_started' => 'Your import has started, we\'ll send you an email once it completes.',
'listening' => 'Listening...',
'microphone_help' => 'Say \'new invoice for...\'',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
'surcharge_label' => 'Surcharge Label',
'contact_fields' => 'Contact Fields',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
);

View File

@ -1713,6 +1713,7 @@ $LANG = array(
'lang_Spanish - Spain' => 'Spanish - Spain',
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2258,7 +2259,7 @@ $LANG = array(
'edit_credit' => 'Edit Credit',
'live_preview_help' => 'Display a live PDF preview on the invoice page.<br/>Disable this to improve performance when editing invoices.',
'force_pdfjs_help' => 'Replace the built-in PDF viewer in :chrome_link and :firefox_link.<br/>Enable this if your browser is automatically downloading the PDF.',
'force_pdfjs' => 'PDF Viewer',
'force_pdfjs' => 'Prevent Download',
'redirect_url' => 'Redirect URL',
'redirect_url_help' => 'Optionally specify a URL to redirect to after a payment is entered.',
'save_draft' => 'Save Draft',
@ -2295,6 +2296,7 @@ $LANG = array(
'renew_license' => 'Renew License',
'iphone_app_message' => 'Consider downloading our :link',
'iphone_app' => 'iPhone app',
'android_app' => 'Android app',
'logged_in' => 'Logged In',
'switch_to_primary' => 'Switch to your primary company (:name) to manage your plan.',
'inclusive' => 'Inclusive',
@ -2467,11 +2469,31 @@ $LANG = array(
'confirm_account_to_import' => 'Please confirm your account to import data.',
'import_started' => 'Your import has started, we\'ll send you an email once it completes.',
'listening' => 'Listening...',
'microphone_help' => 'Say \'new invoice for...\'',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
'surcharge_label' => 'Surcharge Label',
'contact_fields' => 'Contact Fields',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
);

View File

@ -1711,6 +1711,7 @@ $LANG = array(
'lang_Spanish - Spain' => 'Spanish - Spain',
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2256,7 +2257,7 @@ $LANG = array(
'edit_credit' => 'Edit Credit',
'live_preview_help' => 'Display a live PDF preview on the invoice page.<br/>Disable this to improve performance when editing invoices.',
'force_pdfjs_help' => 'Replace the built-in PDF viewer in :chrome_link and :firefox_link.<br/>Enable this if your browser is automatically downloading the PDF.',
'force_pdfjs' => 'PDF Viewer',
'force_pdfjs' => 'Prevent Download',
'redirect_url' => 'Redirect URL',
'redirect_url_help' => 'Optionally specify a URL to redirect to after a payment is entered.',
'save_draft' => 'Save Draft',
@ -2293,6 +2294,7 @@ $LANG = array(
'renew_license' => 'Renew License',
'iphone_app_message' => 'Consider downloading our :link',
'iphone_app' => 'iPhone app',
'android_app' => 'Android app',
'logged_in' => 'Logged In',
'switch_to_primary' => 'Switch to your primary company (:name) to manage your plan.',
'inclusive' => 'Inclusive',
@ -2465,11 +2467,31 @@ $LANG = array(
'confirm_account_to_import' => 'Please confirm your account to import data.',
'import_started' => 'Your import has started, we\'ll send you an email once it completes.',
'listening' => 'Listening...',
'microphone_help' => 'Say \'new invoice for...\'',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
'surcharge_label' => 'Surcharge Label',
'contact_fields' => 'Contact Fields',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
);

View File

@ -369,7 +369,7 @@ $LANG = array(
'confirm_email_quote' => 'Bist du sicher, dass du dieses Angebot per E-Mail versenden möchtest',
'confirm_recurring_email_invoice' => 'Wiederkehrende Rechnung ist aktiv. Bis du sicher, dass du diese Rechnung weiterhin als E-Mail verschicken möchtest?',
'cancel_account' => 'Konto Kündigen',
'cancel_account_message' => 'Warnung: Diese Aktion wird Ihr Konto unwiederbringlich löschen.',
'cancel_account_message' => 'Warnung: Diese Aktion wird dein Konto unwiederbringlich löschen.',
'go_back' => 'Zurück',
'data_visualizations' => 'Datenvisualisierungen',
'sample_data' => 'Beispieldaten werden angezeigt',
@ -732,7 +732,7 @@ $LANG = array(
'recurring_hour' => 'Wiederholende Stunde',
'pattern' => 'Schema',
'pattern_help_title' => 'Schema-Hilfe',
'pattern_help_1' => 'Create custom numbers by specifying a pattern',
'pattern_help_1' => 'Erstellen Sie benutzerdefinierte Nummernkreise durch eigene Nummernsschemata.',
'pattern_help_2' => 'Verfügbare Variablen:',
'pattern_help_3' => 'Zum Beispiel: :example würde zu :value konvertiert werden.',
'see_options' => 'Optionen ansehen',
@ -851,7 +851,7 @@ $LANG = array(
'dark' => 'Dunkel',
'industry_help' => 'Wird genutzt um Vergleiche zwischen den Durchschnittswerten von Firmen ähnlicher Größe und Branche ermitteln zu können.',
'subdomain_help' => 'Passen Sie die Rechnungslink-Subdomäne an oder stellen Sie die Rechnung auf Ihrer eigenen Webseite zur Verfügung.',
'website_help' => 'Display the invoice in an iFrame on your own website',
'website_help' => 'Zeige die Rechnung als iFrame auf deiner eigenen Webseite an',
'invoice_number_help' => 'Geben Sie einen Präfix oder ein benutzerdefiniertes Schema an, um die Rechnungsnummer dynamisch zu erzeugen.',
'quote_number_help' => 'Geben Sie einen Präfix oder ein benutzerdefiniertes Schema an, um die Angebotsnummer dynamisch zu erzeugen.',
'custom_client_fields_helps' => 'Füge ein Feld hinzu, wenn ein neuer Kunde erstellt wird und zeige die Bezeichnung und den Wert auf der PDF-Datei an.',
@ -1042,7 +1042,7 @@ $LANG = array(
'invoiced_amount' => 'Rechnungsbetrag',
'invoice_item_fields' => 'Rechnungspositionsfeld',
'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.',
'recurring_invoice_number' => 'Recurring Number',
'recurring_invoice_number' => 'Wiederkehrende Nummer',
'recurring_invoice_number_prefix_help' => 'Geben Sie einen Präfix für wiederkehrende Rechnungen an. Standard ist \'R\'.',
// Client Passwords
@ -1711,6 +1711,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'lang_Spanish - Spain' => 'Spanisch - Spanien',
'lang_Swedish' => 'Schwedisch',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'Englisch (UK)',
// Frequencies
'freq_weekly' => 'Wöchentlich',
@ -2256,7 +2257,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'edit_credit' => 'Saldo bearbeiten',
'live_preview_help' => 'Zeige Live-Vorschau der PDF auf der Rechnungsseite.<br/>Schalte dies ab, falls es zu Leistungsproblemen während der Rechnungsbearbeitung führt.',
'force_pdfjs_help' => 'Ersetze den eingebauten PDF-Viewer in :chrome_link und :firefox_link.<br/>Aktiviere dies, wenn dein Browser die PDFs automatisch herunterlädt.',
'force_pdfjs' => 'PDF Viewer',
'force_pdfjs' => 'Verhindere Download',
'redirect_url' => 'Umleitungs-URL',
'redirect_url_help' => 'Gebe optional eine URL an, zu der umgeleitet werden soll, wenn eine Zahlung getätigt wurde.',
'save_draft' => 'Speichere Entwurf',
@ -2293,6 +2294,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'renew_license' => 'Verlängere die Lizenz',
'iphone_app_message' => 'Berücksichtigen Sie unser :link herunterzuladen',
'iphone_app' => 'iPhone-App',
'android_app' => 'Android app',
'logged_in' => 'Eingeloggt',
'switch_to_primary' => 'Wechseln Sie zu Ihrem Primärunternehmen (:name), um Ihren Plan zu managen.',
'inclusive' => 'Inklusive',
@ -2338,8 +2340,8 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'profit_and_loss' => 'Gewinn und Verlust',
'revenue' => 'Einnahmen',
'profit' => 'Profit',
'group_when_sorted' => 'Gruppe bei Sortierung',
'group_dates_by' => 'Gruppe Daten nach',
'group_when_sorted' => 'Gruppiere bei Sortierung',
'group_dates_by' => 'Gruppiere Daten nach',
'year' => 'Jahr',
'view_statement' => 'Zeige Bericht',
'statement' => 'Bericht',
@ -2396,7 +2398,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'create_vendor' => 'Lieferanten erstellen',
'create_expense_category' => 'Kategorie erstellen',
'pro_plan_reports' => ':link to enable reports by joining the Pro Plan',
'mark_ready' => 'Mark Ready',
'mark_ready' => 'Als bereit markieren',
'limits' => 'Limits',
'fees' => 'Gebühren',
@ -2409,31 +2411,31 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'gateway_fees_disclaimer' => 'Warning: not all states/payment gateways allow adding fees, please review local laws/terms of service.',
'percent' => 'Prozent',
'location' => 'Ort',
'line_item' => 'Line Item',
'line_item' => 'Posten',
'surcharge' => 'Gebühr',
'location_first_surcharge' => 'Enabled - First surcharge',
'location_second_surcharge' => 'Enabled - Second surcharge',
'location_line_item' => 'Enabled - Line item',
'location_line_item' => 'Aktiv - Posten',
'online_payment_surcharge' => 'Online Payment Surcharge',
'gateway_fees' => 'Gateway Fees',
'fees_disabled' => 'Gebühren sind deaktiviert',
'gateway_fees_help' => 'Automatically add an online payment surcharge/discount.',
'gateway' => 'Gateway',
'gateway' => 'Provider',
'gateway_fee_change_warning' => 'If there are unpaid invoices with fees they need to be updated manually.',
'fees_surcharge_help' => 'Gebühren anpassen :link.',
'label_and_taxes' => 'label and taxes',
'billable' => 'Billable',
'logo_warning_too_large' => 'The image file is too large.',
'logo_warning_fileinfo' => 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.',
'logo_warning_invalid' => 'There was a problem reading the image file, please try a different format.',
'billable' => 'Abrechenbar',
'logo_warning_too_large' => 'Die Bilddatei ist zu groß.',
'logo_warning_fileinfo' => 'Warnung: Um gif-Dateien zu unterstützen muss die fileinfo PHP-Erweiterung aktiv sein.',
'logo_warning_invalid' => 'Es gab ein Problem beim Einlesen der Bilddatei. Bitte verwende ein anderes Dateiformat.',
'error_refresh_page' => 'An error occurred, please refresh the page and try again.',
'error_refresh_page' => 'Es ist ein Fehler aufgetreten. Bitte aktualisiere die Webseite und probiere es erneut.',
'data' => 'Data',
'imported_settings' => 'Einstellungen erfolgreich aktualisiert',
'lang_Greek' => 'Griechisch',
'reset_counter' => 'Reset Counter',
'next_reset' => 'Next Reset',
'reset_counter_help' => 'Automatically reset the invoice and quote counters.',
'reset_counter' => 'Zähler-Reset',
'next_reset' => 'Nächster Reset',
'reset_counter_help' => 'Setze automatisch den Rechnungs- und Angebotszähler zurück.',
'auto_bill_failed' => 'Auto-billing for invoice :invoice_number failed',
'online_payment_discount' => 'Online-Zahlungsrabatt',
'created_new_company' => 'Neues Unternehmen erfolgreich erstellt',
@ -2455,7 +2457,7 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'cancel_account_help' => 'Lösche unwiederbringlich das Konto, mitsamt aller Daten und Einstellungen.',
'purge_successful' => 'Erfolgreich Kontodaten gelöscht',
'forbidden' => 'Verboten',
'purge_data_message' => 'Warning: This will permanently erase your data, there is no undo.',
'purge_data_message' => 'Achtung: Alle Daten werden vollständig gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.',
'contact_phone' => 'Telefonnummer des Kontakts',
'contact_email' => 'E-Mail-Adresse des Kontakts',
'reply_to_email' => 'Antwort-E-Mail-Adresse',
@ -2464,12 +2466,32 @@ Sobald Sie die Beträge erhalten haben, kommen Sie bitte wieder zurück zu diese
'import_complete' => 'Ihr Import wurde erfolgreich abgeschlossen.',
'confirm_account_to_import' => 'Bitte bestätigen Sie Ihr Konto um Daten zu importieren.',
'import_started' => 'Ihr Import wurde gestartet, wir senden Ihnen eine E-Mail zu, sobald er abgeschlossen wurde.',
'listening' => 'Listening...',
'microphone_help' => 'Say \'new invoice for...\'',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'listening' => 'Höre zu...',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"',
'voice_commands' => '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.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
'surcharge_label' => 'Surcharge Label',
'contact_fields' => 'Contact Fields',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
);

View File

@ -383,7 +383,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'share_invoice_counter' => 'Μοιραστείτε τον μετρητή τιμολογίου',
'invoice_issued_to' => 'Έκδοση τιμολογίου προς',
'invalid_counter' => 'Για να αποφείγετε πιθανή σύγχυση, παρακαλώ ορίστε σειρά σε τιμολόγιο ή προσφορά',
'mark_sent' => 'Μαρκάρισε ως Απεσταλμένο',
'mark_sent' => 'Σήμανση ως Απεσταλμένο',
'gateway_help_1' => ':link για εγγραφή στο Authorize.net.',
'gateway_help_2' => ':link για εγγραφή στο Authorize.net.',
'gateway_help_17' => ':link για να πάρετε υπογραφή για το API του PayPal.',
@ -851,7 +851,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'dark' => 'Σκούρο',
'industry_help' => 'Χρησιμοποιείται για να παρέχει συγκρίσεις μέσων όρων εταιριών ίδιου μεγέθους στους ίδιους επαγγελματικούς τομείς.',
'subdomain_help' => 'Ορίστε τον υποτομέα ή εμφάνιστε το τιμολόγιο στη δική σας ιστοσελίδα',
'website_help' => 'Display the invoice in an iFrame on your own website',
'website_help' => 'Εμφανίστε το τιμολόγιο σε ένα iFrame στη δική σας ιστοσελίδα',
'invoice_number_help' => 'Ορίστε ένα πρόθεμα ή χρησιμοποιήστε ένα προσαρμοσμένο μοτίβο για να καθορίζετε δυναμικά τον αριθμό τιμολογίου.',
'quote_number_help' => 'Ορίστε ένα πρόθεμα ή χρησιμοποιήστε ένα προσαρμοσμένο μοτίβο για να καθορίζετε δυναμικά τον αριθμό προσφοράς.',
'custom_client_fields_helps' => 'Προσθέστε ένα πεδίο όταν δημιουργείτε ένα πελάτη και εμφανίστε την ετικέτα και την τιμή στο αρχείο PDF.',
@ -1042,7 +1042,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'invoiced_amount' => 'Τιμολογημένο Ποσό',
'invoice_item_fields' => 'Πεδία Προϊόντων Τιμολογίου',
'custom_invoice_item_fields_help' => 'Προσθέστε ένα πεδίο όταν δημιουργείτε ένα προϊόν τιμολογίου και εμφανίστε την ετικέτα και την τιμή στο αρχείο PDF.',
'recurring_invoice_number' => 'Recurring Number',
'recurring_invoice_number' => 'Επαναλαμβανόμενος Αριθμός',
'recurring_invoice_number_prefix_help' => 'Ορίστε ένα πρόθεμα που να περιλαμβάνεται στον αριθμό τιμολογίου των επαναλαμβανόμενων τιμολογίων. Η εξ\'ορισμού τιμή είναι \'R\'.',
// Client Passwords
@ -1711,6 +1711,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'lang_Spanish - Spain' => 'Ισπανικά Ισπανίας',
'lang_Swedish' => 'Σουηδικά',
'lang_Albanian' => 'Αλβανικά',
'lang_English - United Kingdom' => 'Αγγλικά - Ηνωμένο Βασίλειο',
// Frequencies
'freq_weekly' => 'Εβδομάδα',
@ -2256,7 +2257,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'edit_credit' => 'Επεξεργασία Πίστωσης',
'live_preview_help' => 'Εμφάνιση μιας ζωντανής προεπισκόπησης του PDF του τιμολογίου.<br/>Απενεργοποιήστε αυτή την επιλογή για βελτίωση της ταχύτητας επεξεργασίας τιμολογίων.',
'force_pdfjs_help' => 'Αντικαταστήστε τον ενσωματωμένο παρουσιαστή PDF στο :chrome_link και :firefox_link.<br/>Ενεργοποιήστε αυτή την επιλογή εάν ο browser σας κατεβάζει αυτόματα το PDF.',
'force_pdfjs' => 'Παρουσιαστής PDF',
'force_pdfjs' => 'Παρεμπόδιση Μεταφόρτωσης',
'redirect_url' => 'URL Ανακατεύθυνσης',
'redirect_url_help' => 'Εναλλακτικά ορίστε ένα URL για ανακατεύθυνση μετά την πραγματοποίηση μιας πληρωμής.',
'save_draft' => 'Αποθήκευση Πρόχειρου',
@ -2293,6 +2294,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'renew_license' => 'Ανανέωση Άδειας Χρήσης',
'iphone_app_message' => 'Σκεφτείτε να κατεβάσετε το :link',
'iphone_app' => 'Εφαρμογή iPhone',
'android_app' => 'Εφαρμογή Android',
'logged_in' => 'Εισηγμένος',
'switch_to_primary' => 'Αλλάξτε στην πρωτεύουσα επιχείρηση (:name) για να διαχειριστείτε το πλάνο σας.',
'inclusive' => 'Συμπεριλαμβάνεται',
@ -2345,7 +2347,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'statement' => 'Δήλωση',
'statement_date' => 'Ημ/νία Δήλωσης',
'inactivity_logout' => 'Έχετε αποσυνδεθεί λόγω αδράνειας.',
'mark_active' => 'Σήμανση ως Ενεργός',
'mark_active' => 'Σήμανση ως Ενεργό',
'send_automatically' => 'Αυτόματη Αποστολή',
'initial_email' => 'Αρχικό Email',
'invoice_not_emailed' => 'Το τιμολόγιο δεν έχει αποσταλεί με email.',
@ -2396,7 +2398,7 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'create_vendor' => 'Δημιουργία προμηθευτή',
'create_expense_category' => 'Δημιουργία κατηγορίας',
'pro_plan_reports' => ':link για την ενεργοποίηση των εκθέσεων με τη συμμετοχή στο Επαγγελματικό Πλάνο',
'mark_ready' => 'Μαρκάρισμα ως Έτοιμο',
'mark_ready' => 'Σήμανση ως Έτοιμο',
'limits' => 'Όρια',
'fees' => 'Προμήθειες',
@ -2464,12 +2466,32 @@ email που είναι συνδεδεμένη με το λογαριασμό σ
'import_complete' => 'Επιτυχής ολοκλήρωση της εισαγωγής',
'confirm_account_to_import' => 'Παρακαλώ επιβεβαιώστε το λογαριασμό σας για εισαγωγή δεδομένων',
'import_started' => 'Η εισαγωγή δεδομένων ξεκίνησε, θα σας αποσταλεί email με την ολοκλήρωση.',
'listening' => 'Listening...',
'microphone_help' => 'Say \'new invoice for...\'',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'listening' => 'Ηχητική καταγραφή...',
'microphone_help' => 'Πείτε "νέο τιμολόγιο για [client]" ή "εμφάνισέ μου τις αρχειοθετημένες πληρωμές του [client]"',
'voice_commands' => 'Ηχητικές Εντολές',
'sample_commands' => 'Δείγματα εντολών',
'voice_commands_feedback' => 'Εργαζόμαστε για να βελτιώσουμε αυτό το χαρακτηριστικό, εάν υπάρχει μία εντολή που θέλετε να υποστηρίζουμε παρακαλούμε στείλτε μας email στο :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Επιτυχής αρχειοθέτηση :count προϊόντων',
'recommend_on' => 'Προτείνουμε την <b>ενεργοποίηση</b> αυτής της ρύθμισης.',
'recommend_off' => 'Προτείνουμε την <b>απενεργοποίηση</b> αυτής της ρύθμισης.',
'notes_auto_billed' => 'Αυτόματη χρέωση',
'surcharge_label' => 'Ετικέτα Επιβάρυνσης',
'contact_fields' => 'Πεδία Επαφής',
'custom_contact_fields_help' => 'Προσθέστε ένα πεδίο όταν δημιουργείτε μία επαφή και εμφανίστε την ετικέτα και την τιμή του στο αρχείο PDF.',
'datatable_info' => 'Εμφάνιση :start έως :end από :total εγγραφές',
'credit_total' => 'Συνολική Πίστωση',
'mark_billable' => 'Σήμανση ως χρεώσιμο',
'billed' => 'Τιμολογήθηκαν',
'company_variables' => 'Μεταβλητές Εταιρίας',
'client_variables' => 'Μεταβλητές Πελάτη',
'invoice_variables' => 'Μεταβλητές Τιμολογίου',
'navigation_variables' => 'Μεταβλητές Πλοήγησης',
'custom_variables' => 'Προσαρμοσμένες Μεταβλητές',
'invalid_file' => 'Μη έγκυρος τύπος αρχείου',
'add_documents_to_invoice' => 'Προσθέστε έγγραφα στο τιμολόγιο',
'mark_expense_paid' => 'Σήμανση ως εξοφλημένο',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
);

View File

@ -1711,6 +1711,7 @@ $LANG = array(
'lang_Spanish - Spain' => 'Spanish - Spain',
'lang_Swedish' => 'Swedish',
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
// Frequencies
'freq_weekly' => 'Weekly',
@ -2256,7 +2257,7 @@ $LANG = array(
'edit_credit' => 'Edit Credit',
'live_preview_help' => 'Display a live PDF preview on the invoice page.<br/>Disable this to improve performance when editing invoices.',
'force_pdfjs_help' => 'Replace the built-in PDF viewer in :chrome_link and :firefox_link.<br/>Enable this if your browser is automatically downloading the PDF.',
'force_pdfjs' => 'PDF Viewer',
'force_pdfjs' => 'Prevent Download',
'redirect_url' => 'Redirect URL',
'redirect_url_help' => 'Optionally specify a URL to redirect to after a payment is entered.',
'save_draft' => 'Save Draft',
@ -2293,6 +2294,7 @@ $LANG = array(
'renew_license' => 'Renew License',
'iphone_app_message' => 'Consider downloading our :link',
'iphone_app' => 'iPhone app',
'android_app' => 'Android app',
'logged_in' => 'Logged In',
'switch_to_primary' => 'Switch to your primary company (:name) to manage your plan.',
'inclusive' => 'Inclusive',
@ -2465,11 +2467,31 @@ $LANG = array(
'confirm_account_to_import' => 'Please confirm your account to import data.',
'import_started' => 'Your import has started, we\'ll send you an email once it completes.',
'listening' => 'Listening...',
'microphone_help' => 'Say \'new invoice for...\'',
'microphone_help' => 'Say "new invoice for [client]" or "show me [client]\'s archived payments"',
'voice_commands' => 'Voice Commands',
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
'notes_auto_billed' => 'Auto-billed',
'surcharge_label' => 'Surcharge Label',
'contact_fields' => 'Contact Fields',
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total',
'mark_billable' => 'Mark billable',
'billed' => 'Billed',
'company_variables' => 'Company Variables',
'client_variables' => 'Client Variables',
'invoice_variables' => 'Invoice Variables',
'navigation_variables' => 'Navigation Variables',
'custom_variables' => 'Custom Variables',
'invalid_file' => 'Invalid file type',
'add_documents_to_invoice' => 'Add documents to invoice',
'mark_expense_paid' => 'Mark paid',
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
);

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