Fixes for pdf_variables validation (#3419)

* Client and System Notifications

* Fix for group settings currency not applying correctly.

* Split head out of design in order to reuse headers and footers

* export the designs

* Fixes for pdf_variables
This commit is contained in:
David Bomba 2020-03-04 22:09:43 +11:00 committed by GitHub
parent d14b21f471
commit 6d5d1da472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 714 additions and 125 deletions

View File

@ -13,6 +13,7 @@ namespace App\Designs;
abstract class AbstractDesign
{
abstract public function include();
abstract public function header();

View File

@ -17,8 +17,8 @@ class Bold extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -41,7 +41,12 @@ class Bold extends AbstractDesign
margin-top: 5mm;
}
</style>
';
}
public function header() {
return '
<div class="flex static bg-gray-800 p-12">
<div class="w-1/2">
<div class="absolute bg-white pt-10 px-10 pb-4 inline-block align-middle">

View File

@ -17,8 +17,8 @@ class Business extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -48,6 +48,13 @@ class Business extends AbstractDesign
}
</style>
';
}
public function header() {
return '
<div class="my-16 mx-10">
<div class="flex justify-between">
<div class="w-1/2">

View File

@ -17,8 +17,9 @@ class Clean extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -41,6 +42,14 @@ class Clean extends AbstractDesign
margin-top: 5mm;
}
</style>
';
}
public function header() {
return '
<div class="px-12 my-10">
<div class="flex items-center">

View File

@ -20,8 +20,9 @@ class Creative extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -45,6 +46,14 @@ class Creative extends AbstractDesign
}
</style>
';
}
public function header() {
return '
<div class="py-16 mx-16">
<div class="flex justify-between">
<div class="w-2/3 flex">

View File

@ -13,6 +13,7 @@ namespace App\Designs;
class Custom extends AbstractDesign
{
private $include;
private $header;
@ -26,6 +27,7 @@ class Custom extends AbstractDesign
public function __construct($design)
{
$this->include = $design->include;
$this->header = $design->header;
@ -39,6 +41,11 @@ class Custom extends AbstractDesign
}
public function include()
{
return $this->include;
}
public function header()
{

View File

@ -69,6 +69,7 @@ class Designer {
{
$this->exportVariables($entity)
->setDesign($this->getSection('include'))
->setDesign($this->getSection('header'))
->setDesign($this->getSection('body'))
->setDesign($this->getTable($entity))

View File

@ -17,8 +17,9 @@ class Elegant extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -42,6 +43,14 @@ class Elegant extends AbstractDesign
}
</style>
';
}
public function header() {
return '
<div class="py-16 px-8">
<div class="flex flex justify-between border-b-4 border-black">
<div style="margin-bottom: 15px">

View File

@ -17,8 +17,9 @@ class Hipster extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -42,6 +43,15 @@ class Hipster extends AbstractDesign
}
</style>
';
}
public function header() {
return '
<div class="px-12 py-16">
<div class="flex">
<div class="w-1/2 border-l pl-4 border-black mr-4">

View File

@ -17,8 +17,9 @@ class Modern extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -34,6 +35,14 @@ class Modern extends AbstractDesign
</head>
<body>
';
}
public function header() {
return '
<div class="bg-orange-600 flex justify-between py-12 px-12">
<div class="w-1/2">
<h1 class="text-white font-bold text-5xl">$company.name</h1>

View File

@ -17,8 +17,9 @@ class Photo extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -34,6 +35,16 @@ class Photo extends AbstractDesign
</head>
<body>
';
}
public function header() {
return '
<style>
@page
{

View File

@ -17,8 +17,9 @@ class Plain extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -43,6 +44,15 @@ class Plain extends AbstractDesign
</style>
<body>
';
}
public function header() {
return '
<div class="px-12 py-8">
<div class="flex justify-between">
$company_logo

View File

@ -17,8 +17,9 @@ class Playful extends AbstractDesign
public function __construct() {
}
public function header() {
public function include()
{
return '
<!DOCTYPE html>
<html lang="en">
@ -41,7 +42,13 @@ class Playful extends AbstractDesign
margin-top: 5mm;
}
</style>
';
}
public function header() {
return '
<div class="my-12 mx-16">
<div class="flex items-center justify-between">
<div class="w-1/2">

View File

@ -0,0 +1,40 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Events\Misc;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
/**
* Class InvitationWasViewed.
*/
class InvitationWasViewed
{
use SerializesModels;
/**
* @var Invoice
*/
public $invitation;
public $entity;
/**
* Create a new event instance.
*
* @param Invoice $invoice
*/
public function __construct($entity, $invitation)
{
$this->entity = $entity;
$this->invitation = $invitation;
}
}

View File

@ -11,6 +11,7 @@ namespace App\Helpers\Email;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Utils\Number;
class InvoiceEmail extends EmailBuilder
{
@ -30,7 +31,12 @@ class InvoiceEmail extends EmailBuilder
/* Use default translations if a custom message has not been set*/
if (iconv_strlen($body_template) == 0) {
$body_template = trans('texts.invoice_message',
['invoice' => $invoice->number, 'company' => $invoice->company->present()->name()], null,
[
'invoice' => $invoice->number,
'company' => $invoice->company->present()->name(),
'amount' => Number::formatMoney($invoice->balance, $invoice->client),
],
null,
$invoice->client->locale());
}

View File

@ -42,8 +42,14 @@ class InvitationController extends Controller
auth()->guard('contact')->login($invitation->contact, false);
}
if(!request()->has('is_admin')){
$invitation->markViewed();
event(new InvitationWasViewed($entity, $invitation));
}
return redirect()->route('client.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->{$key})]);
} else {
abort(404);

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers;
use App\Events\Invoice\InvoiceWasCreated;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Factory\CloneInvoiceFactory;
use App\Factory\CloneInvoiceToQuoteFactory;
@ -675,6 +676,9 @@ class InvoiceController extends BaseController {
}
break;
case 'email':
$this->reminder_template = $invoice->calculateTemplate();
$invoice->invitations->each(function ($invitation) use($invoice){
$email_builder = (new InvoiceEmail())->build($invitation, $this->reminder_template);
@ -683,6 +687,11 @@ class InvoiceController extends BaseController {
});
if($invoice->invitations->count() > 0){
\Log::error("more than one invitation to send");
event(new InvoiceWasEmailed($invoice->invitations->first()));
}
if (!$bulk) {
return response()->json(['message' => 'email sent'], 200);
}

View File

@ -94,7 +94,7 @@ class StoreClientRequest extends Request
{
$group_settings = GroupSetting::find($input['group_settings_id']);
if($group_settings && property_exists($group_settings, 'currency_id') && is_int($group_settings->currency_id))
if($group_settings && property_exists($group_settings->settings, 'currency_id') && is_int($group_settings->settings->currency_id))
$input['settings']->currency_id = $group_settings->currency_id;
else
$input['settings']->currency_id = auth()->user()->company()->settings->currency_id;

View File

@ -74,7 +74,7 @@ class StoreUserRequest extends Request
$this->replace($input);
}
//@todo make sure the user links back to the account ID for this company!!!!!!
public function fetchUser() :User
{
$user = MultiDB::hasUser(['email' => $this->input('email')]);

View File

@ -46,9 +46,9 @@ class InvoiceEmailActivity implements ShouldQueue
$fields->invoice_id = $event->invitation->invoice->id;
$fields->user_id = $event->invitation->invoice->user_id;
$fields->company_id = $event->invitation->invoice->company_id;
$fields->contact_id = $event->invitation->invoice->client_contact_id;
$fields->client_contact_id = $event->invitation->invoice->client_contact_id;
$fields->activity_type_id = Activity::EMAIL_INVOICE;
$this->activity_repo->save($fields, $event->invoice);
$this->activity_repo->save($fields, $event->invitation->invoice);
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Listeners\Invoice;
use App\Models\Activity;
use App\Models\ClientContact;
use App\Models\InvoiceInvitation;
use App\Notifications\Admin\InvoiceSentNotification;
use App\Repositories\ActivityRepository;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class InvoiceEmailedNotification implements ShouldQueue
{
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$invitation = $event->invitation;
foreach($invitation->company->company_users as $company_user)
{
$company_user->user->notify(new InvoiceSentNotification($invitation, $invitation->company));
}
if(isset($invitation->company->slack_webhook_url)){
Notification::route('slack', $invitation->company->slack_webhook_url)
->notify(new InvoiceSentNotification($invitation, $invitation->company, true));
}
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Listeners\Misc;
use App\Notifications\Admin\EntityViewedNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Notification;
class InvitationViewedListener implements ShouldQueue
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct(){}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$entity_name = $event->entity;
$invitation = $event->invitation;
$notification = new EntityViewedNotification($invitation, $entity_name);
foreach($invitation->company->company_users as $company_user)
{
$company_user->user->notify($notification);
}
if(isset($invitation->company->slack_webhook_url)){
$notification->is_system = true;
Notification::route('slack', $payment->company->slack_webhook_url)
->notify($notification);
}
}
}

View File

@ -14,7 +14,7 @@ namespace App\Listeners\Payment;
use App\Models\Activity;
use App\Models\Invoice;
use App\Models\Payment;
use App\Notifications\Payment\NewPaymentNotification;
use App\Notifications\Admin\NewPaymentNotification;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
@ -41,7 +41,8 @@ class PaymentNotification implements ShouldQueue
{
$payment = $event->payment;
//$invoices = $payment->invoices;
//todo need to iterate through teh company user and determine if the user
//will receive this notification.
foreach($payment->company->company_users as $company_user)
{

View File

@ -18,6 +18,7 @@ class Currency extends StaticModel
public $timestamps = false;
protected $casts = [
'exchange_rate' => 'float',
'swap_currency_symbol' => 'boolean',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',

View File

@ -71,4 +71,9 @@ class QuoteInvitation extends BaseModel
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, ctrans('texts.signed'), $this->createClientDate($this->signature_date, $this->contact->client->timezone()->name));
}
public function markViewed() {
$this->viewed_date = Carbon::now();
$this->save();
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace App\Notifications\Admin;
use App\Utils\Number;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class EntityViewedNotification extends Notification implements ShouldQueue
{
use Queueable, Dispatchable;
/**
* Create a new notification instance.
*
* @return void
* @
*/
protected $invitation;
protected $entity_name;
protected $entity;
protected $company;
protected $settings;
public $is_system;
protected $contact;
public function __construct($invitation, $entity_name, $is_system = false, $settings = null)
{
$this->entity = $invitation->{$entity_name};
$this->contact = $invitation->contact;
$this->company = $invitation->company;
$this->settings = $this->entity->client->getMergedSettings();
$this->is_system = $is_system;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return $this->is_system ? ['slack'] : ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$data = $this->buildDataArray();
$subject = $this->buildSubject();
return (new MailMessage)
->subject($subject)
->markdown('email.admin.generic', $data);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$logo = $this->company->present()->logo();
$amount = Number::formatMoney($this->entity->amount, $this->entity->client);
return (new SlackMessage)
->success()
->from(ctrans('texts.notification_bot'))
->image($logo)
->content(ctrans("texts.notification_{$this->entity_name}_viewed",
[
'amount' => $amount,
'client' => $this->contact->present()->name(),
$this->entity_name => $this->entity->number
]));
}
private function buildDataArray()
{
$amount = Number::formatMoney($this->entity->amount, $this->entity->client);
$subject = ctrans("texts.notification_{$this->entity_name}_viewed_subject",
[
'client' => $this->contact->present()->name(),
$this->entity_name => $this->entity->number,
]);
$data = [
'title' => $subject,
'message' => ctrans("texts.notification_{$this->entity_name}_viewed",
[
'amount' => $amount,
'client' => $this->contact->present()->name(),
$this->entity_name => $this->entity->number,
]),
'url' => config('ninja.site_url') . "/{$this->entity_name}s/" . $this->entity->hashed_id,
'button' => ctrans("texts.view_{$this->entity_name}"),
'signature' => $this->settings->email_signature,
'logo' => $this->company->present()->logo(),
];
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace App\Notifications\Admin;
use App\Utils\Number;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class InvoiceSentNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
protected $invitation;
protected $invoice;
protected $company;
protected $settings;
public $is_system;
protected $contact;
public function __construct($invitation, $company, $is_system = false, $settings = null)
{
$this->invoice = $invitation->invoice;
$this->contact = $invitation->contact;
$this->company = $company;
$this->settings = $this->invoice->client->getMergedSettings();
$this->is_system = $is_system;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return $this->is_system ? ['slack'] : ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$amount = Number::formatMoney($this->invoice->amount, $this->invoice->client);
$subject = ctrans('texts.notification_invoice_sent_subject',
[
'client' => $this->contact->present()->name(),
'invoice' => $this->invoice->number,
]);
$data = [
'title' => $subject,
'message' => ctrans('texts.notification_invoice_sent',
[
'amount' => $amount,
'client' => $this->contact->present()->name(),
'invoice' => $this->invoice->number,
]),
'url' => config('ninja.site_url') . '/invoices/' . $this->invoice->hashed_id,
'button' => ctrans('texts.view_invoice'),
'signature' => $this->settings->email_signature,
'logo' => $this->company->present()->logo(),
];
return (new MailMessage)
->subject($subject)
->markdown('email.admin.generic', $data);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$logo = $this->company->present()->logo();
$amount = Number::formatMoney($this->invoice->amount, $this->invoice->client);
// return (new SlackMessage)
// ->success()
// ->from(ctrans('texts.notification_bot'))
// ->image($logo)
// ->content(ctrans('texts.notification_invoice_sent',
// [
// 'amount' => $amount,
// 'client' => $this->contact->present()->name(),
// 'invoice' => $this->invoice->number
// ]));
return (new SlackMessage)
->from(ctrans('texts.notification_bot'))
->success()
->image('https://app.invoiceninja.com/favicon-v2.png')
->content(trans('texts.notification_invoice_sent_subject',
[
'amount' => $amount,
'client' => $this->contact->present()->name(),
'invoice' => $this->invoice->number
]))
->attachment(function ($attachment) use($amount){
$attachment->title(ctrans('texts.invoice_number_placeholder', ['invoice' => $this->invoice->number]), 'http://linky')
->fields([
ctrans('texts.client') => $this->contact->present()->name(),
ctrans('texts.amount') => $amount,
]);
});
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Notifications\Payment;
namespace App\Notifications\Admin;
use App\Utils\Number;
use Illuminate\Bus\Queueable;
@ -116,8 +116,9 @@ class InvoiceViewedNotification extends Notification implements ShouldQueue
[
'amount' => $amount,
'client' => $this->contact->present()->name(),
'invoice' => $this->invoice->number,
]);
'invoice' => $this->invoice->number
]));
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Notifications\Payment;
namespace App\Notifications\Admin;
use App\Mail\Signup\NewSignup;
use App\Utils\Number;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Notifications\Payment;
namespace App\Notifications\Admin;
use App\Mail\Signup\NewSignup;
use App\Utils\Number;

View File

@ -19,6 +19,7 @@ use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasMarkedSent;
use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Events\Misc\InvitationWasViewed;
use App\Events\Payment\PaymentWasCreated;
use App\Events\Payment\PaymentWasDeleted;
use App\Events\Payment\PaymentWasRefunded;
@ -37,8 +38,10 @@ use App\Listeners\Invoice\CreateInvoiceInvitation;
use App\Listeners\Invoice\CreateInvoicePdf;
use App\Listeners\Invoice\InvoiceEmailActivity;
use App\Listeners\Invoice\InvoiceEmailFailedActivity;
use App\Listeners\Invoice\InvoiceEmailedNotification;
use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Invoice\UpdateInvoiceInvitations;
use App\Listeners\Misc\InvitationViewedListener;
use App\Listeners\Payment\PaymentNotification;
use App\Listeners\SendVerificationNotification;
use App\Listeners\SetDBListener;
@ -121,11 +124,16 @@ class EventServiceProvider extends ServiceProvider
],
InvoiceWasEmailed::class => [
InvoiceEmailActivity::class,
InvoiceEmailedNotification::class,
],
InvoiceWasEmailedAndFailed::class => [
InvoiceEmailFailedActivity::class,
],
InvitationWasViewed::class => [
InvitationViewedListener::class
],
];
/**

View File

@ -67,4 +67,10 @@ trait Inviteable
}
}
public function getAdminLink() :string
{
return $this->getLink(). '?is_admin=true';
}
}

View File

@ -112,6 +112,11 @@ trait SettingsSaver
private function checkSettingType($settings) : \stdClass
{
$settings = (object)$settings;
/* Because of the object casting we cannot check pdf_variables */
if(property_exists($settings, 'pdf_variables'))
unset($settings->pdf_variables);
$casts = CompanySettings::$casts;
foreach ($casts as $key => $value) {

View File

@ -36,5 +36,22 @@ class DesignSeeder extends Seeder
Design::create($design);
}
foreach(Design::all() as $design){
$class = 'App\Designs\\'.$design->name;
$invoice_design = new $class();
$design_object = new \stdClass;
$design_object->include = $invoice_design->include();
$design_object->header = $invoice_design->header();
$design_object->body = $invoice_design->body();
$design_object->table_styles = $invoice_design->table_styles();
$design_object->table = $invoice_design->table();
$design_object->footer = $invoice_design->footer();
$design->design = $design_object;
$design->save();
}
}
}

View File

@ -251,9 +251,13 @@ $LANG = array(
'notification_invoice_paid_subject' => 'Invoice :invoice was paid by :client',
'notification_invoice_sent_subject' => 'Invoice :invoice was sent to :client',
'notification_invoice_viewed_subject' => 'Invoice :invoice was viewed by :client',
'notification_credit_viewed_subject' => 'Credit :credit was viewed by :client',
'notification_quote_viewed_subject' => 'Quote :quote was viewed by :client',
'notification_invoice_paid' => 'A payment of :amount was made by client :client towards Invoice :invoice.',
'notification_invoice_sent' => 'The following client :client was emailed Invoice :invoice for :amount.',
'notification_invoice_viewed' => 'The following client :client viewed Invoice :invoice for :amount.',
'notification_credit_viewed' => 'The following client :client viewed Credit :credit for :amount.',
'notification_quote_viewed' => 'The following client :client viewed Quote :quote for :amount.',
'reset_password' => 'You can reset your account password by clicking the following button:',
'secure_payment' => 'Secure Payment',
'card_number' => 'Card Number',
@ -3124,8 +3128,9 @@ $LANG = array(
'notification_payment_paid' => 'A payment of :amount was made by client :client towards :invoice',
'notification_partial_payment_paid' => 'A partial payment of :amount was made by client :client towards :invoice',
'notification_bot' => 'Notification Bot',
'invoice_number_placeholder' => 'Invoice # :invoice',
'email_link_not_working' => 'If button above isn\'t working for you, please click on the link',
);
return $LANG;