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:
commit
be4878db43
@ -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');
|
||||
|
@ -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)) {
|
||||
|
223
app/Console/Commands/CreateLuisData.php
Normal file
223
app/Console/Commands/CreateLuisData.php
Normal 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 [];
|
||||
}
|
||||
}
|
@ -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'),
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
29
app/Events/InvoiceItemsWereCreated.php
Normal file
29
app/Events/InvoiceItemsWereCreated.php
Normal 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;
|
||||
}
|
||||
}
|
29
app/Events/InvoiceItemsWereUpdated.php
Normal file
29
app/Events/InvoiceItemsWereUpdated.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
24
app/Events/QuoteItemsWereCreated.php
Normal file
24
app/Events/QuoteItemsWereCreated.php
Normal 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;
|
||||
}
|
||||
}
|
24
app/Events/QuoteItemsWereUpdated.php
Normal file
24
app/Events/QuoteItemsWereUpdated.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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'];
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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')];
|
||||
|
@ -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') {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
223
app/Http/Controllers/ProjectApiController.php
Normal file
223
app/Http/Controllers/ProjectApiController.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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'),
|
||||
|
@ -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));
|
||||
|
||||
|
@ -152,7 +152,6 @@ class VendorController extends BaseController
|
||||
'data' => Input::old('data'),
|
||||
'account' => Auth::user()->account,
|
||||
'currencies' => Cache::get('currencies'),
|
||||
'countries' => Cache::get('countries'),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 . '">
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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' : ''
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -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'])) {
|
||||
|
@ -37,6 +37,8 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
|
||||
'email',
|
||||
'phone',
|
||||
'send_invoice',
|
||||
'custom_value1',
|
||||
'custom_value2',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
163
app/Models/Traits/HasLogo.php
Normal file
163
app/Models/Traits/HasLogo.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ trait SendsEmails
|
||||
$template .= "$message<p/>";
|
||||
}
|
||||
|
||||
return $template . '$footer';
|
||||
return $template . '$emailSignature';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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(' <i class="fa fa-globe" style="cursor:pointer" title="%s" onclick="openUrl(\'%s\', \'IP Lookup\')"></i>', $model->ip, $ipLookUpLink);
|
||||
}
|
||||
|
||||
return $str;
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -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>";
|
||||
|
@ -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 : '');
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -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,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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.'%');
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
]);
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
);
|
||||
|
@ -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' => [
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
551
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
18
docs/api.rst
18
docs/api.rst
@ -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 @@ Here’s 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, here’s 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
|
||||
"""""""""""""""""
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
"""""""""""""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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
2
public/css/built.css
vendored
2
public/css/built.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
resources/assets/css/style.css
vendored
12
resources/assets/css/style.css
vendored
@ -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;
|
||||
|
@ -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 = [];
|
||||
|
@ -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) {
|
||||
|
@ -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.',
|
||||
|
||||
);
|
||||
|
||||
|
@ -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.',
|
||||
|
||||
);
|
||||
|
||||
|
@ -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.',
|
||||
|
||||
);
|
||||
|
||||
|
@ -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.',
|
||||
|
||||
);
|
||||
|
||||
|
@ -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.',
|
||||
|
||||
);
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user