Modifications to Designs (#3353)

* Working on Quotes

* Naming refactor for Quotes

* Quote Actions

* Quote Pdfs

* Quote PDFs

* Refunds in Stripe

* Fixes tests

* Company Ledger work
This commit is contained in:
David Bomba 2020-02-20 07:44:12 +11:00 committed by GitHub
parent 4a41685e94
commit 9e9cd37b87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 918 additions and 378 deletions

View File

@ -521,7 +521,7 @@ class CreateTestData extends Command
$invoice_calc = new InvoiceSum($credit); $invoice_calc = new InvoiceSum($credit);
$invoice_calc->build(); $invoice_calc->build();
$credit = $invoice_calc->getInvoice(); $credit = $invoice_calc->getCredit();
$credit->save(); $credit->save();
@ -533,8 +533,7 @@ class CreateTestData extends Command
{ {
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
$quote = QuoteFactory::create($client->company->id, $client->user->id);//stub the company and user_id $quote =factory(\App\Models\Quote::class)->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]);
$quote->client_id = $client->id;
$quote->date = $faker->date(); $quote->date = $faker->date();
$quote->line_items = $this->buildLineItems(rand(1,10)); $quote->line_items = $this->buildLineItems(rand(1,10));
@ -560,9 +559,8 @@ class CreateTestData extends Command
$quote_calc = new InvoiceSum($quote); $quote_calc = new InvoiceSum($quote);
$quote_calc->build(); $quote_calc->build();
$quote = $quote_calc->getInvoice(); $quote = $quote_calc->getQuote();
$quote->service()->markSent()->save();
$quote->save();
CreateQuoteInvitations::dispatch($quote, $quote->company); CreateQuoteInvitations::dispatch($quote, $quote->company);
} }

View File

@ -217,7 +217,7 @@ class CompanySettings extends BaseSettings {
public $embed_documents = false; public $embed_documents = false;
public $all_pages_header = true; public $all_pages_header = true;
public $all_pages_footer = true; public $all_pages_footer = true;
public $invoice_variables = []; public $pdf_variables = [];
public static $casts = [ public static $casts = [
'portal_design_id' => 'string', 'portal_design_id' => 'string',
@ -372,7 +372,7 @@ class CompanySettings extends BaseSettings {
'counter_padding' => 'integer', 'counter_padding' => 'integer',
'design' => 'string', 'design' => 'string',
'website' => 'string', 'website' => 'string',
'invoice_variables' => 'object', 'pdf_variables' => 'object',
]; ];
/** /**
@ -415,7 +415,7 @@ class CompanySettings extends BaseSettings {
$data->date_format_id = (string) config('ninja.i18n.date_format_id'); $data->date_format_id = (string) config('ninja.i18n.date_format_id');
$data->country_id = (string) config('ninja.i18n.country_id'); $data->country_id = (string) config('ninja.i18n.country_id');
$data->translations = (object) []; $data->translations = (object) [];
$data->invoice_variables = (array) self::getInvoiceVariableDefaults(); $data->pdf_variables = (array) self::getEntityVariableDefaults();
// $data->email_subject_invoice = EmailTemplateDefaults::emailInvoiceSubject(); // $data->email_subject_invoice = EmailTemplateDefaults::emailInvoiceSubject();
// $data->email_template_invoice = EmailTemplateDefaults:: emailInvoiceTemplate(); // $data->email_template_invoice = EmailTemplateDefaults:: emailInvoiceTemplate();
@ -456,7 +456,7 @@ class CompanySettings extends BaseSettings {
return $settings; return $settings;
} }
private static function getInvoiceVariableDefaults() { private static function getEntityVariableDefaults() {
$variables = [ $variables = [
'client_details' => [ 'client_details' => [
'name', 'name',
@ -490,6 +490,21 @@ class CompanySettings extends BaseSettings {
'balance_due', 'balance_due',
'invoice_total', 'invoice_total',
], ],
'quote_details' => [
'quote_number',
'po_number',
'date',
'valid_until',
'balance_due',
'quote_total',
],
'credit_details' => [
'credit_number',
'po_number',
'date',
'credit_balance',
'credit_amount',
],
'table_columns' => [ 'table_columns' => [
'product_key', 'product_key',
'notes', 'notes',

View File

@ -63,10 +63,10 @@ class Bold extends AbstractDesign
<div class="w-1/2"> <div class="w-1/2">
<div class="w-full bg-teal-600 px-5 py-3 text-white flex"> <div class="w-full bg-teal-600 px-5 py-3 text-white flex">
<div class="w-48 flex flex-col text-white"> <div class="w-48 flex flex-col text-white">
$invoice_details_labels $entity_labels
</div> </div>
<div class="w-32 flex flex-col text-white"> <div class="w-32 flex flex-col text-white">
$invoice_details $entity_details
</div> </div>
</div> </div>
</div> </div>

View File

@ -65,10 +65,10 @@ class Clean extends AbstractDesign
<div class="ml-4 py-4"> <div class="ml-4 py-4">
<div class="flex"> <div class="flex">
<div class="w-40 flex flex-col"> <div class="w-40 flex flex-col">
$invoice_details_labels $entity_labels
</div> </div>
<div class="w-48 flex flex-col"> <div class="w-48 flex flex-col">
$invoice_details $entity_details
</div> </div>
<div class="w-56 flex flex-col"> <div class="w-56 flex flex-col">
$client_details $client_details

View File

@ -24,6 +24,8 @@ class Designer {
protected $html; protected $html;
protected $entity_string;
private static $custom_fields = [ private static $custom_fields = [
'invoice1', 'invoice1',
'invoice2', 'invoice2',
@ -47,10 +49,15 @@ class Designer {
'company4', 'company4',
]; ];
public function __construct($design, $input_variables) { public function __construct($design, $input_variables, $entity_string)
{
$this->design = $design; $this->design = $design;
$this->input_variables = (array) $input_variables; $this->input_variables = (array) $input_variables;
$this->entity_string = $entity_string;
} }
/** /**
@ -58,21 +65,24 @@ 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($entity):Designer
{
$this->exportVariables($invoice) $this->exportVariables($entity)
->setDesign($this->getSection('header')) ->setDesign($this->getSection('header'))
->setDesign($this->getSection('body')) ->setDesign($this->getSection('body'))
->setDesign($this->getTable($invoice)) ->setDesign($this->getTable($entity))
->setDesign($this->getSection('footer')); ->setDesign($this->getSection('footer'));
return $this; return $this;
} }
public function getTable(Invoice $invoice):string { public function getTable($entity):string
{
$table_header = $invoice->table_header($this->input_variables['table_columns'], $this->design->table_styles()); $table_header = $entity->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 = $entity->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);
@ -81,11 +91,13 @@ 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,23 +111,40 @@ class Designer {
* @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($entity)
$company = $invoice->company; {
$this->exported_variables['$client_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['client_details']), $this->clientDetails($company)); $company = $entity->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['$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['$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_address'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['company_address']), $this->companyAddress($company));
if($this->entity_string == 'invoice')
{
$this->exported_variables['$entity_labels'] = $this->processLabels($this->processInputVariables($company, $this->input_variables['invoice_details']), $this->invoiceDetails($company));
$this->exported_variables['$entity_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['invoice_details']), $this->invoiceDetails($company));
}
elseif($this->entity_string == 'credit')
{
$this->exported_variables['$entity_labels'] = $this->processLabels($this->processInputVariables($company, $this->input_variables['credit_details']), $this->creditDetails($company));
$this->exported_variables['$entity_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['credit_details']), $this->creditDetails($company));
}
elseif($this->entity_string == 'quote')
{
$this->exported_variables['$entity_labels'] = $this->processLabels($this->processInputVariables($company, $this->input_variables['quote_details']), $this->quoteDetails($company));
$this->exported_variables['$entity_details'] = $this->processVariables($this->processInputVariables($company, $this->input_variables['quote_details']), $this->quoteDetails($company));
}
return $this; return $this;
} }
private function processVariables($input_variables, $variables):string { private function processVariables($input_variables, $variables):string
{
$output = ''; $output = '';
@ -126,7 +155,8 @@ class Designer {
} }
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) {
@ -142,8 +172,8 @@ class Designer {
// private function exportVariables() // private function exportVariables()
// { // {
// /* // /*
// * $invoice_details_labels // * $entity_labels
// * $invoice_details // * $entity_details
// */ // */
// $header = $this->design->header(); // $header = $this->design->header();
@ -168,7 +198,8 @@ 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>',
@ -193,7 +224,9 @@ class Designer {
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>',
@ -208,9 +241,11 @@ class Designer {
]; ];
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>',
@ -225,9 +260,11 @@ class Designer {
]; ];
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>',
@ -248,9 +285,60 @@ class Designer {
]; ];
return $this->processCustomFields($company, $data); return $this->processCustomFields($company, $data);
} }
private function processCustomFields(Company $company, $data) { private function quoteDetails(Company $company)
{
$data = [
'quote_number' => '<span>$quote_number</span>',
'po_number' => '<span>$po_number</span>',
'date' => '<span>$date</span>',
'valid_until' => '<span>$valid_until</span>',
'balance_due' => '<span>$balance_due</span>',
'quote_total' => '<span>$quote_total</span>',
'partial_due' => '<span>$partial_due</span>',
'quote1' => '<span>$quote1</span>',
'quote2' => '<span>$quote2</span>',
'quote3' => '<span>$quote3</span>',
'quote4' => '<span>$quote4</span>',
'surcharge1' => '<span>$surcharge1</span>',
'surcharge2' => '<span>$surcharge2</span>',
'surcharge3' => '<span>$surcharge3</span>',
'surcharge4' => '<span>$surcharge4</span>',
];
return $this->processCustomFields($company, $data);
}
private function creditDetails(Company $company)
{
$data = [
'credit_number' => '<span>$credit_number</span>',
'po_number' => '<span>$po_number</span>',
'date' => '<span>$date</span>',
'credit_balance' => '<span>$credit_balance</span>',
'credit_amount' => '<span>$credit_amount</span>',
'partial_due' => '<span>$partial_due</span>',
'invoice1' => '<span>$invoice1</span>',
'invoice2' => '<span>$invoice2</span>',
'invoice3' => '<span>$invoice3</span>',
'invoice4' => '<span>$invoice4</span>',
'surcharge1' => '<span>$surcharge1</span>',
'surcharge2' => '<span>$surcharge2</span>',
'surcharge3' => '<span>$surcharge3</span>',
'surcharge4' => '<span>$surcharge4</span>',
];
return $this->processCustomFields($company, $data);
}
private function processCustomFields(Company $company, $data)
{
$custom_fields = $company->custom_fields; $custom_fields = $company->custom_fields;
@ -270,7 +358,8 @@ class Designer {
} }
private function processInputVariables($company, $variables) { private function processInputVariables($company, $variables)
{
$custom_fields = $company->custom_fields; $custom_fields = $company->custom_fields;
@ -291,4 +380,5 @@ class Designer {
return $variables; return $variables;
} }
} }

