diff --git a/README.md b/README.md index ccbcfb4e4826..190b5eddab4e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Sublime's custom image +Sublime's custom image

![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) @@ -8,25 +8,30 @@ # Invoice Ninja 5 -## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) +Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and added the most requested features to create an invoicing application like no other. Check the [Invoice Ninja YouTube Channel](https://www.youtube.com/@appinvoiceninja) to get up to speed, or try the [Demo](https://react.invoicing.co/demo) now. -Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com) +**Choose your setup** -## Introduction +- [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure. +- [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility. -Version 5 of Invoice Ninja is here! -We took the best parts of version 4 and add the most requested features -to produce a invoicing application like no other. +All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app. -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. +#### Get social with us -* [Videos](https://www.youtube.com/@appinvoiceninja) -* [API Documentation](https://api-docs.invoicing.co/) -* [APP Documentation](https://invoiceninja.github.io/) * [Support Forum](https://forum.invoiceninja.com) +* [Slack](http://slack.invoiceninja.com) +* [Discord](https://discord.gg/ZwEdtfCwXA) +* [Instagram](https://www.instagram.com/appinvoiceninja) -## Setup +#### Documentation + +* [Invoice Ninja - API](https://api-docs.invoicing.co/) +* [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) +* [Invoice Ninja - User Guide](https://invoiceninja.github.io/en/user-guide/) +* [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) + +## Installation Options and Clients ### Mobile Apps * [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone) @@ -39,16 +44,21 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding * [Linux - Snap](https://snapcraft.io/invoiceninja) * [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja) -### Installation Options +### Self-Hosted Server Installation +**Note:** The self-hosted options do support the desktop and mobile apps. + +* [Server or VM](https://invoiceninja.github.io/en/self-host-installation/) * [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) -* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html) +* [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html) * [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja) ### Recommended Providers * [Stripe](https://stripe.com/) * [Postmark](https://postmarkapp.com/) -## Quick Hosting Setup +## [Advanced] Quick Hosting Setup + +In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you. ```sh git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git @@ -84,6 +94,7 @@ pass: password ``` ## Developers Guide +In addition to the official [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) we've got your back with some insights. ### App Design diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 605abff1e234..4ea22fe974ee 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -1258,7 +1258,7 @@ class BaseExport $date_range = $this->input['date_range']; - if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) { + if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key'] ?? '') > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) { $this->date_key = $this->input['date_key']; } @@ -1269,7 +1269,7 @@ class BaseExport $custom_start_date = now()->startOfYear(); $custom_end_date = now(); } - + switch ($date_range) { case 'all': $this->start_date = 'All available data'; diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index bc357d61a96f..155d61a88f48 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -29,7 +29,7 @@ class TaskExport extends BaseExport { private $entity_transformer; - public string $date_key = 'created_at'; + public string $date_key = 'calculated_start_date'; private string $date_format = 'Y-m-d'; diff --git a/app/Filters/BankTransactionFilters.php b/app/Filters/BankTransactionFilters.php index faaeaa7ade4a..f786f49e78ed 100644 --- a/app/Filters/BankTransactionFilters.php +++ b/app/Filters/BankTransactionFilters.php @@ -155,11 +155,13 @@ class BankTransactionFilters extends QueryFilters $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc'; if ($sort_col[0] == 'deposit') { - return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir); + return $this->builder->orderByRaw("(CASE WHEN base_type = 'CREDIT' THEN amount END) $dir")->orderBy('amount', $dir); + // return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir); } if ($sort_col[0] == 'withdrawal') { - return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir); + return $this->builder->orderByRaw("(CASE WHEN base_type = 'DEBIT' THEN amount END) $dir")->orderBy('amount', $dir); + // return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir); } if ($sort_col[0] == 'status') { diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 76ba72ca5e4f..d1d7dd56cff0 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -11,16 +11,18 @@ namespace App\Helpers\Invoice; -use App\DataMapper\BaseSettings; -use App\DataMapper\InvoiceItem; -use App\DataMapper\Tax\RuleInterface; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; use App\Models\Credit; +use App\Models\Vendor; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\DataMapper\InvoiceItem; +use App\DataMapper\BaseSettings; +use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSum @@ -120,7 +122,7 @@ class InvoiceItemSum private $tax_collection; - private ?Client $client; + private Client | Vendor $client; private bool $calc_tax = false; @@ -131,10 +133,10 @@ class InvoiceItemSum $this->tax_collection = collect([]); $this->invoice = $invoice; + $this->client = $invoice->client ?? $invoice->vendor; if ($this->invoice->client) { $this->currency = $this->invoice->client->currency(); - $this->client = $this->invoice->client; $this->shouldCalculateTax(); } else { $this->currency = $this->invoice->vendor->currency(); @@ -313,7 +315,7 @@ class InvoiceItemSum $key = str_replace(' ', '', $tax_name.$tax_rate); - $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.floatval($tax_rate).'%']; + $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%']; $this->tax_collection->push(collect($group_tax)); } diff --git a/app/Helpers/Invoice/InvoiceItemSumInclusive.php b/app/Helpers/Invoice/InvoiceItemSumInclusive.php index da4c3e1f3aa4..df9a897abefa 100644 --- a/app/Helpers/Invoice/InvoiceItemSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceItemSumInclusive.php @@ -11,14 +11,16 @@ namespace App\Helpers\Invoice; -use App\DataMapper\Tax\RuleInterface; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; use App\Models\Credit; +use App\Models\Vendor; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSumInclusive @@ -109,7 +111,7 @@ class InvoiceItemSumInclusive private bool $calc_tax = false; - private ?Client $client; + private Client | Vendor $client; private RuleInterface $rule; @@ -118,10 +120,10 @@ class InvoiceItemSumInclusive $this->tax_collection = collect([]); $this->invoice = $invoice; + $this->client = $invoice->client ?? $invoice->vendor; if ($this->invoice->client) { $this->currency = $this->invoice->client->currency(); - $this->client = $this->invoice->client; $this->shouldCalculateTax(); } else { $this->currency = $this->invoice->vendor->currency(); @@ -265,7 +267,7 @@ class InvoiceItemSumInclusive $key = str_replace(' ', '', $tax_name.$tax_rate); - $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.$tax_rate.'%']; + $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%']; $this->tax_collection->push(collect($group_tax)); } diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 32a821445d17..e69cf394a6ca 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -11,12 +11,14 @@ namespace App\Helpers\Invoice; +use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Vendor; use App\Utils\Number; use App\Utils\Traits\NumberFormatter; use Illuminate\Support\Collection; @@ -50,6 +52,8 @@ class InvoiceSum private $precision; + private Client | Vendor $client; + public InvoiceItemSum $invoice_items; private $rappen_rounding = false; @@ -60,18 +64,15 @@ class InvoiceSum */ public function __construct($invoice) { + $this->invoice = $invoice; + $this->client = $invoice->client ?? $invoice->vendor; - if ($this->invoice->client) { - $this->precision = $this->invoice->client->currency()->precision; - $this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding'); - } else { - $this->precision = $this->invoice->vendor->currency()->precision; - $this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding'); - - } + $this->precision = $this->client->currency()->precision; + $this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding'); $this->tax_map = new Collection(); + } public function build() @@ -131,7 +132,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) { @@ -139,7 +140,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) >= 2) { @@ -147,7 +148,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax]; } return $this; diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 6df714231f04..07fdf3e86894 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -12,7 +12,10 @@ namespace App\Helpers\Invoice; use App\Models\Quote; +use App\Utils\Number; +use App\Models\Client; use App\Models\Credit; +use App\Models\Vendor; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Models\RecurringQuote; @@ -49,6 +52,8 @@ class InvoiceSumInclusive private $rappen_rounding = false; + private Client | Vendor $client; + public InvoiceItemSumInclusive $invoice_items; /** * Constructs the object with Invoice and Settings object. @@ -58,14 +63,10 @@ class InvoiceSumInclusive public function __construct($invoice) { $this->invoice = $invoice; - - if ($this->invoice->client) { - $this->precision = $this->invoice->client->currency()->precision; - $this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding'); - } else { - $this->precision = $this->invoice->vendor->currency()->precision; - $this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding'); - } + $this->client = $invoice->client ?? $invoice->vendor; + + $this->precision = $this->client->currency()->precision; + $this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding'); $this->tax_map = new Collection(); } @@ -157,19 +158,19 @@ class InvoiceSumInclusive $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) { $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) { $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax]; } return $this; diff --git a/app/PaymentDrivers/Stripe/BrowserPay.php b/app/PaymentDrivers/Stripe/BrowserPay.php index 9af3eb90d579..09d71fcdb17c 100644 --- a/app/PaymentDrivers/Stripe/BrowserPay.php +++ b/app/PaymentDrivers/Stripe/BrowserPay.php @@ -153,7 +153,7 @@ class BrowserPay implements MethodInterface $this->stripe->client->company, ); - return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id)]); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); } /** diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php index 8b2266c3c2a5..eab1a98c3262 100644 --- a/app/PaymentDrivers/Stripe/CreditCard.php +++ b/app/PaymentDrivers/Stripe/CreditCard.php @@ -160,7 +160,7 @@ class CreditCard } } - return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id)]); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); } public function processUnsuccessfulPayment($server_response) diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index fe27b6979af3..093b8b15774d 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -739,7 +739,7 @@ class PdfBuilder if ($item->is_amount_discount) { $data[$key][$table_type.'.discount'] = $this->service->config->formatMoney($item->discount); } else { - $data[$key][$table_type.'.discount'] = floatval($item->discount).'%'; + $data[$key][$table_type.'.discount'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->discount)).'%'; } } else { $data[$key][$table_type.'.discount'] = ''; @@ -749,17 +749,17 @@ class PdfBuilder // but that's no longer necessary. if (isset($item->tax_rate1)) { - $data[$key][$table_type.'.tax_rate1'] = floatval($item->tax_rate1).'%'; + $data[$key][$table_type.'.tax_rate1'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate1)).'%'; $data[$key][$table_type.'.tax1'] = &$data[$key][$table_type.'.tax_rate1']; } if (isset($item->tax_rate2)) { - $data[$key][$table_type.'.tax_rate2'] = floatval($item->tax_rate2).'%'; + $data[$key][$table_type.'.tax_rate2'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate2)).'%'; $data[$key][$table_type.'.tax2'] = &$data[$key][$table_type.'.tax_rate2']; } if (isset($item->tax_rate3)) { - $data[$key][$table_type.'.tax_rate3'] = floatval($item->tax_rate3).'%'; + $data[$key][$table_type.'.tax_rate3'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate3)).'%'; $data[$key][$table_type.'.tax3'] = &$data[$key][$table_type.'.tax_rate3']; } diff --git a/composer.lock b/composer.lock index 9cd2207e89a2..fa1c4b178c59 100644 --- a/composer.lock +++ b/composer.lock @@ -16199,16 +16199,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.60.0", + "version": "v3.61.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "e595e4e070d17c5d42ed8c4206f630fcc5f401a4" + "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/e595e4e070d17c5d42ed8c4206f630fcc5f401a4", - "reference": "e595e4e070d17c5d42ed8c4206f630fcc5f401a4", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", + "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", "shasum": "" }, "require": { @@ -16290,7 +16290,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.60.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" }, "funding": [ { @@ -16298,7 +16298,7 @@ "type": "github" } ], - "time": "2024-07-25T09:26:51+00:00" + "time": "2024-07-31T14:33:15+00:00" }, { "name": "hamcrest/hamcrest-php",