Download Invoice by Invitation (#3312)

* style cs

* Style CS

* Throw Record not found exception if invalid primary key hash is provided

* Improve error handling

* Create abstract implementation for designs

* working on custom designs

* Add Design Model

* invoice services

* Download Invoice by Invitation
This commit is contained in:
David Bomba 2020-02-12 11:41:17 +11:00 committed by GitHub
parent dee99b1a62
commit be3ade65f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 498 additions and 201 deletions

View File

@ -0,0 +1,27 @@
<?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\Designs;
abstract class AbstractDesign
{
abstract public function header();
abstract public function body();
abstract public function table();
abstract public function footer();
abstract public function table_styles();
}

77
app/Designs/Custom.php Normal file
View File

@ -0,0 +1,77 @@
<?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\Designs;
class Custom extends AbstractDesign
{
private $header;
private $body;
private $table;
private $footer;
private $table_styles;
public function __construct(array $data)
{
$this->header = $data['header'];
$this->body = $data['body'];
$this->table = $data['table'];
$this->footer = $data['footer'];
$this->table_styles = $data['table_styles'];
}
public function header()
{
return $this->header;
}
public function body()
{
return $this->body;
}
public function table_styles()
{
return $this->table_styles;
}
public function table()
{
return $this->table;
}
public function footer()
{
return $this->footer;
}
}

View File

@ -14,8 +14,7 @@ namespace App\Designs;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
class Designer class Designer {
{
protected $design; protected $design;
@ -26,33 +25,32 @@ class Designer
protected $html; protected $html;
private static $custom_fields = [ private static $custom_fields = [
'invoice1', 'invoice1',
'invoice2', 'invoice2',
'invoice3', 'invoice3',
'invoice4', 'invoice4',
'surcharge1', 'surcharge1',
'surcharge2', 'surcharge2',
'surcharge3', 'surcharge3',
'surcharge4', 'surcharge4',
'client1', 'client1',
'client2', 'client2',
'client3', 'client3',
'client4', 'client4',
'contact1', 'contact1',
'contact2', 'contact2',
'contact3', 'contact3',
'contact4', 'contact4',
'company1', 'company1',
'company2', 'company2',
'company3', 'company3',
'company4', 'company4',
]; ];
public function __construct($design, $input_variables) public function __construct($design, $input_variables) {
{
$this->design = $design; $this->design = $design;
$this->input_variables = (array)$input_variables; $this->input_variables = (array) $input_variables;
} }
/** /**
@ -60,23 +58,21 @@ class Designer
* formatted HTML * formatted HTML
* @return string The HTML design built * @return string The HTML design built
*/ */
public function build(Invoice $invoice) :Designer public function build(Invoice $invoice):Designer {
{
$this->exportVariables($invoice) $this->exportVariables($invoice)
->setDesign($this->getSection('header')) ->setDesign($this->getSection('header'))
->setDesign($this->getSection('body')) ->setDesign($this->getSection('body'))
->setDesign($this->getTable($invoice)) ->setDesign($this->getTable($invoice))
->setDesign($this->getSection('footer')); ->setDesign($this->getSection('footer'));
return $this; return $this;
} }
public function getTable(Invoice $invoice) :string public function getTable(Invoice $invoice):string {
{
$table_header = $invoice->table_header($this->input_variables['table_columns'], $this->design->table_styles()); $table_header = $invoice->table_header($this->input_variables['table_columns'], $this->design->table_styles());
$table_body = $invoice->table_body($this->input_variables['table_columns'], $this->design->table_styles()); $table_body = $invoice->table_body($this->input_variables['table_columns'], $this->design->table_styles());
$data = str_replace('$table_header', $table_header, $this->getSection('table')); $data = str_replace('$table_header', $table_header, $this->getSection('table'));
$data = str_replace('$table_body', $table_body, $data); $data = str_replace('$table_body', $table_body, $data);
@ -85,13 +81,11 @@ class Designer
} }
public function getHtml() :string public function getHtml():string {
{
return $this->html; return $this->html;
} }
private function setDesign($section) private function setDesign($section) {
{
$this->html .= $section; $this->html .= $section;
@ -99,48 +93,44 @@ class Designer
} }
/** /**
* Returns the template section on with the * Returns the template section on with the
* stacked variables replaced with single variables. * stacked variables replaced with single variables.
* *
* @param string $section the method name to be executed ie header/body/table/footer * @param string $section the method name to be executed ie header/body/table/footer
* @return string The HTML of the template section * @return string The HTML of the template section
*/ */
public function getSection($section) :string public function getSection($section):string {
{ return str_replace(array_keys($this->exported_variables), array_values($this->exported_variables), $this->design->{ $section}());
return str_replace(array_keys($this->exported_variables), array_values($this->exported_variables), $this->design->{$section}());
} }
private function exportVariables($invoice) private function exportVariables($invoice) {
{
$company = $invoice->company; $company = $invoice->company;
$this->exported_variables['$client_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['client_details']), $this->clientDetails($company)); $this->exported_variables['$client_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['client_details']), $this->clientDetails($company));
$this->exported_variables['$company_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['company_details']), $this->companyDetails($company)); $this->exported_variables['$company_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['company_details']), $this->companyDetails($company));
$this->exported_variables['$company_address'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['company_address']), $this->companyAddress($company)); $this->exported_variables['$company_address'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['company_address']), $this->companyAddress($company));
$this->exported_variables['$invoice_details_labels'] = $this->processLabels($this->processInputVariables($company, $this->input_variables['invoice_details']), $this->invoiceDetails($company)); $this->exported_variables['$invoice_details_labels'] = $this->processLabels($this->processInputVariables($company, $this->input_variables['invoice_details']), $this->invoiceDetails($company));
$this->exported_variables['$invoice_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['invoice_details']), $this->invoiceDetails($company)); $this->exported_variables['$invoice_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['invoice_details']), $this->invoiceDetails($company));
return $this; return $this;
} }
private function processVariables($input_variables, $variables) :string private function processVariables($input_variables, $variables):string {
{
$output = ''; $output = '';
foreach($input_variables as $value) foreach ($input_variables as $value)
$output .= $variables[$value]; $output .= $variables[$value];
return $output; return $output;
} }
private function processLabels($input_variables, $variables) :string private function processLabels($input_variables, $variables):string {
{
$output = ''; $output = '';
foreach($input_variables as $value) { foreach ($input_variables as $value) {
$tmp = str_replace("</span>", "_label</span>", $variables[$value]); $tmp = str_replace("</span>", "_label</span>", $variables[$value]);
$output .= $tmp; $output .= $tmp;
@ -156,19 +146,19 @@ class Designer
// * $invoice_details // * $invoice_details
// */ // */
// $header = $this->design->header(); // $header = $this->design->header();
// /* // /*
// * $company_logo - full URL // * $company_logo - full URL
// * $client_details // * $client_details
// */ // */
// $body = $this->design->body(); // $body = $this->design->body();
// /* // /*
// * $table_header // * $table_header
// * $table_body // * $table_body
// * $total_labels // * $total_labels
// * $total_values // * $total_values
// */ // */
// $table = $this->design->table(); // $table = $this->design->table();
// /* // /*
@ -178,132 +168,124 @@ class Designer
// $footer = $this->design->footer(); // $footer = $this->design->footer();
// } // }
private function clientDetails(Company $company) private function clientDetails(Company $company) {
{
$data = [ $data = [
'name' => '<p>$client.name</p>', 'name' => '<p>$client.name</p>',
'id_number' => '<p>$client.id_number</p>', 'id_number' => '<p>$client.id_number</p>',
'vat_number' => '<p>$client.vat_number</p>', 'vat_number' => '<p>$client.vat_number</p>',
'address1' => '<p>$client.address1</p>', 'address1' => '<p>$client.address1</p>',
'address2' => '<p>$client.address2</p>', 'address2' => '<p>$client.address2</p>',
'city_state_postal' => '<p>$client.city_state_postal</p>', 'city_state_postal' => '<p>$client.city_state_postal</p>',
'postal_city_state' => '<p>$client.postal_city_state</p>', 'postal_city_state' => '<p>$client.postal_city_state</p>',
'country' => '<p>$client.country</p>', 'country' => '<p>$client.country</p>',
'email' => '<p>$client.email</p>', 'email' => '<p>$client.email</p>',
'client1' => '<p>$client1</p>', 'client1' => '<p>$client1</p>',
'client2' => '<p>$client2</p>', 'client2' => '<p>$client2</p>',
'client3' => '<p>$client3</p>', 'client3' => '<p>$client3</p>',
'client4' => '<p>$client4</p>', 'client4' => '<p>$client4</p>',
'contact1' => '<p>$contact1</p>', 'contact1' => '<p>$contact1</p>',
'contact2' => '<p>$contact2</p>', 'contact2' => '<p>$contact2</p>',
'contact3' => '<p>$contact3</p>', 'contact3' => '<p>$contact3</p>',
'contact4' => '<p>$contact4</p>', 'contact4' => '<p>$contact4</p>',
]; ];
return $this->processCustomFields($company, $data); return $this->processCustomFields($company, $data);
} }
private function companyDetails(Company $company) private function companyDetails(Company $company) {
{
$data = [ $data = [
'company_name' => '<span>$company.company_name</span>', 'company_name' => '<span>$company.company_name</span>',
'id_number' => '<span>$company.id_number</span>', 'id_number' => '<span>$company.id_number</span>',
'vat_number' => '<span>$company.vat_number</span>', 'vat_number' => '<span>$company.vat_number</span>',
'website' => '<span>$company.website</span>', 'website' => '<span>$company.website</span>',
'email' => '<span>$company.email</span>', 'email' => '<span>$company.email</span>',
'phone' => '<span>$company.phone</span>', 'phone' => '<span>$company.phone</span>',
'company1' => '<span>$company1</span>', 'company1' => '<span>$company1</span>',
'company2' => '<span>$company2</span>', 'company2' => '<span>$company2</span>',
'company3' => '<span>$company3</span>', 'company3' => '<span>$company3</span>',
'company4' => '<span>$company4</span>', 'company4' => '<span>$company4</span>',
]; ];
return $this->processCustomFields($company, $data); return $this->processCustomFields($company, $data);
} }
private function companyAddress(Company $company) private function companyAddress(Company $company) {
{
$data = [ $data = [
'address1' => '<span>$company.address1</span>', 'address1' => '<span>$company.address1</span>',
'address2' => '<span>$company.address1</span>', 'address2' => '<span>$company.address1</span>',
'city_state_postal' => '<span>$company.city_state_postal</span>', 'city_state_postal' => '<span>$company.city_state_postal</span>',
'postal_city_state' => '<span>$company.postal_city_state</span>', 'postal_city_state' => '<span>$company.postal_city_state</span>',
'country' => '<span>$company.country</span>', 'country' => '<span>$company.country</span>',
'company1' => '<span>$company1</span>', 'company1' => '<span>$company1</span>',
'company2' => '<span>$company2</span>', 'company2' => '<span>$company2</span>',
'company3' => '<span>$company3</span>', 'company3' => '<span>$company3</span>',
'company4' => '<span>$company4</span>', 'company4' => '<span>$company4</span>',
]; ];
return $this->processCustomFields($company, $data); return $this->processCustomFields($company, $data);
} }
private function invoiceDetails(Company $company) private function invoiceDetails(Company $company) {
{
$data = [ $data = [
'invoice_number' => '<span>$invoice_number</span>', 'invoice_number' => '<span>$invoice_number</span>',
'po_number' => '<span>$po_number</span>', 'po_number' => '<span>$po_number</span>',
'date' => '<span>$date</span>', 'date' => '<span>$date</span>',
'due_date' => '<span>$due_date</span>', 'due_date' => '<span>$due_date</span>',
'balance_due' => '<span>$balance_due</span>', 'balance_due' => '<span>$balance_due</span>',
'invoice_total' => '<span>$invoice_total</span>', 'invoice_total' => '<span>$invoice_total</span>',
'partial_due' => '<span>$partial_due</span>', 'partial_due' => '<span>$partial_due</span>',
'invoice1' => '<span>$invoice1</span>', 'invoice1' => '<span>$invoice1</span>',
'invoice2' => '<span>$invoice2</span>', 'invoice2' => '<span>$invoice2</span>',
'invoice3' => '<span>$invoice3</span>', 'invoice3' => '<span>$invoice3</span>',
'invoice4' => '<span>$invoice4</span>', 'invoice4' => '<span>$invoice4</span>',
'surcharge1' =>'<span>$surcharge1</span>', 'surcharge1' => '<span>$surcharge1</span>',
'surcharge2' =>'<span>$surcharge2</span>', 'surcharge2' => '<span>$surcharge2</span>',
'surcharge3' =>'<span>$surcharge3</span>', 'surcharge3' => '<span>$surcharge3</span>',
'surcharge4' =>'<span>$surcharge4</span>', 'surcharge4' => '<span>$surcharge4</span>',
]; ];
return $this->processCustomFields($company, $data); return $this->processCustomFields($company, $data);
} }
private function processCustomFields(Company $company, $data) private function processCustomFields(Company $company, $data) {
{
$custom_fields = $company->custom_fields; $custom_fields = $company->custom_fields;
foreach(self::$custom_fields as $cf) if (!$custom_fields) {
{ return [];
}
if(!property_exists($custom_fields, $cf) || (strlen($custom_fields->{$cf}) == 0)) foreach (self::$custom_fields as $cf) {
if (!property_exists($custom_fields, $cf) || (strlen($custom_fields->{ $cf}) == 0)) {
unset($data[$cf]); unset($data[$cf]);
}
} }
return $data; return $data;
} }
private function processInputVariables($company, $variables) private function processInputVariables($company, $variables) {
{
$custom_fields = $company->custom_fields; $custom_fields = $company->custom_fields;
$matches = array_intersect(self::$custom_fields, $variables); $matches = array_intersect(self::$custom_fields, $variables);
foreach($matches as $match) foreach ($matches as $match) {
{
if(!property_exists($custom_fields, $match) || (strlen($custom_fields->{$match}) == 0)) if (!property_exists($custom_fields, $match) || (strlen($custom_fields->{ $match}) == 0)) {
{ foreach ($variables as $key => $value) {
foreach($variables as $key => $value) if ($value == $match) {
{
if($value == $match)
{
unset($variables[$key]); unset($variables[$key]);
} }
} }
} }
} }
return $variables; return $variables;

View File

@ -11,15 +11,13 @@
namespace App\Designs; namespace App\Designs;
class Modern class Modern extends AbstractDesign
{ {
public function __construct() public function __construct() {
{ }
}
public function header() public function header() {
{
return ' return '
<!DOCTYPE html> <!DOCTYPE html>
@ -35,7 +33,7 @@ class Modern
</style> </style>
</head> </head>
<body> <body>
<div class="bg-orange-600 flex justify-between py-12 px-12"> <div class="bg-orange-600 flex justify-between py-12 px-12">
<div class="w-1/2"> <div class="w-1/2">
<h1 class="text-white font-bold text-5xl">$company.name</h1> <h1 class="text-white font-bold text-5xl">$company.name</h1>
@ -53,8 +51,7 @@ class Modern
} }
public function body() public function body() {
{
return ' return '
<div class="flex justify-between px-12 pt-12"> <div class="flex justify-between px-12 pt-12">
@ -74,17 +71,15 @@ class Modern
} }
public function table_styles() public function table_styles() {
{
return [ return [
'table_header_thead_class' => "text-left text-white bg-gray-900", 'table_header_thead_class' => "text-left text-white bg-gray-900",
'table_header_td_class' => "px-4 py-2", 'table_header_td_class' => "px-4 py-2",
'table_body_td_class' => "border-t border-b border-gray-900 px-4 py-4", 'table_body_td_class' => "border-t border-b border-gray-900 px-4 py-4",
]; ];
} }
public function table() public function table() {
{
return ' return '
<div class="px-12 pt-5 pb-20"> <div class="px-12 pt-5 pb-20">
@ -115,14 +110,14 @@ class Modern
</div> </div>
</div> </div>
<div class="flex px-4 mt-4 w-full items-end mt-5"> <div class="flex px-4 mt-4 w-full items-end mt-5" style="page-break-inside: avoid;">
<div class="w-1/2"> <div class="w-1/2">
<p class="font-semibold">$terms_label</p> <p class="font-semibold">$terms_label</p>
$terms $terms
</div> </div>
</div> </div>
<div class="mt-8 px-4 py-2 bg-gray-900 text-white"> <div class="mt-8 px-4 py-2 bg-gray-900 text-white" style="page-break-inside: avoid;">
<div class="w-1/2"></div> <div class="w-1/2"></div>
<div class="w-auto flex justify-end"> <div class="w-auto flex justify-end">
<div class="w-56"> <div class="w-56">
@ -136,8 +131,7 @@ class Modern
'; ';
} }
public function footer() public function footer() {
{
return ' return '
<div class="bg-orange-600 flex justify-between py-8 px-12" style="page-break-inside: avoid;"> <div class="bg-orange-600 flex justify-between py-8 px-12" style="page-break-inside: avoid;">

View File

@ -28,10 +28,13 @@ use App\Jobs\Invoice\CreateInvoicePdf;
use App\Jobs\Invoice\EmailInvoice; use App\Jobs\Invoice\EmailInvoice;
use App\Jobs\Invoice\StoreInvoice; use App\Jobs\Invoice\StoreInvoice;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Repositories\InvoiceRepository; use App\Repositories\InvoiceRepository;
use App\Transformers\InvoiceTransformer; use App\Transformers\InvoiceTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
/** /**
* Class InvoiceController * Class InvoiceController
@ -57,9 +60,11 @@ class InvoiceController extends BaseController {
* @param \App\Repositories\InvoiceRepository $invoice_repo The invoice repo * @param \App\Repositories\InvoiceRepository $invoice_repo The invoice repo
*/ */
public function __construct(InvoiceRepository $invoice_repo) { public function __construct(InvoiceRepository $invoice_repo) {
parent::__construct(); parent::__construct();
$this->invoice_repo = $invoice_repo; $this->invoice_repo = $invoice_repo;
} }
/** /**
@ -75,8 +80,8 @@ class InvoiceController extends BaseController {
* tags={"invoices"}, * tags={"invoices"},
* summary="Gets a list of invoices", * summary="Gets a list of invoices",
* description="Lists invoices, search and filters allow fine grained lists to be generated. * description="Lists invoices, search and filters allow fine grained lists to be generated.
*
Query parameters can be added to performed more fine grained filtering of the invoices, these are handled by the InvoiceFilters class which defines the methods available", * Query parameters can be added to performed more fine grained filtering of the invoices, these are handled by the InvoiceFilters class which defines the methods available",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"), * @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"), * @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
@ -519,17 +524,17 @@ class InvoiceController extends BaseController {
* tags={"invoices"}, * tags={"invoices"},
* summary="Performs a custom action on an invoice", * summary="Performs a custom action on an invoice",
* description="Performs a custom action on an invoice. * description="Performs a custom action on an invoice.
*
The current range of actions are as follows * The current range of actions are as follows
- clone_to_invoice * - clone_to_invoice
- clone_to_quote * - clone_to_quote
- history * - history
- delivery_note * - delivery_note
- mark_paid * - mark_paid
- download * - download
- archive * - archive
- delete * - delete
- email", * - email",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"), * @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"), * @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
@ -649,11 +654,11 @@ class InvoiceController extends BaseController {
public function downloadPdf($invitation_key) { public function downloadPdf($invitation_key) {
$invitation = InvoiceInvitation::whereKey($invitation_key)->company()->first(); $invitation = $this->invoice_repo->getInvitationByKey($invitation_key);
$contact = $invitation->contact; $contact = $invitation->contact;
$invoice = $invitation->invoice; $invoice = $invitation->invoice;
$file_path = CreateInvoicePdf::dispatchNow($invoice, $invoice->company, $contact); $file_path = $invoice->service()->getInvoicePdf(); //CreateInvoicePdf::dispatchNow($invoice, $invoice->company, $contact);
return response()->download($file_path); return response()->download($file_path);

View File

@ -39,27 +39,35 @@ class CreateInvoicePdf implements ShouldQueue {
public $company; public $company;
public $contact; public $contact;
private $disk;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Invoice $invoice, Company $company, ClientContact $contact) { public function __construct(Invoice $invoice, Company $company, ClientContact $contact, $disk = 'public')
{
$this->invoice = $invoice; $this->invoice = $invoice;
$this->company = $company; $this->company = $company;
$this->contact = $contact; $this->contact = $contact;
$this->disk = $disk ?? config('filesystems.default');
} }
public function handle() { public function handle() {
MultiDB::setDB($this->company->db); MultiDB::setDB($this->company->db);
App::setLocale($this->contact->preferredLocale()); App::setLocale($this->contact->preferredLocale());
$this->invoice->load('client'); $this->invoice->load('client');
$path = 'public/'.$this->invoice->client->client_hash.'/invoices/'; $path = $this->invoice->client->client_hash . '/invoices/';
$file_path = $path.$this->invoice->number.'.pdf'; $file_path = $path . $this->invoice->number . '.pdf';
$modern = new Modern(); $modern = new Modern();
$designer = new Designer($modern, $this->invoice->client->getSetting('invoice_variables')); $designer = new Designer($modern, $this->invoice->client->getSetting('invoice_variables'));
@ -74,9 +82,14 @@ class CreateInvoicePdf implements ShouldQueue {
//create pdf //create pdf
$pdf = $this->makePdf(null, null, $html); $pdf = $this->makePdf(null, null, $html);
$path = Storage::put($file_path, $pdf); $instance = Storage::disk($this->disk)->put($file_path, $pdf);
\Log::error($instance);
return $path; //$instance = Storage::disk($this->disk)->putFileAs($path, $pdf, $this->invoice->number . '.pdf');
//$instance = Storage::putFileAs($path, $pdf, $this->invoice->number . '.pdf');
return $instance;
} }
/** /**

View File

@ -12,40 +12,37 @@
namespace App\Jobs\Invoice; namespace App\Jobs\Invoice;
use App\Models\Invoice; use App\Models\Invoice;
use App\Repositories\InvoiceRepository;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class InvoiceNotification implements ShouldQueue class InvoiceNotification implements ShouldQueue {
{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $invoice; public $invoice;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Invoice $invoice) public function __construct(Invoice $invoice) {
{ $this->invoice = $invoice;
$this->invoice = $invoice; }
}
/** /**
* Execute the job. * Execute the job.
* *
* *
* @return void * @return void
*/ */
public function handle() public function handle() {
{
//notification for the invoice. //notification for the invoice.
// //
//could mean a email, sms, slack, push //could mean a email, sms, slack, push
} }
} }

View File

@ -47,7 +47,7 @@ class UploadFile implements ShouldQueue
public $entity; public $entity;
public function __construct($file, $type, $user, $company, $entity, $disk = 'local') public function __construct($file, $type, $user, $company, $entity, $disk = 'public')
{ {
$this->file = $file; $this->file = $file;
$this->type = $type; $this->type = $type;

25
app/Models/Design.php Normal file
View File

@ -0,0 +1,25 @@
<?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\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Design extends BaseModel
{
public function company()
{
return $this->belongsTo(Company::class);
}
}

View File

@ -342,9 +342,14 @@ class Invoice extends BaseModel
/** TODO// DOCUMENT THIS FUNCTIONALITY */ /** TODO// DOCUMENT THIS FUNCTIONALITY */
public function pdf_url() public function pdf_url()
{ {
$public_path = 'storage/' . $this->client->client_hash . '/invoices/'. $this->number . '.pdf'; // $public_path = 'storage/' . $this->client->client_hash . '/invoices/'. $this->number . '.pdf';
// $storage_path = 'public/' . $this->client->client_hash . '/invoices/'. $this->number . '.pdf';
$public_path = $this->client->client_hash . '/invoices/'. $this->number . '.pdf';
$storage_path = $this->client->client_hash . '/invoices/'. $this->number . '.pdf';
$storage_path = 'public/' . $this->client->client_hash . '/invoices/'. $this->number . '.pdf';
if (!Storage::exists($storage_path)) { if (!Storage::exists($storage_path)) {
event(new InvoiceWasUpdated($this, $this->company)); event(new InvoiceWasUpdated($this, $this->company));

View File

@ -131,4 +131,9 @@ class InvoiceRepository extends BaseRepository {
public function markSent(Invoice $invoice):?Invoice { public function markSent(Invoice $invoice):?Invoice {
return $invoice->service()->markSent()->save(); return $invoice->service()->markSent()->save();
} }
public function getInvitationByKey($key)
{
return InvoiceInvitation::whereRaw("BINARY `key`= ?", [$key])->first();
}
} }

View File

@ -0,0 +1,52 @@
<?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\Services\Invoice;
use App\Jobs\Invoice\CreateInvoicePdf;
use Illuminate\Support\Facades\Storage;
class GetInvoicePdf
{
public function __construct()
{
}
public function __invoke($invoice, $contact = null)
{
$path = $invoice->client->client_hash . '/invoices/';
$file_path = $path . $invoice->number . '.pdf';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
if(!$contact)
$contact = $invoice->client->primary_contact()->first();
if(!$file)
{
$file_path = CreateInvoicePdf::dispatchNow($invoice, $invoice->company, $contact);
}
//return $file_path;
return Storage::disk($disk)->path($file_path);
}
}

View File

@ -17,6 +17,7 @@ use App\Services\Client\ClientService;
use App\Services\Invoice\ApplyNumber; use App\Services\Invoice\ApplyNumber;
use App\Services\Invoice\ApplyPayment; use App\Services\Invoice\ApplyPayment;
use App\Services\Invoice\CreateInvitations; use App\Services\Invoice\CreateInvitations;
use App\Services\Invoice\GetInvoicePdf;
use App\Services\Invoice\MarkInvoicePaid; use App\Services\Invoice\MarkInvoicePaid;
use App\Services\Invoice\MarkSent; use App\Services\Invoice\MarkSent;
use App\Services\Invoice\UpdateBalance; use App\Services\Invoice\UpdateBalance;
@ -112,6 +113,22 @@ class InvoiceService
return $this; return $this;
} }
public function getInvoicePdf()
{
$get_invoice_pdf = new GetInvoicePdf();
return $get_invoice_pdf($this->invoice);
}
public function markViewed() public function markViewed()
{ {
$this->invoice->last_viewed = Carbon::now()->format('Y-m-d H:i'); $this->invoice->last_viewed = Carbon::now()->format('Y-m-d H:i');
@ -159,7 +176,6 @@ class InvoiceService
/** /**
* Saves the invoice * Saves the invoice
* @return Invoice object * @return Invoice object

View File

@ -11,6 +11,7 @@
namespace App\Utils\Traits; namespace App\Utils\Traits;
use App\Exceptions\ModelNotFoundException;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use Hashids\Hashids; use Hashids\Hashids;
@ -69,6 +70,9 @@ trait MakesHash
// \Log::error($decoded_array); // \Log::error($decoded_array);
if(!is_array($decoded_array))
throw new ModelNotFoundException("Resource not found", 1);
return $decoded_array[0]; return $decoded_array[0];
} catch (\Exception $e) { } catch (\Exception $e) {
return response()->json(['error'=>'Invalid primary key'], 400); return response()->json(['error'=>'Invalid primary key'], 400);

View File

@ -13,7 +13,7 @@ return [
| |
*/ */
'default' => env('FILESYSTEM_DRIVER', 'local'), 'default' => env('FILESYSTEM_DRIVER', 'public'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -1353,6 +1353,20 @@ class CreateUsersTable extends Migration
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade')->onUpdate('cascade'); $table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade')->onUpdate('cascade');
}); });
Schema::create('designs', function ($table) {
$table->increments('id');
$table->unsignedInteger('user_id')->nullable();
$table->unsignedInteger('company_id')->nullable()->index();
$table->string('name');
$table->boolean('is_custom')->default(true);
$table->boolean('is_active')->default(true);
$table->mediumText('design')->nullable();
$table->timestamps(6);
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
});
} }
/** /**

View File

@ -31,6 +31,7 @@ class DatabaseSeeder extends Seeder
$this->call('PaymentTypesSeeder'); $this->call('PaymentTypesSeeder');
$this->call('GatewayTypesSeeder'); $this->call('GatewayTypesSeeder');
$this->call('DateFormatsSeeder'); $this->call('DateFormatsSeeder');
$this->call('DesignSeeder');
} }
} }

View File

@ -0,0 +1,40 @@
<?php
use App\Models\Bank;
use App\Models\Design;
use Illuminate\Database\Seeder;
class DesignSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->createDesigns();
}
private function createDesigns()
{
$designs = [
['id' => 1, 'name' => 'Plain', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 2, 'name' => 'Clean', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 3, 'name' => 'Bold', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 4, 'name' => 'Modern', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 5, 'name' => 'Business', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 6, 'name' => 'Creative', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 7, 'name' => 'Elegant', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 8, 'name' => 'Hipster', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 9, 'name' => 'Playful', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 10, 'name' => 'Photo', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
];
foreach($designs as $design) {
$d = Design::find($design['id']);
if(!$d)
Design::create($design);
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Tests\Integration;
use App\Jobs\Invoice\CreateInvoicePdf;
use Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Services\Invoice\GetInvoicePdf
*/
class InvoiceUploadTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
}
public function testInvoiceUploadWorks()
{
CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->invoice->client->primary_contact()->first());
$this->assertNotNull($this->invoice->service()->getInvoicePdf());
}
}

View File

@ -19,6 +19,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Tests\MockAccountData; use Tests\MockAccountData;
use Tests\TestCase; use Tests\TestCase;
@ -50,7 +51,7 @@ class UploadFileTest extends TestCase
$document = UploadFile::dispatchNow( $document = UploadFile::dispatchNow(
$image, UploadFile::IMAGE, $this->invoice->user, $this->invoice->company, $this->invoice $image, UploadFile::IMAGE, $this->invoice->user, $this->invoice->company, $this->invoice
); );
$this->assertNotNull($document); $this->assertNotNull($document);
} }