View File

@ -40,10 +40,10 @@ class Modern extends AbstractDesign
</div> </div>
<div class="w-1/2 flex justify-end"> <div class="w-1/2 flex justify-end">
<div class="w-56 flex flex-col text-white"> <div class="w-56 flex flex-col text-white">
$invoice_details_labels $entity_labels
</div> </div>
<div class="w-32 flex flex-col text-left text-white"> <div class="w-32 flex flex-col text-left text-white">
$invoice_details $entity_details
</div> </div>
</div> </div>
</div> </div>

View File

@ -49,8 +49,8 @@ class Photo extends AbstractDesign
<div class="px-16 py-10"> <div class="px-16 py-10">
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-orange-700">$invoice_number_label</span> <span class="text-orange-700">$entity_labels</span>
<span class="ml-6">$invoice_number</span> <span class="ml-6">$entity_details</span>
</div> </div>
<div class="flex items-center justify-between mt-2"> <div class="flex items-center justify-between mt-2">
<div ref="logo" class="h-14"> <div ref="logo" class="h-14">

View File

@ -47,10 +47,10 @@ class Plain extends AbstractDesign
<div class="h-14">$company_logo</div> <div class="h-14">$company_logo</div>
<div class="flex px-3 mt-6"> <div class="flex px-3 mt-6">
<section class="w-1/2 flex flex-col"> <section class="w-1/2 flex flex-col">
$invoice_details_labels $entity_labels
</section> </section>
<section class="flex flex-col"> <section class="flex flex-col">
$invoice_details $entity_details
</section> </section>
</div> </div>
<section class="flex bg-gray-300 px-3"> <section class="flex bg-gray-300 px-3">

View File

@ -26,17 +26,17 @@ class PaymentWasRefunded
*/ */
public $payment; public $payment;
public $refundAmount; public $refund_amount;
/** /**
* Create a new event instance. * Create a new event instance.
* *
* @param Payment $payment * @param Payment $payment
* @param $refundAmount * @param $refund_amount
*/ */
public function __construct(Payment $payment, $refundAmount) public function __construct(Payment $payment, $refund_amount)
{ {
$this->payment = $payment; $this->payment = $payment;
$this->refundAmount = $refundAmount; $this->refund_amount = $refund_amount;
} }
} }

View File

