Merge pull request #8678 from turbo124/v5-develop

v5.6.24
This commit is contained in:
David Bomba 2023-08-01 19:40:09 +10:00 committed by GitHub
commit 12e9a78881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 263 additions and 16 deletions

View File

@ -1 +1 @@
5.6.23 5.6.24

View File

@ -40,16 +40,13 @@ class RecurringExpenseToExpenseFactory
$expense->tax_name3 = $recurring_expense->tax_name3; $expense->tax_name3 = $recurring_expense->tax_name3;
$expense->tax_rate3 = $recurring_expense->tax_rate3; $expense->tax_rate3 = $recurring_expense->tax_rate3;
$expense->date = now()->format('Y-m-d'); $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->amount = $recurring_expense->amount;
$expense->foreign_amount = $recurring_expense->foreign_amount ?: 0; $expense->foreign_amount = $recurring_expense->foreign_amount ?: 0;
//11-09-2022 - we should be tracking the recurring expense!! //11-09-2022 - we should be tracking the recurring expense!!
$expense->recurring_expense_id = $recurring_expense->id; $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->public_notes = self::transformObject($recurring_expense->public_notes, $recurring_expense);
$expense->private_notes = self::transformObject($recurring_expense->private_notes, $recurring_expense); $expense->private_notes = self::transformObject($recurring_expense->private_notes, $recurring_expense);

View File

@ -183,6 +183,48 @@ class InvoiceFilters extends QueryFilters
->where('client_id', $this->decodePrimaryKey($client_id)); ->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)) {
$date = Carbon::createFromTimestamp((int)$date);
} else {
$date = 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)) {
$date = Carbon::createFromTimestamp((int)$date);
} else {
$date = Carbon::parse($date);
}
return $this->builder->where('due_date', '>=', $date);
}
/** /**
* Sorts the list based on $sort. * Sorts the list based on $sort.
* *

View File

@ -12,11 +12,13 @@
namespace App\Helpers\Invoice; namespace App\Helpers\Invoice;
use App\Models\Quote; use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\RecurringQuote; use App\Models\RecurringQuote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\DataMapper\Tax\RuleInterface;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
class InvoiceItemSumInclusive class InvoiceItemSumInclusive
@ -25,6 +27,71 @@ class InvoiceItemSumInclusive
use Discounter; use Discounter;
use Taxer; 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; protected RecurringInvoice | Invoice | Quote | Credit | PurchaseOrder | RecurringQuote $invoice;
private $currency; private $currency;
@ -39,6 +106,12 @@ class InvoiceItemSumInclusive
private $tax_collection; private $tax_collection;
private bool $calc_tax = false;
private ?Client $client;
private RuleInterface $rule;
public function __construct(RecurringInvoice | Invoice | Quote | Credit | PurchaseOrder | RecurringQuote $invoice) public function __construct(RecurringInvoice | Invoice | Quote | Credit | PurchaseOrder | RecurringQuote $invoice)
{ {
$this->tax_collection = collect([]); $this->tax_collection = collect([]);
@ -47,6 +120,7 @@ class InvoiceItemSumInclusive
if ($this->invoice->client) { if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency(); $this->currency = $this->invoice->client->currency();
$this->shouldCalculateTax();
} else { } else {
$this->currency = $this->invoice->vendor->currency(); $this->currency = $this->invoice->vendor->currency();
} }
@ -107,12 +181,46 @@ class InvoiceItemSumInclusive
return $this; 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 * Taxes effect the line totals and item costs. we decrement both on
* application of inclusive tax rates. * application of inclusive tax rates.
*/ */
private function calcTaxes() private function calcTaxes()
{ {
if ($this->calc_tax) {
$this->calcTaxesAutomatically();
}
$item_tax = 0; $item_tax = 0;
$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / 100)); $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / 100));
@ -275,4 +383,36 @@ class InvoiceItemSumInclusive
$this->setTotalTaxes($item_tax); $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;
}
} }

View File

