1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 12:12:48 +01:00

Added auto-reminder emails

This commit is contained in:
Hillel Coren 2015-09-17 22:01:06 +03:00
parent 4741fad4be
commit 98cabd4138
48 changed files with 911 additions and 173 deletions

View File

@ -19,3 +19,5 @@ MAIL_USERNAME
MAIL_FROM_ADDRESS
MAIL_FROM_NAME
MAIL_PASSWORD
PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'

View File

@ -111,7 +111,7 @@ class CheckData extends Command {
->first(['invoices.amount', 'invoices.is_recurring', 'invoices.is_quote', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']);
// Check if this invoice was once set as recurring invoice
if (!$invoice->is_recurring && DB::table('invoices')
if ($invoice && !$invoice->is_recurring && DB::table('invoices')
->where('recurring_invoice_id', '=', $activity->invoice_id)
->first(['invoices.id'])) {
$invoice->is_recurring = 1;

View File

@ -37,12 +37,14 @@ class SendRecurringInvoices extends Command
$this->info(count($invoices).' recurring invoice(s) found');
foreach ($invoices as $recurInvoice) {
$this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO'));
$this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO'));
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice && !$invoice->isPaid()) {
$recurInvoice->account->loadLocalizationSettings($invoice->client);
if ($invoice->account->pdf_email_attachment) {
$invoice->updateCachedPDF();
}
$this->mailer->sendInvoice($invoice);
}
}

View File

@ -0,0 +1,65 @@
<?php namespace App\Console\Commands;
use DB;
use DateTime;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use App\Models\Account;
use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\accountRepository;
use App\Ninja\Repositories\InvoiceRepository;
class SendReminders extends Command
{
protected $name = 'ninja:send-reminders';
protected $description = 'Send reminder emails';
protected $mailer;
protected $invoiceRepo;
protected $accountRepo;
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo)
{
parent::__construct();
$this->mailer = $mailer;
$this->invoiceRepo = $invoiceRepo;
$this->accountRepo = $accountRepo;
}
public function fire()
{
$this->info(date('Y-m-d').' Running SendReminders...');
$today = new DateTime();
$accounts = $this->accountRepo->findWithReminders();
$this->info(count($accounts).' accounts found');
foreach ($accounts as $account) {
$invoices = $this->invoiceRepo->findNeedingReminding($account);
$this->info($account->name . ': ' . count($invoices).' invoices found');
foreach ($invoices as $invoice) {
if ($reminder = $invoice->getReminder()) {
$this->mailer->sendInvoice($invoice, $reminder);
}
}
}
$this->info('Done');
}
protected function getArguments()
{
return array(
//array('example', InputArgument::REQUIRED, 'An example argument.'),
);
}
protected function getOptions()
{
return array(
//array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null),
);
}
}

View File

@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel {
'App\Console\Commands\ResetData',
'App\Console\Commands\CheckData',
'App\Console\Commands\SendRenewalInvoices',
'App\Console\Commands\SendReminders',
];
/**

View File

@ -249,10 +249,14 @@ class AccountController extends BaseController
if ($subSection == ACCOUNT_CUSTOMIZE_DESIGN) {
$data['customDesign'] = ($account->custom_design && !$design) ? $account->custom_design : $design;
}
} else if ($subSection == ACCOUNT_EMAIL_TEMPLATES) {
$data['invoiceEmail'] = $account->getEmailTemplate(ENTITY_INVOICE);
$data['quoteEmail'] = $account->getEmailTemplate(ENTITY_QUOTE);
$data['paymentEmail'] = $account->getEmailTemplate(ENTITY_PAYMENT);
} else if ($subSection == ACCOUNT_TEMPLATES_AND_REMINDERS) {
$data['templates'] = [];
foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) {
$data['templates'][$type] = [
'subject' => $account->getEmailSubject($type),
'template' => $account->getEmailTemplate($type),
];
}
$data['emailFooter'] = $account->getEmailFooter();
$data['title'] = trans('texts.email_templates');
} else if ($subSection == ACCOUNT_USER_MANAGEMENT) {
@ -289,7 +293,7 @@ class AccountController extends BaseController
return AccountController::saveInvoiceDesign();
} elseif ($subSection == ACCOUNT_CUSTOMIZE_DESIGN) {
return AccountController::saveCustomizeDesign();
} elseif ($subSection == ACCOUNT_EMAIL_TEMPLATES) {
} elseif ($subSection == ACCOUNT_TEMPLATES_AND_REMINDERS) {
return AccountController::saveEmailTemplates();
}
} elseif ($section == ACCOUNT_PRODUCTS) {
@ -315,16 +319,28 @@ class AccountController extends BaseController
if (Auth::user()->account->isPro()) {
$account = Auth::user()->account;
$account->email_template_invoice = Input::get('email_template_invoice', $account->getEmailTemplate(ENTITY_INVOICE));
$account->email_template_quote = Input::get('email_template_quote', $account->getEmailTemplate(ENTITY_QUOTE));
$account->email_template_payment = Input::get('email_template_payment', $account->getEmailTemplate(ENTITY_PAYMENT));
foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) {
$subjectField = "email_subject_{$type}";
$account->$subjectField = Input::get($subjectField, $account->getEmailSubject($type));
$bodyField = "email_template_{$type}";
$account->$bodyField = Input::get($bodyField, $account->getEmailTemplate($type));
}
foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) {
$enableField = "enable_{$type}";
$account->$enableField = Input::get($enableField) ? true : false;
$numDaysField = "num_days_{$type}";
$account->$numDaysField = Input::get($numDaysField);
}
$account->save();
Session::flash('message', trans('texts.updated_settings'));
}
return Redirect::to('company/advanced_settings/email_templates');
return Redirect::to('company/advanced_settings/templates_and_reminders');
}
private function saveProducts()
@ -346,7 +362,7 @@ class AccountController extends BaseController
$rules = [];
$user = Auth::user();
$iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH));
$subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('substr(string, start)')), 0, MAX_SUBDOMAIN_LENGTH));
$subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH));
if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) {
$subdomain = null;
}
@ -361,7 +377,6 @@ class AccountController extends BaseController
->withErrors($validator)
->withInput();
} else {
$account = Auth::user()->account;
$account->subdomain = $subdomain;
$account->iframe_url = $iframeURL;

View File

@ -239,7 +239,7 @@ class AccountGatewayController extends BaseController
foreach ($fields as $field => $details) {
$value = trim(Input::get($gateway->id.'_'.$field));
// if the new value is masked use the original value
if ($value && $value === str_repeat('*', strlen($value))) {
if ($oldConfig && $value && $value === str_repeat('*', strlen($value))) {
$value = $oldConfig->$field;
}
if (!$value && ($field == 'testMode' || $field == 'developerMode')) {

View File

@ -62,12 +62,10 @@ class InvoiceApiController extends Controller
// check if the invoice number is set and unique
if (!isset($data['invoice_number']) && !isset($data['id'])) {
$data['invoice_number'] = Auth::user()->account->getNextInvoiceNumber();
} else if (isset($data['invoice_number'])) {
} else if (isset($data['invoice_number'])) {
$invoice = Invoice::scope()->where('invoice_number', '=', $data['invoice_number'])->first();
if ($invoice) {
$error = trans('validation.unique', ['attribute' => 'texts.invoice_number']);
} else {
$data['id'] = $invoice->public_id;
}
}

View File

@ -1,5 +1,6 @@
<?php namespace App\Http\Controllers;
use App;
use Auth;
use Session;
use Utils;
@ -174,11 +175,16 @@ class InvoiceController extends BaseController
public function view($invitationKey)
{
$invitation = Invitation::where('invitation_key', '=', $invitationKey)->firstOrFail();
$invoice = $invitation->invoice;
$invitation = Invitation::where('invitation_key', '=', $invitationKey)->first();
if (!$invitation) {
App::abort(404, trans('texts.invoice_not_found'));
}
$invoice = $invitation->invoice;
if (!$invoice || $invoice->is_deleted) {
return View::make('invoices.deleted');
App::abort(404, trans('texts.invoice_not_found'));
}
$invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country');
@ -186,7 +192,7 @@ class InvoiceController extends BaseController
$account = $client->account;
if (!$client || $client->is_deleted) {
return View::make('invoices.deleted');
App::abort(404, trans('texts.invoice_not_found'));
}
if ($account->subdomain) {
@ -198,7 +204,7 @@ class InvoiceController extends BaseController
}
}
if (!Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
if (!Input::has('phantomjs') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
Activity::viewInvoice($invitation);
Event::fire(new InvoiceViewed($invoice));
}
@ -261,6 +267,7 @@ class InvoiceController extends BaseController
'contact' => $contact,
'paymentTypes' => $paymentTypes,
'paymentURL' => $paymentURL,
'phantomjs' => Input::has('phantomjs'),
);
return View::make('invoices.view', $data);
@ -521,7 +528,7 @@ class InvoiceController extends BaseController
$pdfUpload = Input::get('pdfupload');
if (!empty($pdfUpload) && strpos($pdfUpload, 'data:application/pdf;base64,') === 0) {
$this->storePDF(Input::get('pdfupload'), $invoice);
$invoice->updateCachedPDF(Input::get('pdfupload'));
}
if ($action == 'clone') {
@ -684,10 +691,4 @@ class InvoiceController extends BaseController
return View::make('invoices.history', $data);
}
private function storePDF($encodedString, $invoice)
{
$encodedString = str_replace('data:application/pdf;base64,', '', $encodedString);
file_put_contents($invoice->getPDFPath(), base64_decode($encodedString));
}
}

View File

@ -225,7 +225,6 @@ Route::get('/forgot_password', function() {
return Redirect::to(NINJA_APP_URL.'/forgot', 301);
});
if (!defined('CONTACT_EMAIL')) {
define('CONTACT_EMAIL', Config::get('mail.from.address'));
define('CONTACT_NAME', Config::get('mail.from.name'));
@ -260,7 +259,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACCOUNT_CHART_BUILDER', 'chart_builder');
define('ACCOUNT_USER_MANAGEMENT', 'user_management');
define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations');
define('ACCOUNT_EMAIL_TEMPLATES', 'email_templates');
define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders');
define('ACCOUNT_TOKEN_MANAGEMENT', 'token_management');
define('ACCOUNT_CUSTOMIZE_DESIGN', 'customize_design');
@ -391,6 +390,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ZAPIER_URL', 'https://zapier.com/developer/invite/11276/85cf0ee4beae8e802c6c579eb4e351f1/');
define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/');
define('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html');
define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/single/browser/v1/');
define('COUNT_FREE_DESIGNS', 4);
define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design
@ -425,6 +425,10 @@ if (!defined('CONTACT_EMAIL')) {
define('PAYMENT_TYPE_TOKEN', 'PAYMENT_TYPE_TOKEN');
define('PAYMENT_TYPE_ANY', 'PAYMENT_TYPE_ANY');
define('REMINDER1', 'reminder1');
define('REMINDER2', 'reminder2');
define('REMINDER3', 'reminder3');
$creditCards = [
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],
2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'],
@ -487,4 +491,5 @@ if (Auth::check() && Auth::user()->id === 1)
{
Auth::loginUsingId(1);
}
*/
*/

View File

@ -776,4 +776,14 @@ class Utils
}
return $domain;
}
public static function replaceSubdomain($domain, $subdomain) {
$parsedUrl = parse_url($domain);
$host = explode('.', $parsedUrl['host']);
if (count($host) > 0) {
$oldSubdomain = $host[0];
$domain = str_replace("://{$oldSubdomain}.", "://{$subdomain}.", $domain);
}
return $domain;
}
}

