mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-08-07 09:11:48 -04:00
Proposals
This commit is contained in:
parent
5dfb90604b
commit
36489b936b
@ -46,6 +46,7 @@ if (! defined('APP_NAME')) {
|
|||||||
define('ENTITY_PROPOSAL_TEMPLATE', 'proposal_template');
|
define('ENTITY_PROPOSAL_TEMPLATE', 'proposal_template');
|
||||||
define('ENTITY_PROPOSAL_SNIPPET', 'proposal_snippet');
|
define('ENTITY_PROPOSAL_SNIPPET', 'proposal_snippet');
|
||||||
define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category');
|
define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category');
|
||||||
|
define('ENTITY_PROPOSAL_INVITATION', 'proposal_invitation');
|
||||||
|
|
||||||
define('INVOICE_TYPE_STANDARD', 1);
|
define('INVOICE_TYPE_STANDARD', 1);
|
||||||
define('INVOICE_TYPE_QUOTE', 2);
|
define('INVOICE_TYPE_QUOTE', 2);
|
||||||
|
@ -22,7 +22,7 @@ class CreateProposalRequest extends ProposalRequest
|
|||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'quote_id' => 'required',
|
'invoice_id' => 'required',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class UpdateProposalRequest extends ProposalRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'quote_id' => 'required',
|
'invoice_id' => 'required',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Utils;
|
|
||||||
use App\Models\LookupInvitation;
|
use App\Models\LookupInvitation;
|
||||||
|
use App\Models\Traits\Inviteable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Invitation.
|
* Class Invitation.
|
||||||
@ -13,6 +12,8 @@ use App\Models\LookupInvitation;
|
|||||||
class Invitation extends EntityModel
|
class Invitation extends EntityModel
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
|
use Inviteable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
@ -57,111 +58,6 @@ class Invitation extends EntityModel
|
|||||||
{
|
{
|
||||||
return $this->belongsTo('App\Models\Account');
|
return $this->belongsTo('App\Models\Account');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're getting the link for PhantomJS to generate the PDF
|
|
||||||
// we need to make sure it's served from our site
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $type
|
|
||||||
* @param bool $forceOnsite
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getLink($type = 'view', $forceOnsite = false, $forcePlain = false)
|
|
||||||
{
|
|
||||||
if (! $this->account) {
|
|
||||||
$this->load('account');
|
|
||||||
}
|
|
||||||
|
|
||||||
$account = $this->account;
|
|
||||||
$iframe_url = $account->iframe_url;
|
|
||||||
$url = trim(SITE_URL, '/');
|
|
||||||
|
|
||||||
if (env('REQUIRE_HTTPS')) {
|
|
||||||
$url = str_replace('http://', 'https://', $url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
|
|
||||||
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
|
|
||||||
$url = $account->present()->clientPortalLink();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($iframe_url && ! $forceOnsite) {
|
|
||||||
return "{$iframe_url}?{$this->invitation_key}";
|
|
||||||
} elseif ($this->account->subdomain && ! $forcePlain) {
|
|
||||||
$url = Utils::replaceSubdomain($url, $account->subdomain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{$url}/{$type}/{$this->invitation_key}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool|string
|
|
||||||
*/
|
|
||||||
public function getStatus()
|
|
||||||
{
|
|
||||||
$hasValue = false;
|
|
||||||
$parts = [];
|
|
||||||
$statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
|
|
||||||
|
|
||||||
foreach ($statuses as $status) {
|
|
||||||
$field = "{$status}_date";
|
|
||||||
$date = '';
|
|
||||||
if ($this->$field && $this->field != '0000-00-00 00:00:00') {
|
|
||||||
$date = Utils::dateToString($this->$field);
|
|
||||||
$hasValue = true;
|
|
||||||
$parts[] = trans('texts.invitation_status_' . $status) . ': ' . $date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $hasValue ? implode($parts, '<br/>') : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getName()
|
|
||||||
{
|
|
||||||
return $this->invitation_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param null $messageId
|
|
||||||
*/
|
|
||||||
public function markSent($messageId = null)
|
|
||||||
{
|
|
||||||
$this->message_id = $messageId;
|
|
||||||
$this->email_error = null;
|
|
||||||
$this->sent_date = Carbon::now()->toDateTimeString();
|
|
||||||
$this->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isSent()
|
|
||||||
{
|
|
||||||
return $this->sent_date && $this->sent_date != '0000-00-00 00:00:00';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function markViewed()
|
|
||||||
{
|
|
||||||
$invoice = $this->invoice;
|
|
||||||
$client = $invoice->client;
|
|
||||||
|
|
||||||
$this->viewed_date = Carbon::now()->toDateTimeString();
|
|
||||||
$this->save();
|
|
||||||
|
|
||||||
$invoice->markViewed();
|
|
||||||
$client->markLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function signatureDiv()
|
|
||||||
{
|
|
||||||
if (! $this->signature_base64) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Invitation::creating(function ($invitation)
|
Invitation::creating(function ($invitation)
|
||||||
|
@ -64,6 +64,14 @@ class Proposal extends EntityModel
|
|||||||
return $this->belongsTo('App\Models\Invoice')->withTrashed();
|
return $this->belongsTo('App\Models\Invoice')->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function proposal_invitations()
|
||||||
|
{
|
||||||
|
return $this->hasMany('App\Models\ProposalInvitation')->orderBy('proposal_invitations.contact_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
*/
|
*/
|
||||||
|
86
app/Models/ProposalInvitation.php
Normal file
86
app/Models/ProposalInvitation.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
//use App\Models\LookupProposalInvitation;
|
||||||
|
use App\Models\Traits\Inviteable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Invitation.
|
||||||
|
*/
|
||||||
|
class ProposalInvitation extends EntityModel
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
use Inviteable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dates = ['deleted_at'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getEntityType()
|
||||||
|
{
|
||||||
|
return ENTITY_PROPOSAL_INVITATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function proposal()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\Proposal')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function contact()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\Contact')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\User')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function account()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\Account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ProposalInvitation::creating(function ($invitation)
|
||||||
|
{
|
||||||
|
LookupProposalInvitation::createNew($invitation->account->account_key, [
|
||||||
|
'invitation_key' => $invitation->invitation_key,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
ProposalInvitation::updating(function ($invitation) {
|
||||||
|
$dirty = $invitation->getDirty();
|
||||||
|
if (array_key_exists('message_id', $dirty)) {
|
||||||
|
LookupProposalInvitation::updateInvitation($invitation->account->account_key, $invitation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ProposalInvitation::deleted(function ($invitation)
|
||||||
|
{
|
||||||
|
if ($invitation->forceDeleting) {
|
||||||
|
LookupProposalInvitation::deleteWhere([
|
||||||
|
'invitation_key' => $invitation->invitation_key,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
118
app/Models/Traits/Inviteable.php
Normal file
118
app/Models/Traits/Inviteable.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Traits;
|
||||||
|
|
||||||
|
use Carbon;
|
||||||
|
use Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SendsEmails.
|
||||||
|
*/
|
||||||
|
trait Inviteable
|
||||||
|
{
|
||||||
|
// If we're getting the link for PhantomJS to generate the PDF
|
||||||
|
// we need to make sure it's served from our site
|
||||||
|
/**
|
||||||
|
* @param string $type
|
||||||
|
* @param bool $forceOnsite
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLink($type = 'view', $forceOnsite = false, $forcePlain = false)
|
||||||
|
{
|
||||||
|
if (! $this->account) {
|
||||||
|
$this->load('account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->account;
|
||||||
|
$iframe_url = $account->iframe_url;
|
||||||
|
$url = trim(SITE_URL, '/');
|
||||||
|
|
||||||
|
if (env('REQUIRE_HTTPS')) {
|
||||||
|
$url = str_replace('http://', 'https://', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
|
||||||
|
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
|
||||||
|
$url = $account->present()->clientPortalLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iframe_url && ! $forceOnsite) {
|
||||||
|
return "{$iframe_url}?{$this->invitation_key}";
|
||||||
|
} elseif ($this->account->subdomain && ! $forcePlain) {
|
||||||
|
$url = Utils::replaceSubdomain($url, $account->subdomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$url}/{$type}/{$this->invitation_key}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
public function getStatus()
|
||||||
|
{
|
||||||
|
$hasValue = false;
|
||||||
|
$parts = [];
|
||||||
|
$statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
|
||||||
|
|
||||||
|
foreach ($statuses as $status) {
|
||||||
|
$field = "{$status}_date";
|
||||||
|
$date = '';
|
||||||
|
if ($this->$field && $this->field != '0000-00-00 00:00:00') {
|
||||||
|
$date = Utils::dateToString($this->$field);
|
||||||
|
$hasValue = true;
|
||||||
|
$parts[] = trans('texts.invitation_status_' . $status) . ': ' . $date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasValue ? implode($parts, '<br/>') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->invitation_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param null $messageId
|
||||||
|
*/
|
||||||
|
public function markSent($messageId = null)
|
||||||
|
{
|
||||||
|
$this->message_id = $messageId;
|
||||||
|
$this->email_error = null;
|
||||||
|
$this->sent_date = Carbon::now()->toDateTimeString();
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSent()
|
||||||
|
{
|
||||||
|
return $this->sent_date && $this->sent_date != '0000-00-00 00:00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markViewed()
|
||||||
|
{
|
||||||
|
$this->viewed_date = Carbon::now()->toDateTimeString();
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
if ($this->invoice) {
|
||||||
|
$invoice = $this->invoice;
|
||||||
|
$client = $invoice->client;
|
||||||
|
|
||||||
|
$invoice->markViewed();
|
||||||
|
$client->markLoggedIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function signatureDiv()
|
||||||
|
{
|
||||||
|
if (! $this->signature_base64) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date));
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ namespace App\Ninja\Repositories;
|
|||||||
use App\Models\Proposal;
|
use App\Models\Proposal;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\ProposalTemplate;
|
use App\Models\ProposalTemplate;
|
||||||
|
use App\Models\ProposalInvitation;
|
||||||
use Auth;
|
use Auth;
|
||||||
use DB;
|
use DB;
|
||||||
use Utils;
|
use Utils;
|
||||||
@ -89,6 +90,34 @@ class ProposalRepository extends BaseRepository
|
|||||||
|
|
||||||
$proposal->save();
|
$proposal->save();
|
||||||
|
|
||||||
|
// create invitations
|
||||||
|
$contactIds = [];
|
||||||
|
|
||||||
|
foreach ($proposal->invoice->invitations as $invitation) {
|
||||||
|
$conactIds[] = $invitation->contact_id;
|
||||||
|
$found = false;
|
||||||
|
foreach ($proposal->proposal_invitations as $proposalInvitation) {
|
||||||
|
if ($invitation->contact_id == $proposalInvitation->contact_id) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (! $found) {
|
||||||
|
$proposalInvitation = ProposalInvitation::createNew();
|
||||||
|
$proposalInvitation->proposal_id = $proposal->id;
|
||||||
|
$proposalInvitation->contact_id = $invitation->contact_id;
|
||||||
|
$proposalInvitation->invitation_key = strtolower(str_random(RANDOM_KEY_LENGTH));
|
||||||
|
$proposalInvitation->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete invitations
|
||||||
|
foreach ($proposal->proposal_invitations as $proposalInvitation) {
|
||||||
|
if (! in_array($proposalInvitation->contact_id, $conactIds)) {
|
||||||
|
$proposalInvitation->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $proposal;
|
return $proposal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -878,7 +878,6 @@
|
|||||||
ko.mapping.fromJS(invoice, model.invoice().mapping, model.invoice);
|
ko.mapping.fromJS(invoice, model.invoice().mapping, model.invoice);
|
||||||
model.invoice().is_recurring({{ $invoice->is_recurring ? '1' : '0' }});
|
model.invoice().is_recurring({{ $invoice->is_recurring ? '1' : '0' }});
|
||||||
model.invoice().start_date_orig(model.invoice().start_date());
|
model.invoice().start_date_orig(model.invoice().start_date());
|
||||||
|
|
||||||
@if ($invoice->id)
|
@if ($invoice->id)
|
||||||
var invitationContactIds = {!! json_encode($invitationContactIds) !!};
|
var invitationContactIds = {!! json_encode($invitationContactIds) !!};
|
||||||
var client = clientMap[invoice.client.public_id];
|
var client = clientMap[invoice.client.public_id];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user