diff --git a/app/Console/Commands/PostUpdate.php b/app/Console/Commands/PostUpdate.php index f66d651062f2..37f349908dfd 100644 --- a/app/Console/Commands/PostUpdate.php +++ b/app/Console/Commands/PostUpdate.php @@ -65,7 +65,7 @@ class PostUpdate extends Command putenv('COMPOSER_HOME=' . __DIR__ . '/vendor/bin/composer'); - $input = new ArrayInput(array('command' => 'install')); + $input = new ArrayInput(array('command' => 'install', '--no-dev' => 'true')); $application = new Application(); $application->setAutoExit(false); $application->run($input); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 94c3bf14393a..2307feb938ee 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -55,6 +55,8 @@ class Kernel extends ConsoleKernel $schedule->job(new UpdateExchangeRates)->daily(); + $schedule->job(new RecurringInvoicesCron)->hourly(); + /* Run hosted specific jobs */ if (Ninja::isHosted()) { $schedule->job(new AdjustEmailQuota())->daily(); diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 28f7d06ecf81..a3b5cf613ecd 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -109,6 +109,7 @@ class CompanySettings extends BaseSettings public $counter_padding = 4; public $auto_bill = 'off'; //off,always,optin,optout + public $auto_bill_date = 'on_due_date'; // on_due_date , on_send_date public $design = 'views/pdf/design1.blade.php'; @@ -222,6 +223,9 @@ class CompanySettings extends BaseSettings public $font_size = 9; public $primary_font = 'Roboto'; public $secondary_font = 'Roboto'; + public $primary_color = '#4caf50'; + public $secondary_color = '#2196f3'; + public $hide_paid_to_date = false; public $embed_documents = false; public $all_pages_header = false; @@ -238,9 +242,14 @@ class CompanySettings extends BaseSettings public $client_portal_privacy_policy = ''; public $client_portal_enable_uploads = false; public $client_portal_allow_under_payment = false; + public $client_portal_under_payment_minimum = 0; public $client_portal_allow_over_payment = false; public static $casts = [ + 'client_portal_under_payment_minimum'=> 'float', + 'auto_bill_date' => 'string', + 'primary_color' => 'string', + 'secondary_color' => 'string', 'client_portal_allow_under_payment' => 'bool', 'client_portal_allow_over_payment' => 'bool', 'auto_bill' => 'string', diff --git a/app/Filters/SystemLogFilters.php b/app/Filters/SystemLogFilters.php index 5123f6773b7d..cb659724f0b4 100644 --- a/app/Filters/SystemLogFilters.php +++ b/app/Filters/SystemLogFilters.php @@ -51,23 +51,13 @@ class SystemLogFilters extends QueryFilters */ public function filter(string $filter = '') : Builder { + if (strlen($filter) == 0) { return $this->builder; } return $this->builder; - // return $this->builder->where(function ($query) use ($filter) { - // $query->where('vendors.name', 'like', '%'.$filter.'%') - // ->orWhere('vendors.id_number', 'like', '%'.$filter.'%') - // ->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%') - // ->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%') - // ->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%') - // ->orWhere('vendors.custom_value1', 'like', '%'.$filter.'%') - // ->orWhere('vendors.custom_value2', 'like', '%'.$filter.'%') - // ->orWhere('vendors.custom_value3', 'like', '%'.$filter.'%') - // ->orWhere('vendors.custom_value4', 'like', '%'.$filter.'%'); - // }); } /** @@ -102,8 +92,6 @@ class SystemLogFilters extends QueryFilters */ public function entityFilter() { - - //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->company(); } } diff --git a/app/Http/Controllers/SystemLogController.php b/app/Http/Controllers/SystemLogController.php index 3023b6623351..2881eb65e6e6 100644 --- a/app/Http/Controllers/SystemLogController.php +++ b/app/Http/Controllers/SystemLogController.php @@ -60,7 +60,10 @@ class SystemLogController extends BaseController { $system_logs = SystemLog::filter($filters); - return $this->listResponse($system_logs); + if(auth()->user()->isAdmin()) + return $this->listResponse($system_logs); + + return $this->errorResponse('Insufficient permissions', 403); } /** diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index 4841c12bbc8b..d7edc9eccc19 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -53,7 +53,7 @@ class QueryLogging Log::info($request->method().' - '.$request->url().": $count queries - ".$time); // if($count > 100) - // Log::info($queries); + // Log::info($queries); } } diff --git a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php index 3a70da5fc97f..844e309eb525 100644 --- a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php @@ -31,7 +31,9 @@ class StoreGroupSettingRequest extends Request public function rules() { - $rules['name'] = 'required'; + + $rules['name'] = 'required|unique:group_settings,name,null,null,company_id,'.auth()->user()->companyId(); + $rules['settings'] = new ValidClientGroupSettingsRule(); return $rules; diff --git a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php index f442508832ef..0e2aa6f45f76 100644 --- a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php @@ -34,6 +34,8 @@ class UpdateGroupSettingRequest extends Request { $rules['settings'] = new ValidClientGroupSettingsRule(); + $rules['name'] = 'unique:group_settings,name,'.$this->id.',id,company_id,'.$this->group_setting->company_id; + return $rules; } diff --git a/app/Jobs/Cron/RecurringInvoicesCron.php b/app/Jobs/Cron/RecurringInvoicesCron.php index c6d693c70b74..55999756233e 100644 --- a/app/Jobs/Cron/RecurringInvoicesCron.php +++ b/app/Jobs/Cron/RecurringInvoicesCron.php @@ -37,23 +37,26 @@ class RecurringInvoicesCron public function handle() : void { /* Get all invoices where the send date is less than NOW + 30 minutes() */ + info("Sending recurring invoices {Carbon::now()->format('Y-m-d h:i:s')}"); if (! config('ninja.db.multi_db_enabled')) { - $recurring_invoices = RecurringInvoice::where('next_send_date', '<=', Carbon::now()->addMinutes(30))->get(); + + $recurring_invoices = RecurringInvoice::where('next_send_date', '<=', Carbon::now()->addMinutes(30))->cursor(); Log::info(Carbon::now()->addMinutes(30).' Sending Recurring Invoices. Count = '.$recurring_invoices->count()); $recurring_invoices->each(function ($recurring_invoice, $key) { SendRecurring::dispatch($recurring_invoice, $recurring_invoice->company->db); }); + } else { //multiDB environment, need to foreach (MultiDB::$dbs as $db) { MultiDB::setDB($db); - $recurring_invoices = RecurringInvoice::where('next_send_date', '<=', Carbon::now()->addMinutes(30))->get(); + $recurring_invoices = RecurringInvoice::where('next_send_date', '<=', Carbon::now()->addMinutes(30))->cursor(); - Log::info(Carbon::now()->addMinutes(30).' Sending Recurring Invoices. Count = '.$recurring_invoices->count().'On Database # '.$db); + Log::info(Carbon::now()->addMinutes(30).' Sending Recurring Invoices. Count = '.$recurring_invoices->count().' On Database # '.$db); $recurring_invoices->each(function ($recurring_invoice, $key) { SendRecurring::dispatch($recurring_invoice, $recurring_invoice->company->db); diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 2486ad3a8181..82e645f24ace 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -11,7 +11,10 @@ namespace App\Jobs\RecurringInvoice; +use App\Events\Invoice\InvoiceWasEmailed; use App\Factory\RecurringInvoiceToInvoiceFactory; +use App\Helpers\Email\InvoiceEmail; +use App\Jobs\Invoice\EmailInvoice; use App\Models\Invoice; use App\Models\RecurringInvoice; use App\Utils\Traits\GeneratesCounter; @@ -53,29 +56,40 @@ class SendRecurring implements ShouldQueue // Generate Standard Invoice $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); - $invoice->number = $this->getNextRecurringInvoiceNumber($this->recurring_invoice->client); - $invoice->status_id = Invoice::STATUS_SENT; - $invoice->save(); + $invoice = $invoice->service() + ->markSent() + ->applyRecurringNumber() + ->createInvitations() + ->save(); - // Queue: Emails for invoice - // foreach invoice->invitations + $invoice->invitations->each(function ($invitation) use ($invoice) { - // Fire Payment if auto-bill is enabled - if ($this->recurring_invoice->settings->auto_bill) { - //PAYMENT ACTION HERE TODO + $email_builder = (new InvoiceEmail())->build($invitation); - // Clean up recurring invoice object + EmailInvoice::dispatch($email_builder, $invitation, $invoice->company); - $this->recurring_invoice->remaining_cycles = $this->recurring_invoice->remainingCycles(); - } + info("Firing email for invoice {$invoice->number}"); + + }); + + /* Set next date here to prevent a recurring loop forming */ + $this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate()->format('Y-m-d'); + $this->recurring_invoice->remaining_cycles = $this->recurring_invoice->remainingCycles(); $this->recurring_invoice->last_sent_date = date('Y-m-d'); - if ($this->recurring_invoice->remaining_cycles != 0) { - $this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate()->format('Y-m-d'); - } else { + /* Set completed if we don't have any more cycles remaining*/ + if ($this->recurring_invoice->remaining_cycles == 0) $this->recurring_invoice->setCompleted(); - } $this->recurring_invoice->save(); + + if ($invoice->invitations->count() > 0) + event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars())); + + // Fire Payment if auto-bill is enabled + if ($this->recurring_invoice->auto_bill) + $invoice->service()->autoBill()->save(); + } + } diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 13c0fa5b815e..01826882b267 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -56,14 +56,19 @@ class ReminderJob implements ShouldQueue $this->processReminders($db); } } + } private function processReminders($db = null) { - $invoices = Invoice::where('next_send_date', Carbon::today()->format('Y-m-d'))->get(); - $invoices->each(function ($invoice) { + Invoice::where('next_send_date', Carbon::today()->format('Y-m-d'))->with('invitations')->cursor()->each(function ($invoice) { + if ($invoice->isPayable()) { + + $reminder_template = $invoice->calculateTemplate(); + $invoice->service()->touchReminder($this->reminder_template)->save(); + $invoice->invitations->each(function ($invitation) use ($invoice) { $email_builder = (new InvoiceEmail())->build($invitation); @@ -72,13 +77,18 @@ class ReminderJob implements ShouldQueue info("Firing email for invoice {$invoice->number}"); }); - if ($invoice->invitations->count() > 0) { + if ($invoice->invitations->count() > 0) event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars())); - } + + } else { + $invoice->next_send_date = null; $invoice->save(); + } + }); + } } diff --git a/app/Mail/DownloadInvoices.php b/app/Mail/DownloadInvoices.php index 67f01b183f03..cb76e3e807bb 100644 --- a/app/Mail/DownloadInvoices.php +++ b/app/Mail/DownloadInvoices.php @@ -40,10 +40,5 @@ class DownloadInvoices extends Mailable ] ); - // return $this->from(config('mail.from.address')) //todo this needs to be fixed to handle the hosted version - // ->subject(ctrans('texts.download_documents',['size'=>''])) - // ->markdown('email.admin.download_files', [ - // 'file_path' => $this->file_path - // ]); } } diff --git a/app/Mail/ExampleMail.php b/app/Mail/ExampleMail.php deleted file mode 100644 index 5a3db457a44f..000000000000 --- a/app/Mail/ExampleMail.php +++ /dev/null @@ -1,33 +0,0 @@ -markdown('email.example'); - } -} diff --git a/app/Services/Invoice/ApplyRecurringNumber.php b/app/Services/Invoice/ApplyRecurringNumber.php new file mode 100644 index 000000000000..50a5b14b68cf --- /dev/null +++ b/app/Services/Invoice/ApplyRecurringNumber.php @@ -0,0 +1,62 @@ +client = $client; + + $this->invoice = $invoice; + } + + public function run() + { + if ($this->invoice->number != '') { + return $this->invoice; + } + + switch ($this->client->getSetting('counter_number_applied')) { + case 'when_saved': + $this->invoice->number = $this->getNextRecurringInvoiceNumber($this->client);; + break; + case 'when_sent': + if ($this->invoice->status_id == Invoice::STATUS_SENT) { + $this->invoice->number = $this->getNextRecurringInvoiceNumber($this->client);; + } + break; + + default: + // code... + break; + } + + return $this->invoice; + } +} diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index ee76aa4a1973..8ce07c3c8b8c 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -74,22 +74,6 @@ class AutoBillInvoice extends AbstractService $payment = $gateway_token->gateway->driver($this->client)->tokenBilling($gateway_token, $payment_hash); - //this is redundant - taken care of much further down. - // if($payment){ - - // if($this->invoice->partial > 0) - // $amount = $this->invoice->partial; - // else - // $amount = $this->invoice->balance; - - // $this->invoice = $this->invoice->service()->addGatewayFee($gateway_token->gateway, $amount)->save(); - - // } - // else - // { - // //TODO autobill failed - // } - return $this->invoice; } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 97f96efb55a4..b646ffee4386 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -17,6 +17,7 @@ use App\Models\Payment; use App\Services\Client\ClientService; use App\Services\Invoice\ApplyNumber; use App\Services\Invoice\ApplyPayment; +use App\Services\Invoice\ApplyRecurringNumber; use App\Services\Invoice\AutoBillInvoice; use App\Services\Invoice\CreateInvitations; use App\Services\Invoice\GetInvoicePdf; @@ -64,6 +65,18 @@ class InvoiceService return $this; } + + /** + * Applies the recurring invoice number. + * @return $this InvoiceService object + */ + public function applyRecurringNumber() + { + $this->invoice = (new ApplyRecurringNumber($this->invoice->client, $this->invoice))->run(); + + return $this; + } + /** * Apply a payment amount to an invoice. * @param Payment $payment The Payment diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 110f0aa714bb..b0c33fc9cc1c 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -234,6 +234,10 @@ class HtmlEngine $data['$client.country'] = &$data['$country']; $data['$client.email'] = &$data['$email']; + + $data['$client.balance'] = ['value' => $this->client->balance, 'label' => ctrans('texts.balance')]; + $data['$paid_to_date'] = ['value' => $this->client->paid_to_date, 'label' => ctrans('texts.paid_to_date')]; + $data['$contact.full_name'] = ['value' => $this->contact->present()->name(), 'label' => ctrans('texts.name')]; $data['$contact.email'] = ['value' => $this->contact->email, 'label' => ctrans('texts.email')]; $data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')]; @@ -271,6 +275,11 @@ class HtmlEngine $data['$company3'] = ['value' => $this->settings->custom_value3 ?: ' ', 'label' => $this->makeCustomField('company3')]; $data['$company4'] = ['value' => $this->settings->custom_value4 ?: ' ', 'label' => $this->makeCustomField('company4')]; + $data['$custom_surcharge1'] = ['value' => $this->entity->custom_surcharge1, 'label' => $this->makeCustomField('custom_surcharge1')]; + $data['$custom_surcharge2'] = ['value' => $this->entity->custom_surcharge2, 'label' => $this->makeCustomField('custom_surcharge2')]; + $data['$custom_surcharge3'] = ['value' => $this->entity->custom_surcharge3, 'label' => $this->makeCustomField('custom_surcharge3')]; + $data['$custom_surcharge4'] = ['value' => $this->entity->custom_surcharge4, 'label' => $this->makeCustomField('custom_surcharge4')]; + $data['$product.date'] = ['value' => '', 'label' => ctrans('texts.date')]; $data['$product.discount'] = ['value' => '', 'label' => ctrans('texts.discount')]; $data['$product.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')]; diff --git a/app/Utils/Traits/MakesInvoiceValues.php b/app/Utils/Traits/MakesInvoiceValues.php index 0cba97f7d120..a81fde1d7aaa 100644 --- a/app/Utils/Traits/MakesInvoiceValues.php +++ b/app/Utils/Traits/MakesInvoiceValues.php @@ -237,7 +237,6 @@ trait MakesInvoiceValues $data['$entity_number'] = &$data['$number']; - //$data['$paid_to_date'] = ; $data['$invoice.discount'] = ['value' => Number::formatMoney($calc->getTotalDiscount(), $this->client) ?: ' ', 'label' => ctrans('texts.discount')]; $data['$discount'] = &$data['$invoice.discount']; $data['$subtotal'] = ['value' => Number::formatMoney($calc->getSubTotal(), $this->client) ?: ' ', 'label' => ctrans('texts.subtotal')]; @@ -314,6 +313,10 @@ trait MakesInvoiceValues $data['$email'] = ['value' => isset($contact) ? $contact->email : 'no contact email on record', 'label' => ctrans('texts.email')]; $data['$client_name'] = ['value' => $this->present()->clientName() ?: ' ', 'label' => ctrans('texts.client_name')]; $data['$client.name'] = &$data['$client_name']; + $data['$client.balance'] = ['value' => $this->client->balance, 'label' => ctrans('texts.balance')]; + + $data['$paid_to_date'] = ['value' => $this->client->paid_to_date, 'label' => ctrans('texts.paid_to_date')]; + $data['$client.address1'] = &$data['$address1']; $data['$client.address2'] = &$data['$address2']; $data['$client_address'] = ['value' => $this->present()->address() ?: ' ', 'label' => ctrans('texts.address')]; @@ -366,6 +369,11 @@ trait MakesInvoiceValues $data['$company3'] = ['value' => $settings->custom_value3 ?: ' ', 'label' => $this->makeCustomField('company3')]; $data['$company4'] = ['value' => $settings->custom_value4 ?: ' ', 'label' => $this->makeCustomField('company4')]; + $data['$custom_surcharge1'] = ['value' => $this->custom_surcharge1, 'label' => $this->makeCustomField('custom_surcharge1')]; + $data['$custom_surcharge2'] = ['value' => $this->custom_surcharge2, 'label' => $this->makeCustomField('custom_surcharge2')]; + $data['$custom_surcharge3'] = ['value' => $this->custom_surcharge3, 'label' => $this->makeCustomField('custom_surcharge3')]; + $data['$custom_surcharge4'] = ['value' => $this->custom_surcharge4, 'label' => $this->makeCustomField('custom_surcharge4')]; + $data['$product.date'] = ['value' => '', 'label' => ctrans('texts.date')]; $data['$product.discount'] = ['value' => '', 'label' => ctrans('texts.discount')]; $data['$product.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')]; diff --git a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php index d3ed44854c1f..5f10098b9f5f 100644 --- a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php +++ b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php @@ -44,13 +44,9 @@ class AddIsPublicToDocumentsTable extends Migration }); Schema::table('recurring_invoices', function ($table) { - $table->string('auto_bill'); + $table->boolean('auto_bill')->default(0); }); - // Schema::table('recurring_expenses', function ($table) { - // $table->string('auto_bill'); - // }); - Schema::table('companies', function ($table) { $table->enum('default_auto_bill', ['off', 'always', 'optin', 'optout'])->default('off'); }); diff --git a/resources/views/email/template/plain.blade.php b/resources/views/email/template/plain.blade.php index 2f6adf20f03b..78db0c803729 100644 --- a/resources/views/email/template/plain.blade.php +++ b/resources/views/email/template/plain.blade.php @@ -7,6 +7,13 @@ {!! $body !!} + + + @if($signature) +
+ {!! $signature !!} +
+ @endif