1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-21 00:41:34 +02:00

Merge pull request #4798 from turbo124/v5-develop

Fixes for counters
This commit is contained in:
David Bomba 2021-01-30 00:02:20 +11:00 committed by GitHub
commit f2d4c7c72c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 308 additions and 211 deletions

View File

@ -4,12 +4,18 @@
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![codecov](https://codecov.io/gh/invoiceninja/invoiceninja/branch/v2/graph/badge.svg)](https://codecov.io/gh/invoiceninja/invoiceninja)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade)
# Invoice Ninja version 5 is in Beta!
# Invoice Ninja version 5.1 RC2!
We will be using the lessons learnt in Invoice Ninja 4.0 to build a bigger better platform to work from. If you would like to contribute to the project we will gladly accept contributions for code, user guides, bug tracking and feedback! Please consider the following guidelines prior to submitting a pull request:
Invoice Ninja version 5.1 has now reached Release Candidate 2!
What does this mean exactly? We consider this version _almost_ stable. There may be some remaining small issues which we would love to get feedback on. We would really appreciate the community booting up this version and attempting the migration from their Invoice Ninja V4 application and inspect the migrated data.
We'd also like feedback on any issues that you can see, and help us nail down the few remaining issues before Version 5 graduates to Stable Gold Release.
Please note we do not consider this version ready for production use, please stick with your V4 installation for your production clients!
## Quick Start
@ -17,13 +23,10 @@ Currently the client portal and API are of alpha quality, to get started:
```bash
git clone https://github.com/invoiceninja/invoiceninja.git
git checkout v2
git checkout v5-stable
cp .env.example .env
cp .env.dusk.example .env.dusk.local
php artisan key:generate
composer update
npm i
npm run production
```
Please Note: Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application.
@ -33,7 +36,7 @@ Run if you want to load sample data, remember to configure .env
php artisan migrate:fresh --seed && php artisan db:seed && php artisan ninja:create-test-data
```
To Run the web server
To run the web server
```
php artisan serve
```

View File

@ -1 +1 @@
5.0.55
5.0.56

View File

@ -235,8 +235,8 @@ class CompanySettings extends BaseSettings
public $font_size = 7; //@implemented
public $primary_font = 'Roboto';
public $secondary_font = 'Roboto';
public $primary_color = '#4caf50';
public $secondary_color = '#2196f3';
public $primary_color = '#142cb5';
public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where?
public $embed_documents = false; //@TODO where?

View File

@ -184,7 +184,9 @@ class BaseController extends Controller
protected function refreshResponse($query)
{
if (auth()->user()->getCompany()->is_large)
$user = auth()->user();
if ($user->getCompany()->is_large)
$this->manager->parseIncludes($this->mini_load);
else
$this->manager->parseIncludes($this->first_load);
@ -200,74 +202,145 @@ class BaseController extends Controller
$transformer = new $this->entity_transformer($this->serializer);
$updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0;
// if (auth()->user()->getCompany()->is_large && ! request()->has('updated_at')) {
// return response()->json(['message' => ctrans('texts.large_account_update_parameter'), 'errors' =>[]], 401);
// }
$updated_at = date('Y-m-d H:i:s', $updated_at);
$query->with(
[
'company' => function ($query) use ($updated_at) {
'company' => function ($query) use ($updated_at, $user) {
$query->whereNotNull('updated_at')->with('documents');
},
'company.clients' => function ($query) use ($updated_at) {
'company.clients' => function ($query) use ($updated_at, $user) {
$query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents');
if(!$user->hasPermission('view_client'))
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
},
'company.company_gateways' => function ($query) {
'company.company_gateways' => function ($query) use ($user) {
$query->whereNotNull('updated_at');
if(!$user->isAdmin())
$query->where('company_gateways.user_id', $user->id);
},
'company.credits'=> function ($query) use ($updated_at) {
'company.credits'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_credit'))
$query->where('credits.user_id', $user->id)->orWhere('credits.assigned_user_id', $user->id);
},
'company.designs'=> function ($query) use ($updated_at) {
'company.designs'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('company');
if(!$user->isAdmin())
$query->where('designs.user_id', $user->id);
},
'company.documents'=> function ($query) use ($updated_at) {
'company.documents'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
},
'company.expenses'=> function ($query) use ($updated_at) {
'company.expenses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_expense'))
$query->where('expenses.user_id', $user->id)->orWhere('expenses.assigned_user_id', $user->id);
},
'company.groups' => function ($query) use ($updated_at) {
'company.groups' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('group_settings.user_id', $user->id);
},
'company.invoices'=> function ($query) use ($updated_at) {
'company.invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_invoice'))
$query->where('invoices.user_id', $user->id)->orWhere('invoices.assigned_user_id', $user->id);
},
'company.payments'=> function ($query) use ($updated_at) {
'company.payments'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('paymentables', 'documents');
if(!$user->hasPermission('view_payment'))
$query->where('payments.user_id', $user->id)->orWhere('payments.assigned_user_id', $user->id);
},
'company.payment_terms'=> function ($query) use ($updated_at) {
'company.payment_terms'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('payment_terms.user_id', $user->id);
},
'company.products' => function ($query) use ($updated_at) {
'company.products' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_product'))
$query->where('products.user_id', $user->id)->orWhere('products.assigned_user_id', $user->id);
},
'company.projects'=> function ($query) use ($updated_at) {
'company.projects'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.quotes'=> function ($query) use ($updated_at) {
'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_quote'))
$query->where('quotes.user_id', $user->id)->orWhere('quotes.assigned_user_id', $user->id);
},
'company.recurring_invoices'=> function ($query) use ($updated_at) {
'company.recurring_invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_recurring_invoice'))
$query->where('recurring_invoices.user_id', $user->id)->orWhere('recurring_invoices.assigned_user_id', $user->id);
},
'company.tasks'=> function ($query) use ($updated_at) {
'company.tasks'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_task'))
$query->where('tasks.user_id', $user->id)->orWhere('tasks.assigned_user_id', $user->id);
},
'company.tax_rates' => function ($query) use ($updated_at) {
'company.tax_rates' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
},
'company.vendors'=> function ($query) use ($updated_at) {
'company.vendors'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('contacts', 'documents');
if(!$user->hasPermission('view_vendor'))
$query->where('vendors.user_id', $user->id)->orWhere('vendors.assigned_user_id', $user->id);
},
'company.expense_categories'=> function ($query) use ($updated_at) {
'company.expense_categories'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('expense_categories.user_id', $user->id);
},
'company.task_statuses'=> function ($query) use ($updated_at) {
'company.task_statuses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('task_statuses.user_id', $user->id);
},
'company.activities'=> function ($query) use($user) {
if(!$user->isAdmin())
$query->where('activities.user_id', $user->id);
}
]
);

View File

@ -370,14 +370,25 @@ class UserController extends BaseController
public function update(UpdateUserRequest $request, User $user)
{
$old_email = $user->email;
$old_company_user = $user->company_user;
$old_user = $user;
$new_email = $request->input('email');
$user = $this->user_repo->save($request->all(), $user);
$user = $user->fresh();
if ($old_email != $new_email) {
UserEmailChanged::dispatch($new_email, $old_email, auth()->user()->company());
}
if(
strcasecmp($old_company_user->permissions, $user->company_user->permissions) != 0 ||
$old_company_user->is_admin != $user->company_user->is_admin
){
$user->company_user()->update(["permissions_updated_at" => now()]);
}
event(new UserWasUpdated($user, auth()->user(), auth()->user()->company, Ninja::eventVars()));
return $this->itemResponse($user);

View File

@ -28,6 +28,7 @@ class CompanyUser extends Pivot
* @var array
*/
protected $casts = [
'permissions_updated_at' => 'timestamp',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',

View File

@ -202,15 +202,22 @@ class User extends Authenticatable implements MustVerifyEmail
$this->id = auth()->user()->id;
}
if (request()->header('X-API-TOKEN')) {
return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
->where('company_tokens.token', request()->header('X-API-TOKEN'))
->withTrashed();
} else {
return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
->where('company_user.user_id', $this->id)
->withTrashed();
}
return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'user_id', 'id', 'user_id')
->withTrashed();
// if (request()->header('X-API-TOKEN')) {
// nlog("with an API token");
// nlog(request()->header('X-API-TOKEN'));
// return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
// ->where('company_tokens.token', request()->header('X-API-TOKEN'))
// ->withTrashed();
// } else {
// return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
// ->where('company_user.user_id', $this->id)
// ->withTrashed();
// }
}
/**

View File

@ -48,7 +48,7 @@ class CompanyUserTransformer extends EntityTransformer
'updated_at' => (int) $company_user->updated_at,
'archived_at' => (int) $company_user->deleted_at,
'created_at' => (int) $company_user->created_at,
'permissions_updated_at' => (int) $company_user->permissions_updated_at,
];
}

View File

@ -33,6 +33,119 @@ trait GeneratesCounter
//todo in the form validation, we need to ensure that if a prefix and pattern is set we throw a validation error,
//only one type is allow else this will cause confusion to the end user
private function getNextEntityNumber($entity, Client $client)
{
$prefix = '';
$this->resetCounters($client);
$is_client_counter = false;
$counter_string = $this->getEntityCounter($entity, $client);
$pattern = $this->getNumberPattern($entity, $client);
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
if (property_exists($client->settings, $counter_string)) {
$counter = $client->settings->{$counter_string};
} else {
$counter = 1;
}
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
if (property_exists($client->group_settings, $counter_string)) {
$counter = $client->group_settings->{$counter_string};
} else {
$counter = 1;
}
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->{$counter_string};
$counter_entity = $client->company;
}
//If it is a quote - we need to
$pattern = $this->getNumberPattern($entity, $client);
$padding = $client->getSetting('counter_padding');
if($entity instanceof Invoice && $entity && $entity->recurring_id)
$prefix = $client->getSetting('recurring_number_prefix');
$entity_number = $this->checkEntityNumber($entity, $client, $counter, $padding, $pattern, $prefix);
$this->incrementCounter($counter_entity, $counter_string);
return $entity_number;
}
private function getNumberPattern($entity, Client $client)
{
$pattern_string = '';
switch ($entity) {
case Invoice::class:
$pattern_string = 'invoice_number_pattern';
break;
case Quote::class:
$pattern_string = 'quote_number_pattern';
break;
case RecurringInvoice::class:
$pattern_string = 'recurring_invoice_number_pattern';
break;
case Payment::class:
$pattern_string = 'payment_number_pattern';
break;
case Credit::class:
$pattern_string = 'credit_number_pattern';
break;
case Project::class:
$pattern_string = 'project_number_pattern';
break;
}
return $client->getSetting($pattern_string);
}
private function getEntityCounter($entity, $client)
{
switch ($entity) {
case Invoice::class:
return 'invoice_number_counter';
break;
case Quote::class:
if ($this->hasSharedCounter($client))
return 'invoice_number_counter';
return 'quote_number_counter';
break;
case RecurringInvoice::class:
return 'recurring_invoice_number_counter';
break;
case Payment::class:
return 'payment_number_counter';
break;
case Credit::class:
return 'credit_number_counter';
break;
case Project::class:
return 'project_number_counter';
break;
default:
return 'default_number_counter';
break;
}
}
/**
* Gets the next invoice number.
*
@ -43,42 +156,7 @@ trait GeneratesCounter
*/
public function getNextInvoiceNumber(Client $client, ?Invoice $invoice) :string
{
//Reset counters if enabled
$this->resetCounters($client);
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('invoice_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
if (property_exists($client->settings, 'invoice_number_counter')) {
$counter = $client->settings->invoice_number_counter;
} else {
$counter = 1;
}
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
$counter = $client->group_settings->invoice_number_counter;
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->invoice_number_counter;
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('invoice_number_pattern');
$padding = $client->getSetting('counter_padding');
$prefix = '';
if ($invoice && $invoice->recurring_id) {
$prefix = $client->getSetting('recurring_number_prefix');
}
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern, $prefix);
$this->incrementCounter($counter_entity, 'invoice_number_counter');
return $invoice_number;
return $this->getNextEntityNumber(Invoice::class, $client);
}
/**
@ -90,145 +168,36 @@ trait GeneratesCounter
*/
public function getNextCreditNumber(Client $client) :string
{
//Reset counters if enabled
$this->resetCounters($client);
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('credit_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
$counter = $client->settings->credit_number_counter;
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
$counter = $client->group_settings->credit_number_counter;
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->credit_number_counter;
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('credit_number_pattern');
$padding = $client->getSetting('counter_padding');
$credit_number = $this->checkEntityNumber(Credit::class, $client, $counter, $padding, $pattern);
$this->incrementCounter($counter_entity, 'credit_number_counter');
return $credit_number;
return $this->getNextEntityNumber(Credit::class, $client);
}
/**
* Gets the next quote number.
*
* @param Client $client The client
*
* @return string The next credit number.
*/
public function getNextQuoteNumber(Client $client)
{
//Reset counters if enabled
$this->resetCounters($client);
$used_counter = 'quote_number_counter';
if ($this->hasSharedCounter($client)) {
$used_counter = 'invoice_number_counter';
}
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('quote_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
$counter = $client->settings->{$used_counter};
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
$counter = $client->group_settings->{$used_counter};
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->{$used_counter};
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('quote_number_pattern');
$padding = $client->getSetting('counter_padding');
$quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern);
// if($this->recurring_id)
// $quote_number = $this->prefixCounter($quote_number, $client->getSetting('recurring_number_prefix'));
$this->incrementCounter($counter_entity, $used_counter);
return $quote_number;
return $this->getNextEntityNumber(Quote::class, $client);
}
public function getNextRecurringInvoiceNumber(Client $client)
{
//Reset counters if enabled
$this->resetCounters($client);
$is_client_counter = false;
//todo handle if we have specific client patterns in the future
$pattern = $client->company->settings->recurring_invoice_number_pattern;
//Determine if we are using client_counters
if (strpos($pattern, 'client_counter') === false) {
$counter = $client->company->settings->recurring_invoice_number_counter;
} else {
$counter = $client->settings->recurring_invoice_number_counter;
$is_client_counter = true;
}
//Return a valid counter
$pattern = '';
$padding = $client->getSetting('counter_padding');
$invoice_number = $this->checkEntityNumber(RecurringInvoice::class, $client, $counter, $padding, $pattern);
//$invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix'));
//increment the correct invoice_number Counter (company vs client)
if ($is_client_counter) {
$this->incrementCounter($client, 'recurring_invoice_number_counter');
} else {
$this->incrementCounter($client->company, 'recurring_invoice_number_counter');
}
return $invoice_number;
return $this->getNextEntityNumber(RecurringInvoice::class, $client);
}
/**
* Payment Number Generator.
* @param Client $client
* @return string The payment number
* Gets the next Payment number.
*
* @param Client $client The client
*
* @return string The next payment number.
*/
public function getNextPaymentNumber(Client $client) :string
{
//Reset counters if enabled
$this->resetCounters($client);
$is_client_counter = false;
//todo handle if we have specific client patterns in the future
$pattern = $client->company->settings->payment_number_pattern;
//Determine if we are using client_counters
if (strpos($pattern, 'client_counter') === false) {
$counter = $client->company->settings->payment_number_counter;
} else {
$counter = $client->settings->payment_number_counter;
$is_client_counter = true;
}
//Return a valid counter
$pattern = '';
$padding = $client->getSetting('counter_padding');
$payment_number = $this->checkEntityNumber(Payment::class, $client, $counter, $padding, $pattern);
//increment the correct invoice_number Counter (company vs client)
if ($is_client_counter) {
$this->incrementCounter($client, 'payment_number_counter');
} else {
$this->incrementCounter($client->company, 'payment_number_counter');
}
return (string) $payment_number;
return $this->getNextEntityNumber(Payment::class, $client);
}
/**
@ -342,7 +311,7 @@ trait GeneratesCounter
*
* @return bool True if has shared counter, False otherwise.
*/
public function hasSharedCounter(Client $client) : bool
public function hasSharedCounter(Client $client) : bool
{
return (bool) $client->getSetting('shared_invoice_quote_counter');
}
@ -404,6 +373,9 @@ trait GeneratesCounter
$settings->invoice_number_counter = 0;
}
if(!property_exists($settings, $counter_name))
$settings->{$counter_name} = 1;
$settings->{$counter_name} = $settings->{$counter_name} + 1;
$entity->settings = $settings;

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.0.55',
'app_version' => '5.0.56',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPermissionChangedTimestamp extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('company_user', function (Blueprint $table) {
$table->timestamp('permissions_updated_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -286,7 +286,7 @@ return [
'password' => 'Password',
'pro_plan_product' => 'Pro Plan',
'pro_plan_success' => 'Thanks for choosing Invoice Ninja\'s Pro plan!&nbsp;<br/>
Next StepsA payable invoice has been sent to the email
Next Steps. A payable invoice has been sent to the email
address associated with your account. To unlock all of the awesome
Pro features, please follow the instructions on the invoice to pay
for a year of Pro-level invoicing.
@ -1091,7 +1091,7 @@ return [
'invoice_item_fields' => 'Invoice Item Fields',
'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_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices.',
'recurring_invoice_number_prefix_help' => 'Specify a prefix to be added to the invoice number for recurring invoices.',
// Client Passwords
'enable_portal_password' => 'Password Protect Invoices',