1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Recurring Quotes

This commit is contained in:
David Bomba 2021-08-24 12:57:46 +10:00
parent 8d15e181c3
commit ee855824db
10 changed files with 828 additions and 140 deletions

View File

@ -12,9 +12,12 @@
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Recurring\UniqueRecurringQuoteNumberRule;
use App\Models\Client;
use App\Models\RecurringQuote;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\UploadedFile;
class StoreRecurringQuoteRequest extends Request
{
@ -33,17 +36,39 @@ class StoreRecurringQuoteRequest extends Request
public function rules()
{
return [
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
'client_id' => 'required|exists:clients,id,company_id,'.auth()->user()->company()->id,
];
$rules = [];
if ($this->input('documents') && is_array($this->input('documents'))) {
$documents = count($this->input('documents'));
foreach (range(0, $documents) as $index) {
$rules['documents.'.$index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
} elseif ($this->input('documents')) {
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id;
$rules['invitations.*.client_contact_id'] = 'distinct';
$rules['frequency_id'] = 'required|integer|digits_between:1,12';
$rules['number'] = new UniqueRecurringQuoteNumberRule($this->all());
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
if ($input['client_id']) {
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']);
}
if (array_key_exists('client_id', $input) && is_string($input['client_id'])) {
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
}
@ -51,8 +76,56 @@ class StoreRecurringQuoteRequest extends Request
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
}
if (isset($input['client_contacts'])) {
foreach ($input['client_contacts'] as $key => $contact) {
if (! array_key_exists('send_email', $contact) || ! array_key_exists('id', $contact)) {
unset($input['client_contacts'][$key]);
}
}
}
if (isset($input['invitations'])) {
foreach ($input['invitations'] as $key => $value) {
if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) {
unset($input['invitations'][$key]['id']);
}
if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) {
$input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']);
}
if (is_string($input['invitations'][$key]['client_contact_id'])) {
$input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']);
}
}
}
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']);
if (isset($input['auto_bill'])) {
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);
} else {
if ($client = Client::find($input['client_id'])) {
$input['auto_bill'] = $client->getSetting('auto_bill');
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);
}
}
$this->replace($input);
}
private function setAutoBillFlag($auto_bill)
{
if ($auto_bill == 'always' || $auto_bill == 'optout') {
return true;
}
return false;
}
public function messages()
{
return [];
}
}

View File

