Set casts on Invoice Line Items (#3049)

* Implement client/group/company level counters clientCounter, groupCounter and counter

* Implement functionalityfor customising the timing of invoice_number creation

* Add Jobs

* adjustments

* clean line items at the request layer

* Clean line items at the request layer
This commit is contained in:
David Bomba 2019-11-08 11:38:22 +11:00 committed by GitHub
parent a6f928b181
commit a78b6aaacd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 201 additions and 27 deletions

View File

@ -72,6 +72,7 @@ class CompanySettings extends BaseSettings
public $translations;
public $counter_number_applied = 'when_saved'; // when_saved , when_sent , when_paid
/* Counters */
public $invoice_number_pattern = '';
public $invoice_number_counter = 1;
@ -132,7 +133,6 @@ class CompanySettings extends BaseSettings
public $tax_rate2 = 0;
public $tax_name3 = '';
public $tax_rate3 = 0;
public $number_of_tax_rates = 1;
public $payment_type_id = '1';
public $custom_fields = '';
public $invoice_fields = '';
@ -226,7 +226,7 @@ class CompanySettings extends BaseSettings
public static $casts = [
'number_of_tax_rates' => 'int',
'counter_number_applied' => 'string',
'email_subject_custom1' => 'string',
'email_subject_custom2' => 'string',
'email_subject_custom3' => 'string',

View File

@ -241,6 +241,9 @@ class InvoiceItemSum
foreach($this->line_items as $this->item)
{
if($this->item->line_total == 0)
continue;
$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount/$this->sub_total));
$item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount);

View File