@ -25,6 +25,7 @@ use App\Models\Subscription;
use App\Repositories\ClientContactRepository; use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository; use App\Repositories\ClientRepository;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -34,6 +35,7 @@ use Livewire\Component;
class BillingPortalPurchasev2 extends Component class BillingPortalPurchasev2 extends Component
{ {
use MakesHash;
/** /**
* Random hash generated by backend to handle the tracking of state. * 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()); $client_repo = new ClientRepository(new ClientContactRepository());
$data = [ $data = [
'name' => '', 'name' => '',
'group_id' => $this->encodePrimaryKey($this->subscription->group_id),
'contacts' => [ 'contacts' => [
['email' => $this->email], ['email' => $this->email],
], ],

View File

@ -71,7 +71,7 @@ class UpdateInvoiceRequest extends Request
$rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = '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'; $rules['exchange_rate'] = 'bail|sometimes|gt:0';
// not needed. // not needed.

View File

@ -106,7 +106,7 @@ class ValidRefundableRequest implements Rule
if ($payment->credits()->exists()) { if ($payment->credits()->exists()) {
$paymentable_credit = $payment->credits->where('id', $credit->id)->first(); $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]); $this->error_msg = ctrans('texts.credit_not_related_to_payment', ['credit' => $credit->hashed_id]);
return false; return false;

View File

@ -103,8 +103,11 @@ class RecurringExpensesCron
$expense = RecurringExpenseToExpenseFactory::create($recurring_expense); $expense = RecurringExpenseToExpenseFactory::create($recurring_expense);
$expense->saveQuietly(); $expense->saveQuietly();
if($expense->company->mark_expenses_paid)
$expense->payment_date = now()->format('Y-m-d');
$expense->number = $this->getNextExpenseNumber($expense); $expense->number = $this->getNextExpenseNumber($expense);
$expense->save(); $expense->saveQuietly();
$recurring_expense->next_send_date = $recurring_expense->nextSendDate(); $recurring_expense->next_send_date = $recurring_expense->nextSendDate();
$recurring_expense->next_send_date_client = $recurring_expense->next_send_date; $recurring_expense->next_send_date_client = $recurring_expense->next_send_date;

View File

@ -15,12 +15,13 @@ use App\Events\Payment\PaymentWasRefunded;
use App\Events\Payment\PaymentWasVoided; use App\Events\Payment\PaymentWasVoided;
use App\Services\Ledger\LedgerService; use App\Services\Ledger\LedgerService;
use App\Services\Payment\PaymentService; use App\Services\Payment\PaymentService;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\Inviteable; use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Payment\Refundable; use App\Utils\Traits\Payment\Refundable;
use Awobaz\Compoships\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
/** /**
@ -251,6 +252,11 @@ class Payment extends BaseModel
return $this->belongsTo(Currency::class); return $this->belongsTo(Currency::class);
} }
public function transaction(): BelongsTo
{
return $this->belongsTo(BankTransaction::class);
}
public function exchange_currency() public function exchange_currency()
{ {
return $this->belongsTo(Currency::class, 'exchange_currency_id', 'id'); return $this->belongsTo(Currency::class, 'exchange_currency_id', 'id');

View File

@ -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 public function portalUrl($use_react_url): string

View File

@ -54,13 +54,14 @@ class DeletePayment
/** @return $this */ /** @return $this */
private function cleanupPayment() private function cleanupPayment()
{ {
$this->payment->is_deleted = true; $this->payment->is_deleted = true;
$this->payment->delete(); $this->payment->delete();
// BankTransaction::where('payment_id', $this->payment->id)->cursor()->each(function ($bt){ BankTransaction::where('payment_id', $this->payment->id)->cursor()->each(function ($bt){
// $bt->payment_id = null; $bt->payment_id = null;
// $bt->save(); $bt->save();
// }); });
return $this; return $this;
} }

View File

@ -15,8 +15,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION','5.6.23'), 'app_version' => env('APP_VERSION','5.6.24'),
'app_tag' => env('APP_TAG','5.6.23'), 'app_tag' => env('APP_TAG','5.6.24'),
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

View File

@ -33,9 +33,14 @@ class PermissionsTest extends TestCase
public Company $company; public Company $company;
public $faker;
public $token;
protected function setUp() :void protected function setUp() :void
{ {
parent::setUp(); parent::setUp();
$this->faker = \Faker\Factory::create(); $this->faker = \Faker\Factory::create();
$account = Account::factory()->create([ $account = Account::factory()->create([
@ -75,6 +80,56 @@ class PermissionsTest extends TestCase
$company_token->save(); $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() public function testHasExcludedPermissions()
{ {
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first(); $low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();