@ -14,12 +14,15 @@ namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\Rule;
class UpdateRecurringQuoteRequest extends Request
{
use ChecksEntityStatus;
use CleanLineItems;
use MakesHash;
/**
* Determine if the user is authorized to make this request.
@ -33,24 +36,91 @@ class UpdateRecurringQuoteRequest extends Request
public function rules()
{
return [
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
];
$rules = [];
if ($this->input('documents') && is_array($this->input('documents'))) {
$documents = count($this->input('documents'));
foreach (range(0, $documents) as $index) {
$rules['documents.'.$index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
} elseif ($this->input('documents')) {
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
if($this->number)
$rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id);
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']);
}
if (isset($input['client_id'])) {
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
}
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
}
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
if (isset($input['invitations'])) {
foreach ($input['invitations'] as $key => $value) {
if (is_numeric($input['invitations'][$key]['id'])) {
unset($input['invitations'][$key]['id']);
}
if($this->number)
$rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id);
if (array_key_exists('id', $input['invitations'][$key]) && is_string($input['invitations'][$key]['id'])) {
$input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']);
}
if (is_string($input['invitations'][$key]['client_contact_id'])) {
$input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']);
}
}
}
if (isset($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}
if (isset($input['auto_bill'])) {
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);
}
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}
$this->replace($input);
}
/**
* if($auto_bill == '')
* off / optin / optout will reset the status of this field to off to allow
* the client to choose whether to auto_bill or not.
*
* @param enum $auto_bill off/always/optin/optout
*
* @return bool
*/
private function setAutoBillFlag($auto_bill) :bool
{
if ($auto_bill == 'always') {
return true;
}
// if($auto_bill == '')
// off / optin / optout will reset the status of this field to off to allow
// the client to choose whether to auto_bill or not.
return false;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Quote Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://paymentninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
class UploadRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->recurring_quote);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* Quote Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\Recurring;
use App\Models\RecurringQuote;
use Illuminate\Contracts\Validation\Rule;
/**
* Class UniqueRecurringQuoteNumberRule.
*/
class UniqueRecurringQuoteNumberRule implements Rule
{
public $input;
public function __construct($input)
{
$this->input = $input;
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->checkIfQuoteNumberUnique(); //if it exists, return false!
}
/**
* @return string
*/
public function message()
{
return ctrans('texts.recurring_quote_number_taken', ['number' => $this->input['number']]);
}
/**
* @return bool
*/
private function checkIfQuoteNumberUnique() : bool
{
if (empty($this->input['number'])) {
return true;
}
$invoice = RecurringQuote::where('client_id', $this->input['client_id'])
->where('number', $this->input['number'])
->withTrashed()
->exists();
if ($invoice) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models\Presenters;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use Laracasts\Presenter\PresentableTrait;
/**
* Class QuotePresenter.
*
* For convenience and to allow users to easiliy
* customise their invoices, we provide all possible
* invoice variables to be available from this presenter.
*
* Shortcuts to other presenters are here to facilitate
* a clean UI / UX
*/
class RecurringQuotePresenter extends InvoicePresenter
{
}

View File

@ -11,56 +11,73 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Models\Presenters\RecurringQuotePresenter;
use App\Models\Quote;
use App\Models\RecurringQuoteInvitation;
use App\Services\Recurring\RecurringService;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Recurring\HasRecurrence;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Laracasts\Presenter\PresentableTrait;
/**
* Class for Recurring Invoices.
* Class for Recurring Quotes.
*/
class RecurringQuote extends BaseModel
{
use MakesHash;
use SoftDeletes;
use Filterable;
use MakesDates;
use HasRecurrence;
use PresentableTrait;
protected $presenter = RecurringQuotePresenter::class;
/**
* Invoice Statuses.
* Quote Statuses.
*/
const STATUS_DRAFT = 2;
const STATUS_ACTIVE = 3;
const STATUS_DRAFT = 1;
const STATUS_ACTIVE = 2;
const STATUS_PAUSED = 3;
const STATUS_COMPLETED = 4;
const STATUS_PENDING = -1;
const STATUS_COMPLETED = -2;
const STATUS_CANCELLED = -3;
/**
* Recurring intervals.
* Quote Frequencies.
*/
const FREQUENCY_WEEKLY = 1;
const FREQUENCY_TWO_WEEKS = 2;
const FREQUENCY_FOUR_WEEKS = 3;
const FREQUENCY_MONTHLY = 4;
const FREQUENCY_TWO_MONTHS = 5;
const FREQUENCY_THREE_MONTHS = 6;
const FREQUENCY_FOUR_MONTHS = 7;
const FREQUENCY_SIX_MONTHS = 8;
const FREQUENCY_ANNUALLY = 9;
const FREQUENCY_TWO_YEARS = 10;
const FREQUENCY_DAILY = 1;
const FREQUENCY_WEEKLY = 2;
const FREQUENCY_TWO_WEEKS = 3;
const FREQUENCY_FOUR_WEEKS = 4;
const FREQUENCY_MONTHLY = 5;
const FREQUENCY_TWO_MONTHS = 6;
const FREQUENCY_THREE_MONTHS = 7;
const FREQUENCY_FOUR_MONTHS = 8;
const FREQUENCY_SIX_MONTHS = 9;
const FREQUENCY_ANNUALLY = 10;
const FREQUENCY_TWO_YEARS = 11;
const FREQUENCY_THREE_YEARS = 12;
const RECURS_INDEFINITELY = -1;
protected $fillable = [
'client_id',
'quote_number',
'project_id',
'number',
'discount',
'is_amount_discount',
'po_number',
'quote_date',
'valid_until',
'date',
'due_date',
'due_date_days',
'line_items',
'settings',
'footer',
'public_note',
'public_notes',
'private_notes',
'terms',
'tax_name1',
@ -74,26 +91,42 @@ class RecurringQuote extends BaseModel
'custom_value3',
'custom_value4',
'amount',
'partial',
'frequency_id',
'due_date_days',
'next_send_date',
'remaining_cycles',
'auto_bill',
'auto_bill_enabled',
'design_id',
'custom_surcharge1',
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',
];
protected $touches = [];
protected $casts = [
'settings' => 'object',
'line_items' => 'object',
'backup' => 'object',
'settings' => 'object',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
];
protected $with = [
// 'client',
// 'company',
protected $appends = [
'hashed_id',
'status',
];
protected $touches = [];
public function getEntityType()
{
return self::class;
@ -126,6 +159,16 @@ class RecurringQuote extends BaseModel
return $value;
}
public function activities()
{
return $this->hasMany(Activity::class)->orderBy('id', 'DESC')->take(50);
}
public function history()
{
return $this->hasManyThrough(Backup::class, Activity::class);
}
public function company()
{
return $this->belongsTo(Company::class);
@ -136,6 +179,11 @@ class RecurringQuote extends BaseModel
return $this->belongsTo(Client::class)->withTrashed();
}
public function project()
{
return $this->belongsTo(Project::class)->withTrashed();
}
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
@ -146,8 +194,299 @@ class RecurringQuote extends BaseModel
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
}
public function quotes()
{
return $this->hasMany(Quote::class, 'recurring_id', 'id')->withTrashed();
}
public function invitations()
{
$this->morphMany(RecurringQuoteInvitation::class);
return $this->hasMany(RecurringQuoteInvitation::class);
}
public function documents()
{
return $this->morphMany(Document::class, 'documentable');
}
public function getStatusAttribute()
{
if ($this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture()) {
return self::STATUS_PENDING;
} else {
return $this->status_id;
}
}
public function nextSendDate() :?Carbon
{
if (!$this->next_send_date) {
return null;
}
$offset = $this->client->timezone_offset();
/*
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
to add ON a day - a day = 86400 seconds
*/
if($offset < 0)
$offset += 86400;
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date)->startOfDay()->addDay()->addSeconds($offset);
case self::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeek()->addSeconds($offset);
case self::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2)->addSeconds($offset);
case self::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4)->addSeconds($offset);
case self::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
case self::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset);
case self::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
case self::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset);
case self::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset);
case self::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addYear()->addSeconds($offset);
case self::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2)->addSeconds($offset);
case self::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3)->addSeconds($offset);
default:
return null;
}
}
public function nextDateByFrequency($date)
{
$offset = $this->client->timezone_offset();
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return Carbon::parse($date)->startOfDay()->addDay()->addSeconds($offset);
case self::FREQUENCY_WEEKLY:
return Carbon::parse($date)->startOfDay()->addWeek()->addSeconds($offset);
case self::FREQUENCY_TWO_WEEKS:
return Carbon::parse($date)->startOfDay()->addWeeks(2)->addSeconds($offset);
case self::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($date)->startOfDay()->addWeeks(4)->addSeconds($offset);
case self::FREQUENCY_MONTHLY:
return Carbon::parse($date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
case self::FREQUENCY_TWO_MONTHS:
return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset);
case self::FREQUENCY_THREE_MONTHS:
return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
case self::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset);
case self::FREQUENCY_SIX_MONTHS:
return Carbon::parse($date)->addMonthsNoOverflow(6)->addSeconds($offset);
case self::FREQUENCY_ANNUALLY:
return Carbon::parse($date)->startOfDay()->addYear()->addSeconds($offset);
case self::FREQUENCY_TWO_YEARS:
return Carbon::parse($date)->startOfDay()->addYears(2)->addSeconds($offset);
case self::FREQUENCY_THREE_YEARS:
return Carbon::parse($date)->startOfDay()->addYears(3)->addSeconds($offset);
default:
return null;
}
}
public function remainingCycles() : int
{
if ($this->remaining_cycles == 0) {
return 0;
} elseif ($this->remaining_cycles == -1) {
return -1;
} else {
return $this->remaining_cycles - 1;
}
}
public function setCompleted() : void
{
$this->status_id = self::STATUS_COMPLETED;
$this->next_send_date = null;
$this->remaining_cycles = 0;
$this->save();
}
public static function badgeForStatus(int $status)
{
switch ($status) {
case self::STATUS_DRAFT:
return '<h4><span class="badge badge-light">'.ctrans('texts.draft').'</span></h4>';
break;
case self::STATUS_PENDING:
return '<h4><span class="badge badge-primary">'.ctrans('texts.pending').'</span></h4>';
break;
case self::STATUS_ACTIVE:
return '<h4><span class="badge badge-primary">'.ctrans('texts.active').'</span></h4>';
break;
case self::STATUS_COMPLETED:
return '<h4><span class="badge badge-success">'.ctrans('texts.status_completed').'</span></h4>';
break;
case self::STATUS_PAUSED:
return '<h4><span class="badge badge-danger">'.ctrans('texts.paused').'</span></h4>';
break;
default:
// code...
break;
}
}
public static function frequencyForKey(int $frequency_id) :string
{
switch ($frequency_id) {
case self::FREQUENCY_DAILY:
return ctrans('texts.freq_daily');
break;
case self::FREQUENCY_WEEKLY:
return ctrans('texts.freq_weekly');
break;
case self::FREQUENCY_TWO_WEEKS:
return ctrans('texts.freq_two_weeks');
break;
case self::FREQUENCY_FOUR_WEEKS:
return ctrans('texts.freq_four_weeks');
break;
case self::FREQUENCY_MONTHLY:
return ctrans('texts.freq_monthly');
break;
case self::FREQUENCY_TWO_MONTHS:
return ctrans('texts.freq_two_months');
break;
case self::FREQUENCY_THREE_MONTHS:
return ctrans('texts.freq_three_months');
break;
case self::FREQUENCY_FOUR_MONTHS:
return ctrans('texts.freq_four_months');
break;
case self::FREQUENCY_SIX_MONTHS:
return ctrans('texts.freq_six_months');
break;
case self::FREQUENCY_ANNUALLY:
return ctrans('texts.freq_annually');
break;
case self::FREQUENCY_TWO_YEARS:
return ctrans('texts.freq_two_years');
break;
default:
// code...
break;
}
}
public function calc()
{
$invoice_calc = null;
if ($this->uses_inclusive_taxes) {
$invoice_calc = new InvoiceSumInclusive($this);
} else {
$invoice_calc = new InvoiceSum($this);
}
return $invoice_calc->build();
}
/*
* Important to note when playing with carbon dates - in order
* not to modify the original instance, always use a `->copy()`
*
*/
public function recurringDates()
{
/* Return early if nothing to send back! */
if ($this->status_id == self::STATUS_COMPLETED ||
$this->remaining_cycles == 0 ||
!$this->next_send_date) {
return [];
}
/* Endless - lets send 10 back*/
$iterations = $this->remaining_cycles;
if ($this->remaining_cycles == -1) {
$iterations = 10;
}
$data = [];
if (!Carbon::parse($this->next_send_date)) {
return $data;
}
$next_send_date = Carbon::parse($this->next_send_date)->copy();
for ($x=0; $x<$iterations; $x++) {
// we don't add the days... we calc the day of the month!!
$next_due_date = $this->calculateDueDate($next_send_date->copy()->format('Y-m-d'));
$next_due_date_string = $next_due_date ? $next_due_date->format('Y-m-d') : '';
$next_send_date = Carbon::parse($next_send_date);
$data[] = [
'send_date' => $next_send_date->format('Y-m-d'),
'due_date' => $next_due_date_string
];
/* Fixes the timeshift in case the offset is negative which cause a infinite loop due to UTC +0*/
if($this->client->timezone_offset() < 0){
$next_send_date = $this->nextDateByFrequency($next_send_date->addDay()->format('Y-m-d'));
}
else
$next_send_date = $this->nextDateByFrequency($next_send_date->format('Y-m-d'));
}
return $data;
}
public function calculateDueDate($date)
{
switch ($this->due_date_days) {
case 'terms':
return $this->calculateDateFromTerms($date);
break;
default:
return $this->setDayOfMonth($date, $this->due_date_days);
break;
}
}
/**
* Calculates a date based on the client payment terms.
*
* @param Carbon $date A given date
* @return NULL|Carbon The date
*/
public function calculateDateFromTerms($date)
{
$new_date = Carbon::parse($date);
$client_payment_terms = $this->client->getSetting('payment_terms');
if ($client_payment_terms == '') {//no due date! return null;
return null;
}
return $new_date->addDays($client_payment_terms); //add the number of days in the payment terms to the date
}
/**
* Service entry points.
*/
public function service() :RecurringService
{
return new RecurringService($this);
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Invoice Ninja (https://quoteninja.com).
*
* @link https://github.com/quoteninja/quoteninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://quoteninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use App\Models\RecurringQuote;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class RecurringQuoteInvitation extends BaseModel
{
use MakesDates;
use SoftDeletes;
use Inviteable;
protected $fillable = ['client_contact_id'];
protected $touches = ['recurring_quote'];
protected $with = [
'company',
'contact',
];
public function getEntityType()
{
return self::class;
}
public function entityType()
{
return RecurringQuote::class;
}
/**
* @return mixed
*/
public function recurring_quote()
{
return $this->belongsTo(RecurringQuote::class)->withTrashed();
}
/**
* @return mixed
*/
public function contact()
{
return $this->belongsTo(ClientContact::class, 'client_contact_id', 'id')->withTrashed();
}
/**
* @return mixed
*/
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
/**
* @return BelongsTo
*/
public function company()
{
return $this->belongsTo(Company::class);
}
public function markViewed()
{
$this->viewed_date = now();
$this->save();
}
public function markOpened()
{
$this->opened_date = now();
$this->save();
}
}

View File

@ -37,36 +37,6 @@ class RecurringInvoiceTransformer extends EntityTransformer
// 'history',
// 'client',
];
/*
public function includeInvoiceItems(Invoice $invoice)
{
$transformer = new InvoiceItemTransformer($this->serializer);
return $this->includeCollection($invoice->invoice_items, $transformer, ENTITY_INVOICE_ITEM);
}
public function includeInvitations(Invoice $invoice)
{
$transformer = new InvitationTransformer($this->account, $this->serializer);
return $this->includeCollection($invoice->invitations, $transformer, ENTITY_INVITATION);
}
public function includePayments(Invoice $invoice)
{
$transformer = new PaymentTransformer($this->account, $this->serializer, $invoice);
return $this->includeCollection($invoice->payments, $transformer, ENTITY_PAYMENT);
}
public function includeClient(Invoice $invoice)
{
$transformer = new ClientTransformer($this->account, $this->serializer);
return $this->includeItem($invoice->client, $transformer, ENTITY_CLIENT);
}
*/
public function includeHistory(RecurringInvoice $invoice)
{

View File

@ -11,7 +11,14 @@
namespace App\Transformers;
use App\Models\Activity;
use App\Models\Backup;
use App\Models\Document;
use App\Models\Quote;
use App\Models\RecurringQuote;
use App\Models\RecurringQuoteInvitation;
use App\Transformers\ActivityTransformer;
use App\Transformers\QuoteHistoryTransformer;
use App\Utils\Traits\MakesHash;
class RecurringQuoteTransformer extends EntityTransformer
@ -19,107 +26,110 @@ class RecurringQuoteTransformer extends EntityTransformer
use MakesHash;
protected $defaultIncludes = [
// 'invoice_items',
'invitations',
'documents',
];
protected $availableIncludes = [
// 'invitations',
// 'payments',
'invitations',
'documents',
'activities',
// 'history',
// 'client',
// 'documents',
];
public function includeHistory(RecurringQuote $quote)
{
$transformer = new QuoteHistoryTransformer($this->serializer);
/*
public function includeInvoiceItems(Invoice $quote)
{
$transformer = new InvoiceItemTransformer($this->serializer);
return $this->includeCollection($quote->history, $transformer, Backup::class);
}
public function includeActivities(RecurringQuote $quote)
{
$transformer = new ActivityTransformer($this->serializer);
return $this->includeCollection($quote->invoice_items, $transformer, ENTITY_INVOICE_ITEM);
}
return $this->includeCollection($quote->activities, $transformer, Activity::class);
}
public function includeInvitations(Invoice $quote)
{
$transformer = new InvitationTransformer($this->account, $this->serializer);
public function includeInvitations(RecurringQuote $quote)
{
$transformer = new RecurringQuoteInvitationTransformer($this->serializer);
return $this->includeCollection($quote->invitations, $transformer, ENTITY_INVITATION);
}
return $this->includeCollection($quote->invitations, $transformer, RecurringQuoteInvitation::class);
}
public function includePayments(Invoice $quote)
{
$transformer = new PaymentTransformer($this->account, $this->serializer, $quote);
public function includeDocuments(RecurringQuote $quote)
{
$transformer = new DocumentTransformer($this->serializer);
return $this->includeCollection($quote->payments, $transformer, ENTITY_PAYMENT);
}
public function includeClient(Invoice $quote)
{
$transformer = new ClientTransformer($this->account, $this->serializer);
return $this->includeItem($quote->client, $transformer, ENTITY_CLIENT);
}
public function includeExpenses(Invoice $quote)
{
$transformer = new ExpenseTransformer($this->account, $this->serializer);
return $this->includeCollection($quote->expenses, $transformer, ENTITY_EXPENSE);
}
public function includeDocuments(Invoice $quote)
{
$transformer = new DocumentTransformer($this->account, $this->serializer);
$quote->documents->each(function ($document) use ($quote) {
$document->setRelation('invoice', $quote);
});
return $this->includeCollection($quote->documents, $transformer, ENTITY_DOCUMENT);
}
*/
return $this->includeCollection($quote->documents, $transformer, Document::class);
}
public function transform(RecurringQuote $quote)
{
return [
'id' => $this->encodePrimaryKey($quote->id),
'user_id' => $this->encodePrimaryKey($quote->user_id),
'project_id' => $this->encodePrimaryKey($quote->project_id),
'assigned_user_id' => $this->encodePrimaryKey($quote->assigned_user_id),
'amount' => (float) $quote->amount ?: '',
'balance' => (float) $quote->balance ?: '',
'client_id' => (string) $quote->client_id,
'amount' => (float) $quote->amount,
'balance' => (float) $quote->balance,
'client_id' => (string) $this->encodePrimaryKey($quote->client_id),
'vendor_id' => (string) $this->encodePrimaryKey($quote->vendor_id),
'status_id' => (string) ($quote->status_id ?: 1),
'design_id' => (string) $this->encodePrimaryKey($quote->design_id),
'created_at' => (int) $quote->created_at,
'updated_at' => (int) $quote->updated_at,
'archived_at' => (int) $quote->deleted_at,
'discount' => (float) $quote->discount ?: '',
'is_deleted' => (bool) $quote->is_deleted,
'number' => $quote->number ?: '',
'discount' => (float) $quote->discount,
'po_number' => $quote->po_number ?: '',
'quote_date' => $quote->quote_date ?: '',
'valid_until' => $quote->valid_until ?: '',
'date' => $quote->date ?: '',
'last_sent_date' => $quote->last_sent_date ?: '',
'next_send_date' => $quote->next_send_date ?: '',
'due_date' => $quote->due_date ?: '',
'terms' => $quote->terms ?: '',
'public_notes' => $quote->public_notes ?: '',
'private_notes' => $quote->private_notes ?: '',
'is_deleted' => (bool) $quote->is_deleted,
'uses_inclusive_taxes' => (bool) $quote->uses_inclusive_taxes,
'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '',
'tax_rate1' => (float) $quote->tax_rate1 ?: '',
'tax_rate1' => (float) $quote->tax_rate1,
'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '',
'tax_rate2' => (float) $quote->tax_rate2 ?: '',
'tax_rate2' => (float) $quote->tax_rate2,
'tax_name3' => $quote->tax_name3 ? $quote->tax_name3 : '',
'tax_rate3' => (float) $quote->tax_rate3 ?: '',
'tax_rate3' => (float) $quote->tax_rate3,
'total_taxes' => (float) $quote->total_taxes,
'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false),
'quote_footer' => $quote->quote_footer ?: '',
'footer' => $quote->footer ?: '',
'partial' => (float) ($quote->partial ?: 0.0),
'partial_due_date' => $quote->partial_due_date ?: '',
'custom_value1' => (float) $quote->custom_value1 ?: '',
'custom_value2' => (float) $quote->custom_value2 ?: '',
'custom_taxes1' => (bool) $quote->custom_taxes1 ?: '',
'custom_taxes2' => (bool) $quote->custom_taxes2 ?: '',
'custom_value1' => (string) $quote->custom_value1 ?: '',
'custom_value2' => (string) $quote->custom_value2 ?: '',
'custom_value3' => (string) $quote->custom_value3 ?: '',
'custom_value4' => (string) $quote->custom_value4 ?: '',
'has_tasks' => (bool) $quote->has_tasks,
'has_expenses' => (bool) $quote->has_expenses,
'custom_text_value1' => $quote->custom_text_value1 ?: '',
'custom_text_value2' => $quote->custom_text_value2 ?: '',
'settings' => $quote->settings ?: '',
'frequency_id' => (int) $quote->frequency_id,
'last_sent_date' => $quote->last_sent_date ?: '',
'next_send_date' => $quote->next_send_date ?: '',
'custom_surcharge1' => (float) $quote->custom_surcharge1,
'custom_surcharge2' => (float) $quote->custom_surcharge2,
'custom_surcharge3' => (float) $quote->custom_surcharge3,
'custom_surcharge4' => (float) $quote->custom_surcharge4,
'exchange_rate' => (float) $quote->exchange_rate,
'custom_surcharge_tax1' => (bool) $quote->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool) $quote->custom_surcharge_tax2,
'custom_surcharge_tax3' => (bool) $quote->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool) $quote->custom_surcharge_tax4,
'line_items' => $quote->line_items ?: (array) [],
'entity_type' => 'recurringQuote',
'frequency_id' => (string) $quote->frequency_id,
'remaining_cycles' => (int) $quote->remaining_cycles,
'recurring_dates' => (array) $quote->recurringDates(),
'auto_bill' => (string) $quote->auto_bill,
'auto_bill_enabled' => (bool) $quote->auto_bill_enabled,
'due_date_days' => (string) $quote->due_date_days ?: '',
'paid_to_date' => (float) $quote->paid_to_date,
'subscription_id' => (string)$this->encodePrimaryKey($quote->subscription_id),
];
}
}

View File

@ -4297,7 +4297,8 @@ $LANG = array(
'lang_Latvian' => 'Latvian',
'expiry_date' => 'Expiry date',
'cardholder_name' => 'Card holder name',
'recurring_quote_number_taken' => 'Recurring Quote number :number already taken',
);
return $LANG;