View File

@ -25,6 +25,13 @@ class Account extends Eloquent
return $this->hasMany('App\Models\User');
}
public function getPrimaryUser()
{
return $this->hasMany('App\Models\User')
->whereRaw('public_id = 0 OR public_id IS NULL')
->first();
}
public function clients()
{
return $this->hasMany('App\Models\Client');
@ -405,15 +412,35 @@ class Account extends Eloquent
return $this;
}
public function getEmailSubject($entityType)
{
$field = "email_subject_{$entityType}";
$value = $this->$field;
if ($value) {
return $value;
}
if (strpos($entityType, 'reminder') !== false) {
$entityType = 'reminder';
}
return trans("texts.{$entityType}_subject", ['invoice' => '$invoice', 'account' => '$account']);
}
public function getEmailTemplate($entityType, $message = false)
{
$field = "email_template_$entityType";
$field = "email_template_{$entityType}";
$template = $this->$field;
if ($template) {
return $template;
}
if (strpos($entityType, 'reminder') >= 0) {
$entityType = ENTITY_INVOICE;
}
$template = "\$client,<p/>\r\n\r\n" .
trans("texts.{$entityType}_message", ['amount' => '$amount']) . "<p/>\r\n\r\n" .
"<a href=\"\$link\">\$link</a><p/>\r\n\r\n";
@ -431,7 +458,7 @@ class Account extends Eloquent
// Add line breaks if HTML isn't already being used
return strip_tags($this->email_footer) == $this->email_footer ? nl2br($this->email_footer) : $this->email_footer;
} else {
return "<p>" . trans('texts.email_signature') . "<br>\$account</p>";
return "<p>" . trans('texts.email_signature') . "\n<br>\$account</p>";
}
}
@ -449,6 +476,20 @@ class Account extends Eloquent
{
return $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT;
}
public function getSiteUrl()
{
$url = SITE_URL;
$iframe_url = $this->iframe_url;
if ($iframe_url) {
return "{$iframe_url}/?";
} else if ($this->subdomain) {
$url = Utils::replaceSubdomain($url, $this->subdomain);
}
return $url;
}
}
Account::updated(function ($account) {

View File

@ -1,5 +1,6 @@
<?php namespace App\Models;
use Utils;
use Illuminate\Database\Eloquent\SoftDeletes;
class Invitation extends EntityModel
@ -37,12 +38,9 @@ class Invitation extends EntityModel
$iframe_url = $this->account->iframe_url;
if ($iframe_url) {
return "{$iframe_url}?{$this->invitation_key}";
return "{$iframe_url}/?{$this->invitation_key}";
} else if ($this->account->subdomain) {
$parsedUrl = parse_url($url);
$host = explode('.', $parsedUrl['host']);
$subdomain = $host[0];
$url = str_replace("://{$subdomain}.", "://{$this->account->subdomain}.", $url);
$url = Utils::replaceSubdomain($url, $this->subdomain);
}
return "{$url}/view/{$this->invitation_key}";

View File

@ -262,6 +262,57 @@ class Invoice extends EntityModel
return false;
}
public function getReminder() {
for ($i=1; $i<=3; $i++) {
$field = "enable_reminder{$i}";
if (!$this->account->$field) {
continue;
}
$field = "num_days_reminder{$i}";
$date = date('Y-m-d', strtotime("- {$this->account->$field} days"));
if ($this->due_date == $date) {
return "reminder{$i}";
}
}
return false;
}
public function updateCachedPDF($encodedString = false)
{
if (!$encodedString) {
$invitation = $this->invitations[0];
$key = $invitation->getLink();
$curl = curl_init();
$jsonEncodedData = json_encode([
'targetUrl' => SITE_URL . "/view/{$key}/?phantomjs=true",
'requestType' => 'raw',
]);
$opts = [
CURLOPT_URL => PHANTOMJS_CLOUD . env('PHANTOMJS_CLOUD_KEY'),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $jsonEncodedData,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Content-Length: '.strlen($jsonEncodedData)],
];
curl_setopt_array($curl, $opts);
$encodedString = strip_tags(curl_exec($curl));
curl_close($curl);
if (!$encodedString) {
return false;
}
}
$encodedString = str_replace('data:application/pdf;base64,', '', $encodedString);
file_put_contents($this->getPDFPath(), base64_decode($encodedString));
}
}
Invoice::creating(function ($invoice) {

View File

@ -3,6 +3,7 @@
use Utils;
use Event;
use URL;
use Auth;
use App\Models\Invoice;
use App\Models\Payment;
@ -12,7 +13,7 @@ use App\Events\InvoiceSent;
class ContactMailer extends Mailer
{
public function sendInvoice(Invoice $invoice)
public function sendInvoice(Invoice $invoice, $reminder = false)
{
$invoice->load('invitations', 'client.language', 'account');
$entityType = $invoice->getEntityType();
@ -23,65 +24,69 @@ class ContactMailer extends Mailer
$account->loadLocalizationSettings($client);
$view = 'invoice';
$subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]);
$accountName = $invoice->account->getDisplayName();
$emailTemplate = $invoice->account->getEmailTemplate($entityType);
$invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $client->getCurrencyId());
$emailTemplate = $invoice->account->getEmailTemplate($reminder ?: $entityType);
$emailSubject = $invoice->account->getEmailSubject($reminder ?: $entityType);
$this->initClosure($invoice);
$response = false;
$sent = false;
foreach ($invoice->invitations as $invitation) {
if (!$invitation->user || !$invitation->user->email || $invitation->user->trashed()) {
return false;
if (Auth::check()) {
$user = Auth::user();
} else {
$user = $invitation->user;
if ($invitation->user->trashed()) {
$user = $account->getPrimaryUser();
}
}
if (!$invitation->contact || !$invitation->contact->email || $invitation->contact->trashed()) {
return false;
if (!$user->email || !$user->confirmed) {
continue;
}
if (!$invitation->contact->email
|| $invitation->contact->trashed()) {
continue;
}
$invitation->sent_date = \Carbon::now()->toDateTimeString();
$invitation->save();
$variables = [
'$footer' => $invoice->account->getEmailFooter(),
'$link' => $invitation->getLink(),
'$client' => $client->getDisplayName(),
'$account' => $accountName,
'$contact' => $invitation->contact->getDisplayName(),
'$amount' => $invoiceAmount,
'$advancedRawInvoice->' => '$'
'account' => $account,
'client' => $client,
'invitation' => $invitation,
'amount' => $invoice->getRequestedAmount()
];
// Add variables for available payment types
foreach (Gateway::getPaymentTypeLinks() as $type) {
$variables["\${$type}_link"] = URL::to("/payment/{$invitation->invitation_key}/{$type}");
}
$data['body'] = str_replace(array_keys($variables), array_values($variables), $emailTemplate);
$data['body'] = preg_replace_callback('/\{\{\$?(.*)\}\}/', $this->advancedTemplateHandler, $data['body']);
$data['body'] = $this->processVariables($emailTemplate, $variables);
$data['link'] = $invitation->getLink();
$data['entityType'] = $entityType;
$data['invoice_id'] = $invoice->id;
$fromEmail = $invitation->user->email;
$subject = $this->processVariables($emailSubject, $variables);
$fromEmail = $user->email;
$response = $this->sendTo($invitation->contact->email, $fromEmail, $accountName, $subject, $view, $data);
if ($response !== true) {
return $response;
if ($response === true) {
$sent = true;
Activity::emailInvoice($invitation);
}
Activity::emailInvoice($invitation);
}
if (!$invoice->isSent()) {
$invoice->invoice_status_id = INVOICE_STATUS_SENT;
$invoice->save();
if ($sent === true) {
if (!$invoice->isSent()) {
$invoice->invoice_status_id = INVOICE_STATUS_SENT;
$invoice->save();
}
$account->loadLocalizationSettings();
Event::fire(new InvoiceSent($invoice));
}
$account->loadLocalizationSettings();
Event::fire(new InvoiceSent($invoice));
return $response;
return $response ?: trans('texts.email_error');
}
public function sendPaymentConfirmation(Payment $payment)
@ -93,30 +98,38 @@ class ContactMailer extends Mailer
$invoice = $payment->invoice;
$view = 'payment_confirmation';
$subject = trans('texts.payment_subject', ['invoice' => $invoice->invoice_number]);
$accountName = $account->getDisplayName();
$emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT);
$emailSubject = $invoice->account->getEmailSubject(ENTITY_PAYMENT);
$variables = [
'$footer' => $account->getEmailFooter(),
'$client' => $client->getDisplayName(),
'$account' => $accountName,
'$amount' => Utils::formatMoney($payment->amount, $client->getCurrencyId())
];
$this->initClosure($invoice);
if ($payment->invitation) {
$user = $payment->invitation->user;
$contact = $payment->contact;
$variables['$link'] = $payment->invitation->getLink();
$invitation = $payment->invitation;
} else {
$user = $payment->user;
$contact = $client->contacts[0];
$variables['$link'] = $payment->invoice->invitations[0]->getLink();
$invitation = $payment->invoice->invitations[0];
}
$data = ['body' => str_replace(array_keys($variables), array_values($variables), $emailTemplate)];
//$data['invoice_id'] = $payment->invoice->id;
$variables = [
'account' => $account,
'client' => $client,
'invitation' => $invitation,
'amount' => $payment->amount
];
$data = [
'body' => $this->processVariables($emailTemplate, $variables)
];
$subject = $this->processVariables($emailSubject, $variables);
$data['invoice_id'] = $payment->invoice->id;
if ($invoice->account->pdf_email_attachment) {
$invoice->updateCachedPDF();
}
if ($user->email && $contact->email) {
$this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data);
@ -148,6 +161,31 @@ class ContactMailer extends Mailer
$this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
private function processVariables($template, $data)
{
$variables = [
'$footer' => $data['account']->getEmailFooter(),
'$link' => $data['invitation']->getLink(),
'$client' => $data['client']->getDisplayName(),
'$account' => $data['account']->getDisplayName(),
'$contact' => $data['invitation']->contact->getDisplayName(),
'$amount' => Utils::formatMoney($data['amount'], $data['client']->getCurrencyId()),
'$invoice' => $data['invitation']->invoice->invoice_number,
'$quote' => $data['invitation']->invoice->invoice_number,
'$advancedRawInvoice->' => '$'
];
// Add variables for available payment types
foreach (Gateway::getPaymentTypeLinks() as $type) {
$variables["\${$type}_link"] = URL::to("/payment/{$data['invitation']->invitation_key}/{$type}");
}
$str = str_replace(array_keys($variables), array_values($variables), $template);
$str = preg_replace_callback('/\{\{\$?(.*)\}\}/', $this->advancedTemplateHandler, $str);
return $str;
}
private function initClosure($object)
{
$this->advancedTemplateHandler = function($match) use ($object) {

View File

@ -21,25 +21,25 @@ class Mailer
$replyEmail = $fromEmail;
$fromEmail = CONTACT_EMAIL;
$message->to($toEmail)
->from($fromEmail, $fromName)
->replyTo($replyEmail, $fromName)
->subject($subject);
if (isset($data['invoice_id'])) {
$invoice = Invoice::with('account')->where('id', '=', $data['invoice_id'])->get()->first();
if($invoice->account->pdf_email_attachment && file_exists($invoice->getPDFPath())) {
$invoice = Invoice::with('account')->where('id', '=', $data['invoice_id'])->first();
if ($invoice->account->pdf_email_attachment && file_exists($invoice->getPDFPath())) {
$message->attach(
$invoice->getPDFPath(),
array('as' => $invoice->getFileName(), 'mime' => 'application/pdf')
);
}
}
$message->to($toEmail)
->from($fromEmail, $fromName)
->replyTo($replyEmail, $fromName)
->subject($subject);
});
return true;
} catch (Exception $exception) {
Utils::logError('Email Error: ' . $exception->getMessage());
if (isset($_ENV['POSTMARK_API_TOKEN'])) {
$response = $exception->getResponse()->getBody()->getContents();
$response = json_decode($response);

View File

@ -392,4 +392,9 @@ class AccountRepository
$userAccount->save();
}
}
public function findWithReminders()
{
return Account::whereRaw('enable_reminder1 = 1 OR enable_reminder2 = 1 OR enable_reminder3 = 1')->get();
}
}

View File

@ -658,4 +658,25 @@ class InvoiceRepository
return $invoice;
}
public function findNeedingReminding($account)
{
$dates = [];
for ($i=1; $i<=3; $i++) {
$field = "enable_reminder{$i}";
if (!$account->$field) {
continue;
}
$field = "num_days_reminder{$i}";
$dates[] = "due_date = '" . date('Y-m-d', strtotime("- {$account->$field} days")) . "'";
}
$sql = implode(' OR ', $dates);
$invoices = Invoice::whereAccountId($account->id)
->where('balance', '>', 0)
->whereRaw($sql)
->get();
return $invoices;
}
}