@ -15,7 +15,7 @@ use App\Models\Invoice;
class CloneInvoiceFactory class CloneInvoiceFactory
{ {
public static function create(Invoice $invoice, $user_id) : ?Invoice public static function create($invoice, $user_id)
{ {
$clone_invoice = $invoice->replicate(); $clone_invoice = $invoice->replicate();
$clone_invoice->status_id = Invoice::STATUS_DRAFT; $clone_invoice->status_id = Invoice::STATUS_DRAFT;
@ -25,6 +25,7 @@ class CloneInvoiceFactory
$clone_invoice->partial_due_date = null; $clone_invoice->partial_due_date = null;
$clone_invoice->user_id = $user_id; $clone_invoice->user_id = $user_id;
$clone_invoice->balance = $invoice->amount; $clone_invoice->balance = $invoice->amount;
$clone_invoice->amount = $invoice->amount;
$clone_invoice->line_items = $invoice->line_items; $clone_invoice->line_items = $invoice->line_items;
$clone_invoice->backup = null; $clone_invoice->backup = null;

View File

@ -19,9 +19,6 @@ class CloneInvoiceToQuoteFactory
public static function create(Invoice $invoice, $user_id) : ?Quote public static function create(Invoice $invoice, $user_id) : ?Quote
{ {
$quote = new Quote(); $quote = new Quote();
$quote->client_id = $invoice->client_id;
$quote->user_id = $user_id;
$quote->company_id = $invoice->company_id;
$quote->discount = $invoice->discount; $quote->discount = $invoice->discount;
$quote->is_amount_discount = $invoice->is_amount_discount; $quote->is_amount_discount = $invoice->is_amount_discount;
$quote->po_number = $invoice->po_number; $quote->po_number = $invoice->po_number;
@ -35,12 +32,14 @@ class CloneInvoiceToQuoteFactory
$quote->tax_rate1 = $invoice->tax_rate1; $quote->tax_rate1 = $invoice->tax_rate1;
$quote->tax_name2 = $invoice->tax_name2; $quote->tax_name2 = $invoice->tax_name2;
$quote->tax_rate2 = $invoice->tax_rate2; $quote->tax_rate2 = $invoice->tax_rate2;
$quote->tax_rate3 = $invoice->tax_rate3;
$quote->tax_rate3 = $invoice->tax_rate3;
$quote->custom_value1 = $invoice->custom_value1; $quote->custom_value1 = $invoice->custom_value1;
$quote->custom_value2 = $invoice->custom_value2; $quote->custom_value2 = $invoice->custom_value2;
$quote->custom_value3 = $invoice->custom_value3; $quote->custom_value3 = $invoice->custom_value3;
$quote->custom_value4 = $invoice->custom_value4; $quote->custom_value4 = $invoice->custom_value4;
$quote->amount = $invoice->amount; $quote->amount = $invoice->amount;
$quote->balance = $invoice->balance; $quote->balance = $invoice->amount;
$quote->partial = $invoice->partial; $quote->partial = $invoice->partial;
$quote->partial_due_date = $invoice->partial_due_date; $quote->partial_due_date = $invoice->partial_due_date;
$quote->last_viewed = $invoice->last_viewed; $quote->last_viewed = $invoice->last_viewed;
@ -50,7 +49,6 @@ class CloneInvoiceToQuoteFactory
$quote->date = null; $quote->date = null;
$quote->due_date = null; $quote->due_date = null;
$quote->partial_due_date = null; $quote->partial_due_date = null;
$quote->balance = $invoice->amount;
$quote->line_items = $invoice->line_items; $quote->line_items = $invoice->line_items;
return $quote; return $quote;

View File

@ -0,0 +1,34 @@
<?php
/**
* quote Ninja (https://quoteninja.com)
*
* @link https://github.com/quoteninja/quoteninja source repository
*
* @copyright Copyright (c) 2020. quote Ninja LLC (https://quoteninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Factory;
use App\Models\quote;
class CloneQuoteFactory
{
public static function create($quote, $user_id)
{
$clone_quote = $quote->replicate();
$clone_quote->status_id = quote::STATUS_DRAFT;
$clone_quote->number = null;
$clone_quote->date = null;
$clone_quote->due_date = null;
$clone_quote->partial_due_date = null;
$clone_quote->user_id = $user_id;
$clone_quote->balance = $quote->amount;
$clone_quote->amount = $quote->amount;
$clone_quote->line_items = $quote->line_items;
$clone_quote->backup = null;
return $clone_quote;
}
}

View File

@ -6,14 +6,14 @@ use App\Models\Quote;
class CloneQuoteToInvoiceFactory class CloneQuoteToInvoiceFactory
{ {
public function create(Quote $quote, $user_id, $company_id) : ?Invoice public static function create(Quote $quote, $user_id) : ?Invoice
{ {
$invoice = new Invoice(); $invoice = new Invoice();
$invoice->company_id = $company_id;
$invoice->client_id = $quote->client_id;
$invoice->user_id = $user_id; $invoice->user_id = $user_id;
$invoice->po_number = $quote->po_number; $invoice->po_number = $quote->po_number;
$invoice->footer = $quote->footer; $invoice->footer = $quote->footer;
$invoice->line_items = $quote->line_items;
return $invoice; return $invoice;
} }
} }

View File

@ -171,6 +171,25 @@ class InvoiceSum
return $this->invoice; return $this->invoice;
} }
public function getQuote()
{
$this->setCalculatedAttributes();
$this->invoice->save();
return $this->invoice;
}
public function getCredit()
{
$this->setCalculatedAttributes();
$this->invoice->save();
return $this->invoice;
}
/** /**
* Build $this->invoice variables after * Build $this->invoice variables after

View File

@ -12,6 +12,7 @@
* @OA\Property(property="date", type="string", example="1-1-2014", description="The Payment date"), * @OA\Property(property="date", type="string", example="1-1-2014", description="The Payment date"),
* @OA\Property(property="transaction_reference", type="string", example="xcsSxcs124asd", description="The transaction reference as defined by the payment gateway"), * @OA\Property(property="transaction_reference", type="string", example="xcsSxcs124asd", description="The transaction reference as defined by the payment gateway"),
* @OA\Property(property="assigned_user_id", type="string", example="Opnel5aKBz", description="______"), * @OA\Property(property="assigned_user_id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="private_notes", type="string", example="The payment was refunded due to error", description="______"),
* @OA\Property(property="is_manual", type="boolean", example=true, description="______"), * @OA\Property(property="is_manual", type="boolean", example=true, description="______"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="______"), * @OA\Property(property="is_deleted", type="boolean", example=true, description="______"),
* @OA\Property(property="amount", type="number", example=10.00, description="The amount of this payment"), * @OA\Property(property="amount", type="number", example=10.00, description="The amount of this payment"),

View File

@ -11,6 +11,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Factory\CloneInvoiceFactory;
use App\Factory\CloneInvoiceToQuoteFactory;
use App\Factory\CloneQuoteFactory;
use App\Factory\CloneQuoteToInvoiceFactory;
use App\Factory\QuoteFactory; use App\Factory\QuoteFactory;
use App\Filters\QuoteFilters; use App\Filters\QuoteFilters;
use App\Http\Requests\Quote\ActionQuoteRequest; use App\Http\Requests\Quote\ActionQuoteRequest;
@ -20,8 +24,10 @@ use App\Http\Requests\Quote\EditQuoteRequest;
use App\Http\Requests\Quote\ShowQuoteRequest; use App\Http\Requests\Quote\ShowQuoteRequest;
use App\Http\Requests\Quote\StoreQuoteRequest; use App\Http\Requests\Quote\StoreQuoteRequest;
use App\Http\Requests\Quote\UpdateQuoteRequest; use App\Http\Requests\Quote\UpdateQuoteRequest;
use App\Models\Invoice;
use App\Models\Quote; use App\Models\Quote;
use App\Repositories\QuoteRepository; use App\Repositories\QuoteRepository;
use App\Transformers\InvoiceTransformer;
use App\Transformers\QuoteTransformer; use App\Transformers\QuoteTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -578,12 +584,16 @@ class QuoteController extends BaseController
{ {
switch ($action) { switch ($action) {
case 'clone_to_invoice': case 'clone_to_invoice':
//$quote = CloneInvoiceFactory::create($quote, auth()->user()->id);
return $this->itemResponse($quote); $this->entity_type = Invoice::class;
$this->entity_transformer = InvoiceTransformer::class;
$invoice = CloneQuoteToInvoiceFactory::create($quote, auth()->user()->id);
return $this->itemResponse($invoice);
break; break;
case 'clone_to_quote': case 'clone_to_quote':
//$quote = CloneInvoiceToQuoteFactory::create($quote, auth()->user()->id); $quote = CloneQuoteFactory::create($quote, auth()->user()->id);
// todo build the quote transformer and return response here return $this->itemResponse($quote);
break; break;
case 'history': case 'history':
# code... # code...

View File

@ -27,10 +27,4 @@ class EditInvoiceRequest extends Request
return auth()->user()->can('edit', $this->invoice); return auth()->user()->can('edit', $this->invoice);
} }
public function rules()
{
$rules = [];
return $rules;
}
} }

View File

@ -49,6 +49,18 @@ class StoreInvoiceRequest extends Request
if($input['client_id']) if($input['client_id'])
$input['client_id'] = $this->decodePrimaryKey($input['client_id']); $input['client_id'] = $this->decodePrimaryKey($input['client_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]);
}
}
}
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']); //$input['line_items'] = json_encode($input['line_items']);
$this->replace($input); $this->replace($input);

View File

@ -27,20 +27,5 @@ class EditQuoteRequest extends Request
return auth()->user()->can('edit', $this->quote); return auth()->user()->can('edit', $this->quote);
} }
public function rules()
{
$rules = [];
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
//$input['id'] = $this->encodePrimaryKey($input['id']);
$this->replace($input);
}
} }

View File

@ -42,6 +42,18 @@ class StoreQuoteRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
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]);
}
}
}
$this->replace($input); $this->replace($input);
} }

View File

@ -46,9 +46,10 @@ class UpdateQuoteRequest extends Request
protected function prepareForValidation() protected function prepareForValidation()
{ {
$input = $this->all(); $input = $this->all();
// if(isset($input['client_id'])) if (isset($input['client_id'])) {
// $input['client_id'] = $this->decodePrimaryKey($input['client_id']); $input['client_id'] = $this->decodePrimaryKey($input['client_id']);
}
if (isset($input['line_items'])) { if (isset($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];

View File

@ -19,6 +19,7 @@ use App\Models\ClientContact;
use App\Models\Company; use App\Models\Company;
use App\Models\Design; use App\Models\Design;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\Traits\MakesInvoiceHtml; use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -32,7 +33,7 @@ use Spatie\Browsershot\Browsershot;
class CreateInvoicePdf implements ShouldQueue { class CreateInvoicePdf implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, MakesInvoiceHtml; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, MakesInvoiceHtml, PdfMaker;
public $invoice; public $invoice;
@ -47,7 +48,7 @@ class CreateInvoicePdf implements ShouldQueue {
* *
* @return void * @return void
*/ */
public function __construct(Invoice $invoice, Company $company, ClientContact $contact = null, $disk = 'public') public function __construct($invoice, Company $company, ClientContact $contact = null)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
@ -73,7 +74,6 @@ class CreateInvoicePdf implements ShouldQueue {
$path = $this->invoice->client->invoice_filepath(); $path = $this->invoice->client->invoice_filepath();
//$file_path = $path . $this->invoice->number . '-' . $this->contact->contact_key .'.pdf';
$file_path = $path . $this->invoice->number . '.pdf'; $file_path = $path . $this->invoice->number . '.pdf';
$design = Design::find($this->invoice->client->getSetting('invoice_design_id')); $design = Design::find($this->invoice->client->getSetting('invoice_design_id'));
@ -86,7 +86,7 @@ class CreateInvoicePdf implements ShouldQueue {
$invoice_design = new $class(); $invoice_design = new $class();
} }
$designer = new Designer($invoice_design, $this->invoice->client->getSetting('invoice_variables')); $designer = new Designer($invoice_design, $this->invoice->client->getSetting('pdf_variables'), 'invoice');
//get invoice design //get invoice design
$html = $this->generateInvoiceHtml($designer->build($this->invoice)->getHtml(), $this->invoice, $this->contact); $html = $this->generateInvoiceHtml($designer->build($this->invoice)->getHtml(), $this->invoice, $this->contact);
@ -95,7 +95,6 @@ class CreateInvoicePdf implements ShouldQueue {
Storage::makeDirectory($path, 0755); Storage::makeDirectory($path, 0755);
//\Log::error($html); //\Log::error($html);
//create pdf
$pdf = $this->makePdf(null, null, $html); $pdf = $this->makePdf(null, null, $html);
$instance = Storage::disk($this->disk)->put($file_path, $pdf); $instance = Storage::disk($this->disk)->put($file_path, $pdf);
@ -105,24 +104,5 @@ class CreateInvoicePdf implements ShouldQueue {
return $file_path; return $file_path;
} }
/**
* Returns a PDF stream
*
* @param string $header Header to be included in PDF
* @param string $footer Footer to be included in PDF
* @param string $html The HTML object to be converted into PDF
*
* @return string The PDF string
*/
private function makePdf($header, $footer, $html) {
return Browsershot::html($html)
//->showBrowserHeaderAndFooter()
//->headerHtml($header)
//->footerHtml($footer)
->deviceScaleFactor(1)
->showBackground()
->waitUntilNetworkIdle(true) ->pdf();
//->margins(10,10,10,10)
//->savePdf('test.pdf');
}
} }

View File

@ -0,0 +1,127 @@
<?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\Jobs\Quote;
use App\Designs\Custom;
use App\Designs\Designer;
use App\Designs\Modern;
use App\Libraries\MultiDB;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Design;
use App\Models\Invoice;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Storage;
use Spatie\Browsershot\Browsershot;
class CreateQuotePdf implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, MakesInvoiceHtml, PdfMaker;
public $quote;
public $company;
public $contact;
private $disk;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($quote, Company $company, ClientContact $contact = null)
{
$this->quote = $quote;
$this->company = $company;
$this->contact = $contact;
$this->disk = $disk ?? config('filesystems.default');
}
public function handle() {
MultiDB::setDB($this->company->db);
$this->quote->load('client');
if(!$this->contact)
$this->contact = $this->quote->client->primary_contact()->first();
App::setLocale($this->contact->preferredLocale());
$path = $this->quote->client->quote_filepath();
$file_path = $path . $this->quote->number . '.pdf';
$design = Design::find($this->quote->client->getSetting('quote_design_id'));
if($design->is_custom){
$quote_design = new Custom($design->design);
}
else{
$class = 'App\Designs\\'.$design->name;
$quote_design = new $class();
}
$designer = new Designer($quote_design, $this->quote->client->getSetting('pdf_variables'), 'quote');
//get invoice design
$html = $this->generateInvoiceHtml($designer->build($this->quote)->getHtml(), $this->quote, $this->contact);
//todo - move this to the client creation stage so we don't keep hitting this unnecessarily
Storage::makeDirectory($path, 0755);
//\Log::error($html);
$pdf = $this->makePdf(null, null, $html);
$instance = Storage::disk($this->disk)->put($file_path, $pdf);
//$instance= Storage::disk($this->disk)->path($file_path);
return $file_path;
}
/**
* Returns a PDF stream
*
* @param string $header Header to be included in PDF
* @param string $footer Footer to be included in PDF
* @param string $html The HTML object to be converted into PDF
*
* @return string The PDF string
*/
private function makePdf($header, $footer, $html) {
return Browsershot::html($html)
//->showBrowserHeaderAndFooter()
//->headerHtml($header)
//->footerHtml($footer)
->deviceScaleFactor(1)
->showBackground()
->waitUntilNetworkIdle(true) ->pdf();
//->margins(10,10,10,10)
//->savePdf('test.pdf');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use App\Exceptions\ProcessingMigrationArchiveFailed; use App\Exceptions\ProcessingMigrationArchiveFailed;
use App\Libraries\MultiDB;
use App\Models\Company; use App\Models\Company;
use App\Models\User; use App\Models\User;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -49,6 +50,9 @@ class StartMigration implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
MultiDB::setDb($this->company->db);
$zip = new \ZipArchive(); $zip = new \ZipArchive();
$archive = $zip->open($this->filepath); $archive = $zip->open($this->filepath);

View File

@ -0,0 +1,50 @@
<?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\Activity;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class PaymentRefundedActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$fields = new \stdClass;
$fields->client_id = $event->payment->id;
$fields->user_id = $event->payment->user_id;
$fields->company_id = $event->payment->company_id;
$fields->activity_type_id = Activity::REFUNDED_PAYMENT;
$fields->payment_id = $event->payment->id;
$this->activity_repo->save($fields, $event->client);
}
}

View File

@ -0,0 +1,50 @@
<?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\Activity;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class PaymentVoidedActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$fields = new \stdClass;
$fields->client_id = $event->payment->id;
$fields->user_id = $event->payment->user_id;
$fields->company_id = $event->payment->company_id;
$fields->activity_type_id = Activity::VOIDED_PAYMENT;
$fields->payment_id = $event->payment->id;
$this->activity_repo->save($fields, $event->client);
}
}

View File

@ -445,4 +445,13 @@ class Client extends BaseModel implements HasLocalePreference
return $this->client_hash . '/invoices/'; return $this->client_hash . '/invoices/';
} }
public function quote_filepath()
{
return $this->client_hash . '/quotes/';
}
public function credit_filepath()
{
return $this->client_hash . '/credits/';
}
} }