@ -14,11 +14,13 @@ namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
class StoreInvoiceRequest extends Request
{
use MakesHash;
use CleanLineItems;
/**
* Determine if the user is authorized to make this request.
@ -34,6 +36,7 @@ class StoreInvoiceRequest extends Request
public function rules()
{
$this->sanitize();
return [
'client_id' => 'required',
// 'invoice_type_id' => 'integer',
@ -46,11 +49,12 @@ class StoreInvoiceRequest extends Request
$input = $this->all();
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
$input['line_items'] = isset($input['line_items']) ? $input['line_items'] : [];
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']);
$this->replace($input);
return $this->all();
}
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
@ -19,6 +20,7 @@ use Illuminate\Validation\Rule;
class UpdateInvoiceRequest extends Request
{
use MakesHash;
use CleanLineItems;
/**
* Determine if the user is authorized to make this request.
*
@ -52,6 +54,8 @@ class UpdateInvoiceRequest extends Request
if(isset($input['client_id']))
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$this->replace($input);
return $this->all();

View File

@ -0,0 +1,81 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Invoice;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentTerm;
use App\Repositories\InvoiceRepository;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class ApplyInvoiceNumber implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, GeneratesCounter;
public $invoice;
public $settings;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Invoice $invoice, $settings)
{
$this->invoice = $invoice;
$this->settings = $settings;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
//return early
if($this->invoice->invoice_number != '')
return $this->invoice;
switch ($this->settings->counter_number_applied) {
case 'when_saved':
$this->invoice->invoice_number = $this->getNextInvoiceNumber($this->invoice->client);
break;
case 'when_sent':
if($this->invoice->status_id == Invoice::STATUS_SENT)
$this->invoice->invoice_number = $this->getNextInvoiceNumber($this->invoice->client);
break;
default:
# code...
break;
}
$this->invoice->save();
return $this->invoice;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Invoice;
use App\Jobs\Invoice\ApplyInvoiceNumber;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentTerm;
@ -108,6 +109,8 @@ class ApplyPaymentToInvoice implements ShouldQueue
$this->invoice->save();
$this->invoice = ApplyInvoiceNumber::dispatchNow($this->invoice, $invoice->client->getMergedSettings());
return $this->invoice;
}
}

View File

@ -16,6 +16,7 @@ use App\Events\Invoice\InvoiceWasUpdated;
use App\Factory\InvoiceInvitationFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Jobs\Invoice\ApplyInvoiceNumber;
use App\Listeners\Invoice\CreateInvoiceInvitation;
use App\Models\ClientContact;
use App\Models\Invoice;
@ -120,6 +121,8 @@ class InvoiceRepository extends BaseRepository
if($finished_amount != $starting_amount)
UpdateCompanyLedgerWithInvoice::dispatchNow($invoice, ($finished_amount - $starting_amount));
$invoice = ApplyInvoiceNumber::dispatchNow($invoice, $invoice->client->getMergedSettings());
return $invoice->fresh();
}
@ -139,7 +142,7 @@ class InvoiceRepository extends BaseRepository
$invoice->status_id = Invoice::STATUS_SENT;
$this->markInvitationsSent();
$this->markInvitationsSent($invoice);
$invoice->save();
@ -148,7 +151,9 @@ class InvoiceRepository extends BaseRepository
* When marked as sent it becomes a ledgerable item.
*
*/
UpdateCompanyLedgerWithInvoice::dispatchNow($this->invoice, $this->balance);
$invoice = ApplyInvoiceNumber::dispatchNow($invoice, $invoice->client->getMergedSettings());
UpdateCompanyLedgerWithInvoice::dispatchNow($invoice, $this->balance);
return $invoice;

View File

@ -92,7 +92,7 @@ class InvoiceTransformer extends EntityTransformer
'design_id' => (string) ($invoice->design_id ?: 1),
'updated_at' => $invoice->updated_at,
'archived_at' => $invoice->deleted_at,
'invoice_number' => $invoice->invoice_number,
'invoice_number' => $invoice->invoice_number ?: '',
'discount' => (float) $invoice->discount,
'po_number' => $invoice->po_number ?: '',
'invoice_date' => $invoice->invoice_date ?: '',

View File

@ -0,0 +1,67 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils\Traits;
use App\DataMapper\BaseSettings;
use App\DataMapper\InvoiceItem;
/**
* Class CleanLineItems.
*/
trait CleanLineItems
{
public function cleanItems($items) :array
{
if(!isset($items))
return [];
$cleaned_items = [];
foreach($items as $item)
$cleaned_items[] = $this->cleanLineItem($item);
return $cleaned_items;
}
/**
* Sets default values for the line_items
* @return $this
*/
private function cleanLineItem($item)
{
$invoice_item = (object)get_class_vars(InvoiceItem::class);
unset($invoice_item->casts);
foreach($invoice_item as $key => $value)
{
if(!array_key_exists($key, $item) || !isset($item[$key])){
$item[$key] = $value;
$item[$key] = BaseSettings::castAttribute(InvoiceItem::$casts[$key], $value);
}
}
if(array_key_exists("id", $item))
{
$item['id'] = $item['id']*-1;
}
return $item;
}
}

View File

@ -41,33 +41,33 @@ trait GeneratesCounter
//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->invoice_number_pattern;
$pattern = $client->getSetting('invoice_number_pattern');
//Determine if we are using client_counters
if(strpos($pattern, 'client_counter') === false)
if(strpos($pattern, 'clientCounter'))
{
$counter = $client->company->settings->invoice_number_counter;
$counter = $client->settings->invoice_number_counter;
$counter_entity = $client;
}
elseif(strpos($pattern, 'groupCounter'))
{
$counter = $client->group_settings->invoice_number_counter;
$counter_entity = $client->group_settings;
}
else
{
$counter = $client->settings->invoice_number_counter;
$is_client_counter = true;
$counter = $client->company->settings->invoice_number_counter;
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->company->settings->invoice_number_pattern;
$prefix = $client->company->settings->invoice_number_prefix;
$padding = $client->company->settings->counter_padding;
$pattern = $client->getSetting('invoice_number_pattern');
$prefix = $client->getSetting('invoice_number_prefix');
$padding = $client->getSetting('counter_padding');
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $prefix, $pattern);
//increment the correct invoice_number Counter (company vs client)
if($is_client_counter)
$this->incrementCounter($client, 'invoice_number_counter');
else
$this->incrementCounter($client->company, 'invoice_number_counter');
$this->incrementCounter($counter_entity, 'invoice_number_counter');
return $invoice_number;
}
@ -339,7 +339,10 @@ trait GeneratesCounter
$search[] = '{$counter}';
$replace[] = $counter;
$search[] = '{$client_counter}';
$search[] = '{$clientCounter}';
$replace[] = $counter;
$search[] = '{$groupCounter}';
$replace[] = $counter;
if (strstr($pattern, '{$user_id}')) {

View File

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
@ -182,6 +183,8 @@ class CreateUsersTable extends Migration
});
DB::statement('ALTER table companies key_block_size=8 row_format=compressed');
Schema::create('company_user', function (Blueprint $table) {
$table->increments('id');

View File

@ -30,7 +30,7 @@ class PaymentLibrariesSeeder extends Seeder
['name' => 'PayPal Pro', 'provider' => 'PayPal_Pro', 'key' => '80af24a6a69f5c0bbec33e930ab40665', 'fields' => '{"username":"","password":"","signature":"","testMode":false}'],
['name' => 'Pin', 'provider' => 'Pin', 'key' => '0749cb92a6b36c88bd9ff8aabd2efcab', 'fields' => '{"secretKey":"","testMode":false}'],
['name' => 'SagePay Direct', 'provider' => 'SagePay_Direct', 'key' => '4c8f4e5d0f353a122045eb9a60cc0f2d', 'fields' => '{"vendor":"","testMode":false,"referrerId":""}'],
['name' => 'SecurePay DirectPost', 'provider' => 'SecurePay_DirectPost', 'key' => '8036a5aadb2bdaafb23502da8790b6a2', 'fields' => '{"merchantId":"","transactionPassword":"","testMode":false}'],
['name' => 'SecurePay DirectPost', 'provider' => 'SecurePay_DirectPost', 'key' => '8036a5aadb2bdaafb23502da8790b6a2', 'fields' => '{"merchantId":"","transactionPassword":"","testMode":false,"enable_ach":"","enable_sofort":"","enable_apple_pay":"","enable_alipay":""}'],
['name' => 'Stripe', 'provider' => 'Stripe', 'sort_order' => 1, 'key' => 'd14dd26a37cecc30fdd65700bfb55b23', 'fields' => '{"apiKey":"", "publishableKey":""}'],
['name' => 'TargetPay Direct eBanking', 'provider' => 'TargetPay_Directebanking', 'key' => 'd14dd26a37cdcc30fdd65700bfb55b23', 'fields' => '{"subAccountId":""}'],
['name' => 'TargetPay Ideal', 'provider' => 'TargetPay_Ideal', 'key' => 'ea3b328bd72d381387281c3bd83bd97c', 'fields' => '{"subAccountId":""}'],

View File

@ -114,7 +114,7 @@ class RandomDataSeeder extends Seeder
factory(\App\Models\Product::class,50)->create(['user_id' => $user->id, 'company_id' => $company->id]);
/** Invoice Factory */
factory(\App\Models\Invoice::class,50)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, 'settings' => ClientSettings::buildClientSettings($company->settings, $client->settings)]);
factory(\App\Models\Invoice::class,500)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, 'settings' => ClientSettings::buildClientSettings($company->settings, $client->settings)]);
$invoices = Invoice::all();
$invoice_repo = new InvoiceRepository();

View File

@ -103,7 +103,8 @@ class GeneratesCounterTest extends TestCase
public function testInvoiceNumberPattern()
{
$settings = $this->client->company->settings;
$settings->invoice_number_prefix = null;
$settings->invoice_number_prefix = '';
$settings->invoice_number_counter = 1;
$settings->invoice_number_pattern = '{$year}-{$counter}';
$this->client->company->settings = $settings;
@ -122,8 +123,8 @@ class GeneratesCounterTest extends TestCase
{
$settings = $this->client->company->settings;
$settings->invoice_number_prefix = null;
$settings->invoice_number_pattern = '{$year}-{$client_counter}';
$settings->invoice_number_prefix = '';
$settings->invoice_number_pattern = '{$year}-{$clientCounter}';
$this->client->company->settings = $settings;
$this->client->company->save();