From 34f2b04e33c137e1df150f24301434fe9bc5885c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 30 Jul 2023 08:10:41 +1000 Subject: [PATCH 1/7] Updates for recurring expense payment dates --- app/Factory/RecurringExpenseToExpenseFactory.php | 5 +---- app/Jobs/Cron/RecurringExpensesCron.php | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Factory/RecurringExpenseToExpenseFactory.php b/app/Factory/RecurringExpenseToExpenseFactory.php index 82ea9224f75a..1f162eb8d73c 100644 --- a/app/Factory/RecurringExpenseToExpenseFactory.php +++ b/app/Factory/RecurringExpenseToExpenseFactory.php @@ -40,16 +40,13 @@ class RecurringExpenseToExpenseFactory $expense->tax_name3 = $recurring_expense->tax_name3; $expense->tax_rate3 = $recurring_expense->tax_rate3; $expense->date = now()->format('Y-m-d'); - $expense->payment_date = $recurring_expense->payment_date ?: now()->format('Y-m-d'); + // $expense->payment_date = $recurring_expense->payment_date ?: now()->format('Y-m-d'); $expense->amount = $recurring_expense->amount; $expense->foreign_amount = $recurring_expense->foreign_amount ?: 0; //11-09-2022 - we should be tracking the recurring expense!! $expense->recurring_expense_id = $recurring_expense->id; - // $expense->private_notes = $recurring_expense->private_notes; - // $expense->public_notes = $recurring_expense->public_notes; - $expense->public_notes = self::transformObject($recurring_expense->public_notes, $recurring_expense); $expense->private_notes = self::transformObject($recurring_expense->private_notes, $recurring_expense); diff --git a/app/Jobs/Cron/RecurringExpensesCron.php b/app/Jobs/Cron/RecurringExpensesCron.php index 796d38a3c49d..b3f785444a5c 100644 --- a/app/Jobs/Cron/RecurringExpensesCron.php +++ b/app/Jobs/Cron/RecurringExpensesCron.php @@ -103,8 +103,11 @@ class RecurringExpensesCron $expense = RecurringExpenseToExpenseFactory::create($recurring_expense); $expense->saveQuietly(); + if($expense->company->mark_expenses_paid) + $expense->payment_date = now()->format('Y-m-d'); + $expense->number = $this->getNextExpenseNumber($expense); - $expense->save(); + $expense->saveQuietly(); $recurring_expense->next_send_date = $recurring_expense->nextSendDate(); $recurring_expense->next_send_date_client = $recurring_expense->next_send_date; From e13d867c7f4bcde84c6f6217bd0bd95874208f7d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 31 Jul 2023 11:17:36 +1000 Subject: [PATCH 2/7] Additional query filters for invoices --- app/Filters/InvoiceFilters.php | 42 +++++++++++++++++++ app/Http/Livewire/BillingPortalPurchasev2.php | 3 ++ 2 files changed, 45 insertions(+) diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 1e07412eda4c..a1f522168e69 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -183,6 +183,48 @@ class InvoiceFilters extends QueryFilters ->where('client_id', $this->decodePrimaryKey($client_id)); } + + /** + * @param string $date + * @return Builder + * @throws InvalidArgumentException + */ + public function date(string $date = ''): Builder + { + if (strlen($date) == 0) { + return $this->builder; + } + + if (is_numeric($date)) { + $created_at = Carbon::createFromTimestamp((int)$date); + } else { + $created_at = Carbon::parse($date); + } + + return $this->builder->where('date', '>=', $date); + } + + /** + * @param string $date + * @return Builder + * @throws InvalidArgumentException + */ + public function due_date(string $date = ''): Builder + { + if (strlen($date) == 0) { + return $this->builder; + } + + if (is_numeric($date)) { + $created_at = Carbon::createFromTimestamp((int)$date); + } else { + $created_at = Carbon::parse($date); + } + + return $this->builder->where('due_date', '>=', $date); + } + + /** * Sorts the list based on $sort. * diff --git a/app/Http/Livewire/BillingPortalPurchasev2.php b/app/Http/Livewire/BillingPortalPurchasev2.php index aaf9bf65cbfe..2e1272e5e896 100644 --- a/app/Http/Livewire/BillingPortalPurchasev2.php +++ b/app/Http/Livewire/BillingPortalPurchasev2.php @@ -25,6 +25,7 @@ use App\Models\Subscription; use App\Repositories\ClientContactRepository; use App\Repositories\ClientRepository; use App\Utils\Number; +use App\Utils\Traits\MakesHash; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; @@ -34,6 +35,7 @@ use Livewire\Component; class BillingPortalPurchasev2 extends Component { + use MakesHash; /** * Random hash generated by backend to handle the tracking of state. * @@ -422,6 +424,7 @@ class BillingPortalPurchasev2 extends Component $client_repo = new ClientRepository(new ClientContactRepository()); $data = [ 'name' => '', + 'group_id' => $this->encodePrimaryKey($this->subscription->group_id), 'contacts' => [ ['email' => $this->email], ], From 924a45db7c7b1269d90745618bb199deeea1d622 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 31 Jul 2023 11:22:21 +1000 Subject: [PATCH 3/7] Additional query filters for invoices --- app/Filters/InvoiceFilters.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index a1f522168e69..deab3483fd8b 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -196,9 +196,9 @@ class InvoiceFilters extends QueryFilters } if (is_numeric($date)) { - $created_at = Carbon::createFromTimestamp((int)$date); + $date = Carbon::createFromTimestamp((int)$date); } else { - $created_at = Carbon::parse($date); + $date = Carbon::parse($date); } return $this->builder->where('date', '>=', $date); @@ -216,9 +216,9 @@ class InvoiceFilters extends QueryFilters } if (is_numeric($date)) { - $created_at = Carbon::createFromTimestamp((int)$date); + $date = Carbon::createFromTimestamp((int)$date); } else { - $created_at = Carbon::parse($date); + $date = Carbon::parse($date); } return $this->builder->where('due_date', '>=', $date); From cbbac0b18c63090c6cbbb26f68a7cc47c5512ab7 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 31 Jul 2023 11:28:30 +1000 Subject: [PATCH 4/7] Working on tax calculations for inclusive taxes --- .../Invoice/InvoiceItemSumInclusive.php | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/app/Helpers/Invoice/InvoiceItemSumInclusive.php b/app/Helpers/Invoice/InvoiceItemSumInclusive.php index 7f8d33b2dd02..499051be522a 100644 --- a/app/Helpers/Invoice/InvoiceItemSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceItemSumInclusive.php @@ -12,11 +12,13 @@ namespace App\Helpers\Invoice; use App\Models\Quote; +use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Models\RecurringQuote; use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSumInclusive @@ -25,6 +27,71 @@ class InvoiceItemSumInclusive use Discounter; use Taxer; + + private array $eu_tax_jurisdictions = [ + 'AT', // Austria + 'BE', // Belgium + 'BG', // Bulgaria + 'CY', // Cyprus + 'CZ', // Czech Republic + 'DE', // Germany + 'DK', // Denmark + 'EE', // Estonia + 'ES', // Spain + 'FI', // Finland + 'FR', // France + 'GR', // Greece + 'HR', // Croatia + 'HU', // Hungary + 'IE', // Ireland + 'IT', // Italy + 'LT', // Lithuania + 'LU', // Luxembourg + 'LV', // Latvia + 'MT', // Malta + 'NL', // Netherlands + 'PL', // Poland + 'PT', // Portugal + 'RO', // Romania + 'SE', // Sweden + 'SI', // Slovenia + 'SK', // Slovakia + ]; + + private array $tax_jurisdictions = [ + 'AT', // Austria + 'BE', // Belgium + 'BG', // Bulgaria + 'CY', // Cyprus + 'CZ', // Czech Republic + 'DE', // Germany + 'DK', // Denmark + 'EE', // Estonia + 'ES', // Spain + 'FI', // Finland + 'FR', // France + 'GR', // Greece + 'HR', // Croatia + 'HU', // Hungary + 'IE', // Ireland + 'IT', // Italy + 'LT', // Lithuania + 'LU', // Luxembourg + 'LV', // Latvia + 'MT', // Malta + 'NL', // Netherlands + 'PL', // Poland + 'PT', // Portugal + 'RO', // Romania + 'SE', // Sweden + 'SI', // Slovenia + 'SK', // Slovakia + + 'US', // USA + + 'AU', // Australia + ]; + protected RecurringInvoice | Invoice | Quote | Credit | PurchaseOrder | RecurringQuote $invoice; private $currency; @@ -39,6 +106,12 @@ class InvoiceItemSumInclusive private $tax_collection; + private bool $calc_tax = false; + + private ?Client $client; + + private RuleInterface $rule; + public function __construct(RecurringInvoice | Invoice | Quote | Credit | PurchaseOrder | RecurringQuote $invoice) { $this->tax_collection = collect([]); @@ -47,6 +120,7 @@ class InvoiceItemSumInclusive if ($this->invoice->client) { $this->currency = $this->invoice->client->currency(); + $this->shouldCalculateTax(); } else { $this->currency = $this->invoice->vendor->currency(); } @@ -107,12 +181,46 @@ class InvoiceItemSumInclusive return $this; } + + /** + * Attempts to calculate taxes based on the clients location + * + * @return self + */ + private function calcTaxesAutomatically(): self + { + $this->rule->tax($this->item); + + $precision = strlen(substr(strrchr($this->rule->tax_rate1, "."), 1)); + + $this->item->tax_name1 = $this->rule->tax_name1; + $this->item->tax_rate1 = round($this->rule->tax_rate1, $precision); + + $precision = strlen(substr(strrchr($this->rule->tax_rate2, "."), 1)); + + $this->item->tax_name2 = $this->rule->tax_name2; + $this->item->tax_rate2 = round($this->rule->tax_rate2, $precision); + + $precision = strlen(substr(strrchr($this->rule->tax_rate3, "."), 1)); + + $this->item->tax_name3 = $this->rule->tax_name3; + $this->item->tax_rate3 = round($this->rule->tax_rate3, $precision); + + return $this; + } + + /** * Taxes effect the line totals and item costs. we decrement both on * application of inclusive tax rates. */ private function calcTaxes() { + + if ($this->calc_tax) { + $this->calcTaxesAutomatically(); + } + $item_tax = 0; $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / 100)); @@ -275,4 +383,36 @@ class InvoiceItemSumInclusive $this->setTotalTaxes($item_tax); } + + + private function shouldCalculateTax(): self + { + + if (!$this->invoice->company?->calculate_taxes || $this->invoice->company->account->isFreeHostedClient()) { + $this->calc_tax = false; + return $this; + } + + if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) ) { //only calculate for supported tax jurisdictions + + $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; + + $this->rule = new $class(); + + if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2)) + return $this; + + $this->rule + ->setEntity($this->invoice) + ->init(); + + $this->calc_tax = $this->rule->shouldCalcTax(); + + return $this; + } + + return $this; + } + + } From 69786492e0be3d10552941f5e286b9f4d942791f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 31 Jul 2023 18:22:34 +1000 Subject: [PATCH 5/7] Minor update --- app/Http/Requests/Invoice/UpdateInvoiceRequest.php | 2 +- .../ValidationRules/Payment/ValidRefundableRequest.php | 2 +- app/Models/Payment.php | 8 +++++++- app/Models/Product.php | 2 +- app/Services/Payment/DeletePayment.php | 9 +++++---- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 971de8712cd3..db055d17c4cf 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -71,7 +71,7 @@ class UpdateInvoiceRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - $rules['status_id'] = 'bail|sometimes|not_in:5'; //do not all cancelled invoices to be modfified. + $rules['status_id'] = 'bail|sometimes|not_in:5'; //do not allow cancelled invoices to be modfified. $rules['exchange_rate'] = 'bail|sometimes|gt:0'; // not needed. diff --git a/app/Http/ValidationRules/Payment/ValidRefundableRequest.php b/app/Http/ValidationRules/Payment/ValidRefundableRequest.php index 3ae582e772a8..29b6a7230d58 100644 --- a/app/Http/ValidationRules/Payment/ValidRefundableRequest.php +++ b/app/Http/ValidationRules/Payment/ValidRefundableRequest.php @@ -106,7 +106,7 @@ class ValidRefundableRequest implements Rule if ($payment->credits()->exists()) { $paymentable_credit = $payment->credits->where('id', $credit->id)->first(); - if (! $paymentable_invoice) { + if (! $paymentable_credit) { $this->error_msg = ctrans('texts.credit_not_related_to_payment', ['credit' => $credit->hashed_id]); return false; diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 4502b389a405..cbcbc1171f76 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -15,12 +15,13 @@ use App\Events\Payment\PaymentWasRefunded; use App\Events\Payment\PaymentWasVoided; use App\Services\Ledger\LedgerService; use App\Services\Payment\PaymentService; -use App\Utils\Ninja; +use App\Utils\Ninja; use App\Utils\Number; use App\Utils\Traits\Inviteable; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use App\Utils\Traits\Payment\Refundable; +use Awobaz\Compoships\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -251,6 +252,11 @@ class Payment extends BaseModel return $this->belongsTo(Currency::class); } + public function transaction(): BelongsTo + { + return $this->belongsTo(BankTransaction::class); + } + public function exchange_currency() { return $this->belongsTo(Currency::class, 'exchange_currency_id', 'id'); diff --git a/app/Models/Product.php b/app/Models/Product.php index cff15a798e69..c3e8724ebc46 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -198,7 +198,7 @@ class Product extends BaseModel ], ]); - return $converter->convert($this->notes); + return $converter->convert($this->notes ?? ''); } public function portalUrl($use_react_url): string diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index 38002a9057be..6c99757ea25e 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -54,13 +54,14 @@ class DeletePayment /** @return $this */ private function cleanupPayment() { + $this->payment->is_deleted = true; $this->payment->delete(); - // BankTransaction::where('payment_id', $this->payment->id)->cursor()->each(function ($bt){ - // $bt->payment_id = null; - // $bt->save(); - // }); + BankTransaction::where('payment_id', $this->payment->id)->cursor()->each(function ($bt){ + $bt->payment_id = null; + $bt->save(); + }); return $this; } From f21471ba3b8d655c09dc551dc7f652edf0a969db Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 1 Aug 2023 07:49:18 +1000 Subject: [PATCH 6/7] Updates for permission tests --- tests/Unit/PermissionsTest.php | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/Unit/PermissionsTest.php b/tests/Unit/PermissionsTest.php index ae1b0f141874..b55271ff9490 100644 --- a/tests/Unit/PermissionsTest.php +++ b/tests/Unit/PermissionsTest.php @@ -33,9 +33,14 @@ class PermissionsTest extends TestCase public Company $company; + public $faker; + + public $token; + protected function setUp() :void { parent::setUp(); + $this->faker = \Faker\Factory::create(); $account = Account::factory()->create([ @@ -75,6 +80,56 @@ class PermissionsTest extends TestCase $company_token->save(); } + public function testClientOverviewPermissions() + { + $u = User::factory()->create([ + 'account_id' => $this->company->account_id, + 'confirmation_code' => '123', + 'email' => $this->faker->safeEmail(), + ]); + + $c = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $u->id, + ]); + + Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'client_id' => $c->id, + 'user_id' => $u->id, + 'status_id' => 2 + ]); + + $low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first(); + $low_cu->permissions = '["edit_client","create_client","create_invoice","edit_invoice","create_quote","edit_quote"]'; + $low_cu->save(); + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/invoices'); + + $response->assertStatus(200); + + $data = $response->json(); + + $this->assertEquals(2, count($data)); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get("/api/v1/invoices?include=client&client_id={$c->hashed_id}&sort=id|desc&per_page=10&page=1&filter=&status=active"); + + $response->assertStatus(200); + + $data = $response->json(); + + $this->assertEquals(2, count($data)); + + } + + public function testHasExcludedPermissions() { $low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first(); From ab58af2b1cc0b38e09edd1ed67b5f646d8c78d26 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 1 Aug 2023 19:39:33 +1000 Subject: [PATCH 7/7] v5.6.24 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 62fcb18413f5..66e48ed4e56c 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.6.23 \ No newline at end of file +5.6.24 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 6fa3bbdaff40..33f173d4113f 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.6.23'), - 'app_tag' => env('APP_TAG','5.6.23'), + 'app_version' => env('APP_VERSION','5.6.24'), + 'app_tag' => env('APP_TAG','5.6.24'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''),