View File

@ -99,6 +99,11 @@ class Credit extends BaseModel
return $this->belongsTo(Invoice::class); return $this->belongsTo(Invoice::class);
} }
public function company_ledger()
{
return $this->morphMany(CompanyLedger::class, 'company_ledgerable');
}
/** /**
* The invoice/s which the credit has * The invoice/s which the credit has
* been applied to. * been applied to.

View File

@ -11,6 +11,7 @@
namespace App\Models; namespace App\Models;
use App\Events\Payment\PaymentWasVoided;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Models\Credit; use App\Models\Credit;
use App\Models\DateFormat; use App\Models\DateFormat;
@ -181,5 +182,67 @@ class Payment extends BaseModel
return $this->processRefund($data); return $this->processRefund($data);
} }
/**
* @return mixed
*/
public function getCompletedAmount() :float
{
return $this->amount - $this->refunded;
}
public function recordRefund($amount = null)
{
//do i need $this->isRefunded() here?
if ($this->isVoided()) {
return false;
}
//if no refund specified
if (! $amount) {
$amount = $this->amount;
}
$new_refund = min($this->amount, $this->refunded + $amount);
$refund_change = $new_refund - $this->refunded;
if ($refund_change) {
$this->refunded = $new_refund;
$this->status_id = $this->refunded == $this->amount ? self::STATUS_REFUNDED : self::STATUS_PARTIALLY_REFUNDED;
$this->save();
event(new PaymentWasRefunded($this, $refund_change));
}
return true;
}
public function isVoided()
{
return $this->status_id == self::STATUS_VOIDED;
}
public function isPartiallyRefunded()
{
return $this->status_id == self::STATUS_PARTIALLY_REFUNDED;
}
public function isRefunded()
{
return $this->status_id == self::STATUS_REFUNDED;
}
public function markVoided()
{
if ($this->isVoided() || $this->isPartiallyRefunded() || $this->isRefunded()) {
return false;
}
$this->refunded = $this->amount;
$this->status_id = self::STATUS_VOIDED;
$this->save();
event(new PaymentWasVoided($this));
}
} }

