diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 64c8dd00db..a5c5fe6a53 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -46,29 +46,78 @@ class CheckData extends Command { public function fire() { $this->info(date('Y-m-d') . ' Running CheckData...'); - $today = new DateTime(); if (!$this->option('client_id')) { - // update client paid_to_date value - $clients = DB::table('clients') - ->join('payments', 'payments.client_id', '=', 'clients.id') - ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') - ->where('payments.is_deleted', '=', 0) - ->where('invoices.is_deleted', '=', 0) - ->groupBy('clients.id') - ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') - ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); - $this->info(count($clients) . ' clients with incorrect paid to date'); + $this->checkPaidToDate(); + } + + $this->checkBalances(); + + $this->checkActivityAccount(); + + $this->info('Done'); + } + + private function checkActivityAccount() + { + $entityTypes = [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_CONTACT, + ENTITY_PAYMENT, + ENTITY_INVITATION, + ]; + + foreach ($entityTypes as $entityType) { + $activities = DB::table('activities') + ->join("{$entityType}s", "{$entityType}s.id", '=', "activities.{$entityType}_id"); + + if ($entityType != ENTITY_CLIENT) { + $activities = $activities->join('clients', 'clients.id', '=', 'activities.client_id'); + } + $activities = $activities->where('activities.account_id', '!=', DB::raw("{$entityType}s.account_id")) + ->get(['activities.id', "clients.account_id", "clients.user_id"]); + + $this->info(count($activities) . " {$entityType} activity with incorrect account id"); + if ($this->option('fix') == 'true') { - foreach ($clients as $client) { - DB::table('clients') - ->where('id', $client->id) - ->update(['paid_to_date' => $client->amount]); + foreach ($activities as $activity) { + DB::table('activities') + ->where('id', $activity->id) + ->update([ + 'account_id' => $activity->account_id, + 'user_id' => $activity->user_id, + ]); } } } + } + private function checkPaidToDate() + { + // update client paid_to_date value + $clients = DB::table('clients') + ->join('payments', 'payments.client_id', '=', 'clients.id') + ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->where('payments.is_deleted', '=', 0) + ->where('invoices.is_deleted', '=', 0) + ->groupBy('clients.id') + ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') + ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); + $this->info(count($clients) . ' clients with incorrect paid to date'); + + if ($this->option('fix') == 'true') { + foreach ($clients as $client) { + DB::table('clients') + ->where('id', $client->id) + ->update(['paid_to_date' => $client->amount]); + } + } + } + + private function checkBalances() + { // find all clients where the balance doesn't equal the sum of the outstanding invoices $clients = DB::table('clients') ->join('invoices', 'invoices.client_id', '=', 'clients.id') @@ -249,8 +298,6 @@ class CheckData extends Command { ->update($data); } } - - $this->info('Done'); } protected function getArguments() diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index d7722136d2..1146fe4fc9 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -432,6 +432,11 @@ class AccountController extends BaseController { if (Auth::user()->account->isPro()) { $account = Auth::user()->account; + $account->email_design_id = Input::get('email_design_id'); + + if (Utils::isNinja()) { + $account->enable_email_markup = Input::get('enable_email_markup') ? true : false; + } foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { $subjectField = "email_subject_{$type}"; diff --git a/app/Http/routes.php b/app/Http/routes.php index eed6098281..738ef1e4aa 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -253,6 +253,7 @@ if (!defined('CONTACT_EMAIL')) { define('ENTITY_CONTACT', 'contact'); define('ENTITY_INVOICE', 'invoice'); define('ENTITY_INVOICE_ITEMS', 'invoice_items'); + define('ENTITY_INVITATION', 'invitation'); define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); define('ENTITY_PAYMENT', 'payment'); define('ENTITY_CREDIT', 'credit'); @@ -446,6 +447,7 @@ if (!defined('CONTACT_EMAIL')) { define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/api/browser/v2/'); define('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php'); define('REFERRAL_PROGRAM_URL', 'https://www.invoiceninja.com/referral-program/'); + define('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup/overview'); define('COUNT_FREE_DESIGNS', 4); define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design @@ -498,6 +500,9 @@ if (!defined('CONTACT_EMAIL')) { define('API_SERIALIZER_ARRAY', 'array'); define('API_SERIALIZER_JSON', 'json'); + define('EMAIL_DESIGN_PLAIN', 1); + define('FLAT_BUTTON_CSS', 'border:0 none;border-radius:6px;padding:12px 40px;margin:0 6px;cursor:hand;display:inline-block;font-size:14px;color:#fff;text-transform:none'); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -522,7 +527,6 @@ if (!defined('CONTACT_EMAIL')) { 'invoiceStatus' => 'App\Models\InvoiceStatus', 'frequencies' => 'App\Models\Frequency', 'gateways' => 'App\Models\Gateway', - 'themes' => 'App\Models\Theme', ]; define('CACHED_TABLES', serialize($cachedTables)); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 4af034825d..4c061b8b81 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -279,9 +279,19 @@ class Utils return json_decode(json_encode((array) $data), true); } - public static function toSpaceCase($camelStr) + public static function toSpaceCase($string) { - return preg_replace('/([a-z])([A-Z])/s', '$1 $2', $camelStr); + return preg_replace('/([a-z])([A-Z])/s', '$1 $2', $string); + } + + public static function toSnakeCase($string) + { + return preg_replace('/([a-z])([A-Z])/s', '$1_$2', $string); + } + + public static function toCamelCase($string) + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $string)))); } public static function timestampToDateTimeString($timestamp) diff --git a/app/Models/Account.php b/app/Models/Account.php index 0842f36434..162b7c6501 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -5,6 +5,7 @@ use Utils; use Session; use DateTime; use Event; +use Cache; use App; use File; use App\Events\UserSettingsChanged; @@ -204,10 +205,26 @@ class Account extends Eloquent return $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT; } + public function formatMoney($amount, $client = null) + { + $currency = ($client && $client->currency) ? $client->currency : $this->currency; + + if ( ! $currency) { + $currencies = Cache::get('currencies')->filter(function($item) { + return $item->id == DEFAULT_CURRENCY; + }); + $currency = $currencies->first(); + } + + return $currency->symbol . number_format($amount, $currency->precision, $currency->decimal_separator, $currency->thousand_separator); + } + public function formatDate($date) { - if (!$date) { + if ( ! $date) { return null; + } elseif ( ! $date instanceof \DateTime) { + $date = new \DateTime($date); } return $date->format($this->getCustomDateFormat()); @@ -677,9 +694,9 @@ class Account extends Eloquent $entityType = ENTITY_INVOICE; } - $template = "
" . trans('texts.email_signature') . "\n
\$account
" . trans('texts.email_signature') . "\n
\$account p>";
}
}
diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php
index 55fc2056f4..da556dfb0e 100644
--- a/app/Models/Gateway.php
+++ b/app/Models/Gateway.php
@@ -2,6 +2,7 @@
use Eloquent;
use Omnipay;
+use Utils;
class Gateway extends Eloquent
{
@@ -44,13 +45,20 @@ class Gateway extends Eloquent
return $this->id == $gatewayId;
}
+ public static function getPaymentTypeName($type)
+ {
+ return Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type)));
+ }
+
+ /*
public static function getPaymentTypeLinks() {
$data = [];
foreach (self::$paymentTypes as $type) {
- $data[] = strtolower(str_replace('PAYMENT_TYPE_', '', $type));
+ $data[] = Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type)));
}
return $data;
}
+ */
public function getHelp()
{
diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php
index 16d5043499..2cc7102957 100644
--- a/app/Models/Invitation.php
+++ b/app/Models/Invitation.php
@@ -29,7 +29,7 @@ class Invitation extends EntityModel
return $this->belongsTo('App\Models\Account');
}
- public function getLink()
+ public function getLink($type = 'view')
{
if (!$this->account) {
$this->load('account');
@@ -46,7 +46,7 @@ class Invitation extends EntityModel
}
}
- return "{$url}/view/{$this->invitation_key}";
+ return "{$url}/{$type}/{$this->invitation_key}";
}
public function getStatus()
diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php
index 67ba09a17d..3d9f473ce2 100644
--- a/app/Ninja/Mailers/ContactMailer.php
+++ b/app/Ninja/Mailers/ContactMailer.php
@@ -1,5 +1,6 @@
load('invitations', 'client.language', 'account');
@@ -97,6 +113,7 @@ class ContactMailer extends Mailer
'invoiceId' => $invoice->id,
'invitation' => $invitation,
'account' => $account,
+ 'client' => $client,
'invoice' => $invoice,
];
@@ -107,8 +124,14 @@ class ContactMailer extends Mailer
$subject = $this->processVariables($subject, $variables);
$fromEmail = $user->email;
+
+ if ($account->email_design_id == EMAIL_DESIGN_PLAIN) {
+ $view = ENTITY_INVOICE;
+ } else {
+ $view = 'design' . ($account->email_design_id - 1);
+ }
- $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, ENTITY_INVOICE, $data);
+ $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data);
if ($response === true) {
return true;
@@ -192,22 +215,33 @@ class ContactMailer extends Mailer
private function processVariables($template, $data)
{
+ $account = $data['account'];
+ $client = $data['client'];
+ $invitation = $data['invitation'];
+ $invoice = $invitation->invoice;
+
$variables = [
- '$footer' => $data['account']->getEmailFooter(),
- '$link' => $data['invitation']->getLink(),
- '$client' => $data['client']->getDisplayName(),
- '$account' => $data['account']->getDisplayName(),
- '$contact' => $data['invitation']->contact->getDisplayName(),
- '$firstName' => $data['invitation']->contact->first_name,
- '$amount' => Utils::formatMoney($data['amount'], $data['client']->getCurrencyId()),
- '$invoice' => $data['invitation']->invoice->invoice_number,
- '$quote' => $data['invitation']->invoice->invoice_number,
- '$advancedRawInvoice->' => '$'
+ '$footer' => $account->getEmailFooter(),
+ '$client' => $client->getDisplayName(),
+ '$account' => $account->getDisplayName(),
+ '$contact' => $invitation->contact->getDisplayName(),
+ '$firstName' => $invitation->contact->first_name,
+ '$amount' => Utils::formatMoney($data['amount'], $client->getCurrencyId()),
+ '$invoice' => $invoice->invoice_number,
+ '$quote' => $invoice->invoice_number,
+ '$link' => $invitation->getLink(),
+ '$viewLink' => $invitation->getLink(),
+ '$viewButton' => HTML::emailViewButton($invitation->getLink(), $invoice->getEntityType()),
+ '$paymentLink' => $invitation->getLink('payment'),
+ '$paymentButton' => HTML::emailPaymentButton($invitation->getLink('payment')),
];
// Add variables for available payment types
- foreach (Gateway::getPaymentTypeLinks() as $type) {
- $variables["\${$type}_link"] = URL::to("/payment/{$data['invitation']->invitation_key}/{$type}");
+ foreach (Gateway::$paymentTypes as $type) {
+ $camelType = Gateway::getPaymentTypeName($type);
+ $type = Utils::toSnakeCase($camelType);
+ $variables["\${$camelType}Link"] = $invitation->getLink() . "/{$type}";
+ $variables["\${$camelType}Button"] = HTML::emailPaymentButton($invitation->getLink('payment') . "/{$type}");
}
$str = str_replace(array_keys($variables), array_values($variables), $template);
diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php
index 34db634f88..d1aa10ec7c 100644
--- a/app/Ninja/Mailers/Mailer.php
+++ b/app/Ninja/Mailers/Mailer.php
@@ -67,7 +67,7 @@ class Mailer
private function handleFailure($exception)
{
- if (isset($_ENV['POSTMARK_API_TOKEN']) && $exception->getResponse()) {
+ if (isset($_ENV['POSTMARK_API_TOKEN']) && method_exists($exception, 'getResponse')) {
$response = $exception->getResponse()->getBody()->getContents();
$response = json_decode($response);
$emailError = nl2br($response->Message);
diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php
index dff59c241b..8a03ff4ac1 100644
--- a/app/Ninja/Presenters/InvoicePresenter.php
+++ b/app/Ninja/Presenters/InvoicePresenter.php
@@ -15,6 +15,17 @@ class InvoicePresenter extends Presenter {
return $this->entity->user->getDisplayName();
}
+ public function balanceDueLabel()
+ {
+ if ($this->entity->partial) {
+ return 'amount_due';
+ } elseif ($this->entity->is_quote) {
+ return 'total';
+ } else {
+ return 'balance_due';
+ }
+ }
+
public function balance_due()
{
$amount = $this->entity->getRequestedAmount();
diff --git a/app/Ninja/Repositories/ActivityRepository.php b/app/Ninja/Repositories/ActivityRepository.php
index 811b5fd5c5..8ac316c747 100644
--- a/app/Ninja/Repositories/ActivityRepository.php
+++ b/app/Ninja/Repositories/ActivityRepository.php
@@ -44,7 +44,7 @@ class ActivityRepository
{
$activity = new Activity();
- if (Auth::check()) {
+ if (Auth::check() && Auth::user()->account_id == $entity->account_id) {
$activity->user_id = Auth::user()->id;
$activity->account_id = Auth::user()->account_id;
} else {
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 8a7e9bfad2..bf67c12498 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -65,6 +65,29 @@ class AppServiceProvider extends ServiceProvider {
return 'data:image/jpeg;base64,' . base64_encode(file_get_contents(public_path().'/'.$imagePath));
});
+ HTML::macro('flatButton', function($label, $color) {
+ return '';
+ });
+
+ HTML::macro('emailViewButton', function($link = '#', $entityType = ENTITY_INVOICE) {
+ return view('partials.email_button')
+ ->with([
+ 'link' => $link,
+ 'field' => "view_{$entityType}",
+ 'color' => '#0b4d78',
+ ])
+ ->render();
+ });
+
+ HTML::macro('emailPaymentButton', function($link = '#') {
+ return view('partials.email_button')
+ ->with([
+ 'link' => $link,
+ 'field' => 'pay_now',
+ 'color' => '#36c157',
+ ])
+ ->render();
+ });
HTML::macro('breadcrumbs', function() {
$str = '