View File

@ -0,0 +1,68 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddReminderEmails extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function ($table) {
$table->string('email_subject_invoice')->nullable();
$table->string('email_subject_quote')->nullable();
$table->string('email_subject_payment')->nullable();
$table->string('email_subject_reminder1')->nullable();
$table->string('email_subject_reminder2')->nullable();
$table->string('email_subject_reminder3')->nullable();
$table->text('email_template_reminder1')->nullable();
$table->text('email_template_reminder2')->nullable();
$table->text('email_template_reminder3')->nullable();
$table->boolean('enable_reminder1')->default(false);
$table->boolean('enable_reminder2')->default(false);
$table->boolean('enable_reminder3')->default(false);
$table->smallInteger('num_days_reminder1')->default(7);
$table->smallInteger('num_days_reminder2')->default(14);
$table->smallInteger('num_days_reminder3')->default(30);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('accounts', function ($table) {
$table->dropColumn('email_subject_invoice');
$table->dropColumn('email_subject_quote');
$table->dropColumn('email_subject_payment');
$table->dropColumn('email_subject_reminder1');
$table->dropColumn('email_subject_reminder2');
$table->dropColumn('email_subject_reminder3');
$table->dropColumn('email_template_reminder1');
$table->dropColumn('email_template_reminder2');
$table->dropColumn('email_template_reminder3');
$table->dropColumn('enable_reminder1');
$table->dropColumn('enable_reminder2');
$table->dropColumn('enable_reminder3');
$table->dropColumn('num_days_reminder1');
$table->dropColumn('num_days_reminder2');
$table->dropColumn('num_days_reminder3');
});
}
}