View File

@ -13,13 +13,17 @@ namespace App\Models;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive; use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Invoice\CreateInvoicePdf;
use App\Jobs\Quote\CreateQuotePdf;
use App\Models\Filterable; use App\Models\Filterable;
use App\Services\Quote\QuoteService; use App\Services\Quote\QuoteService;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\MakesReminders; use App\Utils\Traits\MakesReminders;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Laracasts\Presenter\PresentableTrait;
class Quote extends BaseModel class Quote extends BaseModel
{ {
@ -28,6 +32,7 @@ class Quote extends BaseModel
use SoftDeletes; use SoftDeletes;
use MakesReminders; use MakesReminders;
use PresentableTrait; use PresentableTrait;
use MakesInvoiceValues;
protected $presenter = 'App\Models\Presenters\QuotePresenter'; protected $presenter = 'App\Models\Presenters\QuotePresenter';
@ -135,7 +140,23 @@ class Quote extends BaseModel
} }
public function service(): QuoteService public function service(): QuoteService
{ {
return new QuoteService($this); return new QuoteService($this);
} }
public function pdf_file_path($invitation = null)
{
$storage_path = 'storage/' . $this->client->quote_filepath() . $this->number . '.pdf';
if (Storage::exists($storage_path))
return $storage_path;
if(!$invitation)
CreateQuotePdf::dispatchNow($this, $this->company, $this->client->primary_contact()->first());
else
CreateQuotePdf::dispatchNow($invitation->quote, $invitation->company, $invitation->contact);
return $storage_path;
}
} }

View File

@ -141,8 +141,46 @@ class BasePaymentDriver
* Refunds a given payment * Refunds a given payment
* @return void * @return void
*/ */
public function refundPayment() public function refundPayment($payment, $amount = 0)
{ {
if ($amount) {
$amount = min($amount, $payment->getCompletedAmount());
} else {
$amount = $payment->getCompletedAmount();
}
if ($payment->is_deleted) {
return false;
}
if (! $amount) {
return false;
}
if ($payment->type_id == Payment::TYPE_CREDIT_CARD) {
return $payment->recordRefund($amount);
}
$details = $this->refundDetails($payment, $amount);
$response = $this->gateway()->refund($details)->send();
if ($response->isSuccessful()) {
return $payment->recordRefund($amount);
} elseif ($this->attemptVoidPayment($response, $payment, $amount)) {
$details = ['transactionReference' => $payment->transaction_reference];
$response = $this->gateway->void($details)->send();
if ($response->isSuccessful()) {
return $payment->markVoided();
}
}
return false;
}
protected function attemptVoidPayment($response, $payment, $amount)
{
// Partial refund not allowed for unsettled transactions
return $amount == $payment->amount;
} }
public function authorizeCreditCardView(array $data) public function authorizeCreditCardView(array $data)
@ -246,4 +284,6 @@ class BasePaymentDriver
return $payment; return $payment;
} }
} }

View File

@ -20,11 +20,15 @@ use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Invoice\InvoiceWasUpdated; use App\Events\Invoice\InvoiceWasUpdated;
use App\Events\Payment\PaymentWasCreated; use App\Events\Payment\PaymentWasCreated;
use App\Events\Payment\PaymentWasDeleted; use App\Events\Payment\PaymentWasDeleted;
use App\Events\Payment\PaymentWasRefunded;
use App\Events\Payment\PaymentWasVoided;
use App\Events\User\UserLoggedIn; use App\Events\User\UserLoggedIn;
use App\Events\User\UserWasCreated; use App\Events\User\UserWasCreated;
use App\Listeners\Activity\CreatedClientActivity; use App\Listeners\Activity\CreatedClientActivity;
use App\Listeners\Activity\PaymentCreatedActivity; use App\Listeners\Activity\PaymentCreatedActivity;
use App\Listeners\Activity\PaymentDeletedActivity; use App\Listeners\Activity\PaymentDeletedActivity;
use App\Listeners\Activity\PaymentRefundedActivity;
use App\Listeners\Activity\PaymentVoidedActivity;
use App\Listeners\Contact\UpdateContactLastLogin; use App\Listeners\Contact\UpdateContactLastLogin;
use App\Listeners\Invoice\CreateInvoiceActivity; use App\Listeners\Invoice\CreateInvoiceActivity;
use App\Listeners\Invoice\CreateInvoiceHtmlBackup; use App\Listeners\Invoice\CreateInvoiceHtmlBackup;
@ -75,6 +79,12 @@ class EventServiceProvider extends ServiceProvider
PaymentWasDeleted::class => [ PaymentWasDeleted::class => [
PaymentDeletedActivity::class, PaymentDeletedActivity::class,
], ],
PaymentWasRefunded::class => [
PaymentRefundedActivity::class,
],
PaymentWasVoided::class => [
PaymentVoidedActivity::class,
],
'App\Events\ClientWasArchived' => [ 'App\Events\ClientWasArchived' => [
'App\Listeners\ActivityListener@archivedClient', 'App\Listeners\ActivityListener@archivedClient',
], ],

