diff --git a/README.md b/README.md index 190d4b7be602..5426d3a18220 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,34 @@ Just make sure to add the `invoice-ninja` tag to your question. Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other. -The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it! +All Pro and Enterprise features from the hosted app are included in the open-code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app. -## Referral Program -* Earn 50% of Pro & Enterprise Plans up to 4 years - [Learn more](https://www.invoiceninja.com/referral-program/) +* [Videos](https://www.youtube.com/@appinvoiceninja) +* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja) +* [APP Documentation](https://invoiceninja.github.io/) +* [Support Forum](https://forum.invoiceninja.com) +* [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/) +## Mobile App +* [iPhone](https://apps.apple.com/us/app/invoice-ninja-v5/id1503970375#?platform=iphone) +* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.app) +* [Linux](https://github.com/invoiceninja/flutter-mobile) + +## Desktop App +* [MacOS](https://apps.apple.com/app/id1503970375) +* [Windows](https://microsoft.com/en-us/p/invoice-ninja/9n3f2bbcfdr6) +* [MacOS Desktop](https://snapcraft.io/invoiceninja) + + +## Installation Options +* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) +* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html) +* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja) + ## Recommended Providers * [Stripe](https://stripe.com/) * [Postmark](https://postmarkapp.com/) -## Development -* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja) -* [APP Documentation](https://invoiceninja.github.io/) ## Quick Start @@ -67,43 +83,11 @@ user: user@example.com pass: password ``` -## Contribution guide. - -Code Style to follow [PSR-2](https://www.php-fig.org/psr/psr-2/) standards. - -All methods names to be in CamelCase - -All variables names to be in snake_case - -Where practical code should be strongly typed, ie your methods must return a type ie - -`public function doThis() : void` - -PHP >= 7.3 allows the return type Nullable so there should be no circumstance a type cannot be return by using the following: - -`public function doThat() ?:string` - -To improve chances of PRs being merged please include tests to ensure your code works well and integrates with the rest of the project. - -## Documentation - -API documentation is hosted using Swagger and can be found [HERE](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja) - -Installation, Configuration and Troubleshooting documentation can be found [HERE] (https://invoiceninja.github.io) - ## Credits * [Hillel Coren](https://hillelcoren.com/) * [David Bomba](https://github.com/turbo124) -* [All contributors](https://github.com/invoiceninja/invoiceninja/graphs/contributors) - -**Special thanks to:** -* [Holger Lösken](https://github.com/codedge) - [codedge](http://codedge.de) -* [Samuel Laulhau](https://github.com/lalop) - [Lalop](http://lalop.co/) -* [Alexander Vanderveen](https://blog.technicallycomputers.ca/) - [Technically Computers](https://www.technicallycomputers.ca/) -* [Efthymios Sarmpanis](https://github.com/esarbanis) -* [Gianfranco Gasbarri](https://github.com/gincos) -* [Clemens Mol](https://github.com/clemensmol) * [Benjamin Beganović](https://github.com/beganovich) +* [All contributors](https://github.com/invoiceninja/invoiceninja/graphs/contributors) ## Security diff --git a/VERSION.txt b/VERSION.txt index 398c2027c424..8fa0157a0207 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.39 \ No newline at end of file +5.5.40 \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 2e246e1b514f..5e40c642af1a 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -22,6 +22,7 @@ use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Ninja\QueueSize; use App\Jobs\Ninja\SystemMaintenance; use App\Jobs\Ninja\TaskScheduler; +use App\Jobs\Quote\QuoteCheckExpired; use App\Jobs\Util\DiskCleanup; use App\Jobs\Util\ReminderJob; use App\Jobs\Util\SchedulerCheck; @@ -70,6 +71,9 @@ class Kernel extends ConsoleKernel /* Sends recurring invoices*/ $schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping(); + /* Fires notifications for expired Quotes */ + $schedule->job(new QuoteCheckExpired)->dailyAt('05:00')->withoutOverlapping(); + /* Performs auto billing */ $schedule->job(new AutoBillCron)->dailyAt('06:00')->withoutOverlapping(); diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index a612808e2525..7213ff5e8855 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -28,6 +28,7 @@ class BlackListRule implements Rule 'wnpop.com', 'dataservices.space', 'karenkey.com', + 'sharklasers.com', ]; /** diff --git a/app/Import/Providers/Csv.php b/app/Import/Providers/Csv.php index fdf70ca691e7..ca100364f29f 100644 --- a/app/Import/Providers/Csv.php +++ b/app/Import/Providers/Csv.php @@ -87,7 +87,7 @@ class Csv extends BaseImport implements ImportInterface foreach($data as $key => $value) { - $data[$key]['bank.bank_integration_id'] = $this->decodePrimaryKey($this->request['bank_integration_id']); + $data[$key]['transaction.bank_integration_id'] = $this->decodePrimaryKey($this->request['bank_integration_id']); } } diff --git a/app/Import/Transformers/Bank/BankTransformer.php b/app/Import/Transformers/Bank/BankTransformer.php index cfdb2b01b9bf..da3e088fa052 100644 --- a/app/Import/Transformers/Bank/BankTransformer.php +++ b/app/Import/Transformers/Bank/BankTransformer.php @@ -31,17 +31,17 @@ class BankTransformer extends BaseTransformer $now = now(); $transformed = [ - 'bank_integration_id' => $transaction['bank.bank_integration_id'], - 'transaction_id' => $this->getNumber($transaction,'bank.transaction_id'), - 'amount' => abs($this->getFloat($transaction, 'bank.amount')), - 'currency_id' => $this->getCurrencyByCode($transaction, 'bank.currency'), - 'account_type' => strlen($this->getString($transaction, 'bank.account_type')) > 1 ? $this->getString($transaction, 'bank.account_type') : 'bank', - 'category_id' => $this->getNumber($transaction, 'bank.category_id') > 0 ? $this->getNumber($transaction, 'bank.category_id') : null, - 'category_type' => $this->getString($transaction, 'bank.category_type'), - 'date' => array_key_exists('bank.date', $transaction) ? $this->parseDate($transaction['bank.date']) + 'bank_integration_id' => $transaction['transaction.bank_integration_id'], + 'transaction_id' => $this->getNumber($transaction,'transaction.transaction_id'), + 'amount' => abs($this->getFloat($transaction, 'transaction.amount')), + 'currency_id' => $this->getCurrencyByCode($transaction, 'transaction.currency'), + 'account_type' => strlen($this->getString($transaction, 'transaction.account_type')) > 1 ? $this->getString($transaction, 'transaction.account_type') : 'bank', + 'category_id' => $this->getNumber($transaction, 'transaction.category_id') > 0 ? $this->getNumber($transaction, 'transaction.category_id') : null, + 'category_type' => $this->getString($transaction, 'transaction.category_type'), + 'date' => array_key_exists('transaction.date', $transaction) ? $this->parseDate($transaction['transaction.date']) : now()->format('Y-m-d'), - 'bank_account_id' => array_key_exists('bank.bank_account_id', $transaction) ? $transaction['bank.bank_account_id'] : 0, - 'description' => array_key_exists('bank.description', $transaction) ? $transaction['bank.description'] : '', + 'bank_account_id' => array_key_exists('transaction.bank_account_id', $transaction) ? $transaction['transaction.bank_account_id'] : 0, + 'description' => array_key_exists('transaction.description', $transaction) ? $transaction['transaction.description'] : '', 'base_type' => $this->calculateType($transaction), 'created_at' => $now, 'updated_at' => $now, @@ -56,22 +56,22 @@ class BankTransformer extends BaseTransformer private function calculateType($transaction) { - if(array_key_exists('bank.base_type', $transaction) && ($transaction['bank.base_type'] == 'CREDIT') || strtolower($transaction['bank.base_type']) == 'deposit') + if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'CREDIT') || strtolower($transaction['transaction.base_type']) == 'deposit')) return 'CREDIT'; - if(array_key_exists('bank.base_type', $transaction) && ($transaction['bank.base_type'] == 'DEBIT') || strtolower($transaction['bank.bank_type']) == 'withdrawal') + if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'DEBIT') || strtolower($transaction['transaction.bank_type']) == 'withdrawal')) return 'DEBIT'; - if(array_key_exists('bank.category_id', $transaction)) + if(array_key_exists('transaction.category_id', $transaction)) return 'DEBIT'; - if(array_key_exists('bank.category_type', $transaction) && $transaction['bank.category_type'] == 'Income') + if(array_key_exists('transaction.category_type', $transaction) && $transaction['transaction.category_type'] == 'Income') return 'CREDIT'; - if(array_key_exists('bank.category_type', $transaction)) + if(array_key_exists('transaction.category_type', $transaction)) return 'DEBIT'; - if(array_key_exists('bank.amount', $transaction) && is_numeric($transaction['bank.amount']) && $transaction['bank.amount'] > 0) + if(array_key_exists('transaction.amount', $transaction) && is_numeric($transaction['transaction.amount']) && $transaction['transaction.amount'] > 0) return 'CREDIT'; return 'DEBIT'; diff --git a/app/Jobs/Quote/QuoteCheckExpired.php b/app/Jobs/Quote/QuoteCheckExpired.php new file mode 100644 index 000000000000..4e5c5d77b388 --- /dev/null +++ b/app/Jobs/Quote/QuoteCheckExpired.php @@ -0,0 +1,112 @@ +checkForExpiredQuotes(); + + foreach (MultiDB::$dbs as $db) { + + MultiDB::setDB($db); + + $this->checkForExpiredQuotes(); + + } + + } + + private function checkForExpiredQuotes() + { + Quote::query() + ->where('status_id', Quote::STATUS_SENT) + ->where('is_deleted', false) + ->whereNull('deleted_at') + ->whereNotNull('due_date') + ->whereHas('client', function ($query) { + $query->where('is_deleted', 0) + ->where('deleted_at', null); + }) + ->whereHas('company', function ($query) { + $query->where('is_disabled', 0); + }) + // ->where('due_date', '<='. now()->toDateTimeString()) + ->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()]) + ->cursor() + ->each(function ($quote){ + $this->queueExpiredQuoteNotification($quote); + }); + } + + private function queueExpiredQuoteNotification(Quote $quote) + { + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new QuoteExpiredObject($quote, $quote->company))->build()); + $nmo->company = $quote->company; + $nmo->settings = $quote->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($quote->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + if (! $user) { + continue; + } + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($quote->invitations()->first(), $company_user, 'quote', ['all_notifications', 'quote_expired', 'quote_expired_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + } + } + } + +} diff --git a/app/Listeners/Invoice/InvoicePaidActivity.php b/app/Listeners/Invoice/InvoicePaidActivity.php index 040b12317a1c..d86f1466126d 100644 --- a/app/Listeners/Invoice/InvoicePaidActivity.php +++ b/app/Listeners/Invoice/InvoicePaidActivity.php @@ -20,8 +20,6 @@ use stdClass; class InvoicePaidActivity implements ShouldQueue { protected $activity_repo; - - public $delay = 10; /** * Create the event listener. diff --git a/app/Listeners/Invoice/UpdateInvoiceActivity.php b/app/Listeners/Invoice/UpdateInvoiceActivity.php index 13cc6106d55b..78bea3283369 100644 --- a/app/Listeners/Invoice/UpdateInvoiceActivity.php +++ b/app/Listeners/Invoice/UpdateInvoiceActivity.php @@ -21,8 +21,6 @@ class UpdateInvoiceActivity implements ShouldQueue { protected $activity_repo; - public $delay = 5; - /** * Create the event listener. * diff --git a/app/Mail/Admin/QuoteExpiredObject.php b/app/Mail/Admin/QuoteExpiredObject.php new file mode 100644 index 000000000000..b6fd001ae450 --- /dev/null +++ b/app/Mail/Admin/QuoteExpiredObject.php @@ -0,0 +1,103 @@ +quote = $quote; + $this->company = $company; + } + + public function build() + { + MultiDB::setDb($this->company->db); + + if (! $this->quote) { + return; + } + + App::forgetInstance('translator'); + /* Init a new copy of the translator*/ + $t = app('translator'); + /* Set the locale*/ + App::setLocale($this->company->getLocale()); + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $mail_obj = new stdClass; + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } + + private function getAmount() + { + return Number::formatMoney($this->quote->amount, $this->quote->client); + } + + private function getSubject() + { + return + ctrans( + 'texts.notification_quote_expired_subject', + [ + 'client' => $this->quote->client->present()->name(), + 'invoice' => $this->quote->number, + ] + ); + } + + private function getData() + { + $settings = $this->quote->client->getMergedSettings(); + + $data = [ + 'title' => $this->getSubject(), + 'message' => ctrans( + 'texts.notification_quote_expired', + [ + 'amount' => $this->getAmount(), + 'client' => $this->quote->client->present()->name(), + 'invoice' => $this->quote->number, + ] + ), + 'url' => $this->quote->invitations->first()->getAdminLink(), + 'button' => ctrans('texts.view_quote'), + 'signature' => $settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ]; + + return $data; + } +} diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index 831808aa3722..8cdf39b8694c 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -124,6 +124,7 @@ class RecurringInvoice extends BaseModel 'exchange_rate', 'vendor_id', 'next_send_date_client', + 'uses_inclusive_taxes', ]; protected $casts = [ diff --git a/app/Services/Credit/TriggeredActions.php b/app/Services/Credit/TriggeredActions.php index dc4424be8d30..e08151736c48 100644 --- a/app/Services/Credit/TriggeredActions.php +++ b/app/Services/Credit/TriggeredActions.php @@ -45,6 +45,22 @@ class TriggeredActions extends AbstractService $this->credit = $this->credit->service()->markSent()->save(); } + if($this->request->has('save_default_footer') && $this->request->input('save_default_footer') == 'true') { + $company = $this->credit->company; + $settings = $company->settings; + $settings->credit_footer = $this->credit->footer; + $company->settings = $settings; + $company->save(); + } + + if($this->request->has('save_default_terms') && $this->request->input('save_default_terms') == 'true') { + $company = $this->credit->company; + $settings = $company->settings; + $settings->credit_terms = $this->credit->terms; + $company->settings = $settings; + $company->save(); + } + return $this->credit; } diff --git a/app/Services/PurchaseOrder/TriggeredActions.php b/app/Services/PurchaseOrder/TriggeredActions.php index b6f1f3eee32d..a325a9cc5eef 100644 --- a/app/Services/PurchaseOrder/TriggeredActions.php +++ b/app/Services/PurchaseOrder/TriggeredActions.php @@ -52,6 +52,22 @@ class TriggeredActions extends AbstractService // $this->purchase_order = $this->purchase_order->service()->handleCancellation()->save(); // } + if($this->request->has('save_default_footer') && $this->request->input('save_default_footer') == 'true') { + $company = $this->purchase_order->company; + $settings = $company->settings; + $settings->purchase_order_footer = $this->purchase_order->footer; + $company->settings = $settings; + $company->save(); + } + + if($this->request->has('save_default_terms') && $this->request->input('save_default_terms') == 'true') { + $company = $this->purchase_order->company; + $settings = $company->settings; + $settings->purchase_order_terms = $this->purchase_order->terms; + $company->settings = $settings; + $company->save(); + } + return $this->purchase_order; } diff --git a/config/ninja.php b/config/ninja.php index e837c8091250..a3bb71301c2d 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.5.39', - 'app_tag' => '5.5.39', + 'app_version' => '5.5.40', + 'app_tag' => '5.5.40', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/lang/en/texts.php b/lang/en/texts.php index 3978323e62ba..fe2e75ac1590 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4837,7 +4837,8 @@ $LANG = array( 'enable_applying_payments_later' => 'Enable Applying Payments Later', 'line_item_tax_rates' => 'Line Item Tax Rates', 'show_tasks_in_client_portal' => 'Show Tasks in Client Portal', - + 'notification_quote_expired_subject' => 'Quote :invoice has expired for :client', + 'notification_quote_expired' => 'The following Quote :invoice for client :client and :amount has now expired.', ); return $LANG; diff --git a/resources/views/pdf-designs/calmness.html b/resources/views/pdf-designs/calmness.html new file mode 100644 index 000000000000..2ec356cecf02 --- /dev/null +++ b/resources/views/pdf-designs/calmness.html @@ -0,0 +1,394 @@ + + +
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $entity_label + |
+
+ | + +