View File

@ -14,13 +14,8 @@ class DatabaseSeeder extends Seeder {
Eloquent::unguard();
$this->call('ConstantsSeeder');
$this->command->info('Seeded the constants');
$this->call('CountriesSeeder');
$this->command->info('Seeded the countries');
$this->call('PaymentLibrariesSeeder');
$this->command->info('Seeded the Payment Libraries');
}
}

View File

@ -31707,7 +31707,7 @@ NINJA.decodeJavascript = function(invoice, javascript)
var match = matches[i];
field = match.substring(2, match.indexOf('Value'));
field = toSnakeCase(field);
var value = getDescendantProp(invoice, field) || ' ';
var value = getDescendantProp(invoice, field) || ' ';
value = doubleDollarSign(value);
if (field.toLowerCase().indexOf('date') >= 0 && value != ' ') {
@ -31827,7 +31827,7 @@ NINJA.invoiceLines = function(invoice) {
row.push({style:["quantity", rowStyle], text:qty || ' '});
}
if (showItemTaxes) {
row.push({style:["tax", rowStyle], text:tax ? tax.toString() + '%' : ' '});
row.push({style:["tax", rowStyle], text:tax ? (tax.toString() + '%') : ' '});
}
row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '});

View File

@ -159,7 +159,7 @@ NINJA.decodeJavascript = function(invoice, javascript)
var match = matches[i];
field = match.substring(2, match.indexOf('Value'));
field = toSnakeCase(field);
var value = getDescendantProp(invoice, field) || ' ';
var value = getDescendantProp(invoice, field) || ' ';
value = doubleDollarSign(value);
if (field.toLowerCase().indexOf('date') >= 0 && value != ' ') {
@ -279,7 +279,7 @@ NINJA.invoiceLines = function(invoice) {
row.push({style:["quantity", rowStyle], text:qty || ' '});
}
if (showItemTaxes) {
row.push({style:["tax", rowStyle], text:(tax ? tax.toString() + '%') : ' '});
row.push({style:["tax", rowStyle], text:tax ? (tax.toString() + '%') : ' '});
}
row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '});

View File

@ -776,6 +776,18 @@
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -775,6 +775,18 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -389,7 +389,7 @@ return array(
'deleted_quotes' => 'Successfully deleted :count quotes',
'converted_to_invoice' => 'Successfully converted quote to invoice',
'quote_subject' => 'New quote from :account',
'quote_subject' => 'New quote $quote from :account',
'quote_message' => 'To view your quote for :amount, click the link below.',
'quote_link_message' => 'To view your client quote click the link below:',
'notification_quote_sent_subject' => 'Quote :invoice was sent to :client',
@ -424,7 +424,7 @@ return array(
'confirm_email_invoice' => 'Are you sure you want to email this invoice?',
'confirm_email_quote' => 'Are you sure you want to email this quote?',
'confirm_recurring_email_invoice' => 'Recurring is enabled, are you sure you want this invoice emailed?',
'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?',
'cancel_account' => 'Cancel Account',
'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.',
@ -775,6 +775,18 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -753,6 +753,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',

View File

@ -775,6 +775,18 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -767,6 +767,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',

View File

@ -768,6 +768,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',

View File

@ -416,7 +416,7 @@ return array(
'confirm_email_invoice' => 'Are you sure you want to email this invoice?',
'confirm_email_quote' => 'Are you sure you want to email this quote?',
'confirm_recurring_email_invoice' => 'Recurring is enabled, are you sure you want this invoice emailed?',
'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?',
'cancel_account' => 'Cancel Account',
'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.',
@ -770,6 +770,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -424,7 +424,7 @@ return array(
'confirm_email_invoice' => 'Are you sure you want to email this invoice?',
'confirm_email_quote' => 'Are you sure you want to email this quote?',
'confirm_recurring_email_invoice' => 'Recurring is enabled, are you sure you want this invoice emailed?',
'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?',
'cancel_account' => 'Cancel Account',
'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.',
@ -777,6 +777,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',

View File

@ -424,7 +424,7 @@ return array(
'confirm_email_invoice' => 'Are you sure you want to email this invoice?',
'confirm_email_quote' => 'Are you sure you want to email this quote?',
'confirm_recurring_email_invoice' => 'Recurring is enabled, are you sure you want this invoice emailed?',
'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?',
'cancel_account' => 'Cancel Account',
'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.',
@ -775,6 +775,18 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -770,6 +770,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -417,7 +417,7 @@ return array(
'confirm_email_invoice' => 'Are you sure you want to email this invoice?',
'confirm_email_quote' => 'Are you sure you want to email this quote?',
'confirm_recurring_email_invoice' => 'Recurring is enabled, are you sure you want this invoice emailed?',
'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?',
'cancel_account' => 'Cancel Account',
'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.',
@ -770,6 +770,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
);

View File

@ -773,6 +773,17 @@ return array(
'military_time' => '24 Hour Time',
'last_sent' => 'Last Sent',
'reminder_emails' => 'Reminder Emails',
'templates_and_reminders' => 'Templates & Reminders',
'subject' => 'Subject',
'body' => 'Body',
'first_reminder' => 'First Reminder',
'second_reminder' => 'Second Reminder',
'third_reminder' => 'Third Reminder',
'num_days_reminder' => 'Days after due date',
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',

View File

@ -21,49 +21,107 @@
{!! Former::populateField('email_template_payment', $paymentEmail) !!}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.invoice_email') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{!! Former::textarea('email_template_invoice')->raw() !!}
</div>
<div class="col-md-6" id="invoice_preview"></div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.quote_email') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{!! Former::textarea('email_template_quote')->raw() !!}
</div>
<div class="col-md-6" id="quote_preview"></div>
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.email_templates') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div role="tabpanel">
<ul class="nav nav-tabs" role="tablist" style="border: none">
<li role="presentation" class="active"><a href="#invoice" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.invoice_email') }}</a></li>
<li role="presentation"><a href="#quote" aria-controls="terms" role="tab" data-toggle="tab">{{ trans('texts.quote_email') }}</a></li>
<li role="presentation"><a href="#payment" aria-controls="footer" role="tab" data-toggle="tab">{{ trans('texts.payment_email') }}</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="invoice">
<div class="panel-body">
<div class="col-md-6">
{!! Former::textarea('email_template_invoice')->raw() !!}
</div>
<div class="col-md-6" id="invoice_preview"></div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="quote">
<div class="panel-body">
<div class="col-md-6">
{!! Former::textarea('email_template_quote')->raw() !!}
</div>
<div class="col-md-6" id="quote_preview"></div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="payment">
<div class="panel-body">
<div class="col-md-6">
{!! Former::textarea('email_template_payment')->raw() !!}
</div>
<div class="col-md-6" id="payment_preview"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.payment_email') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{!! Former::textarea('email_template_payment')->raw() !!}
</div>
<div class="col-md-6" id="payment_preview"></div>
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.reminder_emails') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div role="tabpanel">
<ul class="nav nav-tabs" role="tablist" style="border: none">
<li role="presentation" class="active"><a href="#invoice" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.invoice_email') }}</a></li>
<li role="presentation"><a href="#quote" aria-controls="terms" role="tab" data-toggle="tab">{{ trans('texts.quote_email') }}</a></li>
<li role="presentation"><a href="#payment" aria-controls="footer" role="tab" data-toggle="tab">{{ trans('texts.payment_email') }}</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="invoice">
<div class="panel-body">
<div class="col-md-6">
{!! Former::textarea('email_template_invoice')->raw() !!}
</div>
<div class="col-md-6" id="invoice_preview"></div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="quote">
<div class="panel-body">
<div class="col-md-6">
{!! Former::textarea('email_template_quote')->raw() !!}
</div>
<div class="col-md-6" id="quote_preview"></div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="payment">
<div class="panel-body">
<div class="col-md-6">
{!! Former::textarea('email_template_payment')->raw() !!}
</div>
<div class="col-md-6" id="payment_preview"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@if (Auth::user()->isPro())
<center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}

View File

@ -1,7 +1,7 @@
<ul class="nav nav-tabs nav nav-justified">
{!! HTML::nav_link('company/advanced_settings/invoice_design', 'invoice_design') !!}
{!! HTML::nav_link('company/advanced_settings/invoice_settings', 'invoice_settings') !!}
{!! HTML::nav_link('company/advanced_settings/email_templates', 'email_templates') !!}
{!! HTML::nav_link('company/advanced_settings/templates_and_reminders', 'templates_and_reminders') !!}
{!! HTML::nav_link('company/advanced_settings/charts_and_reports', 'charts_and_reports') !!}
{!! HTML::nav_link('company/advanced_settings/user_management', 'users_and_tokens') !!}
</ul>

View File

@ -0,0 +1,32 @@
<div role="tabpanel" class="tab-pane {{ isset($active) && $active ? 'active' : '' }}" id="{{ $field }}">
<div class="panel-body">
@if (isset($isReminder) && $isReminder)
<div class="row">
<div class="col-md-6">
{!! Former::checkbox('enable_' . $field)->text(trans('texts.enable'))->label('') !!}
{!! Former::input('num_days_' . $field)->label(trans('texts.num_days_reminder')) !!}
</div>
</div>
@endif
<div class="row">
<div class="col-md-6">
{!! Former::text('email_subject_' . $field)->label(trans('texts.subject')) !!}
<div class="pull-right"><a href="#" onclick="return resetText('{{ 'subject' }}', '{{ $field }}')">{{ trans("texts.reset") }}</a></div>
</div>
<div class="col-md-6">
<p>&nbsp;<p/>
<div id="{{ $field }}_subject_preview"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{!! Former::textarea('email_template_' . $field)->label(trans('texts.body')) !!}
<div class="pull-right"><a href="#" onclick="return resetText('{{ 'template' }}', '{{ $field }}')">{{ trans("texts.reset") }}</a></div>
</div>
<div class="col-md-6">
<p>&nbsp;<p/>
<div id="{{ $field }}_template_preview"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,178 @@
@extends('accounts.nav')
@section('head')
@parent
<style type="text/css">
textarea {
min-height: 150px !important;
}
</style>
@stop
@section('content')
@parent
@include('accounts.nav_advanced')
{!! Former::vertical_open()->addClass('col-md-10 col-md-offset-1 warn-on-exit') !!}
{!! Former::populate($account) !!}
@foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type)
@foreach (['subject', 'template'] as $field)
{!! Former::populateField("email_{$field}_{$type}", $templates[$type][$field]) !!}
@endforeach
@endforeach
{!! Former::populateField("enable_reminder1", intval($account->enable_reminder1)) !!}
{!! Former::populateField("enable_reminder2", intval($account->enable_reminder2)) !!}
{!! Former::populateField("enable_reminder3", intval($account->enable_reminder3)) !!}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.email_templates') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div role="tabpanel">
<ul class="nav nav-tabs" role="tablist" style="border: none">
<li role="presentation" class="active"><a href="#invoice" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.invoice_email') }}</a></li>
<li role="presentation"><a href="#quote" aria-controls="terms" role="tab" data-toggle="tab">{{ trans('texts.quote_email') }}</a></li>
<li role="presentation"><a href="#payment" aria-controls="footer" role="tab" data-toggle="tab">{{ trans('texts.payment_email') }}</a></li>
</ul>
<div class="tab-content">
@include('accounts.template', ['field' => 'invoice', 'active' => true])
@include('accounts.template', ['field' => 'quote'])
@include('accounts.template', ['field' => 'payment'])
</div>
</div>
</div>
</div>
</div>
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.reminder_emails') !!}</h3>
</div>
<div class="panel-body">
<div class="row">
<div role="tabpanel">
<ul class="nav nav-tabs" role="tablist" style="border: none">
<li role="presentation" class="active"><a href="#reminder1" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.first_reminder') }}</a></li>
<li role="presentation"><a href="#reminder2" aria-controls="terms" role="tab" data-toggle="tab">{{ trans('texts.second_reminder') }}</a></li>
<li role="presentation"><a href="#reminder3" aria-controls="footer" role="tab" data-toggle="tab">{{ trans('texts.third_reminder') }}</a></li>
</ul>
<div class="tab-content">
@include('accounts.template', ['field' => 'reminder1', 'isReminder' => true, 'active' => true])
@include('accounts.template', ['field' => 'reminder2', 'isReminder' => true])
@include('accounts.template', ['field' => 'reminder3', 'isReminder' => true])
</div>
</div>
</div>
</div>
</div>
@if (Auth::user()->isPro())
<center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center>
@else
<script>
$(function() {
$('form.warn-on-exit input').prop('disabled', true);
});
</script>
@endif
{!! Former::close() !!}
<script type="text/javascript">
var entityTypes = ['invoice', 'quote', 'payment', 'reminder1', 'reminder2', 'reminder3'];
var stringTypes = ['subject', 'template'];
var templates = {!! json_encode($templates) !!};
function refreshPreview() {
for (var i=0; i<entityTypes.length; i++) {
var entityType = entityTypes[i];
for (var j=0; j<stringTypes.length; j++) {
var stringType = stringTypes[j];
var idName = '#email_' + stringType + '_' + entityType;
var value = $(idName).val();
var previewName = '#' + entityType + '_' + stringType + '_preview';
$(previewName).html(processVariables(value));
}
}
}
$(function() {
for (var i=0; i<entityTypes.length; i++) {
var entityType = entityTypes[i];
for (var j=0; j<stringTypes.length; j++) {
var stringType = stringTypes[j];
var idName = '#email_' + stringType + '_' + entityType;
$(idName).keyup(refreshPreview);
}
}
refreshPreview();
});
function processVariables(str) {
if (!str) {
return '';
}
keys = [
'footer',
'account',
'client',
'amount',
'link',
'contact',
'invoice',
'quote'
];
vals = [
{!! json_encode($emailFooter) !!},
"{{ Auth::user()->account->getDisplayName() }}",
"Client Name",
formatMoney(100),
"{{ Auth::user()->account->getSiteUrl() . '...' }}",
"Contact Name",
"0001",
"0001"
];
// Add any available payment method links
@foreach (\App\Models\Gateway::getPaymentTypeLinks() as $type)
{!! "keys.push('" . $type.'_link' . "');" !!}
{!! "vals.push('" . URL::to("/payment/xxxxxx/{$type}") . "');" !!}
@endforeach
for (var i=0; i<keys.length; i++) {
var regExp = new RegExp('\\$'+keys[i], 'g');
str = str.replace(regExp, vals[i]);
}
return str;
}
function resetText(section, field) {
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
var fieldName = 'email_' + section + '_' + field;
var value = templates[field][section];
$('#' + fieldName).val(value);
refreshPreview();
}
return false;
}
</script>
@stop

View File

@ -250,7 +250,7 @@
window.setTimeout(function() {
$(".alert-hide").fadeOut(500);
}, 2000);
}, 3000);
$('#search').blur(function(){
$('#search').css('width', '{{ Utils::isEnglish() ? 150 : 110 }}px');

View File

@ -106,7 +106,7 @@
<div class="col-lg-8 col-sm-8">
<div style="padding-top:10px">
<a href="#" data-bind="click: $root.clickLastSentDate">{{ Utils::dateToString($invoice->last_sent_date) }}</a> -
{!! link_to('/invoices/'.$lastSent->public_id, trans('texts.view_invoice'), ['id' => 'lastInvoiceSent', 'target' => '_blank']) !!}
{!! link_to('/invoices/'.$lastSent->public_id, trans('texts.view_invoice'), ['id' => 'lastInvoiceSent']) !!}
</div>
</div>
</div>
@ -783,14 +783,18 @@
}
function resetTerms() {
model.invoice().terms(model.invoice().default_terms());
refreshPDF();
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
model.invoice().terms(model.invoice().default_terms());
refreshPDF();
}
return false;
}
function resetFooter() {
model.invoice().invoice_footer(model.invoice().default_footer());
refreshPDF();
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
model.invoice().invoice_footer(model.invoice().default_footer());
refreshPDF();
}
return false;
}
@ -806,7 +810,7 @@
function onEmailClick() {
if (!NINJA.isRegistered) {
alert("{{ trans('texts.registration_required') }}");
alert("{!! trans('texts.registration_required') !!}");
return;
}
@ -817,11 +821,6 @@
function onSaveClick() {
if (model.invoice().is_recurring()) {
if (!NINJA.isRegistered) {
alert("{{ trans('texts.registration_required') }}");
return;
}
if (confirm("{!! trans("texts.confirm_recurring_email_$entityType") !!}" + '\n\n' + getSendToEmails() + '\n' + "{!! trans("texts.confirm_recurring_timing") !!}")) {
submitAction('');
}
@ -851,9 +850,9 @@
doc = generatePDF(invoice, design, true);
doc.getDataUrl( function(pdfString){
$('#pdfupload').val(pdfString);
submitAction(action);
});
$('#pdfupload').val(pdfString);
submitAction(action);
});
}
function submitAction(value) {

View File

@ -88,7 +88,7 @@
var needsRefresh = false;
function refreshPDF(force) {
getPDFString(refreshPDFCB, force);
return getPDFString(refreshPDFCB, force);
}
function refreshPDFCB(string) {

View File

@ -50,11 +50,19 @@
invoice.contact = {!! $contact->toJson() !!};
function getPDFString(cb) {
generatePDF(invoice, invoice.invoice_design.javascript, true, cb);
return generatePDF(invoice, invoice.invoice_design.javascript, true, cb);
}
$(function() {
refreshPDF();
@if (Input::has('phantomjs'))
doc = getPDFString();
doc.getDataUrl(function(pdfString) {
document.write(pdfString);
document.close();
});
@else
refreshPDF();
@endif
});
function onDownloadClick() {
@ -63,7 +71,6 @@
doc.save(fileName + '-' + invoice.invoice_number + '.pdf');
}
</script>
@include('invoices.pdf', ['account' => $invoice->client->account])

View File

@ -133,13 +133,12 @@
})
window.onDatatableReady = function() {
$(':checkbox').unbind('click').click(function() {
$(':checkbox').click(function() {
setBulkActionsEnabled();
});
$('tbody tr').unbind('click').click(function(event) {
$('tbody tr').unbind('click').click(function(event) {
if (event.target.type !== 'checkbox' && event.target.type !== 'button' && event.target.tagName.toLowerCase() !== 'a') {
console.log('click');
$checkbox = $(this).closest('tr').find(':checkbox:not(:disabled)');
var checked = $checkbox.prop('checked');
$checkbox.prop('checked', !checked);

View File

@ -182,7 +182,7 @@
}
@if ($task && !$task->is_running)
if (!timeLog.isStartValid() || !timeLog.isEndValid()) {
alert("{{ trans('texts.task_errors') }}");
alert("{!! trans('texts.task_errors') !!}");
showTimeDetails();
return;
}

View File

@ -70,7 +70,7 @@ $I->see('Invoice Fields');
$I->amOnPage('/company/advanced_settings/invoice_design');
$I->see('Invoice Design');
$I->amOnPage('/company/advanced_settings/email_templates');
$I->amOnPage('/company/advanced_settings/templates_and_reminders');
$I->see('Invoice Email');
$I->amOnPage('/company/advanced_settings/charts_and_reports');

View File

@ -26,7 +26,7 @@ class OnlinePaymentCest
$I->wantTo('create a gateway');
$I->amOnPage('/company/payments');
if (strpos($I->grabFromCurrentUrl(), 'create') > 0) {
if (strpos($I->grabFromCurrentUrl(), 'create') !== false) {
$I->fillField(['name' =>'23_apiKey'], Fixtures::get('gateway_key'));
$I->selectOption('#token_billing_type_id', 4);
$I->click('Save');
@ -86,6 +86,7 @@ class OnlinePaymentCest
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
$I->checkOption('#auto_bill');
$I->executeJS('preparePdfData(\'email\')');
$I->wait(2);
$I->see("$0.00");
}

View File

@ -170,16 +170,16 @@ class SettingsCest
public function updateEmailTemplates(FunctionalTester $I)
{
$I->wantTo('update email templates');
$I->amOnPage('/company/advanced_settings/email_templates');
$I->amOnPage('/company/advanced_settings/templates_and_reminders');
$string = $this->faker->text(100);
$I->fillField(['name' => 'email_template_payment'], $string);
$I->fillField(['name' => 'email_template_invoice'], $string);
$I->click('Save');
$I->seeResponseCodeIs(200);
$I->see('Successfully updated settings');
$I->seeRecord('accounts', array('email_template_payment' => $string));
$I->seeRecord('accounts', array('email_template_invoice' => $string));
}
public function runReport(FunctionalTester $I)