View File

@ -86,7 +86,7 @@ class CreditRepository extends BaseRepository
* credit note * credit note
*/ */
$credit = $credit->calc()->getInvoice(); $credit = $credit->calc()->getCredit();
$credit->save(); $credit->save();

View File

@ -56,8 +56,8 @@ class InvoiceRepository extends BaseRepository {
if (isset($data['client_contacts'])) { if (isset($data['client_contacts'])) {
foreach ($data['client_contacts'] as $contact) { foreach ($data['client_contacts'] as $contact) {
if ($contact['send_email'] == 1) { if ($contact['send_email'] == 1 && is_string($contact['id'])) {
$client_contact = ClientContact::find($this->decodePrimaryKey($contact['id'])); $client_contact = ClientContact::find($this->decodePrimaryKey($contact['id']));
$client_contact->send_email = true; $client_contact->send_email = true;
$client_contact->save(); $client_contact->save();
} }

View File

@ -47,7 +47,7 @@ class QuoteRepository extends BaseRepository
if (isset($data['client_contacts'])) { if (isset($data['client_contacts'])) {
foreach ($data['client_contacts'] as $contact) { foreach ($data['client_contacts'] as $contact) {
if ($contact['send_email'] == 1) { if ($contact['send_email'] == 1 && is_string($contact['id'])) {
$client_contact = ClientContact::find($this->decodePrimaryKey($contact['id'])); $client_contact = ClientContact::find($this->decodePrimaryKey($contact['id']));
$client_contact->send_email = true; $client_contact->send_email = true;
$client_contact->save(); $client_contact->save();
@ -59,26 +59,29 @@ class QuoteRepository extends BaseRepository
if (isset($data['invitations'])) { if (isset($data['invitations'])) {
$invitations = collect($data['invitations']); $invitations = collect($data['invitations']);
/* Get array of Keyss which have been removed from the invitations array and soft delete each invitation */ /* Get array of Keys which have been removed from the invitations array and soft delete each invitation */
collect($quote->invitations->pluck('key'))->diff($invitations->pluck('key'))->each(function ($invitation) { collect($quote->invitations->pluck('key'))->diff($invitations->pluck('key'))->each(function ($invitation) {
QuoteInvitation::destroy($invitation); $this->getInvitationByKey($invitation)->delete();
}); });
foreach ($data['invitations'] as $invitation) { foreach ($data['invitations'] as $invitation) {
$inv = false; $inv = false;
if (array_key_exists('key', $invitation)) { if (array_key_exists('key', $invitation)) {
$inv = QuoteInvitation::whereKey($invitation['key'])->first(); $inv = $this->getInvitationByKey([$invitation['key']])->first();
} }
if (!$inv) { if (!$inv) {
if (isset($invitation['id'])) {
unset($invitation['id']);
}
$new_invitation = QuoteInvitationFactory::create($quote->company_id, $quote->user_id); $new_invitation = QuoteInvitationFactory::create($quote->company_id, $quote->user_id);
$new_invitation->fill($invitation);
$new_invitation->quote_id = $quote->id; $new_invitation->quote_id = $quote->id;
$new_invitation->client_contact_id = $this->decodePrimaryKey($invitation['client_contact_id']); $new_invitation->client_contact_id = $this->decodePrimaryKey($invitation['client_contact_id']);
$new_invitation->save(); $new_invitation->save();
} }
} }
} }
@ -88,7 +91,7 @@ class QuoteRepository extends BaseRepository
$quote->service()->createInvitations(); $quote->service()->createInvitations();
} }
$quote = $quote->calc()->getInvoice(); $quote = $quote->calc()->getQuote();
$quote->save(); $quote->save();
@ -98,4 +101,10 @@ class QuoteRepository extends BaseRepository
return $quote->fresh(); return $quote->fresh();
} }
public function getInvitationByKey($key) :QuoteInvitation
{
return QuoteInvitation::whereRaw("BINARY `key`= ?", [$key])->first();
}
} }

View File

@ -34,7 +34,7 @@ class RecurringQuoteRepository extends BaseRepository
$quote_calc = new InvoiceSum($quote, $quote->settings); $quote_calc = new InvoiceSum($quote, $quote->settings);
$quote = $quote_calc->build()->getInvoice(); $quote = $quote_calc->build()->getQuote();
//fire events here that cascading from the saving of an Quote //fire events here that cascading from the saving of an Quote
//ie. client balance update... //ie. client balance update...

View File

@ -0,0 +1,65 @@
<?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\Ledger;
use App\Models\CompanyLedger;
class LedgerService
{
private $entity;
public function __construct($entity)
{
$this->entity = $entity;
}
public function updateInvoiceBalance($adjustment)
{
$balance = 0;
if ($this->ledger()) {
$balance = $this->ledger->balance;
}
$adjustment = $balance + $adjustment;
$company_ledger = CompanyLedgerFactory::create($this->entity->company_id, $this->entity->user_id);
$company_ledger->client_id = $this->entity->client_id;
$company_ledger->adjustment = $adjustment;
$company_ledger->balance = $balance + $adjustment;
$company_ledger->save();
$this->entity->company_ledger()->save($company_ledger);
return $this;
}
private function ledger() :CompanyLedger
{
return CompanyLedger::whereClientId($this->entity->client_id)
->whereCompanyId($this->entity->company_id)
->orderBy('id', 'DESC')
->first();
}
public function save()
{
$this->entity->save();
return $this->entity;
}
}

View File

@ -77,6 +77,7 @@ class PaymentTransformer extends EntityTransformer
'is_deleted' => (bool) $payment->is_deleted, 'is_deleted' => (bool) $payment->is_deleted,
'type_id' => (string) $payment->payment_type_id ?: '', 'type_id' => (string) $payment->payment_type_id ?: '',
'invitation_id' => (string) $payment->invitation_id ?: '', 'invitation_id' => (string) $payment->invitation_id ?: '',
'private_notes' => (string) $payment->private_notes ?: '',
'number' => (string) $payment->number ?: '', 'number' => (string) $payment->number ?: '',
'client_id' => (string) $this->encodePrimaryKey($payment->client_id), 'client_id' => (string) $this->encodePrimaryKey($payment->client_id),
'client_contact_id' => (string) $this->encodePrimaryKey($payment->client_contact_id), 'client_contact_id' => (string) $this->encodePrimaryKey($payment->client_contact_id),

View File

@ -298,10 +298,15 @@ trait GeneratesCounter
*/ */
private function incrementCounter($entity, string $counter_name) :void private function incrementCounter($entity, string $counter_name) :void
{ {
$settings = $entity->settings; $settings = $entity->settings;
$settings->$counter_name = $settings->$counter_name + 1;
$settings->{$counter_name} = $settings->{$counter_name} + 1;
$entity->settings = $settings; $entity->settings = $settings;
$entity->save(); $entity->save();
} }
private function prefixCounter($counter, $prefix) : string private function prefixCounter($counter, $prefix) : string

View File

@ -47,6 +47,9 @@ trait MakesInvoiceValues
* @var array * @var array
*/ */
private static $labels = [ private static $labels = [
'credit_balance',
'credit_amount',
'quote_total',
'invoice', 'invoice',
'date', 'date',
'due_date', 'due_date',
@ -312,8 +315,18 @@ trait MakesInvoiceValues
// $data['$your_invoice'] = ; // $data['$your_invoice'] = ;
// $data['$quote'] = ; // $data['$quote'] = ;
// $data['$your_quote'] = ; // $data['$your_quote'] = ;
// $data['$quote_date'] = ; $data['$quote_date'] = &$data['$date'];
// $data['$quote_number'] = ; $data['$quote_number'] = &$data['$number'];
$data['$quote_no'] = &$data['$quote_number'];
$data['$quote.quote_no'] = &$data['$quote_number'];
$data['$valid_until'] = $this->due_date;
$data['$quote_total'] = &$data['$total'];
$data['$credit_amount'] = &$data['$total'];
$data['$credit_balance'] = &$data['$balance'];
$data['$credit.amount'] = &$data['$total'];
// $data['$invoice_issued_to'] = ; // $data['$invoice_issued_to'] = ;
// $data['$quote_issued_to'] = ; // $data['$quote_issued_to'] = ;
// $data['$rate'] = ; // $data['$rate'] = ;
@ -325,8 +338,6 @@ trait MakesInvoiceValues
// $data['$details'] = ; // $data['$details'] = ;
$data['$invoice_no'] = $this->number ?: '&nbsp;'; $data['$invoice_no'] = $this->number ?: '&nbsp;';
$data['$invoice.invoice_no'] = &$data['$invoice_no']; $data['$invoice.invoice_no'] = &$data['$invoice_no'];
// $data['$quote_no'] = ;
// $data['$valid_until'] = ;
$data['$client1'] = $this->client->custom_value1 ?: '&nbsp;'; $data['$client1'] = $this->client->custom_value1 ?: '&nbsp;';
$data['$client2'] = $this->client->custom_value2 ?: '&nbsp;'; $data['$client2'] = $this->client->custom_value2 ?: '&nbsp;';
$data['$client3'] = $this->client->custom_value3 ?: '&nbsp;'; $data['$client3'] = $this->client->custom_value3 ?: '&nbsp;';
@ -529,11 +540,13 @@ trait MakesInvoiceValues
return str_replace( return str_replace(
[ [
'tax_name1', 'tax_name1',
'tax_name2' 'tax_name2',
'tax_name3'
], ],
[ [
'tax', 'tax',
'tax', 'tax',
'tax'
], ],
$columns $columns
); );
@ -558,7 +571,8 @@ trait MakesInvoiceValues
'custom_invoice_label3', 'custom_invoice_label3',
'custom_invoice_label4', 'custom_invoice_label4',
'tax_name1', 'tax_name1',
'tax_name2' 'tax_name2',
'tax_name3'
], ],
[ [
'custom_invoice_value1', 'custom_invoice_value1',
@ -566,7 +580,8 @@ trait MakesInvoiceValues
'custom_invoice_value3', 'custom_invoice_value3',
'custom_invoice_value4', 'custom_invoice_value4',
'tax_rate1', 'tax_rate1',
'tax_rate2' 'tax_rate2',
'tax_rate3'
], ],
$columns $columns
); );

View File

@ -0,0 +1,32 @@
<?php
namespace App\Utils\Traits\Pdf;
use Spatie\Browsershot\Browsershot;
trait PdfMaker
{
/**
* Returns a PDF stream
*
* @param string $header Header to be included in PDF
* @param string $footer Footer to be included in PDF
* @param string $html The HTML object to be converted into PDF
*
* @return string The PDF string
*/
public function makePdf($header, $footer, $html) {
return Browsershot::html($html)
//->showBrowserHeaderAndFooter()
//->headerHtml($header)
//->footerHtml($footer)
->deviceScaleFactor(1)
->showBackground()
->waitUntilNetworkIdle(true) ->pdf();
//->margins(10,10,10,10)
//->savePdf('test.pdf');
}
}

View File

@ -7,7 +7,7 @@ use Faker\Generator as Faker;
$factory->define(App\Models\Quote::class, function (Faker $faker) { $factory->define(App\Models\Quote::class, function (Faker $faker) {
return [ return [
'status_id' => App\Models\Quote::STATUS_DRAFT, 'status_id' => App\Models\Quote::STATUS_DRAFT,
'number' => $faker->text(256), 'number' => '',
'discount' => $faker->numberBetween(1,10), 'discount' => $faker->numberBetween(1,10),
'is_amount_discount' => $faker->boolean(), 'is_amount_discount' => $faker->boolean(),
'tax_name1' => 'GST', 'tax_name1' => 'GST',

View File

@ -949,6 +949,7 @@ class CreateUsersTable extends Migration
$t->string('transaction_reference')->nullable(); $t->string('transaction_reference')->nullable();
$t->string('payer_id')->nullable(); $t->string('payer_id')->nullable();
$t->string('number')->nullable(); $t->string('number')->nullable();
$t->text('private_notes')->nullable();
$t->timestamps(6); $t->timestamps(6);
$t->softDeletes('deleted_at', 6); $t->softDeletes('deleted_at', 6);
$t->boolean('is_deleted')->default(false); $t->boolean('is_deleted')->default(false);

View File

@ -207,7 +207,7 @@ class RandomDataSeeder extends Seeder
else else
$credit_calc = new InvoiceSum($credit); $credit_calc = new InvoiceSum($credit);
$credit = $credit_calc->build()->getInvoice(); $credit = $credit_calc->build()->getCredit();
$credit->save(); $credit->save();

View File

@ -58,6 +58,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::resource('quotes', 'QuoteController');// name = (quotes. index / create / show / update / destroy / edit Route::resource('quotes', 'QuoteController');// name = (quotes. index / create / show / update / destroy / edit
Route::get('quotes/{quote}/{action}', 'QuoteController@action')->name('quotes.action');
Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk'); Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk');
Route::resource('recurring_invoices', 'RecurringInvoiceController');// name = (recurring_invoices. index / create / show / update / destroy / edit Route::resource('recurring_invoices', 'RecurringInvoiceController');// name = (recurring_invoices. index / create / show / update / destroy / edit

View File

@ -15,6 +15,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase; use Tests\TestCase;
/** /**
@ -27,6 +28,7 @@ class QuoteTest extends TestCase
use MakesHash; use MakesHash;
use DatabaseTransactions; use DatabaseTransactions;
use MockAccountData;
public function setUp() :void public function setUp() :void
{ {
@ -39,67 +41,16 @@ class QuoteTest extends TestCase
Model::reguard(); Model::reguard();
$this->makeTestData();
} }
public function testQuoteList() public function testQuoteList()
{ {
$data = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'name' => $this->faker->company,
'email' => $this->faker->unique()->safeEmail,
'password' => 'ALongAndBrilliantPassword123',
'_token' => csrf_token(),
'privacy_policy' => 1,
'terms_of_service' => 1
];
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
])->post('/api/v1/signup?include=account', $data); 'X-API-TOKEN' => $this->token,
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$company_token = $account->default_company->tokens()->first();
$token = $company_token->token;
$company = $company_token->company;
$user = $company_token->user;
$this->assertNotNull($company_token);
$this->assertNotNull($token);
$this->assertNotNull($user);
$this->assertNotNull($company);
//$this->assertNotNull($user->token->company);
factory(\App\Models\Client::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id,
'is_primary' => 1
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id
]);
});
$client = Client::all()->first();
factory(\App\Models\Quote::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->get('/api/v1/quotes'); ])->get('/api/v1/quotes');
$response->assertStatus(200); $response->assertStatus(200);
@ -108,72 +59,18 @@ class QuoteTest extends TestCase
public function testQuoteRESTEndPoints() public function testQuoteRESTEndPoints()
{ {
$data = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'name' => $this->faker->company,
'email' => $this->faker->unique()->safeEmail,
'password' => 'ALongAndBrilliantPassword123',
'_token' => csrf_token(),
'privacy_policy' => 1,
'terms_of_service' => 1
];
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
])->post('/api/v1/signup?include=account', $data); 'X-API-TOKEN' => $this->token,
])->get('/api/v1/quotes/'.$this->encodePrimaryKey($this->quote->id));
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$company_token = $account->default_company->tokens()->first();
$token = $company_token->token;
$company = $company_token->company;
$user = $company_token->user;
$this->assertNotNull($company_token);
$this->assertNotNull($token);
$this->assertNotNull($user);
$this->assertNotNull($company);
//$this->assertNotNull($user->token->company);
factory(\App\Models\Client::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id,
'is_primary' => 1
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id
]);
});
$client = Client::all()->first();
factory(\App\Models\Quote::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]);
$quote = Quote::where('user_id',$user->id)->first();
$quote->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->get('/api/v1/quotes/'.$this->encodePrimaryKey($quote->id));
$response->assertStatus(200); $response->assertStatus(200);
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token, 'X-API-TOKEN' => $this->token,
])->get('/api/v1/quotes/'.$this->encodePrimaryKey($quote->id).'/edit'); ])->get('/api/v1/quotes/'.$this->encodePrimaryKey($this->quote->id).'/edit');
$response->assertStatus(200); $response->assertStatus(200);
@ -182,26 +79,26 @@ class QuoteTest extends TestCase
// 'client_id' => $this->encodePrimaryKey($quote->client_id), // 'client_id' => $this->encodePrimaryKey($quote->client_id),
]; ];
$this->assertNotNull($quote); $this->assertNotNull($this->quote);
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token, 'X-API-TOKEN' => $this->token,
])->put('/api/v1/quotes/'.$this->encodePrimaryKey($quote->id), $quote_update); ])->put('/api/v1/quotes/'.$this->encodePrimaryKey($this->quote->id), $quote_update);
$response->assertStatus(200); $response->assertStatus(200);
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token, 'X-API-TOKEN' => $this->token,
])->delete('/api/v1/quotes/'.$this->encodePrimaryKey($quote->id)); ])->delete('/api/v1/quotes/'.$this->encodePrimaryKey($this->quote->id));
$response->assertStatus(200); $response->assertStatus(200);
$client_contact = ClientContact::whereClientId($client->id)->first(); $client_contact = ClientContact::whereClientId($this->client->id)->first();
$data = [ $data = [
'client_id' => $this->encodePrimaryKey($client->id), 'client_id' => $this->encodePrimaryKey($this->client->id),
'date' => "2019-12-14", 'date' => "2019-12-14",
'line_items' => [], 'line_items' => [],
'invitations' => [ 'invitations' => [
@ -212,7 +109,7 @@ class QuoteTest extends TestCase
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token, 'X-API-TOKEN' => $this->token,
])->post('/api/v1/quotes', $data); ])->post('/api/v1/quotes', $data);
$response->assertStatus(200); $response->assertStatus(200);

View File

@ -0,0 +1,73 @@
<?php
namespace Tests\Integration;
use App\Designs\Designer;
use App\Designs\Modern;
use App\Jobs\Invoice\CreateInvoicePdf;
use App\Jobs\Quote\CreateQuotePdf;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Designs\Designer
*/
class DesignTest extends TestCase
{
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
}
public function testInvoiceDesignExists()
{
$modern = new Modern();
$designer = new Designer($modern, $this->company->settings->pdf_variables, 'quote');
$html = $designer->build($this->invoice)->getHtml();
$this->assertNotNull($html);
//\Log::error($html);
$settings = $this->invoice->client->settings;
$settings->invoice_design_id = "4";
$this->client->settings = $settings;
$this->client->save();
CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->invoice->client->primary_contact()->first());
}
public function testQuoteDesignExists()
{
$modern = new Modern();
$designer = new Designer($modern, $this->company->settings->pdf_variables, 'quote');
$html = $designer->build($this->quote)->getHtml();
$this->assertNotNull($html);
//\Log::error($html);
$settings = $this->invoice->client->settings;
$settings->quote_design_id = "4";
$this->client->settings = $settings;
$this->client->save();
CreateQuotePdf::dispatchNow($this->quote, $this->quote->company, $this->quote->client->primary_contact()->first());
}
}

View File

@ -1,116 +0,0 @@
<?php
namespace Tests\Integration;
use App\Designs\Designer;
use App\Designs\Modern;
use App\Jobs\Invoice\CreateInvoicePdf;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Designs\Designer
*/
class InvoiceDesignTest extends TestCase
{
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
}
public function testDesignExists()
{
$modern = new Modern();
$input_variables = [
'client_details' => [
'name',
'id_number',
'vat_number',
'address1',
'address2',
'city_state_postal',
'postal_city_state',
'country',
'email',
'client1',
'client2',
'client3',
'client4',
'contact1',
'contact2',
'contact3',
'contact4',
],
'company_details' => [
'company_name',
'id_number',
'vat_number',
'website',
'email',
'phone',
'company1',
'company2',
'company3',
'company4',
],
'company_address' => [
'address1',
'address2',
'city_state_postal',
'postal_city_state',
'country',
'company1',
'company2',
'company3',
'company4',
],
'invoice_details' => [
'invoice_number',
'po_number',
'date',
'due_date',
'balance_due',
'invoice_total',
'partial_due',
'invoice1',
'invoice2',
'invoice3',
'invoice4',
'surcharge1',
'surcharge2',
'surcharge3',
'surcharge4',
],
'table_columns' => [
'product_key',
'notes',
'cost',
'quantity',
'discount',
'tax_name1',
'line_total'
],
];
$designer = new Designer($modern, $input_variables);
$html = $designer->build($this->invoice)->getHtml();
$this->assertNotNull($html);
//\Log::error($html);
CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->invoice->client->primary_contact()->first());
}
}

View File

@ -60,6 +60,10 @@ trait MockAccountData
public $token; public $token;
public $invoice;
public $quote;
public function makeTestData() public function makeTestData()
{ {
@ -206,6 +210,29 @@ trait MockAccountData
$this->invoice->service()->markSent(); $this->invoice->service()->markSent();
$this->quote = factory(\App\Models\Quote::class)->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
]);
$this->quote->line_items = $this->buildLineItems();
$this->quote->uses_inclusive_taxes = false;
$this->quote->save();
$this->quote_calc = new InvoiceSum($this->quote);
$this->quote_calc->build();
$this->quote = $this->quote_calc->getQuote();
$this->quote->number = $this->getNextQuoteNumber($this->client);
$this->quote->setRelation('client', $this->client);
$this->quote->setRelation('company', $this->company);
$this->quote->save();
$this->credit = CreditFactory::create($this->company->id,$this->user->id); $this->credit = CreditFactory::create($this->company->id,$this->user->id);
$this->credit->client_id = $this->client->id; $this->credit->client_id = $this->client->id;

View File

@ -89,11 +89,11 @@ class GeneratesCounterTest extends TestCase
$quote_number = $this->getNextQuoteNumber($this->client); $quote_number = $this->getNextQuoteNumber($this->client);
$this->assertEquals($quote_number, 0001); $this->assertEquals($quote_number, 0002);
$quote_number = $this->getNextQuoteNumber($this->client); $quote_number = $this->getNextQuoteNumber($this->client);
$this->assertEquals($quote_number, '0002'); $this->assertEquals($quote_number, '0003');
} }