mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-08 12:12:48 +01:00
Merge branch 'release-4.3.0'
This commit is contained in:
commit
04eec340f2
@ -2,6 +2,7 @@
|
||||
RewriteEngine On
|
||||
RewriteRule "^.env" - [F,L]
|
||||
RewriteRule "^storage" - [F,L]
|
||||
RewriteRule ^(.well-known)($|/) - [L]
|
||||
|
||||
# https://coderwall.com/p/erbaig/laravel-s-htaccess-to-remove-public-from-url
|
||||
# RewriteRule ^(.*)$ public/$1 [L]
|
||||
|
14
.travis.yml
14
.travis.yml
@ -6,13 +6,9 @@ sudo: true
|
||||
group: deprecated-2017Q4
|
||||
|
||||
php:
|
||||
# - 5.5.9
|
||||
# - 5.6
|
||||
# - 5.6
|
||||
# - 7.0
|
||||
- 7.1
|
||||
# - 7.2
|
||||
# - hhvm
|
||||
- 7.2
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
@ -114,15 +110,17 @@ after_script:
|
||||
- mysql -u root -e 'select * from account_gateways;' ninja
|
||||
- mysql -u root -e 'select * from clients;' ninja
|
||||
- mysql -u root -e 'select * from contacts;' ninja
|
||||
- mysql -u root -e 'select * from invoices;' ninja
|
||||
- mysql -u root -e 'select id, account_id, invoice_number, amount, balance from invoices;' ninja
|
||||
- mysql -u root -e 'select * from invoice_items;' ninja
|
||||
- mysql -u root -e 'select * from invitations;' ninja
|
||||
- mysql -u root -e 'select * from payments;' ninja
|
||||
- mysql -u root -e 'select id, account_id, invoice_id, amount, transaction_reference from payments;' ninja
|
||||
- mysql -u root -e 'select * from credits;' ninja
|
||||
- mysql -u root -e 'select * from expenses;' ninja
|
||||
- mysql -u root -e 'select * from accounts;' ninja
|
||||
- mysql -u root -e 'select * from fonts;' ninja
|
||||
- mysql -u root -e 'select * from banks;' ninja
|
||||
- mysql -u root -e 'select * from account_gateway_tokens;' ninja
|
||||
- mysql -u root -e 'select * from payment_methods;' ninja
|
||||
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr)
|
||||
- for i in $FILES; do echo $i; base64 "$i"; break; done
|
||||
|
||||
@ -131,3 +129,5 @@ notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
||||
slack:
|
||||
invoiceninja: SLraaKBDvjeRuRtY9o3Yvp1b
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
[![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=master)](https://travis-ci.org/invoiceninja/invoiceninja)
|
||||
[![Docs](https://readthedocs.org/projects/invoice-ninja/badge/?version=latest)](http://docs.invoiceninja.com/en/latest/?badge=latest)
|
||||
[![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) | [iPhone](https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1220337560&mt=8) | [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.invoiceninja)
|
||||
|
||||
|
@ -5,10 +5,10 @@ namespace App\Console\Commands;
|
||||
use App\Models\Account;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\RecurringExpense;
|
||||
use App\Ninja\Mailers\ContactMailer as Mailer;
|
||||
use App\Ninja\Repositories\InvoiceRepository;
|
||||
use App\Ninja\Repositories\RecurringExpenseRepository;
|
||||
use App\Services\PaymentService;
|
||||
use App\Jobs\SendInvoiceEmail;
|
||||
use DateTime;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@ -31,11 +31,6 @@ class SendRecurringInvoices extends Command
|
||||
*/
|
||||
protected $description = 'Send recurring invoices';
|
||||
|
||||
/**
|
||||
* @var Mailer
|
||||
*/
|
||||
protected $mailer;
|
||||
|
||||
/**
|
||||
* @var InvoiceRepository
|
||||
*/
|
||||
@ -49,15 +44,13 @@ class SendRecurringInvoices extends Command
|
||||
/**
|
||||
* SendRecurringInvoices constructor.
|
||||
*
|
||||
* @param Mailer $mailer
|
||||
* @param InvoiceRepository $invoiceRepo
|
||||
* @param PaymentService $paymentService
|
||||
*/
|
||||
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, PaymentService $paymentService, RecurringExpenseRepository $recurringExpenseRepo)
|
||||
public function __construct(InvoiceRepository $invoiceRepo, PaymentService $paymentService, RecurringExpenseRepository $recurringExpenseRepo)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->mailer = $mailer;
|
||||
$this->invoiceRepo = $invoiceRepo;
|
||||
$this->paymentService = $paymentService;
|
||||
$this->recurringExpenseRepo = $recurringExpenseRepo;
|
||||
@ -115,9 +108,9 @@ class SendRecurringInvoices extends Command
|
||||
|
||||
try {
|
||||
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
|
||||
if ($invoice && ! $invoice->isPaid()) {
|
||||
if ($invoice && ! $invoice->isPaid() && $account->auto_email_invoice) {
|
||||
$this->info('Not billed - Sending Invoice');
|
||||
$this->mailer->sendInvoice($invoice);
|
||||
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id));
|
||||
} elseif ($invoice) {
|
||||
$this->info('Successfully billed invoice');
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ use App\Libraries\CurlUtils;
|
||||
use Carbon;
|
||||
use Str;
|
||||
use Cache;
|
||||
use Exception;
|
||||
use App\Jobs\SendInvoiceEmail;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Currency;
|
||||
use App\Ninja\Mailers\ContactMailer as Mailer;
|
||||
use App\Ninja\Mailers\UserMailer;
|
||||
use App\Ninja\Repositories\AccountRepository;
|
||||
use App\Ninja\Repositories\InvoiceRepository;
|
||||
@ -33,11 +34,6 @@ class SendReminders extends Command
|
||||
*/
|
||||
protected $description = 'Send reminder emails';
|
||||
|
||||
/**
|
||||
* @var Mailer
|
||||
*/
|
||||
protected $mailer;
|
||||
|
||||
/**
|
||||
* @var InvoiceRepository
|
||||
*/
|
||||
@ -55,11 +51,10 @@ class SendReminders extends Command
|
||||
* @param InvoiceRepository $invoiceRepo
|
||||
* @param accountRepository $accountRepo
|
||||
*/
|
||||
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, UserMailer $userMailer)
|
||||
public function __construct(InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, UserMailer $userMailer)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->mailer = $mailer;
|
||||
$this->invoiceRepo = $invoiceRepo;
|
||||
$this->accountRepo = $accountRepo;
|
||||
$this->userMailer = $userMailer;
|
||||
@ -136,7 +131,7 @@ class SendReminders extends Command
|
||||
continue;
|
||||
}
|
||||
$this->info('Send email: ' . $invoice->id);
|
||||
$this->mailer->sendInvoice($invoice, $reminder);
|
||||
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, $reminder));
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +144,7 @@ class SendReminders extends Command
|
||||
continue;
|
||||
}
|
||||
$this->info('Send email: ' . $invoice->id);
|
||||
$this->mailer->sendInvoice($invoice, 'reminder4');
|
||||
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, 'reminder4'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,6 +157,8 @@ class SendReminders extends Command
|
||||
$this->info($scheduledReports->count() . ' scheduled reports');
|
||||
|
||||
foreach ($scheduledReports as $scheduledReport) {
|
||||
$this->info('Processing report: ' . $scheduledReport->id);
|
||||
|
||||
$user = $scheduledReport->user;
|
||||
$account = $scheduledReport->account;
|
||||
|
||||
@ -179,7 +176,14 @@ class SendReminders extends Command
|
||||
$file = dispatch(new ExportReportResults($scheduledReport->user, $config['export_format'], $reportType, $report->exportParams));
|
||||
|
||||
if ($file) {
|
||||
try {
|
||||
$this->userMailer->sendScheduledReport($scheduledReport, $file);
|
||||
$this->info('Sent report');
|
||||
} catch (Exception $exception) {
|
||||
$this->info('ERROR: ' . $exception->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->info('ERROR: Failed to run report');
|
||||
}
|
||||
|
||||
$scheduledReport->updateSendDate();
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
Route::group(['middleware' => 'auth', 'namespace' => '$MODULE_NAMESPACE$\$STUDLY_NAME$\Http\Controllers'], function()
|
||||
Route::group(['middleware' => ['web', 'lookup:user', 'auth:user'], 'namespace' => '$MODULE_NAMESPACE$\$STUDLY_NAME$\Http\Controllers'], function()
|
||||
{
|
||||
Route::resource('$LOWER_NAME$', '$STUDLY_NAME$Controller');
|
||||
Route::post('$LOWER_NAME$/bulk', '$STUDLY_NAME$Controller@bulk');
|
||||
|
@ -301,6 +301,7 @@ if (! defined('APP_NAME')) {
|
||||
define('GATEWAY_BRAINTREE', 61);
|
||||
define('GATEWAY_CUSTOM', 62);
|
||||
define('GATEWAY_GOCARDLESS', 64);
|
||||
define('GATEWAY_PAYMILL', 66);
|
||||
|
||||
// The customer exists, but only as a local concept
|
||||
// The remote gateway doesn't understand the concept of customers
|
||||
@ -338,7 +339,8 @@ if (! defined('APP_NAME')) {
|
||||
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
|
||||
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
|
||||
define('NINJA_DATE', '2000-01-01');
|
||||
define('NINJA_VERSION', '4.2.2' . env('NINJA_VERSION_SUFFIX'));
|
||||
define('NINJA_VERSION', '4.3.0' . env('NINJA_VERSION_SUFFIX'));
|
||||
define('NINJA_TERMS_VERSION', '');
|
||||
|
||||
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
|
||||
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
|
||||
|
29
app/Events/ProjectWasDeleted.php
Normal file
29
app/Events/ProjectWasDeleted.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class ProjectWasDeleted.
|
||||
*/
|
||||
class ProjectWasDeleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Prooject
|
||||
*/
|
||||
public $project;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
*/
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
}
|
29
app/Events/ProposalWasDeleted.php
Normal file
29
app/Events/ProposalWasDeleted.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Proposal;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class ProposalWasDeleted.
|
||||
*/
|
||||
class ProposalWasDeleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Proposal
|
||||
*/
|
||||
public $proposal;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
*/
|
||||
public function __construct(Proposal $proposal)
|
||||
{
|
||||
$this->proposal = $proposal;
|
||||
}
|
||||
}
|
@ -68,7 +68,11 @@ class Handler extends ExceptionHandler
|
||||
}
|
||||
// Log 404s to a separate file
|
||||
$errorStr = date('Y-m-d h:i:s') . ' ' . $e->getMessage() . ' URL:' . request()->url() . "\n" . json_encode(Utils::prepareErrorData('PHP')) . "\n\n";
|
||||
if (config('app.log') == 'single') {
|
||||
@file_put_contents(storage_path('logs/not-found.log'), $errorStr, FILE_APPEND);
|
||||
} else {
|
||||
Utils::logError('[not found] ' . $errorStr);
|
||||
}
|
||||
return false;
|
||||
} elseif ($e instanceof HttpResponseException) {
|
||||
return false;
|
||||
@ -77,7 +81,11 @@ class Handler extends ExceptionHandler
|
||||
if (! Utils::isTravis()) {
|
||||
Utils::logError(Utils::getErrorString($e));
|
||||
$stacktrace = date('Y-m-d h:i:s') . ' ' . $e->getMessage() . ': ' . $e->getTraceAsString() . "\n\n";
|
||||
if (config('app.log') == 'single') {
|
||||
@file_put_contents(storage_path('logs/stacktrace.log'), $stacktrace, FILE_APPEND);
|
||||
} else {
|
||||
Utils::logError('[stacktrace] ' . $stacktrace);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return parent::report($e);
|
||||
|
@ -46,6 +46,7 @@ use Utils;
|
||||
|
||||
use Validator;
|
||||
use View;
|
||||
use App\Jobs\PurgeClientData;
|
||||
|
||||
/**
|
||||
* Class AccountController.
|
||||
@ -139,7 +140,12 @@ class AccountController extends BaseController
|
||||
Session::flash('warning', $message);
|
||||
}
|
||||
|
||||
$redirectTo = Input::get('redirect_to') ? SITE_URL . '/' . ltrim(Input::get('redirect_to'), '/') : 'dashboard';
|
||||
if ($redirectTo = Input::get('redirect_to')) {
|
||||
$redirectTo = SITE_URL . '/' . ltrim($redirectTo, '/');
|
||||
} else {
|
||||
$redirectTo = Input::get('sign_up') ? 'dashboard' : 'invoices/create';
|
||||
}
|
||||
|
||||
return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up'));
|
||||
}
|
||||
|
||||
@ -389,7 +395,9 @@ class AccountController extends BaseController
|
||||
$planDetails = $account->getPlanDetails(true, false);
|
||||
$portalLink = false;
|
||||
|
||||
if (Utils::isNinja() && $planDetails && $ninjaClient = $this->accountRepo->getNinjaClient($account)) {
|
||||
if (Utils::isNinja() && $planDetails
|
||||
&& $account->getPrimaryAccount()->id == auth()->user()->account_id
|
||||
&& $ninjaClient = $this->accountRepo->getNinjaClient($account)) {
|
||||
$contact = $ninjaClient->getPrimaryContact();
|
||||
$portalLink = $contact->link;
|
||||
}
|
||||
@ -473,7 +481,7 @@ class AccountController extends BaseController
|
||||
$trashedCount = AccountGateway::scope()->withTrashed()->count();
|
||||
|
||||
if ($accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) {
|
||||
if (! $accountGateway->getPublishableStripeKey()) {
|
||||
if (! $accountGateway->getPublishableKey()) {
|
||||
Session::now('warning', trans('texts.missing_publishable_key'));
|
||||
}
|
||||
}
|
||||
@ -976,6 +984,9 @@ class AccountController extends BaseController
|
||||
$account->invoice_footer = Input::get('invoice_footer');
|
||||
$account->quote_terms = Input::get('quote_terms');
|
||||
$account->auto_convert_quote = Input::get('auto_convert_quote');
|
||||
$account->auto_archive_quote = Input::get('auto_archive_quote');
|
||||
$account->auto_archive_invoice = Input::get('auto_archive_invoice');
|
||||
$account->auto_email_invoice = Input::get('auto_email_invoice');
|
||||
$account->recurring_invoice_number_prefix = Input::get('recurring_invoice_number_prefix');
|
||||
|
||||
$account->client_number_prefix = trim(Input::get('client_number_prefix'));
|
||||
@ -1067,6 +1078,7 @@ class AccountController extends BaseController
|
||||
$user->notify_viewed = Input::get('notify_viewed');
|
||||
$user->notify_paid = Input::get('notify_paid');
|
||||
$user->notify_approved = Input::get('notify_approved');
|
||||
$user->slack_webhook_url = Input::get('slack_webhook_url');
|
||||
$user->save();
|
||||
|
||||
$account = $user->account;
|
||||
@ -1265,6 +1277,7 @@ class AccountController extends BaseController
|
||||
$account->token_billing_type_id = Input::get('token_billing_type_id');
|
||||
$account->auto_bill_on_due_date = boolval(Input::get('auto_bill_on_due_date'));
|
||||
$account->gateway_fee_enabled = boolval(Input::get('gateway_fee_enabled'));
|
||||
$account->send_item_details = boolval(Input::get('send_item_details'));
|
||||
|
||||
$account->save();
|
||||
|
||||
@ -1326,6 +1339,7 @@ class AccountController extends BaseController
|
||||
public function submitSignup()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$ip = Request::getClientIp();
|
||||
$account = $user->account;
|
||||
|
||||
$rules = [
|
||||
@ -1357,6 +1371,7 @@ class AccountController extends BaseController
|
||||
if ($user->registered) {
|
||||
$newAccount = $this->accountRepo->create($firstName, $lastName, $email, $password, $account->company);
|
||||
$newUser = $newAccount->users()->first();
|
||||
$newUser->acceptLatestTerms($ip)->save();
|
||||
$users = $this->accountRepo->associateAccounts($user->id, $newUser->id);
|
||||
|
||||
Session::flash('message', trans('texts.created_new_company'));
|
||||
@ -1371,12 +1386,13 @@ class AccountController extends BaseController
|
||||
$user->username = $user->email;
|
||||
$user->password = bcrypt($password);
|
||||
$user->registered = true;
|
||||
$user->acceptLatestTerms($ip);
|
||||
$user->save();
|
||||
|
||||
$user->account->startTrial(PLAN_PRO);
|
||||
|
||||
if (Input::get('go_pro') == 'true') {
|
||||
Session::set(REQUESTED_PRO_PLAN, true);
|
||||
session([REQUESTED_PRO_PLAN => true]);
|
||||
}
|
||||
|
||||
return "{$user->first_name} {$user->last_name}";
|
||||
@ -1452,7 +1468,7 @@ class AccountController extends BaseController
|
||||
$refunded = $company->processRefund(Auth::user());
|
||||
|
||||
$ninjaClient = $this->accountRepo->getNinjaClient($account);
|
||||
$ninjaClient->delete();
|
||||
dispatch(new \App\Jobs\PurgeClientData($ninjaClient));
|
||||
}
|
||||
|
||||
Document::scope()->each(function ($item, $key) {
|
||||
|
@ -183,6 +183,8 @@ class AccountGatewayController extends BaseController
|
||||
|
||||
if ($gatewayId == GATEWAY_DWOLLA) {
|
||||
$optional = array_merge($optional, ['key', 'secret']);
|
||||
} elseif ($gatewayId == GATEWAY_PAYMILL) {
|
||||
$rules['publishable_key'] = 'required';
|
||||
} elseif ($gatewayId == GATEWAY_STRIPE) {
|
||||
if (Utils::isNinjaDev()) {
|
||||
// do nothing - we're unable to acceptance test with StripeJS
|
||||
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Events\UserSettingsChanged;
|
||||
use App\Models\Account;
|
||||
use App\Models\Industry;
|
||||
use App\Models\Invoice;
|
||||
use App\Ninja\Mailers\Mailer;
|
||||
use App\Ninja\Repositories\AccountRepository;
|
||||
use App\Services\EmailService;
|
||||
@ -425,4 +426,42 @@ class AppController extends BaseController
|
||||
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
public function testHeadless()
|
||||
{
|
||||
$invoice = Invoice::scope()->orderBy('id')->first();
|
||||
|
||||
if (! $invoice) {
|
||||
dd('Please create an invoice to run this test');
|
||||
}
|
||||
|
||||
header('Content-type:application/pdf');
|
||||
echo $invoice->getPDFString();
|
||||
exit;
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
{
|
||||
if (Utils::isNinjaProd()) {
|
||||
abort(400, 'Not allowed');
|
||||
}
|
||||
|
||||
$command = request()->command;
|
||||
$options = request()->options ?: [];
|
||||
$secret = env('COMMAND_SECRET');
|
||||
|
||||
if (! $secret) {
|
||||
exit('Set a value for COMMAND_SECRET in the .env file');
|
||||
} elseif (! hash_equals($secret, request()->secret ?: '')) {
|
||||
exit('Invalid secret');
|
||||
}
|
||||
|
||||
if (! $command || ! in_array($command, ['send-invoices', 'send-reminders', 'update-key'])) {
|
||||
exit('Invalid command: Valid options are send-invoices, send-reminders or update-key');
|
||||
}
|
||||
|
||||
Artisan::call('ninja:' . $command, $options);
|
||||
|
||||
return response(nl2br(Artisan::output()));
|
||||
}
|
||||
}
|
||||
|
@ -105,8 +105,11 @@ class LoginController extends Controller
|
||||
*/
|
||||
} else {
|
||||
$stacktrace = sprintf("%s %s %s %s\n", date('Y-m-d h:i:s'), $request->input('email'), \Request::getClientIp(), array_get($_SERVER, 'HTTP_USER_AGENT'));
|
||||
if (config('app.log') == 'single') {
|
||||
file_put_contents(storage_path('logs/failed-logins.log'), $stacktrace, FILE_APPEND);
|
||||
error_log('login failed');
|
||||
} else {
|
||||
Utils::logError('[failed login] ' . $stacktrace);
|
||||
}
|
||||
if ($user) {
|
||||
$user->failed_logins = $user->failed_logins + 1;
|
||||
$user->save();
|
||||
|
@ -50,4 +50,21 @@ class BaseController extends Controller
|
||||
return redirect("{$entityTypes}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function downloadResponse($filename, $contents, $type = 'application/pdf')
|
||||
{
|
||||
header('Content-Type: ' . $type);
|
||||
header('Content-Length: ' . strlen($contents));
|
||||
|
||||
if (! request()->debug) {
|
||||
header('Content-disposition: attachment; filename="' . $filename . '"');
|
||||
}
|
||||
|
||||
header('Cache-Control: public, must-revalidate, max-age=0');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
|
||||
echo $contents;
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,13 @@ namespace App\Http\Controllers;
|
||||
use App\Http\Requests\ClientRequest;
|
||||
use App\Http\Requests\CreateClientRequest;
|
||||
use App\Http\Requests\UpdateClientRequest;
|
||||
use App\Jobs\LoadPostmarkHistory;
|
||||
use App\Jobs\ReactivatePostmarkEmail;
|
||||
use App\Models\Account;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Task;
|
||||
use App\Ninja\Datatables\ClientDatatable;
|
||||
use App\Ninja\Repositories\ClientRepository;
|
||||
@ -84,6 +87,7 @@ class ClientController extends BaseController
|
||||
{
|
||||
$client = $request->entity();
|
||||
$user = Auth::user();
|
||||
$account = $user->account;
|
||||
|
||||
$actionLinks = [];
|
||||
if ($user->can('create', ENTITY_INVOICE)) {
|
||||
@ -118,14 +122,16 @@ class ClientController extends BaseController
|
||||
$token = $client->getGatewayToken();
|
||||
|
||||
$data = [
|
||||
'account' => $account,
|
||||
'actionLinks' => $actionLinks,
|
||||
'showBreadcrumbs' => false,
|
||||
'client' => $client,
|
||||
'credit' => $client->getTotalCredit(),
|
||||
'title' => trans('texts.view_client'),
|
||||
'hasRecurringInvoices' => Invoice::scope()->recurring()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'hasQuotes' => Invoice::scope()->quotes()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'hasTasks' => Task::scope()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'hasRecurringInvoices' => $account->isModuleEnabled(ENTITY_RECURRING_INVOICE) && Invoice::scope()->recurring()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'hasQuotes' => $account->isModuleEnabled(ENTITY_QUOTE) && Invoice::scope()->quotes()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'hasTasks' => $account->isModuleEnabled(ENTITY_TASK) && Task::scope()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'hasExpenses' => $account->isModuleEnabled(ENTITY_EXPENSE) && Expense::scope()->withArchived()->whereClientId($client->id)->count() > 0,
|
||||
'gatewayLink' => $token ? $token->gatewayLink() : false,
|
||||
'gatewayName' => $token ? $token->gatewayName() : false,
|
||||
];
|
||||
@ -216,13 +222,22 @@ class ClientController extends BaseController
|
||||
{
|
||||
$action = Input::get('action');
|
||||
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
|
||||
|
||||
if ($action == 'purge' && ! auth()->user()->is_admin) {
|
||||
return redirect('dashboard')->withError(trans('texts.not_authorized'));
|
||||
}
|
||||
|
||||
$count = $this->clientService->bulk($ids, $action);
|
||||
|
||||
$message = Utils::pluralize($action.'d_client', $count);
|
||||
Session::flash('message', $message);
|
||||
|
||||
if ($action == 'purge') {
|
||||
return redirect('dashboard')->withMessage($message);
|
||||
} else {
|
||||
return $this->returnBulk(ENTITY_CLIENT, $action, $ids);
|
||||
}
|
||||
}
|
||||
|
||||
public function statement($clientPublicId, $statusId = false, $startDate = false, $endDate = false)
|
||||
{
|
||||
@ -273,4 +288,18 @@ class ClientController extends BaseController
|
||||
|
||||
return view('clients.statement', $data);
|
||||
}
|
||||
|
||||
public function getEmailHistory()
|
||||
{
|
||||
$history = dispatch(new LoadPostmarkHistory(request()->email));
|
||||
|
||||
return response()->json($history);
|
||||
}
|
||||
|
||||
public function reactivateEmail()
|
||||
{
|
||||
$result = dispatch(new ReactivatePostmarkEmail(request()->bounce_id));
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ class ClientPortalController extends BaseController
|
||||
'custom_value1',
|
||||
'custom_value2',
|
||||
]);
|
||||
$account->load(['date_format', 'datetime_format']);
|
||||
|
||||
// translate the country names
|
||||
if ($invoice->client->country) {
|
||||
@ -987,7 +988,7 @@ class ClientPortalController extends BaseController
|
||||
'email' => 'required',
|
||||
'address1' => 'required',
|
||||
'city' => 'required',
|
||||
'state' => 'required',
|
||||
'state' => $account->requiresAddressState() ? 'required' : '',
|
||||
'postal_code' => 'required',
|
||||
'country_id' => 'required',
|
||||
];
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use mPDF;
|
||||
use App\Models\Account;
|
||||
use App\Models\Document;
|
||||
use App\Models\Invitation;
|
||||
use App\Ninja\Repositories\ProposalRepository;
|
||||
use App\Jobs\ConvertProposalToPdf;
|
||||
|
||||
class ClientPortalProposalController extends BaseController
|
||||
{
|
||||
@ -39,8 +39,12 @@ class ClientPortalProposalController extends BaseController
|
||||
'proposalInvitation' => $invitation,
|
||||
];
|
||||
|
||||
if (request()->phantomjs) {
|
||||
return $proposal->present()->htmlDocument;
|
||||
} else {
|
||||
return view('invited.proposal', $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadProposal($invitationKey)
|
||||
{
|
||||
@ -50,9 +54,9 @@ class ClientPortalProposalController extends BaseController
|
||||
|
||||
$proposal = $invitation->proposal;
|
||||
|
||||
$mpdf = new mPDF();
|
||||
$mpdf->WriteHTML($proposal->present()->htmlDocument);
|
||||
$mpdf->Output($proposal->present()->filename, 'D');
|
||||
$pdf = dispatch(new ConvertProposalToPdf($proposal));
|
||||
|
||||
$this->downloadResponse($proposal->getFilename(), $pdf);
|
||||
}
|
||||
|
||||
public function getProposalImage($accountKey, $documentKey)
|
||||
|
@ -18,6 +18,7 @@ use Auth;
|
||||
use Cache;
|
||||
use Input;
|
||||
use Redirect;
|
||||
use Request;
|
||||
use Session;
|
||||
use URL;
|
||||
use Utils;
|
||||
@ -82,9 +83,7 @@ class ExpenseController extends BaseController
|
||||
'method' => 'POST',
|
||||
'url' => 'expenses',
|
||||
'title' => trans('texts.new_expense'),
|
||||
'vendors' => Vendor::scope()->with('vendor_contacts')->orderBy('name')->get(),
|
||||
'vendor' => $vendor,
|
||||
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
|
||||
'clientPublicId' => $request->client_id,
|
||||
'categoryPublicId' => $request->category_id,
|
||||
];
|
||||
@ -160,14 +159,12 @@ class ExpenseController extends BaseController
|
||||
'url' => $url,
|
||||
'title' => 'Edit Expense',
|
||||
'actions' => $actions,
|
||||
'vendors' => Vendor::scope()->with('vendor_contacts')->orderBy('name')->get(),
|
||||
'vendorPublicId' => $expense->vendor ? $expense->vendor->public_id : null,
|
||||
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
|
||||
'clientPublicId' => $expense->client ? $expense->client->public_id : null,
|
||||
'categoryPublicId' => $expense->expense_category ? $expense->expense_category->public_id : null,
|
||||
];
|
||||
|
||||
$data = array_merge($data, self::getViewModel());
|
||||
$data = array_merge($data, self::getViewModel($expense));
|
||||
|
||||
return View::make('expenses.edit', $data);
|
||||
}
|
||||
@ -227,6 +224,7 @@ class ExpenseController extends BaseController
|
||||
{
|
||||
$action = Input::get('action');
|
||||
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
|
||||
$referer = Request::server('HTTP_REFERER');
|
||||
|
||||
switch ($action) {
|
||||
case 'invoice':
|
||||
@ -238,27 +236,25 @@ class ExpenseController extends BaseController
|
||||
// Validate that either all expenses do not have a client or if there is a client, it is the same client
|
||||
foreach ($expenses as $expense) {
|
||||
if ($expense->client) {
|
||||
if ($expense->client->trashed()) {
|
||||
return redirect($referer)->withError(trans('texts.client_must_be_active'));
|
||||
}
|
||||
|
||||
if (! $clientPublicId) {
|
||||
$clientPublicId = $expense->client->public_id;
|
||||
} elseif ($clientPublicId != $expense->client->public_id) {
|
||||
Session::flash('error', trans('texts.expense_error_multiple_clients'));
|
||||
|
||||
return Redirect::to('expenses');
|
||||
return redirect($referer)->withError(trans('texts.expense_error_multiple_clients'));
|
||||
}
|
||||
}
|
||||
|
||||
if (! $currencyId) {
|
||||
$currencyId = $expense->invoice_currency_id;
|
||||
} elseif ($currencyId != $expense->invoice_currency_id && $expense->invoice_currency_id) {
|
||||
Session::flash('error', trans('texts.expense_error_multiple_currencies'));
|
||||
|
||||
return Redirect::to('expenses');
|
||||
return redirect($referer)->withError(trans('texts.expense_error_multiple_currencies'));
|
||||
}
|
||||
|
||||
if ($expense->invoice_id) {
|
||||
Session::flash('error', trans('texts.expense_error_invoiced'));
|
||||
|
||||
return Redirect::to('expenses');
|
||||
return redirect($referer)->withError(trans('texts.expense_error_invoiced'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,12 +283,14 @@ class ExpenseController extends BaseController
|
||||
return $this->returnBulk($this->entityType, $action, $ids);
|
||||
}
|
||||
|
||||
private static function getViewModel()
|
||||
private static function getViewModel($expense = false)
|
||||
{
|
||||
return [
|
||||
'data' => Input::old('data'),
|
||||
'account' => Auth::user()->account,
|
||||
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(),
|
||||
'vendors' => Vendor::scope()->withActiveOrSelected($expense ? $expense->vendor_id : false)->with('vendor_contacts')->orderBy('name')->get(),
|
||||
'clients' => Client::scope()->withActiveOrSelected($expense ? $expense->client_id : false)->with('contacts')->orderBy('name')->get(),
|
||||
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withActiveOrSelected($expense ? $expense->expense_category_id : false)->orderBy('name')->get(),
|
||||
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(),
|
||||
'isRecurring' => false,
|
||||
];
|
||||
|
@ -67,7 +67,7 @@ class HomeController extends BaseController
|
||||
{
|
||||
// Track the referral/campaign code
|
||||
if (Input::has('rc')) {
|
||||
Session::set(SESSION_REFERRAL_CODE, Input::get('rc'));
|
||||
session([SESSION_REFERRAL_CODE => Input::get('rc')]);
|
||||
}
|
||||
|
||||
if (Auth::check()) {
|
||||
|
@ -327,7 +327,7 @@ class InvoiceController extends BaseController
|
||||
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
|
||||
'tasks' => Session::get('tasks') ? Session::get('tasks') : null,
|
||||
'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null,
|
||||
'expenses' => Session::get('expenses') ? Expense::scope(Session::get('expenses'))->with('documents', 'expense_category')->get() : [],
|
||||
'expenses' => Expense::scope(Session::get('expenses'))->with('documents', 'expense_category')->get(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -92,19 +92,19 @@ class NinjaController extends BaseController
|
||||
public function show_license_payment()
|
||||
{
|
||||
if (Input::has('return_url')) {
|
||||
Session::set('return_url', Input::get('return_url'));
|
||||
session(['return_url' => Input::get('return_url')]);
|
||||
}
|
||||
|
||||
if (Input::has('affiliate_key')) {
|
||||
if ($affiliate = Affiliate::where('affiliate_key', '=', Input::get('affiliate_key'))->first()) {
|
||||
Session::set('affiliate_id', $affiliate->id);
|
||||
session(['affiliate_id' => $affiliate->id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Input::has('product_id')) {
|
||||
Session::set('product_id', Input::get('product_id'));
|
||||
session(['product_id' => Input::get('product_id')]);
|
||||
} elseif (! Session::has('product_id')) {
|
||||
Session::set('product_id', PRODUCT_ONE_CLICK_INSTALL);
|
||||
session(['product_id' => PRODUCT_ONE_CLICK_INSTALL]);
|
||||
}
|
||||
|
||||
if (! Session::get('affiliate_id')) {
|
||||
@ -112,7 +112,7 @@ class NinjaController extends BaseController
|
||||
}
|
||||
|
||||
if (Utils::isNinjaDev() && Input::has('test_mode')) {
|
||||
Session::set('test_mode', Input::get('test_mode'));
|
||||
session(['test_mode' => Input::get('test_mode')]);
|
||||
}
|
||||
|
||||
$account = $this->accountRepo->getNinjaAccount();
|
||||
|
@ -75,14 +75,14 @@ class OnlinePaymentController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $invitation->invoice->canBePaid() && ! request()->update) {
|
||||
if (! request()->capture && ! $invitation->invoice->canBePaid()) {
|
||||
return redirect()->to('view/' . $invitation->invitation_key);
|
||||
}
|
||||
|
||||
$invitation = $invitation->load('invoice.client.account.account_gateways.gateway');
|
||||
$account = $invitation->account;
|
||||
|
||||
if ($account->requiresAuthorization($invitation->invoice) && ! session('authorized:' . $invitation->invitation_key) && ! request()->update) {
|
||||
if (! request()->capture && $account->requiresAuthorization($invitation->invoice) && ! session('authorized:' . $invitation->invitation_key)) {
|
||||
return redirect()->to('view/' . $invitation->invitation_key);
|
||||
}
|
||||
|
||||
@ -126,14 +126,14 @@ class OnlinePaymentController extends BaseController
|
||||
|
||||
$paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId);
|
||||
|
||||
if (! $invitation->invoice->canBePaid() && ! request()->update) {
|
||||
if (! $invitation->invoice->canBePaid() && ! request()->capture) {
|
||||
return redirect()->to('view/' . $invitation->invitation_key);
|
||||
}
|
||||
|
||||
try {
|
||||
$paymentDriver->completeOnsitePurchase($request->all());
|
||||
|
||||
if (request()->update) {
|
||||
if (request()->capture) {
|
||||
return redirect('/client/dashboard')->withMessage(trans('texts.updated_payment_details'));
|
||||
} elseif ($paymentDriver->isTwoStep()) {
|
||||
Session::flash('warning', trans('texts.bank_account_verification_next_steps'));
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\CreatePaymentTermAPIRequest;
|
||||
use App\Http\Requests\CreatePaymentTermRequest;
|
||||
use App\Http\Requests\PaymentTermRequest;
|
||||
use App\Http\Requests\UpdatePaymentTermRequest;
|
||||
use App\Libraries\Utils;
|
||||
@ -39,7 +39,7 @@ class PaymentTermApiController extends BaseAPIController
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="A list of payment terms",
|
||||
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/PaymentTerms"))
|
||||
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/PaymentTerm"))
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response="default",
|
||||
@ -73,7 +73,7 @@ class PaymentTermApiController extends BaseAPIController
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="A single payment term",
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/PaymentTerms"))
|
||||
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/PaymentTerm"))
|
||||
* ),
|
||||
* @SWG\Response(
|
||||
* response="default",
|
||||
@ -110,7 +110,7 @@ class PaymentTermApiController extends BaseAPIController
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function store(CreatePaymentTermAPIRequest $request)
|
||||
public function store(CreatePaymentTermRequest $request)
|
||||
{
|
||||
|
||||
$paymentTerm = PaymentTerm::createNew();
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\CreatePaymentTermRequest;
|
||||
use App\Http\Requests\UpdatePaymentTermRequest;
|
||||
use App\Models\PaymentTerm;
|
||||
use App\Services\PaymentTermService;
|
||||
use Auth;
|
||||
@ -84,7 +86,7 @@ class PaymentTermController extends BaseController
|
||||
/**
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store()
|
||||
public function store(CreatePaymentTermRequest $request)
|
||||
{
|
||||
return $this->save();
|
||||
}
|
||||
@ -94,7 +96,7 @@ class PaymentTermController extends BaseController
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function update($publicId)
|
||||
public function update(UpdatePaymentTermRequest $request, $publicId)
|
||||
{
|
||||
return $this->save($publicId);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProductRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\TaxRate;
|
||||
use App\Ninja\Datatables\ProductDatatable;
|
||||
@ -71,23 +72,39 @@ class ProductController extends BaseController
|
||||
return $this->productService->getDatatable(Auth::user()->account_id, Input::get('sSearch'));
|
||||
}
|
||||
|
||||
public function cloneProduct(ProductRequest $request, $publicId)
|
||||
{
|
||||
return self::edit($request, $publicId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $publicId
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function edit($publicId)
|
||||
public function edit(ProductRequest $request, $publicId, $clone = false)
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$product = Product::scope($publicId)->withTrashed()->firstOrFail();
|
||||
|
||||
if ($clone) {
|
||||
$product->id = null;
|
||||
$product->public_id = null;
|
||||
$product->deleted_at = null;
|
||||
$url = 'products';
|
||||
$method = 'POST';
|
||||
} else {
|
||||
$url = 'products/'.$publicId;
|
||||
$method = 'PUT';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'account' => $account,
|
||||
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get() : null,
|
||||
'product' => $product,
|
||||
'entity' => $product,
|
||||
'method' => 'PUT',
|
||||
'url' => 'products/'.$publicId,
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'title' => trans('texts.edit_product'),
|
||||
];
|
||||
|
||||
@ -149,11 +166,16 @@ class ProductController extends BaseController
|
||||
$message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product');
|
||||
Session::flash('message', $message);
|
||||
|
||||
if (in_array(request('action'), ['archive', 'delete', 'restore', 'invoice'])) {
|
||||
$action = request('action');
|
||||
if (in_array($action, ['archive', 'delete', 'restore', 'invoice'])) {
|
||||
return self::bulk();
|
||||
}
|
||||
|
||||
return Redirect::to("products/{$product->public_id}/edit");
|
||||
if ($action == 'clone') {
|
||||
return redirect()->to(sprintf('products/%s/clone', $product->public_id));
|
||||
} else {
|
||||
return redirect()->to("products/{$product->public_id}/edit");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,6 +6,7 @@ use App\Http\Requests\CreateProposalRequest;
|
||||
use App\Http\Requests\ProposalRequest;
|
||||
use App\Http\Requests\UpdateProposalRequest;
|
||||
use App\Jobs\SendInvoiceEmail;
|
||||
use App\Jobs\ConvertProposalToPdf;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Proposal;
|
||||
use App\Models\ProposalTemplate;
|
||||
@ -17,7 +18,6 @@ use Auth;
|
||||
use Input;
|
||||
use Session;
|
||||
use View;
|
||||
use mPDF;
|
||||
|
||||
class ProposalController extends BaseController
|
||||
{
|
||||
@ -82,13 +82,13 @@ class ProposalController extends BaseController
|
||||
{
|
||||
$proposal = $request->entity();
|
||||
|
||||
$data = array_merge($this->getViewmodel(), [
|
||||
$data = array_merge($this->getViewmodel($proposal), [
|
||||
'proposal' => $proposal,
|
||||
'entity' => $proposal,
|
||||
'method' => 'PUT',
|
||||
'url' => 'proposals/' . $proposal->public_id,
|
||||
'title' => trans('texts.edit_proposal'),
|
||||
'invoices' => Invoice::scope()->with('client.contacts', 'client.country')->unapprovedQuotes($proposal->invoice_id)->orderBy('id')->get(),
|
||||
'invoices' => Invoice::scope()->with('client.contacts', 'client.country')->withActiveOrSelected($proposal->invoice_id)->unapprovedQuotes($proposal->invoice_id)->orderBy('id')->get(),
|
||||
'invoicePublicId' => $proposal->invoice ? $proposal->invoice->public_id : null,
|
||||
'templatePublicId' => $proposal->proposal_template ? $proposal->proposal_template->public_id : null,
|
||||
]);
|
||||
@ -96,10 +96,10 @@ class ProposalController extends BaseController
|
||||
return View::make('proposals.edit', $data);
|
||||
}
|
||||
|
||||
private function getViewmodel()
|
||||
private function getViewmodel($proposal = false)
|
||||
{
|
||||
$account = auth()->user()->account;
|
||||
$templates = ProposalTemplate::whereAccountId($account->id)->orderBy('name')->get();
|
||||
$templates = ProposalTemplate::whereAccountId($account->id)->withActiveOrSelected($proposal ? $proposal->proposal_template_id : false)->orderBy('name')->get();
|
||||
|
||||
if (! $templates->count()) {
|
||||
$templates = ProposalTemplate::whereNull('account_id')->orderBy('name')->get();
|
||||
@ -167,12 +167,8 @@ class ProposalController extends BaseController
|
||||
{
|
||||
$proposal = $request->entity();
|
||||
|
||||
$mpdf = new mPDF();
|
||||
$mpdf->showImageErrors = true;
|
||||
$mpdf->WriteHTML($proposal->present()->htmlDocument);
|
||||
$pdf = dispatch(new ConvertProposalToPdf($proposal));
|
||||
|
||||
//$mpdf->Output();
|
||||
|
||||
$mpdf->Output($proposal->present()->filename, 'D');
|
||||
$this->downloadResponse($proposal->getFilename(), $pdf);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class ProposalSnippetController extends BaseController
|
||||
'method' => 'PUT',
|
||||
'url' => 'proposals/snippets/' . $proposalSnippet->public_id,
|
||||
'title' => trans('texts.edit_proposal_snippet'),
|
||||
'categories' => ProposalCategory::scope()->orderBy('name')->get(),
|
||||
'categories' => ProposalCategory::scope()->withActiveOrSelected($proposalSnippet->proposal_category_id)->orderBy('name')->get(),
|
||||
'categoryPublicId' => $proposalSnippet->proposal_category ? $proposalSnippet->proposal_category->public_id : null,
|
||||
'icons' => $this->getIcons(),
|
||||
];
|
||||
|
@ -108,7 +108,7 @@ class QuoteController extends BaseController
|
||||
'invoiceFonts' => Cache::get('fonts'),
|
||||
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
|
||||
'isRecurring' => false,
|
||||
'expenses' => [],
|
||||
'expenses' => collect(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ExportReportResults;
|
||||
use App\Jobs\LoadPostmarkStats;
|
||||
use App\Jobs\RunReport;
|
||||
use App\Models\Account;
|
||||
use App\Models\ScheduledReport;
|
||||
@ -13,6 +14,7 @@ use View;
|
||||
use Carbon;
|
||||
use Validator;
|
||||
|
||||
|
||||
/**
|
||||
* Class ReportController.
|
||||
*/
|
||||
@ -101,7 +103,8 @@ class ReportController extends BaseController
|
||||
$config = [
|
||||
'date_field' => $dateField,
|
||||
'status_ids' => request()->status_ids,
|
||||
'group_dates_by' => request()->group_dates_by,
|
||||
'group' => request()->group,
|
||||
'subgroup' => request()->subgroup,
|
||||
'document_filter' => request()->document_filter,
|
||||
'currency_type' => request()->currency_type,
|
||||
'export_format' => $format,
|
||||
@ -152,7 +155,8 @@ class ReportController extends BaseController
|
||||
|
||||
unset($options['start_date']);
|
||||
unset($options['end_date']);
|
||||
unset($options['group_dates_by']);
|
||||
unset($options['group']);
|
||||
unset($options['subgroup']);
|
||||
|
||||
$schedule = ScheduledReport::createNew();
|
||||
$schedule->config = json_encode($options);
|
||||
@ -173,4 +177,20 @@ class ReportController extends BaseController
|
||||
|
||||
session()->flash('message', trans('texts.deleted_scheduled_report'));
|
||||
}
|
||||
|
||||
public function showEmailReport()
|
||||
{
|
||||
$data = [
|
||||
'account' => auth()->user()->account,
|
||||
];
|
||||
|
||||
return view('reports.emails', $data);
|
||||
}
|
||||
|
||||
public function loadEmailReport($startDate, $endDate)
|
||||
{
|
||||
$data = dispatch(new LoadPostmarkStats($startDate, $endDate));
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use Auth;
|
||||
use DropdownButton;
|
||||
use Input;
|
||||
use Redirect;
|
||||
use Request;
|
||||
use Session;
|
||||
use URL;
|
||||
use Utils;
|
||||
@ -189,7 +190,7 @@ class TaskController extends BaseController
|
||||
'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(),
|
||||
];
|
||||
|
||||
$data = array_merge($data, self::getViewModel());
|
||||
$data = array_merge($data, self::getViewModel($task));
|
||||
|
||||
return View::make('tasks.edit', $data);
|
||||
}
|
||||
@ -211,12 +212,12 @@ class TaskController extends BaseController
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private static function getViewModel()
|
||||
private static function getViewModel($task = false)
|
||||
{
|
||||
return [
|
||||
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
|
||||
'clients' => Client::scope()->withActiveOrSelected($task ? $task->client_id : false)->with('contacts')->orderBy('name')->get(),
|
||||
'account' => Auth::user()->account,
|
||||
'projects' => Project::scope()->with('client.contacts')->orderBy('name')->get(),
|
||||
'projects' => Project::scope()->withActiveOrSelected($task ? $task->project_id : false)->with('client.contacts')->orderBy('name')->get(),
|
||||
];
|
||||
}
|
||||
|
||||
@ -260,6 +261,7 @@ class TaskController extends BaseController
|
||||
{
|
||||
$action = Input::get('action');
|
||||
$ids = Input::get('public_id') ?: (Input::get('id') ?: Input::get('ids'));
|
||||
$referer = Request::server('HTTP_REFERER');
|
||||
|
||||
if (in_array($action, ['resume', 'stop'])) {
|
||||
$this->taskRepo->save($ids, ['action' => $action]);
|
||||
@ -273,23 +275,21 @@ class TaskController extends BaseController
|
||||
$lastProjectId = false;
|
||||
foreach ($tasks as $task) {
|
||||
if ($task->client) {
|
||||
if ($task->client->trashed()) {
|
||||
return redirect($referer)->withError(trans('texts.client_must_be_active'));
|
||||
}
|
||||
|
||||
if (! $clientPublicId) {
|
||||
$clientPublicId = $task->client->public_id;
|
||||
} elseif ($clientPublicId != $task->client->public_id) {
|
||||
Session::flash('error', trans('texts.task_error_multiple_clients'));
|
||||
|
||||
return Redirect::to('tasks');
|
||||
return redirect($referer)->withError(trans('texts.task_error_multiple_clients'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($task->is_running) {
|
||||
Session::flash('error', trans('texts.task_error_running'));
|
||||
|
||||
return Redirect::to('tasks');
|
||||
return redirect($referer)->withError(trans('texts.task_error_running'));
|
||||
} elseif ($task->invoice_id) {
|
||||
Session::flash('error', trans('texts.task_error_invoiced'));
|
||||
|
||||
return Redirect::to('tasks');
|
||||
return redirect($referer)->withError(trans('texts.task_error_invoiced'));
|
||||
}
|
||||
|
||||
$account = Auth::user()->account;
|
||||
|
@ -45,15 +45,6 @@ class UserController extends BaseController
|
||||
return $this->userService->getDatatable(Auth::user()->account_id);
|
||||
}
|
||||
|
||||
public function setTheme()
|
||||
{
|
||||
$user = User::find(Auth::user()->id);
|
||||
$user->theme_id = Input::get('theme_id');
|
||||
$user->save();
|
||||
|
||||
return Redirect::to(Input::get('path'));
|
||||
}
|
||||
|
||||
public function forcePDFJS()
|
||||
{
|
||||
$user = Auth::user();
|
||||
@ -401,4 +392,18 @@ class UserController extends BaseController
|
||||
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
public function acceptTerms()
|
||||
{
|
||||
$ip = Request::getClientIp();
|
||||
$referer = Request::server('HTTP_REFERER');
|
||||
$message = '';
|
||||
|
||||
if (request()->accepted_terms) {
|
||||
auth()->user()->acceptLatestTerms($ip)->save();
|
||||
$message = trans('texts.accepted_terms');
|
||||
}
|
||||
|
||||
return redirect($referer)->withMessage($message);
|
||||
}
|
||||
}
|
||||
|
@ -30,11 +30,13 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
//\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
|
||||
\App\Http\Middleware\DuplicateSubmissionCheck::class,
|
||||
\App\Http\Middleware\QueryLogging::class,
|
||||
\App\Http\Middleware\StartupCheck::class,
|
||||
],
|
||||
'api' => [
|
||||
\App\Http\Middleware\QueryLogging::class,
|
||||
\App\Http\Middleware\ApiCheck::class,
|
||||
],
|
||||
/*
|
||||
|
@ -54,7 +54,7 @@ class ApiCheck
|
||||
// check if user is archived
|
||||
if ($token && $token->user) {
|
||||
Auth::onceUsingId($token->user_id);
|
||||
Session::set('token_id', $token->id);
|
||||
session(['token_id' => $token->id]);
|
||||
} elseif ($hasApiSecret && $request->is('api/v1/ping')) {
|
||||
// do nothing: allow ping with api_secret or account token
|
||||
} else {
|
||||
|
@ -147,7 +147,7 @@ class StartupCheck
|
||||
if (Input::has('lang')) {
|
||||
$locale = Input::get('lang');
|
||||
App::setLocale($locale);
|
||||
Session::set(SESSION_LOCALE, $locale);
|
||||
session([SESSION_LOCALE => $locale]);
|
||||
|
||||
if (Auth::check()) {
|
||||
if ($language = Language::whereLocale($locale)->first()) {
|
||||
|
@ -4,7 +4,7 @@ namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Invoice;
|
||||
|
||||
class CreatePaymentTermAPIRequest extends Request
|
||||
class CreatePaymentTermRequest extends PaymentTermRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
@ -27,7 +27,8 @@ class CreatePaymentTermAPIRequest extends Request
|
||||
{
|
||||
|
||||
$rules = [
|
||||
'num_days' => 'required|numeric|unique:payment_terms',
|
||||
'num_days' => 'required|numeric|unique:payment_terms,num_days,,id,account_id,' . $this->user()->account_id . ',deleted_at,NULL'
|
||||
. '|unique:payment_terms,num_days,,id,account_id,0,deleted_at,NULL',
|
||||
];
|
||||
|
||||
|
@ -2,16 +2,41 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class UpdatePaymentTermRequest extends EntityRequest
|
||||
use App\Models\Invoice;
|
||||
|
||||
class UpdatePaymentTermRequest extends PaymentTermRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public function authorize()
|
||||
{
|
||||
return $this->entity() && $this->user()->can('edit', $this->entity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
|
||||
public function rules()
|
||||
{
|
||||
if (! $this->entity()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$paymentTermId = $this->entity()->id;
|
||||
|
||||
$rules = [
|
||||
'num_days' => 'required|numeric|unique:payment_terms,num_days,' . $paymentTermId . ',id,account_id,' . $this->user()->account_id . ',deleted_at,NULL'
|
||||
. '|unique:payment_terms,num_days,' . $paymentTermId . ',id,account_id,0,deleted_at,NULL',
|
||||
];
|
||||
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
|
25
app/Jobs/ConvertProposalToPdf.php
Normal file
25
app/Jobs/ConvertProposalToPdf.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Job;
|
||||
use App\Libraries\CurlUtils;
|
||||
|
||||
class ConvertProposalToPdf extends Job
|
||||
{
|
||||
public function __construct($proposal)
|
||||
{
|
||||
$this->proposal = $proposal;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$proposal = $this->proposal;
|
||||
$url = $proposal->getHeadlessLink();
|
||||
|
||||
$filename = sprintf('%s/storage/app/%s.pdf', base_path(), strtolower(str_random(RANDOM_KEY_LENGTH)));
|
||||
$pdf = CurlUtils::renderPDF($url, $filename);
|
||||
|
||||
return $pdf;
|
||||
}
|
||||
}
|
@ -66,8 +66,12 @@ class ExportReportResults extends Job
|
||||
foreach ($each as $dimension => $val) {
|
||||
$tmp = [];
|
||||
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
|
||||
foreach ($val as $id => $field) {
|
||||
$tmp[] = Utils::formatMoney($field, $currencyId);
|
||||
foreach ($val as $field => $value) {
|
||||
if ($field == 'duration') {
|
||||
$tmp[] = Utils::formatTime($value);
|
||||
} else {
|
||||
$tmp[] = Utils::formatMoney($value, $currencyId);
|
||||
}
|
||||
}
|
||||
$summary[] = $tmp;
|
||||
}
|
||||
|
88
app/Jobs/LoadPostmarkHistory.php
Normal file
88
app/Jobs/LoadPostmarkHistory.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Job;
|
||||
use Postmark\PostmarkClient;
|
||||
use stdClass;
|
||||
|
||||
class LoadPostmarkHistory extends Job
|
||||
{
|
||||
public function __construct($email)
|
||||
{
|
||||
$this->email = $email;
|
||||
$this->bounceId = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$str = '';
|
||||
|
||||
if (config('services.postmark')) {
|
||||
$this->account = auth()->user()->account;
|
||||
$this->postmark = new PostmarkClient(config('services.postmark'));
|
||||
|
||||
$str .= $this->loadBounceEvents();
|
||||
$str .= $this->loadEmailEvents();
|
||||
}
|
||||
|
||||
if (! $str) {
|
||||
$str = trans('texts.no_messages_found');
|
||||
}
|
||||
|
||||
$response = new stdClass;
|
||||
$response->str = $str;
|
||||
$response->bounce_id = $this->bounceId;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function loadBounceEvents() {
|
||||
$str = '';
|
||||
$response = $this->postmark->getBounces(5, 0, null, null, $this->email, $this->account->account_key);
|
||||
|
||||
foreach ($response['bounces'] as $bounce) {
|
||||
if (! $bounce['inactive'] || ! $bounce['canactivate']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$str .= sprintf('<b>%s</b><br/>', $bounce['subject']);
|
||||
$str .= sprintf('<span class="text-danger">%s</span> | %s<br/>', $bounce['type'], $this->account->getDateTime($bounce['bouncedat'], true));
|
||||
$str .= sprintf('<span class="text-muted">%s %s</span><p/>', $bounce['description'], $bounce['details']);
|
||||
|
||||
$this->bounceId = $bounce['id'];
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
private function loadEmailEvents() {
|
||||
$str = '';
|
||||
$response = $this->postmark->getOutboundMessages(5, 0, $this->email, null, $this->account->account_key);
|
||||
|
||||
foreach ($response['messages'] as $message) {
|
||||
$details = $this->postmark->getOutboundMessageDetails($message['MessageID']);
|
||||
$str .= sprintf('<b>%s</b><br/>', $details['subject']);
|
||||
|
||||
if (count($details['messageevents'])) {
|
||||
$event = $details['messageevents'][0];
|
||||
$str .= sprintf('%s | %s<br/>', $event['Type'], $this->account->getDateTime($event['ReceivedAt'], true));
|
||||
if ($message = $event['Details']['DeliveryMessage']) {
|
||||
$str .= sprintf('<span class="text-muted">%s</span><br/>', $message);
|
||||
}
|
||||
if ($server = $event['Details']['DestinationServer']) {
|
||||
$str .= sprintf('<span class="text-muted">%s</span><br/>', $server);
|
||||
}
|
||||
} else {
|
||||
$str .= trans('texts.processing') . '...';
|
||||
}
|
||||
|
||||
$str .= '<p/>';
|
||||
}
|
||||
}
|
||||
}
|
157
app/Jobs/LoadPostmarkStats.php
Normal file
157
app/Jobs/LoadPostmarkStats.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Job;
|
||||
use Postmark\PostmarkClient;
|
||||
use stdClass;
|
||||
use DateInterval;
|
||||
use DatePeriod;
|
||||
|
||||
class LoadPostmarkStats extends Job
|
||||
{
|
||||
public function __construct($startDate, $endDate)
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
$this->endDate = $endDate;
|
||||
|
||||
$this->response = new stdClass();
|
||||
$this->postmark = new \Postmark\PostmarkClient(config('services.postmark'));
|
||||
$this->account = auth()->user()->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! auth()->user()->hasPermission('view_all')) {
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
$this->loadOverallStats();
|
||||
$this->loadSentStats();
|
||||
$this->loadPlatformStats();
|
||||
$this->loadEmailClientStats();
|
||||
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
private function loadOverallStats() {
|
||||
$startDate = date_create($this->startDate);
|
||||
$endDate = date_create($this->endDate);
|
||||
|
||||
$eventTypes = ['sent', 'opened'];
|
||||
|
||||
foreach ($eventTypes as $eventType) {
|
||||
$data = [];
|
||||
$endDate->modify('+1 day');
|
||||
$interval = new DateInterval('P1D');
|
||||
$period = new DatePeriod($startDate, $interval, $endDate);
|
||||
$endDate->modify('-1 day');
|
||||
$records = [];
|
||||
|
||||
if ($eventType == 'sent') {
|
||||
$response = $this->postmark->getOutboundSendStatistics($this->account->account_key, request()->start_date, request()->end_date);
|
||||
} else {
|
||||
$response = $this->postmark->getOutboundOpenStatistics($this->account->account_key, request()->start_date, request()->end_date);
|
||||
}
|
||||
|
||||
foreach ($response->days as $key => $val) {
|
||||
$field = $eventType == 'opened' ? 'unique' : $eventType;
|
||||
$data[$val['date']] = $val[$field];
|
||||
}
|
||||
|
||||
foreach ($period as $day) {
|
||||
$date = $day->format('Y-m-d');
|
||||
$records[] = isset($data[$date]) ? $data[$date] : 0;
|
||||
|
||||
if ($eventType == 'sent') {
|
||||
$labels[] = $day->format('m/d/Y');
|
||||
}
|
||||
}
|
||||
|
||||
if ($eventType == 'sent') {
|
||||
$color = '51,122,183';
|
||||
} elseif ($eventType == 'opened') {
|
||||
$color = '54,193,87';
|
||||
} elseif ($eventType == 'bounced') {
|
||||
$color = '128,128,128';
|
||||
}
|
||||
|
||||
$group = new stdClass();
|
||||
$group->data = $records;
|
||||
$group->label = trans("texts.{$eventType}");
|
||||
$group->lineTension = 0;
|
||||
$group->borderWidth = 4;
|
||||
$group->borderColor = "rgba({$color}, 1)";
|
||||
$group->backgroundColor = "rgba({$color}, 0.1)";
|
||||
$datasets[] = $group;
|
||||
}
|
||||
|
||||
$data = new stdClass();
|
||||
$data->labels = $labels;
|
||||
$data->datasets = $datasets;
|
||||
$this->response->data = $data;
|
||||
}
|
||||
|
||||
private function loadSentStats() {
|
||||
$account = $this->account;
|
||||
$data = $this->postmark->getOutboundOverviewStatistics($this->account->account_key, request()->start_date, request()->end_date);
|
||||
$percent = $data->sent ? ($data->uniqueopens / $data->sent * 100) : 0;
|
||||
$this->response->totals = [
|
||||
'sent' => $account->formatNumber($data->sent),
|
||||
'opened' => sprintf('%s | %s%%', $account->formatNumber($data->uniqueopens), $account->formatNumber($percent)),
|
||||
'bounced' => sprintf('%s | %s%%', $account->formatNumber($data->bounced), $account->formatNumber($data->bouncerate, 3)),
|
||||
//'spam' => sprintf('%s | %s%%', $account->formatNumber($data->spamcomplaints), $account->formatNumber($data->spamcomplaintsrate, 3))
|
||||
];
|
||||
}
|
||||
|
||||
private function loadPlatformStats() {
|
||||
$data = $this->postmark->getOutboundPlatformStatistics($this->account->account_key, request()->start_date, request()->end_date);
|
||||
$account = $this->account;
|
||||
$str = '';
|
||||
$total = 0;
|
||||
|
||||
$total = $data['desktop'] + $data['mobile'] + $data['webmail'];
|
||||
|
||||
foreach (['mobile', 'desktop', 'webmail'] as $platform) {
|
||||
$percent = $total ? ($data[$platform] / $total * 100) : 0;
|
||||
$str .= sprintf('<tr><td>%s</td><td>%s%%</td></tr>', trans('texts.' . $platform), $account->formatNumber($percent));
|
||||
}
|
||||
|
||||
$this->response->platforms = $str;
|
||||
}
|
||||
|
||||
private function loadEmailClientStats() {
|
||||
$data = $this->postmark->getOutboundEmailClientStatistics($this->account->account_key, request()->start_date, request()->end_date);
|
||||
$account = $this->account;
|
||||
$str = '';
|
||||
$total = 0;
|
||||
$clients = [];
|
||||
|
||||
foreach ($data as $key => $val) {
|
||||
if ($key == 'days') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total += $val;
|
||||
$clients[$key] = $val;
|
||||
}
|
||||
|
||||
arsort($clients);
|
||||
|
||||
foreach ($clients as $key => $val) {
|
||||
$percent = $total ? ($val / $total * 100) : 0;
|
||||
if ($percent < 0.5) {
|
||||
continue;
|
||||
}
|
||||
$str .= sprintf('<tr><td>%s</td><td>%s%%</td></tr>', ucwords($key), $account->formatNumber($percent));
|
||||
}
|
||||
|
||||
$this->response->emailClients = $str;
|
||||
}
|
||||
|
||||
}
|
@ -55,6 +55,7 @@ class PurgeAccountData extends Job
|
||||
'proposal_snippets',
|
||||
'proposal_categories',
|
||||
'proposal_invitations',
|
||||
'tax_rates',
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
|
44
app/Jobs/PurgeClientData.php
Normal file
44
app/Jobs/PurgeClientData.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Job;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\LookupAccount;
|
||||
use DB;
|
||||
use Exception;
|
||||
use App\Libraries\HistoryUtils;
|
||||
|
||||
class PurgeClientData extends Job
|
||||
{
|
||||
public function __construct($client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$invoices = $this->client->invoices()->withTrashed()->get();
|
||||
$expenses = $this->client->expenses()->withTrashed()->get();
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
foreach ($invoice->documents as $document) {
|
||||
$document->delete();
|
||||
}
|
||||
}
|
||||
foreach ($expenses as $expense) {
|
||||
foreach ($expense->documents as $document) {
|
||||
$document->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$this->client->forceDelete();
|
||||
|
||||
HistoryUtils::deleteHistory($this->client);
|
||||
}
|
||||
}
|
29
app/Jobs/ReactivatePostmarkEmail.php
Normal file
29
app/Jobs/ReactivatePostmarkEmail.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Job;
|
||||
use Postmark\PostmarkClient;
|
||||
|
||||
class ReactivatePostmarkEmail extends Job
|
||||
{
|
||||
public function __construct($bounceId)
|
||||
{
|
||||
$this->bounceId = $bounceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! config('services.postmark')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$postmark = new PostmarkClient(config('services.postmark'));
|
||||
$response = $postmark->activateBounce($this->bounceId);
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ class RunReport extends Job
|
||||
|
||||
$reportType = $this->reportType;
|
||||
$config = $this->config;
|
||||
$config['subgroup'] = false; // don't yet support charts in export
|
||||
|
||||
$isExport = $this->isExport;
|
||||
$reportClass = '\\App\\Ninja\\Reports\\' . Str::studly($reportType) . 'Report';
|
||||
|
||||
|
@ -66,4 +66,32 @@ class CurlUtils
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function renderPDF($url, $filename)
|
||||
{
|
||||
if (! $path = env('PHANTOMJS_BIN_PATH')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$client = Client::getInstance();
|
||||
$client->isLazy();
|
||||
$client->getEngine()->addOption("--load-images=true");
|
||||
$client->getEngine()->setPath($path);
|
||||
|
||||
$request = $client->getMessageFactory()->createPdfRequest($url, 'GET');
|
||||
$request->setOutputFile($filename);
|
||||
//$request->setOrientation('landscape');
|
||||
$request->setMargin('0');
|
||||
|
||||
$response = $client->getMessageFactory()->createResponse();
|
||||
$client->send($request, $response);
|
||||
|
||||
if ($response->getStatus() === 200) {
|
||||
$pdf = file_get_contents($filename);
|
||||
unlink($filename);
|
||||
return $pdf;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,10 @@ class HistoryUtils
|
||||
->get();
|
||||
|
||||
foreach ($activities->reverse() as $activity) {
|
||||
if ($activity->client && $activity->client->is_deleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_CLIENT) {
|
||||
$entity = $activity->client;
|
||||
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_TASK || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_TASK) {
|
||||
@ -78,6 +82,28 @@ class HistoryUtils
|
||||
}
|
||||
}
|
||||
|
||||
public static function deleteHistory(EntityModel $entity)
|
||||
{
|
||||
$history = Session::get(RECENTLY_VIEWED) ?: [];
|
||||
$accountHistory = isset($history[$entity->account_id]) ? $history[$entity->account_id] : [];
|
||||
$remove = [];
|
||||
|
||||
for ($i=0; $i<count($accountHistory); $i++) {
|
||||
$item = $accountHistory[$i];
|
||||
if ($entity->equalTo($item)) {
|
||||
$remove[] = $i;
|
||||
} elseif ($entity->getEntityType() == ENTITY_CLIENT && $entity->public_id == $item->client_id) {
|
||||
$remove[] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
for ($i=count($remove) - 1; $i>=0; $i--) {
|
||||
array_splice($history[$entity->account_id], $remove[$i], 1);
|
||||
}
|
||||
|
||||
Session::put(RECENTLY_VIEWED, $history);
|
||||
}
|
||||
|
||||
public static function trackViewed(EntityModel $entity)
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
@ -136,6 +162,7 @@ class HistoryUtils
|
||||
private static function convertToObject($entity)
|
||||
{
|
||||
$object = new stdClass();
|
||||
$object->id = $entity->id;
|
||||
$object->accountId = $entity->account_id;
|
||||
$object->url = $entity->present()->url;
|
||||
$object->entityType = $entity->subEntityType();
|
||||
|
@ -435,6 +435,7 @@ class Utils
|
||||
'url' => Input::get('url', Request::url()),
|
||||
'previous' => url()->previous(),
|
||||
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
|
||||
'locale' => App::getLocale(),
|
||||
'ip' => Request::getClientIp(),
|
||||
'count' => Session::get('error_count', 0),
|
||||
'is_console' => App::runningInConsole() ? 'yes' : 'no',
|
||||
@ -500,6 +501,21 @@ class Utils
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public static function formatNumber($value, $currencyId = false, $precision = 0)
|
||||
{
|
||||
$value = floatval($value);
|
||||
|
||||
if (! $currencyId) {
|
||||
$currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY);
|
||||
}
|
||||
|
||||
$currency = self::getFromCache($currencyId, 'currencies');
|
||||
$thousand = $currency->thousand_separator;
|
||||
$decimal = $currency->decimal_separator;
|
||||
|
||||
return number_format($value, $precision, $decimal, $thousand);
|
||||
}
|
||||
|
||||
public static function formatMoney($value, $currencyId = false, $countryId = false, $decorator = false)
|
||||
{
|
||||
$value = floatval($value);
|
||||
@ -1327,6 +1343,28 @@ class Utils
|
||||
}
|
||||
|
||||
public static function brewerColor($number) {
|
||||
$colors = [
|
||||
'#337AB7',
|
||||
'#3cb44b',
|
||||
'#e6194b',
|
||||
'#f58231',
|
||||
'#911eb4',
|
||||
'#46f0f0',
|
||||
'#f032e6',
|
||||
'#d2f53c',
|
||||
'#fabebe',
|
||||
'#008080',
|
||||
'#e6beff',
|
||||
'#aa6e28',
|
||||
'#fffac8',
|
||||
'#800000',
|
||||
'#aaffc3',
|
||||
'#808000',
|
||||
'#000080',
|
||||
'#808080',
|
||||
];
|
||||
|
||||
/*
|
||||
$colors = [
|
||||
'#1c9f77',
|
||||
'#d95d02',
|
||||
@ -1337,11 +1375,18 @@ class Utils
|
||||
'#a87821',
|
||||
'#676767',
|
||||
];
|
||||
*/
|
||||
$number = ($number-1) % count($colors);
|
||||
|
||||
return $colors[$number];
|
||||
}
|
||||
|
||||
public static function brewerColorRGB($number) {
|
||||
$color = static::brewerColor($number);
|
||||
list($r, $g, $b) = sscanf($color, "#%02x%02x%02x");
|
||||
return "{$r},{$g},{$b}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace language-specific characters by ASCII-equivalents.
|
||||
* @param string $s
|
||||
|
@ -71,7 +71,7 @@ class HandleUserLoggedIn
|
||||
|
||||
// if they're using Stripe make sure they're using Stripe.js
|
||||
$accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE);
|
||||
if ($accountGateway && ! $accountGateway->getPublishableStripeKey()) {
|
||||
if ($accountGateway && ! $accountGateway->getPublishableKey()) {
|
||||
Session::flash('warning', trans('texts.missing_publishable_key'));
|
||||
} elseif ($account->isLogoTooLarge()) {
|
||||
Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize() . 'KB']));
|
||||
|
74
app/Listeners/HistoryListener.php
Normal file
74
app/Listeners/HistoryListener.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\InvoiceWasDeleted;
|
||||
use App\Events\ClientWasDeleted;
|
||||
use App\Events\QuoteWasDeleted;
|
||||
use App\Events\TaskWasDeleted;
|
||||
use App\Events\ExpenseWasDeleted;
|
||||
use App\Events\ProjectWasDeleted;
|
||||
use App\Events\ProposalWasDeleted;
|
||||
use App\Libraries\HistoryUtils;
|
||||
|
||||
/**
|
||||
* Class InvoiceListener.
|
||||
*/
|
||||
class HistoryListener
|
||||
{
|
||||
/**
|
||||
* @param ClientWasDeleted $event
|
||||
*/
|
||||
public function deletedClient(ClientWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceWasDeleted $event
|
||||
*/
|
||||
public function deletedInvoice(InvoiceWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QuoteWasDeleted $event
|
||||
*/
|
||||
public function deletedQuote(QuoteWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->quote);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TaskWasDeleted $event
|
||||
*/
|
||||
public function deletedTask(TaskWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->task);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExpenseWasDeleted $event
|
||||
*/
|
||||
public function deletedExpense(ExpenseWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->expense);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectWasDeleted $event
|
||||
*/
|
||||
public function deletedProject(ProjectWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->project);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProposalWasDeleted $event
|
||||
*/
|
||||
public function deletedProposal(ProposalWasDeleted $event)
|
||||
{
|
||||
HistoryUtils::deleteHistory($event->proposal);
|
||||
}
|
||||
}
|
@ -90,6 +90,11 @@ class InvoiceListener
|
||||
->first();
|
||||
$activity->json_backup = $invoice->hidePrivateFields()->toJSON();
|
||||
$activity->save();
|
||||
|
||||
if ($invoice->balance == 0 && $payment->account->auto_archive_invoice) {
|
||||
$invoiceRepo = app('App\Ninja\Repositories\InvoiceRepository');
|
||||
$invoiceRepo->archive($invoice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,6 +10,8 @@ use App\Events\QuoteInvitationWasApproved;
|
||||
use App\Events\PaymentWasCreated;
|
||||
use App\Services\PushService;
|
||||
use App\Jobs\SendNotificationEmail;
|
||||
use App\Jobs\SendPaymentEmail;
|
||||
use App\Notifications\PaymentCreated;
|
||||
|
||||
/**
|
||||
* Class NotificationListener
|
||||
@ -47,14 +49,17 @@ class NotificationListener
|
||||
* @param $type
|
||||
* @param null $payment
|
||||
*/
|
||||
private function sendEmails($invoice, $type, $payment = null, $notes = false)
|
||||
private function sendNotifications($invoice, $type, $payment = null, $notes = false)
|
||||
{
|
||||
foreach ($invoice->account->users as $user)
|
||||
{
|
||||
if ($user->{"notify_{$type}"})
|
||||
{
|
||||
if ($user->{"notify_{$type}"}) {
|
||||
dispatch(new SendNotificationEmail($user, $invoice, $type, $payment, $notes));
|
||||
}
|
||||
|
||||
if ($payment && $user->slack_webhook_url) {
|
||||
$user->notify(new PaymentCreated($payment, $invoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +68,7 @@ class NotificationListener
|
||||
*/
|
||||
public function emailedInvoice(InvoiceWasEmailed $event)
|
||||
{
|
||||
$this->sendEmails($event->invoice, 'sent', null, $event->notes);
|
||||
$this->sendNotifications($event->invoice, 'sent', null, $event->notes);
|
||||
$this->pushService->sendNotification($event->invoice, 'sent');
|
||||
}
|
||||
|
||||
@ -72,7 +77,7 @@ class NotificationListener
|
||||
*/
|
||||
public function emailedQuote(QuoteWasEmailed $event)
|
||||
{
|
||||
$this->sendEmails($event->quote, 'sent', null, $event->notes);
|
||||
$this->sendNotifications($event->quote, 'sent', null, $event->notes);
|
||||
$this->pushService->sendNotification($event->quote, 'sent');
|
||||
}
|
||||
|
||||
@ -85,7 +90,7 @@ class NotificationListener
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendEmails($event->invoice, 'viewed');
|
||||
$this->sendNotifications($event->invoice, 'viewed');
|
||||
$this->pushService->sendNotification($event->invoice, 'viewed');
|
||||
}
|
||||
|
||||
@ -98,7 +103,7 @@ class NotificationListener
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendEmails($event->quote, 'viewed');
|
||||
$this->sendNotifications($event->quote, 'viewed');
|
||||
$this->pushService->sendNotification($event->quote, 'viewed');
|
||||
}
|
||||
|
||||
@ -107,7 +112,7 @@ class NotificationListener
|
||||
*/
|
||||
public function approvedQuote(QuoteInvitationWasApproved $event)
|
||||
{
|
||||
$this->sendEmails($event->quote, 'approved');
|
||||
$this->sendNotifications($event->quote, 'approved');
|
||||
$this->pushService->sendNotification($event->quote, 'approved');
|
||||
}
|
||||
|
||||
@ -121,8 +126,8 @@ class NotificationListener
|
||||
return;
|
||||
}
|
||||
|
||||
$this->contactMailer->sendPaymentConfirmation($event->payment);
|
||||
$this->sendEmails($event->payment->invoice, 'paid', $event->payment);
|
||||
dispatch(new SendPaymentEmail($event->payment));
|
||||
$this->sendNotifications($event->payment->invoice, 'paid', $event->payment);
|
||||
|
||||
$this->pushService->sendNotification($event->payment->invoice, 'paid');
|
||||
}
|
||||
|
@ -134,6 +134,9 @@ class Account extends Eloquent
|
||||
'header_font_id',
|
||||
'body_font_id',
|
||||
'auto_convert_quote',
|
||||
'auto_archive_quote',
|
||||
'auto_archive_invoice',
|
||||
'auto_email_invoice',
|
||||
'all_pages_footer',
|
||||
'all_pages_header',
|
||||
'show_currency_code',
|
||||
@ -170,6 +173,7 @@ class Account extends Eloquent
|
||||
'reset_counter_frequency_id',
|
||||
'payment_type_id',
|
||||
'gateway_fee_enabled',
|
||||
'send_item_details',
|
||||
'reset_counter_date',
|
||||
'custom_contact_label1',
|
||||
'custom_contact_label2',
|
||||
@ -232,6 +236,7 @@ class Account extends Eloquent
|
||||
public static $customLabels = [
|
||||
'balance_due',
|
||||
'credit_card',
|
||||
'delivery_note',
|
||||
'description',
|
||||
'discount',
|
||||
'due_date',
|
||||
@ -256,6 +261,7 @@ class Account extends Eloquent
|
||||
'tax',
|
||||
'terms',
|
||||
'unit_cost',
|
||||
'valid_until',
|
||||
'vat_number',
|
||||
];
|
||||
|
||||
@ -624,12 +630,12 @@ class Account extends Eloquent
|
||||
*
|
||||
* @return DateTime|null|string
|
||||
*/
|
||||
public function getDateTime($date = 'now')
|
||||
public function getDateTime($date = 'now', $formatted = false)
|
||||
{
|
||||
$date = $this->getDate($date);
|
||||
$date->setTimeZone(new \DateTimeZone($this->getTimezone()));
|
||||
|
||||
return $date;
|
||||
return $formatted ? $date->format($this->getCustomDateTimeFormat()) : $date;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -682,6 +688,17 @@ class Account extends Eloquent
|
||||
return Utils::formatMoney($amount, $currencyId, $countryId, $decorator);
|
||||
}
|
||||
|
||||
public function formatNumber($amount, $precision = 0)
|
||||
{
|
||||
if ($this->currency_id) {
|
||||
$currencyId = $this->currency_id;
|
||||
} else {
|
||||
$currencyId = DEFAULT_CURRENCY;
|
||||
}
|
||||
|
||||
return Utils::formatNumber($amount, $currencyId, $precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
@ -1780,6 +1797,11 @@ class Account extends Eloquent
|
||||
return url('/');
|
||||
}
|
||||
}
|
||||
|
||||
public function requiresAddressState() {
|
||||
return true;
|
||||
//return ! $this->country_id || $this->country_id == DEFAULT_COUNTRY;
|
||||
}
|
||||
}
|
||||
|
||||
Account::creating(function ($account)
|
||||
|
@ -95,8 +95,17 @@ class AccountGateway extends EntityModel
|
||||
*/
|
||||
public function isGateway($gatewayId)
|
||||
{
|
||||
if (is_array($gatewayId)) {
|
||||
foreach ($gatewayId as $id) {
|
||||
if ($this->gateway_id == $id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return $this->gateway_id == $gatewayId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $config
|
||||
@ -127,9 +136,9 @@ class AccountGateway extends EntityModel
|
||||
/**
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function getPublishableStripeKey()
|
||||
public function getPublishableKey()
|
||||
{
|
||||
if (! $this->isGateway(GATEWAY_STRIPE)) {
|
||||
if (! $this->isGateway([GATEWAY_STRIPE, GATEWAY_PAYMILL])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -254,7 +263,7 @@ class AccountGateway extends EntityModel
|
||||
return null;
|
||||
}
|
||||
|
||||
$stripe_key = $this->getPublishableStripeKey();
|
||||
$stripe_key = $this->getPublishableKey();
|
||||
|
||||
return substr(trim($stripe_key), 0, 8) == 'pk_test_' ? 'tartan' : 'production';
|
||||
}
|
||||
@ -272,7 +281,7 @@ class AccountGateway extends EntityModel
|
||||
public function isTestMode()
|
||||
{
|
||||
if ($this->isGateway(GATEWAY_STRIPE)) {
|
||||
return strpos($this->getPublishableStripeKey(), 'test') !== false;
|
||||
return strpos($this->getPublishableKey(), 'test') !== false;
|
||||
} else {
|
||||
return $this->getConfigField('testMode');
|
||||
}
|
||||
|
@ -137,4 +137,71 @@ class Activity extends Eloquent
|
||||
|
||||
return trans("texts.activity_{$activityTypeId}", $data);
|
||||
}
|
||||
|
||||
public function relatedEntityType()
|
||||
{
|
||||
switch ($this->activity_type_id) {
|
||||
case ACTIVITY_TYPE_CREATE_CLIENT:
|
||||
case ACTIVITY_TYPE_ARCHIVE_CLIENT:
|
||||
case ACTIVITY_TYPE_DELETE_CLIENT:
|
||||
case ACTIVITY_TYPE_RESTORE_CLIENT:
|
||||
case ACTIVITY_TYPE_CREATE_CREDIT:
|
||||
case ACTIVITY_TYPE_ARCHIVE_CREDIT:
|
||||
case ACTIVITY_TYPE_DELETE_CREDIT:
|
||||
case ACTIVITY_TYPE_RESTORE_CREDIT:
|
||||
return ENTITY_CLIENT;
|
||||
break;
|
||||
|
||||
case ACTIVITY_TYPE_CREATE_INVOICE:
|
||||
case ACTIVITY_TYPE_UPDATE_INVOICE:
|
||||
case ACTIVITY_TYPE_EMAIL_INVOICE:
|
||||
case ACTIVITY_TYPE_VIEW_INVOICE:
|
||||
case ACTIVITY_TYPE_ARCHIVE_INVOICE:
|
||||
case ACTIVITY_TYPE_DELETE_INVOICE:
|
||||
case ACTIVITY_TYPE_RESTORE_INVOICE:
|
||||
return ENTITY_INVOICE;
|
||||
break;
|
||||
|
||||
case ACTIVITY_TYPE_CREATE_PAYMENT:
|
||||
case ACTIVITY_TYPE_ARCHIVE_PAYMENT:
|
||||
case ACTIVITY_TYPE_DELETE_PAYMENT:
|
||||
case ACTIVITY_TYPE_RESTORE_PAYMENT:
|
||||
case ACTIVITY_TYPE_VOIDED_PAYMENT:
|
||||
case ACTIVITY_TYPE_REFUNDED_PAYMENT:
|
||||
case ACTIVITY_TYPE_FAILED_PAYMENT:
|
||||
return ENTITY_PAYMENT;
|
||||
break;
|
||||
|
||||
case ACTIVITY_TYPE_CREATE_QUOTE:
|
||||
case ACTIVITY_TYPE_UPDATE_QUOTE:
|
||||
case ACTIVITY_TYPE_EMAIL_QUOTE:
|
||||
case ACTIVITY_TYPE_VIEW_QUOTE:
|
||||
case ACTIVITY_TYPE_ARCHIVE_QUOTE:
|
||||
case ACTIVITY_TYPE_DELETE_QUOTE:
|
||||
case ACTIVITY_TYPE_RESTORE_QUOTE:
|
||||
case ACTIVITY_TYPE_APPROVE_QUOTE:
|
||||
return ENTITY_QUOTE;
|
||||
break;
|
||||
|
||||
case ACTIVITY_TYPE_CREATE_VENDOR:
|
||||
case ACTIVITY_TYPE_ARCHIVE_VENDOR:
|
||||
case ACTIVITY_TYPE_DELETE_VENDOR:
|
||||
case ACTIVITY_TYPE_RESTORE_VENDOR:
|
||||
case ACTIVITY_TYPE_CREATE_EXPENSE:
|
||||
case ACTIVITY_TYPE_ARCHIVE_EXPENSE:
|
||||
case ACTIVITY_TYPE_DELETE_EXPENSE:
|
||||
case ACTIVITY_TYPE_RESTORE_EXPENSE:
|
||||
case ACTIVITY_TYPE_UPDATE_EXPENSE:
|
||||
return ENTITY_EXPENSE;
|
||||
break;
|
||||
|
||||
case ACTIVITY_TYPE_CREATE_TASK:
|
||||
case ACTIVITY_TYPE_UPDATE_TASK:
|
||||
case ACTIVITY_TYPE_ARCHIVE_TASK:
|
||||
case ACTIVITY_TYPE_DELETE_TASK:
|
||||
case ACTIVITY_TYPE_RESTORE_TASK:
|
||||
return ENTITY_TASK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,7 +262,7 @@ class Client extends EntityModel
|
||||
// check if this client wasRecentlyCreated to ensure a new contact is
|
||||
// always created even if the request includes a contact id
|
||||
if (! $this->wasRecentlyCreated && $publicId && $publicId != '-1') {
|
||||
$contact = Contact::scope($publicId)->firstOrFail();
|
||||
$contact = Contact::scope($publicId)->whereClientId($this->id)->firstOrFail();
|
||||
} else {
|
||||
$contact = Contact::createNew();
|
||||
$contact->send_invoice = true;
|
||||
|
@ -155,18 +155,18 @@ class EntityModel extends Eloquent
|
||||
*/
|
||||
public function scopeScope($query, $publicId = false, $accountId = false)
|
||||
{
|
||||
if (! $accountId) {
|
||||
$accountId = Auth::user()->account_id;
|
||||
}
|
||||
|
||||
$query->where($this->getTable() .'.account_id', '=', $accountId);
|
||||
|
||||
// If 'false' is passed as the publicId return nothing rather than everything
|
||||
if (func_num_args() > 1 && ! $publicId && ! $accountId) {
|
||||
$query->where('id', '=', 0);
|
||||
return $query;
|
||||
}
|
||||
|
||||
if (! $accountId) {
|
||||
$accountId = Auth::user()->account_id;
|
||||
}
|
||||
|
||||
$query->where($this->getTable() .'.account_id', '=', $accountId);
|
||||
|
||||
if ($publicId) {
|
||||
if (is_array($publicId)) {
|
||||
$query->whereIn('public_id', $publicId);
|
||||
@ -182,6 +182,15 @@ class EntityModel extends Eloquent
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function scopeWithActiveOrSelected($query, $id = false)
|
||||
{
|
||||
return $query->withTrashed()
|
||||
->where(function ($query) use ($id) {
|
||||
$query->whereNull('deleted_at')
|
||||
->orWhere('id', '=', $id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $query
|
||||
*
|
||||
@ -427,4 +436,13 @@ class EntityModel extends Eloquent
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function equalTo($obj)
|
||||
{
|
||||
if (empty($obj->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->id == $obj->id && $this->getEntityType() == $obj->entityType;
|
||||
}
|
||||
}
|
||||
|
@ -369,7 +369,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
*/
|
||||
public function quote()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Invoice');
|
||||
return $this->belongsTo('App\Models\Invoice')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1093,67 +1093,6 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Recurr\Exception\MissingData
|
||||
*
|
||||
* @return bool|\Recurr\RecurrenceCollection
|
||||
*/
|
||||
public function getSchedule()
|
||||
{
|
||||
if (! $this->start_date || ! $this->is_recurring || ! $this->frequency_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$startDate = $this->getOriginal('last_sent_date') ?: $this->getOriginal('start_date');
|
||||
$startDate .= ' ' . $this->account->recurring_hour . ':00:00';
|
||||
$startDate = $this->account->getDateTime($startDate);
|
||||
$endDate = $this->end_date ? $this->account->getDateTime($this->getOriginal('end_date')) : null;
|
||||
$timezone = $this->account->getTimezone();
|
||||
|
||||
$rule = $this->getRecurrenceRule();
|
||||
$rule = new \Recurr\Rule("{$rule}", $startDate, $endDate, $timezone);
|
||||
|
||||
// Fix for months with less than 31 days
|
||||
$transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig();
|
||||
$transformerConfig->enableLastDayOfMonthFix();
|
||||
|
||||
$transformer = new \Recurr\Transformer\ArrayTransformer();
|
||||
$transformer->setConfig($transformerConfig);
|
||||
$dates = $transformer->transform($rule);
|
||||
|
||||
if (count($dates) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
*/
|
||||
public function getNextSendDate()
|
||||
{
|
||||
if (! $this->is_public) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->start_date && ! $this->last_sent_date) {
|
||||
$startDate = $this->getOriginal('start_date') . ' ' . $this->account->recurring_hour . ':00:00';
|
||||
|
||||
return $this->account->getDateTime($startDate);
|
||||
}
|
||||
|
||||
if (! $schedule = $this->getSchedule()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count($schedule) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $schedule[1]->getStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $invoice_date
|
||||
*
|
||||
@ -1258,7 +1197,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function getPrettySchedule($min = 1, $max = 10)
|
||||
public function getPrettySchedule($min = 0, $max = 10)
|
||||
{
|
||||
if (! $schedule = $this->getSchedule($max)) {
|
||||
return null;
|
||||
@ -1310,6 +1249,9 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
if (strpos($pdfString, 'data') === 0) {
|
||||
break;
|
||||
} else {
|
||||
if (Utils::isNinjaDev() || Utils::isTravis()) {
|
||||
Utils::logError('Failed to generate: ' . $i);
|
||||
}
|
||||
$pdfString = false;
|
||||
sleep(2);
|
||||
}
|
||||
|
@ -96,6 +96,25 @@ class Proposal extends EntityModel
|
||||
{
|
||||
return $this->invoice->invoice_number;
|
||||
}
|
||||
|
||||
public function getLink($forceOnsite = false, $forcePlain = false)
|
||||
{
|
||||
$invitation = $this->invitations->first();
|
||||
|
||||
return $invitation->getLink('proposal', $forceOnsite, $forcePlain);
|
||||
}
|
||||
|
||||
public function getHeadlessLink()
|
||||
{
|
||||
return sprintf('%s?phantomjs=true&phantomjs_secret=%s', $this->getLink(true, true), env('PHANTOMJS_SECRET'));
|
||||
}
|
||||
|
||||
public function getFilename($extension = 'pdf')
|
||||
{
|
||||
$entityType = $this->getEntityType();
|
||||
|
||||
return trans('texts.proposal') . '_' . $this->invoice->invoice_number . '.' . $extension;
|
||||
}
|
||||
}
|
||||
|
||||
Proposal::creating(function ($project) {
|
||||
|
@ -145,6 +145,18 @@ trait HasLogo
|
||||
return round($this->logo_size / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLogoName()
|
||||
{
|
||||
if (! $this->hasLogo()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->logo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -14,6 +14,7 @@ trait HasRecurrence
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
/*
|
||||
public function shouldSendToday()
|
||||
{
|
||||
if (! $this->user->confirmed) {
|
||||
@ -78,6 +79,101 @@ trait HasRecurrence
|
||||
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
|
||||
public function shouldSendToday()
|
||||
{
|
||||
if (! $this->user->confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->account;
|
||||
$timezone = $account->getTimezone();
|
||||
|
||||
if (! $this->start_date || Carbon::parse($this->start_date, $timezone)->isFuture()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->end_date && Carbon::parse($this->end_date, $timezone)->isPast()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->last_sent_date) {
|
||||
return true;
|
||||
} else {
|
||||
// check we don't send a few hours early due to timezone difference
|
||||
if (Utils::isNinja() && Carbon::now()->format('Y-m-d') != Carbon::now($timezone)->format('Y-m-d')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nextSendDate = $this->getNextSendDate();
|
||||
|
||||
if (! $nextSendDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->account->getDateTime() >= $nextSendDate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Recurr\Exception\MissingData
|
||||
*
|
||||
* @return bool|\Recurr\RecurrenceCollection
|
||||
*/
|
||||
public function getSchedule()
|
||||
{
|
||||
if (! $this->start_date || ! $this->frequency_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$startDate = $this->getOriginal('last_sent_date') ?: $this->getOriginal('start_date');
|
||||
$startDate .= ' ' . $this->account->recurring_hour . ':00:00';
|
||||
$timezone = $this->account->getTimezone();
|
||||
|
||||
$rule = $this->getRecurrenceRule();
|
||||
$rule = new \Recurr\Rule("{$rule}", $startDate, null, $timezone);
|
||||
|
||||
// Fix for months with less than 31 days
|
||||
$transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig();
|
||||
$transformerConfig->enableLastDayOfMonthFix();
|
||||
|
||||
$transformer = new \Recurr\Transformer\ArrayTransformer();
|
||||
$transformer->setConfig($transformerConfig);
|
||||
$dates = $transformer->transform($rule);
|
||||
|
||||
if (count($dates) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
*/
|
||||
public function getNextSendDate()
|
||||
{
|
||||
if (! $this->is_public) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->start_date && ! $this->last_sent_date) {
|
||||
$startDate = $this->getOriginal('start_date') . ' ' . $this->account->recurring_hour . ':00:00';
|
||||
|
||||
return $this->account->getDateTime($startDate);
|
||||
}
|
||||
|
||||
if (! $schedule = $this->getSchedule()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count($schedule) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $schedule[1]->getStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
@ -120,21 +216,9 @@ trait HasRecurrence
|
||||
}
|
||||
|
||||
if ($this->end_date) {
|
||||
$rule .= 'UNTIL=' . $this->getOriginal('end_date');
|
||||
$rule .= 'UNTIL=' . $this->getOriginal('end_date') . ' 24:00:00';
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/*
|
||||
public function shouldSendToday()
|
||||
{
|
||||
if (!$nextSendDate = $this->getNextSendDate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->account->getDateTime() >= $nextSendDate;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
@ -336,6 +336,7 @@ trait PresentsInvoice
|
||||
'custom_value1',
|
||||
'custom_value2',
|
||||
'delivery_note',
|
||||
'date',
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
|
@ -70,6 +70,7 @@ class User extends Authenticatable
|
||||
'google_2fa_secret',
|
||||
'google_2fa_phone',
|
||||
'remember_2fa_token',
|
||||
'slack_webhook_url',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -446,6 +447,29 @@ class User extends Authenticatable
|
||||
//$this->notify(new ResetPasswordNotification($token));
|
||||
app('App\Ninja\Mailers\UserMailer')->sendPasswordReset($this, $token);
|
||||
}
|
||||
|
||||
public function routeNotificationForSlack()
|
||||
{
|
||||
return $this->slack_webhook_url;
|
||||
}
|
||||
|
||||
public function hasAcceptedLatestTerms()
|
||||
{
|
||||
if (! NINJA_TERMS_VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->accepted_terms_version == NINJA_TERMS_VERSION;
|
||||
}
|
||||
|
||||
public function acceptLatestTerms($ip)
|
||||
{
|
||||
$this->accepted_terms_version = NINJA_TERMS_VERSION;
|
||||
$this->accepted_terms_timestamp = date('Y-m-d H:i:s');
|
||||
$this->accepted_terms_ip = $ip;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
User::created(function ($user)
|
||||
|
@ -216,8 +216,8 @@ class Vendor extends EntityModel
|
||||
{
|
||||
$publicId = isset($data['public_id']) ? $data['public_id'] : (isset($data['id']) ? $data['id'] : false);
|
||||
|
||||
if ($publicId && $publicId != '-1') {
|
||||
$contact = VendorContact::scope($publicId)->firstOrFail();
|
||||
if (! $this->wasRecentlyCreated && $publicId && $publicId != '-1') {
|
||||
$contact = VendorContact::scope($publicId)->whereVendorId($this->id)->firstOrFail();
|
||||
} else {
|
||||
$contact = VendorContact::createNew();
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class PaymentDatatable extends EntityDatatable
|
||||
[
|
||||
trans('texts.refund_payment'),
|
||||
function ($model) {
|
||||
$max_refund = number_format($model->amount - $model->refunded, 2);
|
||||
$max_refund = $model->amount - $model->refunded;
|
||||
$formatted = Utils::formatMoney($max_refund, $model->currency_id, $model->country_id);
|
||||
$symbol = Utils::getFromCache($model->currency_id ? $model->currency_id : 1, 'currencies')->symbol;
|
||||
$local = in_array($model->gateway_id, [GATEWAY_BRAINTREE, GATEWAY_STRIPE, GATEWAY_WEPAY]) || ! $model->gateway_id ? 0 : 1;
|
||||
|
@ -68,6 +68,15 @@ class ProductDatatable extends EntityDatatable
|
||||
return URL::to("products/{$model->public_id}/edit");
|
||||
},
|
||||
],
|
||||
[
|
||||
trans('texts.clone_product'),
|
||||
function ($model) {
|
||||
return URL::to("products/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('create', ENTITY_PRODUCT);
|
||||
},
|
||||
],
|
||||
[
|
||||
trans('texts.invoice_product'),
|
||||
function ($model) {
|
||||
|
@ -191,7 +191,7 @@ class ContactMailer extends Mailer
|
||||
$data = [
|
||||
'body' => $this->templateService->processVariables($body, $variables),
|
||||
'link' => $invitation->getLink(),
|
||||
'entityType' => $invoice->getEntityType(),
|
||||
'entityType' => $proposal ? ENTITY_PROPOSAL : $invoice->getEntityType(),
|
||||
'invoiceId' => $invoice->id,
|
||||
'invitation' => $invitation,
|
||||
'account' => $account,
|
||||
|
@ -6,6 +6,9 @@ use App\Models\Invoice;
|
||||
use Exception;
|
||||
use Mail;
|
||||
use Utils;
|
||||
use Postmark\PostmarkClient;
|
||||
use Postmark\Models\PostmarkException;
|
||||
use Postmark\Models\PostmarkAttachment;
|
||||
|
||||
/**
|
||||
* Class Mailer.
|
||||
@ -34,6 +37,25 @@ class Mailer
|
||||
'emails.'.$view.'_text',
|
||||
];
|
||||
|
||||
$toEmail = strtolower($toEmail);
|
||||
$replyEmail = $fromEmail;
|
||||
$fromEmail = CONTACT_EMAIL;
|
||||
//\Log::info("{$toEmail} | {$replyEmail} | $fromEmail");
|
||||
|
||||
// Optionally send for alternate domain
|
||||
if (! empty($data['fromEmail'])) {
|
||||
$fromEmail = $data['fromEmail'];
|
||||
}
|
||||
|
||||
if (config('services.postmark')) {
|
||||
return $this->sendPostmarkMail($toEmail, $fromEmail, $fromName, $replyEmail, $subject, $views, $data);
|
||||
} else {
|
||||
return $this->sendLaravelMail($toEmail, $fromEmail, $fromName, $replyEmail, $subject, $views, $data);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendLaravelMail($toEmail, $fromEmail, $fromName, $replyEmail, $subject, $views, $data = [])
|
||||
{
|
||||
if (Utils::isSelfHost()) {
|
||||
if (isset($data['account'])) {
|
||||
$account = $data['account'];
|
||||
@ -60,17 +82,7 @@ class Mailer
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) {
|
||||
$toEmail = strtolower($toEmail);
|
||||
$replyEmail = $fromEmail;
|
||||
$fromEmail = CONTACT_EMAIL;
|
||||
//\Log::info("{$toEmail} | {$replyEmail} | $fromEmail");
|
||||
|
||||
// Optionally send for alternate domain
|
||||
if (! empty($data['fromEmail'])) {
|
||||
$fromEmail = $data['fromEmail'];
|
||||
}
|
||||
|
||||
$response = Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $replyEmail, $subject, $data) {
|
||||
$message->to($toEmail)
|
||||
->from($fromEmail, $fromName)
|
||||
->replyTo($replyEmail, $fromName)
|
||||
@ -95,9 +107,77 @@ class Mailer
|
||||
}
|
||||
});
|
||||
|
||||
return $this->handleSuccess($response, $data);
|
||||
return $this->handleSuccess($data);
|
||||
} catch (Exception $exception) {
|
||||
return $this->handleFailure($exception);
|
||||
return $this->handleFailure($data, $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function sendPostmarkMail($toEmail, $fromEmail, $fromName, $replyEmail, $subject, $views, $data = [])
|
||||
{
|
||||
$htmlBody = view($views[0], $data)->render();
|
||||
$textBody = view($views[1], $data)->render();
|
||||
$attachments = [];
|
||||
|
||||
if (isset($data['account'])) {
|
||||
$account = $data['account'];
|
||||
$logoName = $account->getLogoName();
|
||||
if (strpos($htmlBody, 'cid:' . $logoName) !== false && $account->hasLogo()) {
|
||||
$attachments[] = PostmarkAttachment::fromFile($account->getLogoPath(), $logoName, null, 'cid:' . $logoName);
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($htmlBody, 'cid:invoiceninja-logo.png') !== false) {
|
||||
$attachments[] = PostmarkAttachment::fromFile(public_path('images/invoiceninja-logo.png'), 'invoiceninja-logo.png', null, 'cid:invoiceninja-logo.png');
|
||||
$attachments[] = PostmarkAttachment::fromFile(public_path('images/emails/icon-facebook.png'), 'icon-facebook.png', null, 'cid:icon-facebook.png');
|
||||
$attachments[] = PostmarkAttachment::fromFile(public_path('images/emails/icon-twitter.png'), 'icon-twitter.png', null, 'cid:icon-twitter.png');
|
||||
$attachments[] = PostmarkAttachment::fromFile(public_path('images/emails/icon-github.png'), 'icon-github.png', null, 'cid:icon-github.png');
|
||||
}
|
||||
|
||||
// Handle invoice attachments
|
||||
if (! empty($data['pdfString']) && ! empty($data['pdfFileName'])) {
|
||||
$attachments[] = PostmarkAttachment::fromRawData($data['pdfString'], $data['pdfFileName']);
|
||||
}
|
||||
if (! empty($data['ublString']) && ! empty($data['ublFileName'])) {
|
||||
$attachments[] = PostmarkAttachment::fromRawData($data['ublString'], $data['ublFileName']);
|
||||
}
|
||||
if (! empty($data['documents'])) {
|
||||
foreach ($data['documents'] as $document) {
|
||||
$attachments[] = PostmarkAttachment::fromRawData($document['data'], $document['name']);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$client = new PostmarkClient(config('services.postmark'));
|
||||
$message = [
|
||||
'To' => $toEmail,
|
||||
'From' => $fromEmail,
|
||||
'ReplyTo' => $replyEmail,
|
||||
'Subject' => $subject,
|
||||
'TextBody' => $textBody,
|
||||
'HtmlBody' => $htmlBody,
|
||||
'Attachments' => $attachments,
|
||||
];
|
||||
|
||||
if (! empty($data['bccEmail'])) {
|
||||
$message['Bcc'] = $data['bccEmail'];
|
||||
}
|
||||
|
||||
if (! empty($data['account'])) {
|
||||
$message['Tag'] = $data['account']->account_key;
|
||||
}
|
||||
|
||||
$response = $client->sendEmailBatch([$message]);
|
||||
if ($messageId = $response[0]->messageid) {
|
||||
return $this->handleSuccess($data, $messageId);
|
||||
} else {
|
||||
return $this->handleFailure($data, $response[0]->message);
|
||||
}
|
||||
} catch (PostmarkException $exception) {
|
||||
return $this->handleFailure($data, $exception->getMessage());
|
||||
} catch (Exception $exception) {
|
||||
Utils::logError(Utils::getErrorString($exception));
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,19 +187,11 @@ class Mailer
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function handleSuccess($response, $data)
|
||||
private function handleSuccess($data, $messageId = false)
|
||||
{
|
||||
if (isset($data['invitation'])) {
|
||||
$invitation = $data['invitation'];
|
||||
$invoice = $invitation->invoice;
|
||||
$messageId = false;
|
||||
|
||||
// Track the Postmark message id
|
||||
if (isset($_ENV['POSTMARK_API_TOKEN']) && $response) {
|
||||
$json = json_decode((string) $response->getBody());
|
||||
$messageId = $json->MessageID;
|
||||
}
|
||||
|
||||
$notes = isset($data['notes']) ? $data['notes'] : false;
|
||||
|
||||
if (! empty($data['proposal'])) {
|
||||
@ -137,35 +209,14 @@ class Mailer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function handleFailure($exception)
|
||||
private function handleFailure($data, $emailError)
|
||||
{
|
||||
if (isset($_ENV['POSTMARK_API_TOKEN']) && method_exists($exception, 'getResponse')) {
|
||||
$response = $exception->getResponse();
|
||||
|
||||
if (! $response) {
|
||||
$error = trans('texts.postmark_error', ['link' => link_to('https://status.postmarkapp.com/')]);
|
||||
Utils::logError($error);
|
||||
|
||||
if (config('queue.default') === 'sync') {
|
||||
return $error;
|
||||
} else {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$response = $response->getBody()->getContents();
|
||||
$response = json_decode($response);
|
||||
$emailError = nl2br($response->Message);
|
||||
} else {
|
||||
$emailError = $exception->getMessage();
|
||||
}
|
||||
|
||||
if (isset($data['invitation'])) {
|
||||
$invitation = $data['invitation'];
|
||||
$invitation->email_error = $emailError;
|
||||
$invitation->save();
|
||||
} elseif (! Utils::isNinjaProd()) {
|
||||
Utils::logError(Utils::getErrorString($exception));
|
||||
Utils::logError($emailError);
|
||||
}
|
||||
|
||||
return $emailError;
|
||||
|
@ -14,4 +14,11 @@ class AuthorizeNetAIMPaymentDriver extends BasePaymentDriver
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function createPayment($ref = false, $paymentMethod = null)
|
||||
{
|
||||
$ref = $this->purchaseResponse['transactionResponse']['transId'] ?: $this->purchaseResponse['refId'];
|
||||
|
||||
parent::createPayment($ref, $paymentMethod);
|
||||
}
|
||||
}
|
||||
|
@ -164,8 +164,8 @@ class BasePaymentDriver
|
||||
}
|
||||
|
||||
$url = 'payment/' . $this->invitation->invitation_key;
|
||||
if (request()->update) {
|
||||
$url .= '?update=true';
|
||||
if (request()->capture) {
|
||||
$url .= '?capture=true';
|
||||
}
|
||||
|
||||
$data = [
|
||||
@ -242,10 +242,13 @@ class BasePaymentDriver
|
||||
$rules = array_merge($rules, [
|
||||
'address1' => 'required',
|
||||
'city' => 'required',
|
||||
'state' => 'required',
|
||||
'postal_code' => 'required',
|
||||
'country_id' => 'required',
|
||||
]);
|
||||
|
||||
if ($this->account()->requiresAddressState()) {
|
||||
$rules['state'] = 'required';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,7 +269,7 @@ class BasePaymentDriver
|
||||
|
||||
public function completeOnsitePurchase($input = false, $paymentMethod = false)
|
||||
{
|
||||
$this->input = count($input) ? $input : false;
|
||||
$this->input = $input && count($input) ? $input : false;
|
||||
$gateway = $this->gateway();
|
||||
|
||||
if ($input) {
|
||||
@ -303,17 +306,19 @@ class BasePaymentDriver
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isTwoStep() || request()->update) {
|
||||
if ($this->isTwoStep() || request()->capture) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare and process payment
|
||||
$data = $this->paymentDetails($paymentMethod);
|
||||
|
||||
// TODO move to payment driver class
|
||||
if ($this->isGateway(GATEWAY_SAGE_PAY_DIRECT) || $this->isGateway(GATEWAY_SAGE_PAY_SERVER)) {
|
||||
$items = null;
|
||||
} elseif ($this->account()->send_item_details) {
|
||||
$items = $this->paymentItems();
|
||||
} else {
|
||||
//$items = $this->paymentItems();
|
||||
$items = null;
|
||||
}
|
||||
$response = $gateway->purchase($data)
|
||||
@ -369,7 +374,7 @@ class BasePaymentDriver
|
||||
|
||||
$item = new Item([
|
||||
'name' => $invoiceItem->product_key,
|
||||
'description' => $invoiceItem->notes,
|
||||
'description' => substr($invoiceItem->notes, 0, 100),
|
||||
'price' => $invoiceItem->cost,
|
||||
'quantity' => $invoiceItem->qty,
|
||||
]);
|
||||
@ -867,6 +872,7 @@ class BasePaymentDriver
|
||||
return [
|
||||
'amount' => $amount,
|
||||
'transactionReference' => $payment->transaction_reference,
|
||||
'currency' => $payment->client->getCurrencyCode(),
|
||||
];
|
||||
}
|
||||
|
||||
|
27
app/Ninja/PaymentDrivers/PaymillPaymentDriver.php
Normal file
27
app/Ninja/PaymentDrivers/PaymillPaymentDriver.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\PaymentDrivers;
|
||||
|
||||
class PaymillPaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
public function tokenize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function paymentDetails($paymentMethod = false)
|
||||
{
|
||||
$data = parent::paymentDetails($paymentMethod);
|
||||
|
||||
if ($paymentMethod) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (! empty($this->input['sourceToken'])) {
|
||||
$data['token'] = $this->input['sourceToken'];
|
||||
unset($data['card']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
|
||||
public function tokenize()
|
||||
{
|
||||
return $this->accountGateway->getPublishableStripeKey();
|
||||
return $this->accountGateway->getPublishableKey();
|
||||
}
|
||||
|
||||
public function rules()
|
||||
|
@ -44,6 +44,11 @@ class ExpensePresenter extends EntityPresenter
|
||||
return Utils::formatMoney($this->entity->amountWithTax(), $this->entity->expense_currency_id);
|
||||
}
|
||||
|
||||
public function currencyCode()
|
||||
{
|
||||
return Utils::getFromCache($this->entity->expense_currency_id, 'currencies')->code;
|
||||
}
|
||||
|
||||
public function taxAmount()
|
||||
{
|
||||
return Utils::formatMoney($this->entity->taxAmount(), $this->entity->expense_currency_id);
|
||||
|
@ -27,10 +27,16 @@ class ProductPresenter extends EntityPresenter
|
||||
public function moreActions()
|
||||
{
|
||||
$product = $this->entity;
|
||||
$actions = [];
|
||||
|
||||
if (! $product->trashed()) {
|
||||
if (auth()->user()->can('create', ENTITY_PRODUCT)) {
|
||||
$actions[] = ['url' => 'javascript:submitAction("clone")', 'label' => trans('texts.clone_product')];
|
||||
}
|
||||
if (auth()->user()->can('create', ENTITY_INVOICE)) {
|
||||
$actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans('texts.invoice_product')];
|
||||
}
|
||||
if (count($actions)) {
|
||||
$actions[] = DropdownButton::DIVIDER;
|
||||
}
|
||||
$actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans("texts.archive_product")];
|
||||
|
@ -16,7 +16,7 @@ class ProposalPresenter extends EntityPresenter
|
||||
$invitation = $proposal->invitations->first();
|
||||
$actions = [];
|
||||
|
||||
$actions[] = ['url' => $invitation->getLink('proposal'), 'label' => trans("texts.view_as_recipient")];
|
||||
$actions[] = ['url' => $invitation->getLink('proposal'), 'label' => trans("texts.view_in_portal")];
|
||||
|
||||
$actions[] = DropdownButton::DIVIDER;
|
||||
|
||||
|
@ -2,7 +2,13 @@
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Utils;
|
||||
use Auth;
|
||||
use Carbon;
|
||||
use DateInterval;
|
||||
use DatePeriod;
|
||||
use stdClass;
|
||||
use App\Models\Client;
|
||||
|
||||
class AbstractReport
|
||||
{
|
||||
@ -13,6 +19,7 @@ class AbstractReport
|
||||
|
||||
public $totals = [];
|
||||
public $data = [];
|
||||
public $chartData = [];
|
||||
|
||||
public function __construct($startDate, $endDate, $isExport, $options = false)
|
||||
{
|
||||
@ -69,7 +76,7 @@ class AbstractReport
|
||||
}
|
||||
|
||||
if (strpos($field, 'date') !== false) {
|
||||
$class[] = 'group-date-' . (isset($this->options['group_dates_by']) ? $this->options['group_dates_by'] : 'monthyear');
|
||||
$class[] = 'group-date-' . (isset($this->options['group']) ? $this->options['group'] : 'monthyear');
|
||||
} elseif (in_array($field, ['client', 'vendor', 'product', 'user', 'method', 'category', 'project'])) {
|
||||
$class[] = 'group-letter-100';
|
||||
} elseif (in_array($field, ['amount', 'paid', 'balance'])) {
|
||||
@ -141,4 +148,166 @@ class AbstractReport
|
||||
|
||||
return join('', $reportParts);
|
||||
}
|
||||
|
||||
protected function getDimension($entity)
|
||||
{
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
if ($subgroup == 'user') {
|
||||
return $entity->user->getDisplayName();
|
||||
} elseif ($subgroup == 'client') {
|
||||
if ($entity instanceof Client) {
|
||||
return $entity->getDisplayName();
|
||||
} elseif ($entity->client) {
|
||||
return $entity->client->getDisplayName();
|
||||
} else {
|
||||
return trans('texts.unset');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function addChartData($dimension, $date, $amount)
|
||||
{
|
||||
if (! isset($this->chartData[$dimension])) {
|
||||
$this->chartData[$dimension] = [];
|
||||
}
|
||||
|
||||
$date = $this->formatDate($date);
|
||||
|
||||
if (! isset($this->chartData[$dimension][$date])) {
|
||||
$this->chartData[$dimension][$date] = 0;
|
||||
}
|
||||
|
||||
$this->chartData[$dimension][$date] += $amount;
|
||||
}
|
||||
|
||||
public function chartGroupBy()
|
||||
{
|
||||
$groupBy = empty($this->options['group']) ? 'day' : $this->options['group'];
|
||||
|
||||
if ($groupBy == 'monthyear') {
|
||||
$groupBy = 'month';
|
||||
}
|
||||
|
||||
return strtoupper($groupBy);
|
||||
}
|
||||
|
||||
protected function formatDate($date)
|
||||
{
|
||||
if (! $date instanceof \DateTime) {
|
||||
$date = new \DateTime($date);
|
||||
}
|
||||
|
||||
$groupBy = $this->chartGroupBy();
|
||||
$dateFormat = $groupBy == 'DAY' ? 'z' : ($groupBy == 'MONTH' ? 'm' : '');
|
||||
|
||||
return $date->format('Y' . $dateFormat);
|
||||
}
|
||||
|
||||
public function getLineChartData()
|
||||
{
|
||||
$startDate = date_create($this->startDate);
|
||||
$endDate = date_create($this->endDate);
|
||||
$groupBy = $this->chartGroupBy();
|
||||
|
||||
$datasets = [];
|
||||
$labels = [];
|
||||
|
||||
foreach ($this->chartData as $dimension => $data) {
|
||||
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
|
||||
$intervalStartDate = Carbon::instance($startDate);
|
||||
$intervalEndDate = Carbon::instance($endDate);
|
||||
|
||||
// round dates to match grouping
|
||||
$intervalStartDate->hour(0)->minute(0)->second(0);
|
||||
$intervalEndDate->hour(24)->minute(0)->second(0);
|
||||
if ($groupBy == 'MONTHYEAR' || $groupBy == 'YEAR') {
|
||||
$intervalStartDate->day(1);
|
||||
$intervalEndDate->addMonth(1)->day(1);
|
||||
}
|
||||
if ($groupBy == 'YEAR') {
|
||||
$intervalStartDate->month(1);
|
||||
$intervalEndDate->month(12);
|
||||
}
|
||||
|
||||
$period = new DatePeriod($intervalStartDate, $interval, $intervalEndDate);
|
||||
$records = [];
|
||||
|
||||
foreach ($period as $date) {
|
||||
$labels[] = $date->format('m/d/Y');
|
||||
$date = $this->formatDate($date);
|
||||
$records[] = isset($data[$date]) ? $data[$date] : 0;
|
||||
}
|
||||
|
||||
$record = new stdClass();
|
||||
$datasets[] = $record;
|
||||
$color = Utils::brewerColorRGB(count($datasets));
|
||||
|
||||
$record->data = $records;
|
||||
$record->label = $dimension;
|
||||
$record->lineTension = 0;
|
||||
$record->borderWidth = 3;
|
||||
$record->borderColor = "rgba({$color}, 1)";
|
||||
$record->backgroundColor = "rgba(255,255,255,0)";
|
||||
}
|
||||
|
||||
$data = new stdClass();
|
||||
$data->labels = $labels;
|
||||
$data->datasets = $datasets;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function isLineChartEnabled()
|
||||
{
|
||||
return $this->options['group'];
|
||||
}
|
||||
|
||||
public function isPieChartEnabled()
|
||||
{
|
||||
return $this->options['subgroup'];
|
||||
}
|
||||
|
||||
public function getPieChartData()
|
||||
{
|
||||
if (! $this->isPieChartEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$datasets = [];
|
||||
$labels = [];
|
||||
$totals = [];
|
||||
|
||||
foreach ($this->chartData as $dimension => $data) {
|
||||
foreach ($data as $date => $value) {
|
||||
if (! isset($totals[$dimension])) {
|
||||
$totals[$dimension] = 0;
|
||||
}
|
||||
|
||||
$totals[$dimension] += $value;
|
||||
}
|
||||
}
|
||||
|
||||
$response = new stdClass();
|
||||
$response->labels = [];
|
||||
|
||||
$datasets = new stdClass();
|
||||
$datasets->data = [];
|
||||
$datasets->backgroundColor = [];
|
||||
|
||||
foreach ($totals as $dimension => $value) {
|
||||
$response->labels[] = $dimension;
|
||||
$datasets->data[] = $value;
|
||||
$datasets->lineTension = 0;
|
||||
$datasets->borderWidth = 3;
|
||||
|
||||
$color = count($totals) ? Utils::brewerColorRGB(count($response->labels)) : '51,122,183';
|
||||
$datasets->borderColor[] = "rgba({$color}, 1)";
|
||||
$datasets->backgroundColor[] = "rgba({$color}, 0.1)";
|
||||
}
|
||||
|
||||
$response->datasets = [$datasets];
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ class ActivityReport extends AbstractReport
|
||||
|
||||
$startDate = $this->startDate;;
|
||||
$endDate = $this->endDate;
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$activities = Activity::scope()
|
||||
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'task', 'expense', 'account')
|
||||
@ -37,8 +38,16 @@ class ActivityReport extends AbstractReport
|
||||
$activity->present()->user,
|
||||
$this->isExport ? strip_tags($activity->getMessage()) : $activity->getMessage(),
|
||||
];
|
||||
|
||||
if ($subgroup == 'category') {
|
||||
$dimension = trans('texts.' . $activity->relatedEntityType());
|
||||
} else {
|
||||
$dimension = $this->getDimension($activity);
|
||||
}
|
||||
|
||||
$this->addChartData($dimension, $activity->created_at, 1);
|
||||
}
|
||||
|
||||
//dd($this->getChartData());
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ class AgingReport extends AbstractReport
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
@ -56,6 +57,13 @@ class AgingReport extends AbstractReport
|
||||
//$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
//$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
//$this->addToTotals($client->currency_id, 'balance', $invoice->balance);
|
||||
|
||||
if ($subgroup == 'age') {
|
||||
$dimension = trans('texts.' .$invoice->present()->ageGroup);
|
||||
} else {
|
||||
$dimension = $this->getDimension($client);
|
||||
}
|
||||
$this->addChartData($dimension, $invoice->invoice_date, $invoice->balance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ class ClientReport extends AbstractReport
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
@ -55,6 +56,13 @@ class ClientReport extends AbstractReport
|
||||
foreach ($client->invoices as $invoice) {
|
||||
$amount += $invoice->amount;
|
||||
$paid += $invoice->getAmountPaid();
|
||||
|
||||
if ($subgroup == 'country') {
|
||||
$dimension = $client->present()->country;
|
||||
} else {
|
||||
$dimension = $this->getDimension($client);
|
||||
}
|
||||
$this->addChartData($dimension, $invoice->invoice_date, $invoice->amount);
|
||||
}
|
||||
|
||||
$row = [
|
||||
|
@ -22,11 +22,12 @@ class CreditReport extends AbstractReport
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
->withArchived()
|
||||
->with(['user', 'credits' => function ($query) {
|
||||
->with(['contacts', 'user', 'credits' => function ($query) {
|
||||
$query->where('credit_date', '>=', $this->startDate)
|
||||
->where('credit_date', '<=', $this->endDate)
|
||||
->withArchived();
|
||||
@ -39,6 +40,9 @@ class CreditReport extends AbstractReport
|
||||
foreach ($client->credits as $credit) {
|
||||
$amount += $credit->amount;
|
||||
$balance += $credit->balance;
|
||||
|
||||
$dimension = $this->getDimension($client);
|
||||
$this->addChartData($dimension, $credit->credit_date, $credit->amount);
|
||||
}
|
||||
|
||||
if (! $amount && ! $balance) {
|
||||
|
@ -24,6 +24,7 @@ class DocumentReport extends AbstractReport
|
||||
$account = auth()->user()->account;
|
||||
$filter = $this->options['document_filter'];
|
||||
$exportFormat = $this->options['export_format'];
|
||||
$subgroup = $this->options['subgroup'];
|
||||
$records = false;
|
||||
|
||||
if (! $filter || $filter == ENTITY_INVOICE) {
|
||||
@ -70,12 +71,16 @@ class DocumentReport extends AbstractReport
|
||||
|
||||
foreach ($records as $record) {
|
||||
foreach ($record->documents as $document) {
|
||||
$date = $record->getEntityType() == ENTITY_INVOICE ? $record->invoice_date : $record->expense_date;
|
||||
$this->data[] = [
|
||||
$this->isExport ? $document->name : link_to($document->getUrl(), $document->name),
|
||||
$record->client ? ($this->isExport ? $record->client->getDisplayName() : $record->client->present()->link) : '',
|
||||
$this->isExport ? $record->present()->titledName : ($filter ? $record->present()->link : link_to($record->present()->url, $record->present()->titledName)),
|
||||
$record->getEntityType() == ENTITY_INVOICE ? $record->invoice_date : $record->expense_date,
|
||||
$date,
|
||||
];
|
||||
|
||||
$dimension = $this->getDimension($record);
|
||||
$this->addChartData($dimension, $date, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,10 @@ class ExpenseReport extends AbstractReport
|
||||
$columns['tax'] = ['columnSelector-false'];
|
||||
}
|
||||
|
||||
if ($this->isExport) {
|
||||
$columns['currency'] = ['columnSelector-false'];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@ -34,6 +38,7 @@ class ExpenseReport extends AbstractReport
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$exportFormat = $this->options['export_format'];
|
||||
$subgroup = $this->options['subgroup'];
|
||||
$with = ['client.contacts', 'vendor'];
|
||||
$hasTaxRates = TaxRate::scope()->count();
|
||||
|
||||
@ -84,10 +89,24 @@ class ExpenseReport extends AbstractReport
|
||||
$row[] = $expense->present()->taxAmount;
|
||||
}
|
||||
|
||||
if ($this->isExport) {
|
||||
$row[] = $expense->present()->currencyCode;
|
||||
}
|
||||
|
||||
$this->data[] = $row;
|
||||
|
||||
$this->addToTotals($expense->expense_currency_id, 'amount', $amount);
|
||||
$this->addToTotals($expense->invoice_currency_id, 'amount', 0);
|
||||
|
||||
if ($subgroup == 'category') {
|
||||
$dimension = $expense->present()->category;
|
||||
} elseif ($subgroup == 'vendor') {
|
||||
$dimension = $expense->vendor ? $expense->vendor->name : trans('texts.unset');
|
||||
} else {
|
||||
$dimension = $this->getDimension($expense);
|
||||
}
|
||||
|
||||
$this->addChartData($dimension, $expense->expense_date, $amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ class InvoiceReport extends AbstractReport
|
||||
$account = Auth::user()->account;
|
||||
$statusIds = $this->options['status_ids'];
|
||||
$exportFormat = $this->options['export_format'];
|
||||
$subgroup = $this->options['subgroup'];
|
||||
$hasTaxRates = TaxRate::scope()->count();
|
||||
|
||||
$clients = Client::scope()
|
||||
@ -122,6 +123,14 @@ class InvoiceReport extends AbstractReport
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
$this->addToTotals($client->currency_id, 'balance', $invoice->balance);
|
||||
|
||||
if ($subgroup == 'status') {
|
||||
$dimension = $invoice->statusLabel();
|
||||
} else {
|
||||
$dimension = $this->getDimension($client);
|
||||
}
|
||||
|
||||
$this->addChartData($dimension, $invoice->invoice_date, $invoice->amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ class PaymentReport extends AbstractReport
|
||||
$account = Auth::user()->account;
|
||||
$currencyType = $this->options['currency_type'];
|
||||
$invoiceMap = [];
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$payments = Payment::scope()
|
||||
->orderBy('payment_date', 'desc')
|
||||
@ -80,6 +81,15 @@ class PaymentReport extends AbstractReport
|
||||
}
|
||||
}
|
||||
|
||||
if ($subgroup == 'method') {
|
||||
$dimension = $payment->present()->method;
|
||||
} else {
|
||||
$dimension = $this->getDimension($payment);
|
||||
}
|
||||
|
||||
$convertedAmount = $currencyType == 'converted' ? ($invoice->amount * $payment->exchange_rate) : $invoice->amount;
|
||||
$this->addChartData($dimension, $payment->payment_date, $convertedAmount);
|
||||
|
||||
$lastInvoiceId = $invoice->id;
|
||||
}
|
||||
}
|
||||
|
@ -46,11 +46,12 @@ class ProductReport extends AbstractReport
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$statusIds = $this->options['status_ids'];
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with('contacts', 'user')
|
||||
->with(['invoices' => function ($query) use ($statusIds) {
|
||||
$query->invoices()
|
||||
->withArchived()
|
||||
@ -90,6 +91,13 @@ class ProductReport extends AbstractReport
|
||||
|
||||
$this->data[] = $row;
|
||||
|
||||
if ($subgroup == 'product') {
|
||||
$dimension = $item->product_key;
|
||||
} else {
|
||||
$dimension = $this->getDimension($client);
|
||||
}
|
||||
|
||||
$this->addChartData($dimension, $invoice->invoice_date, $invoice->amount);
|
||||
}
|
||||
|
||||
//$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
|
@ -22,10 +22,11 @@ class ProfitAndLossReport extends AbstractReport
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$payments = Payment::scope()
|
||||
->orderBy('payment_date', 'desc')
|
||||
->with('client.contacts', 'invoice')
|
||||
->with('client.contacts', 'invoice', 'user')
|
||||
->withArchived()
|
||||
->excludeFailed()
|
||||
->where('payment_date', '>=', $this->startDate)
|
||||
@ -48,6 +49,13 @@ class ProfitAndLossReport extends AbstractReport
|
||||
$this->addToTotals($client->currency_id, 'revenue', $payment->getCompletedAmount(), $payment->present()->month);
|
||||
$this->addToTotals($client->currency_id, 'expenses', 0, $payment->present()->month);
|
||||
$this->addToTotals($client->currency_id, 'profit', $payment->getCompletedAmount(), $payment->present()->month);
|
||||
|
||||
if ($subgroup == 'type') {
|
||||
$dimension = trans('texts.payment');
|
||||
} else {
|
||||
$dimension = $this->getDimension($payment);
|
||||
}
|
||||
$this->addChartData($dimension, $payment->payment_date, $payment->getCompletedAmount());
|
||||
}
|
||||
|
||||
$expenses = Expense::scope()
|
||||
@ -70,6 +78,13 @@ class ProfitAndLossReport extends AbstractReport
|
||||
$this->addToTotals($expense->expense_currency_id, 'revenue', 0, $expense->present()->month);
|
||||
$this->addToTotals($expense->expense_currency_id, 'expenses', $expense->amountWithTax(), $expense->present()->month);
|
||||
$this->addToTotals($expense->expense_currency_id, 'profit', $expense->amountWithTax() * -1, $expense->present()->month);
|
||||
|
||||
if ($subgroup == 'type') {
|
||||
$dimension = trans('texts.expense');
|
||||
} else {
|
||||
$dimension = $this->getDimension($expense);
|
||||
}
|
||||
$this->addChartData($dimension, $expense->expense_date, $expense->amountWithTax());
|
||||
}
|
||||
|
||||
//$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
|
@ -43,6 +43,7 @@ class QuoteReport extends AbstractReport
|
||||
$statusIds = $this->options['status_ids'];
|
||||
$exportFormat = $this->options['export_format'];
|
||||
$hasTaxRates = TaxRate::scope()->count();
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
@ -102,6 +103,14 @@ class QuoteReport extends AbstractReport
|
||||
$this->data[] = $row;
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
|
||||
if ($subgroup == 'status') {
|
||||
$dimension = $invoice->statusLabel();
|
||||
} else {
|
||||
$dimension = $this->getDimension($client);
|
||||
}
|
||||
|
||||
$this->addChartData($dimension, $invoice->invoice_date, $invoice->amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ class TaskReport extends AbstractReport
|
||||
{
|
||||
$startDate = date_create($this->startDate);
|
||||
$endDate = date_create($this->endDate);
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$tasks = Task::scope()
|
||||
->orderBy('created_at', 'desc')
|
||||
@ -52,6 +53,13 @@ class TaskReport extends AbstractReport
|
||||
|
||||
$this->addToTotals($currencyId, 'duration', $duration);
|
||||
$this->addToTotals($currencyId, 'amount', $amount);
|
||||
|
||||
if ($subgroup == 'project') {
|
||||
$dimension = $task->present()->project;
|
||||
} else {
|
||||
$dimension = $this->getDimension($task);
|
||||
}
|
||||
$this->addChartData($dimension, $task->created_at, round($duration / 60 / 60, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,11 +24,12 @@ class TaxRateReport extends AbstractReport
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$subgroup = $this->options['subgroup'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->orderBy('name')
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with('contacts', 'user')
|
||||
->with(['invoices' => function ($query) {
|
||||
$query->with('invoice_items')
|
||||
->withArchived()
|
||||
@ -85,6 +86,9 @@ class TaxRateReport extends AbstractReport
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $tax['amount']);
|
||||
$this->addToTotals($client->currency_id, 'paid', $tax['paid']);
|
||||
|
||||
$dimension = $this->getDimension($client);
|
||||
$this->addChartData($dimension, $invoice->invoice_date, $tax['amount']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ class AccountRepository
|
||||
foreach ($clients as $client) {
|
||||
if ($client->name) {
|
||||
$data['clients'][] = [
|
||||
'value' => ($account->clientNumbersEnabled() && $client->id_number ? $client->id_number . ': ' : '') . $client->name,
|
||||
'value' => ($client->id_number ? $client->id_number . ': ' : '') . $client->name,
|
||||
'tokens' => implode(',', [$client->name, $client->id_number, $client->vat_number, $client->work_phone]),
|
||||
'url' => $client->present()->url,
|
||||
];
|
||||
|
@ -30,7 +30,7 @@ class ActivityRepository
|
||||
|
||||
$activity->activity_type_id = $activityTypeId;
|
||||
$activity->adjustment = $balanceChange;
|
||||
$activity->client_id = $client ? $client->id : 0;
|
||||
$activity->client_id = $client ? $client->id : null;
|
||||
$activity->balance = $client ? ($client->balance + $balanceChange) : 0;
|
||||
$activity->notes = $notes ?: '';
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Ninja\Repositories;
|
||||
|
||||
use App\Jobs\PurgeClientData;
|
||||
use App\Events\ClientWasCreated;
|
||||
use App\Events\ClientWasUpdated;
|
||||
use App\Models\Client;
|
||||
@ -75,6 +76,11 @@ class ClientRepository extends BaseRepository
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function purge($client)
|
||||
{
|
||||
dispatch(new PurgeClientData($client));
|
||||
}
|
||||
|
||||
public function save($data, $client = null)
|
||||
{
|
||||
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
|
||||
|
@ -54,8 +54,8 @@ class ExpenseRepository extends BaseRepository
|
||||
->leftJoin('expense_categories', 'expenses.expense_category_id', '=', 'expense_categories.id')
|
||||
->where('expenses.account_id', '=', $accountid)
|
||||
->where('contacts.deleted_at', '=', null)
|
||||
->where('vendors.deleted_at', '=', null)
|
||||
->where('clients.deleted_at', '=', null)
|
||||
//->where('vendors.deleted_at', '=', null)
|
||||
//->where('clients.deleted_at', '=', null)
|
||||
->where(function ($query) { // handle when client isn't set
|
||||
$query->where('contacts.is_primary', '=', true)
|
||||
->orWhere('contacts.is_primary', '=', null);
|
||||
|
@ -16,6 +16,7 @@ class NinjaRepository
|
||||
|
||||
$company = $account->company;
|
||||
$company->fill($data);
|
||||
$company->plan_expires = $company->plan_expires ?: null;
|
||||
$company->save();
|
||||
}
|
||||
}
|
||||
|
@ -237,6 +237,9 @@ class AccountTransformer extends EntityTransformer
|
||||
'header_font_id' => (int) $account->header_font_id,
|
||||
'body_font_id' => (int) $account->body_font_id,
|
||||
'auto_convert_quote' => (bool) $account->auto_convert_quote,
|
||||
'auto_archive_quote' => (bool) $account->auto_archive_quote,
|
||||
'auto_archive_invoice' => (bool) $account->auto_archive_invoice,
|
||||
'auto_email_invoice' => (bool) $account->auto_email_invoice,
|
||||
'all_pages_footer' => (bool) $account->all_pages_footer,
|
||||
'all_pages_header' => (bool) $account->all_pages_header,
|
||||
'show_currency_code' => (bool) $account->show_currency_code,
|
||||
@ -272,6 +275,7 @@ class AccountTransformer extends EntityTransformer
|
||||
'reset_counter_frequency_id' => (int) $account->reset_counter_frequency_id,
|
||||
'payment_type_id' => (int) $account->payment_type_id,
|
||||
'gateway_fee_enabled' => (bool) $account->gateway_fee_enabled,
|
||||
'send_item_details' => (bool) $account->send_item_details,
|
||||
'reset_counter_date' => $account->reset_counter_date,
|
||||
'custom_contact_label1' => $account->custom_contact_label1,
|
||||
'custom_contact_label2' => $account->custom_contact_label2,
|
||||
|
76
app/Notifications/PaymentCreated.php
Normal file
76
app/Notifications/PaymentCreated.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
|
||||
class PaymentCreated extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
protected $payment;
|
||||
protected $invoice;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($payment, $invoice)
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
$this->payment = $payment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['slack'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$url = 'http://www.ninja.test/subscriptions/create';
|
||||
|
||||
return (new SlackMessage)
|
||||
->from(APP_NAME)
|
||||
->image('https://app.invoiceninja.com/favicon-v2.png')
|
||||
->content(trans('texts.received_new_payment'))
|
||||
->attachment(function ($attachment) {
|
||||
$invoiceName = $this->invoice->present()->titledName;
|
||||
$invoiceLink = $this->invoice->present()->multiAccountLink;
|
||||
$attachment->title($invoiceName, $invoiceLink)
|
||||
->fields([
|
||||
trans('texts.client') => $this->invoice->client->getDisplayName(),
|
||||
trans('texts.amount') => $this->payment->present()->amount,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\ClientWasDeleted' => [
|
||||
'App\Listeners\ActivityListener@deletedClient',
|
||||
'App\Listeners\SubscriptionListener@deletedClient',
|
||||
'App\Listeners\HistoryListener@deletedClient',
|
||||
],
|
||||
'App\Events\ClientWasRestored' => [
|
||||
'App\Listeners\ActivityListener@restoredClient',
|
||||
@ -54,6 +55,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Listeners\ActivityListener@deletedInvoice',
|
||||
'App\Listeners\TaskListener@deletedInvoice',
|
||||
'App\Listeners\ExpenseListener@deletedInvoice',
|
||||
'App\Listeners\HistoryListener@deletedInvoice',
|
||||
],
|
||||
'App\Events\InvoiceWasRestored' => [
|
||||
'App\Listeners\ActivityListener@restoredInvoice',
|
||||
@ -89,6 +91,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
],
|
||||
'App\Events\QuoteWasDeleted' => [
|
||||
'App\Listeners\ActivityListener@deletedQuote',
|
||||
'App\Listeners\HistoryListener@deletedQuote',
|
||||
],
|
||||
'App\Events\QuoteWasRestored' => [
|
||||
'App\Listeners\ActivityListener@restoredQuote',
|
||||
@ -188,6 +191,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\TaskWasDeleted' => [
|
||||
'App\Listeners\ActivityListener@deletedTask',
|
||||
'App\Listeners\SubscriptionListener@deletedTask',
|
||||
'App\Listeners\HistoryListener@deletedTask',
|
||||
],
|
||||
|
||||
// Vendor events
|
||||
@ -219,6 +223,17 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\ExpenseWasDeleted' => [
|
||||
'App\Listeners\ActivityListener@deletedExpense',
|
||||
'App\Listeners\SubscriptionListener@deletedExpense',
|
||||
'App\Listeners\HistoryListener@deletedExpense',
|
||||
],
|
||||
|
||||
// Project events
|
||||
'App\Events\ProjectWasDeleted' => [
|
||||
'App\Listeners\HistoryListener@deletedProject',
|
||||
],
|
||||
|
||||
// Proposal events
|
||||
'App\Events\ProposalWasDeleted' => [
|
||||
'App\Listeners\HistoryListener@deletedProposal',
|
||||
],
|
||||
|
||||
'Illuminate\Queue\Events\JobExceptionOccurred' => [
|
||||
|
@ -393,12 +393,14 @@ class ImportService
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// if the invoice number is blank we'll assign it
|
||||
if ($entityType == ENTITY_INVOICE && ! $data['invoice_number']) {
|
||||
$account = Auth::user()->account;
|
||||
$invoice = Invoice::createNew();
|
||||
$data['invoice_number'] = $account->getNextNumber($invoice);
|
||||
}
|
||||
*/
|
||||
|
||||
if (EntityModel::validate($data, $entityType) !== true) {
|
||||
return false;
|
||||
|
@ -110,7 +110,14 @@ class InvoiceService extends BaseService
|
||||
*/
|
||||
public function convertQuote($quote)
|
||||
{
|
||||
return $this->invoiceRepo->cloneInvoice($quote, $quote->id);
|
||||
$account = $quote->account;
|
||||
$invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id);
|
||||
|
||||
if ($account->auto_archive_quote) {
|
||||
$this->invoiceRepo->archive($quote);
|
||||
}
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,6 +148,10 @@ class InvoiceService extends BaseService
|
||||
$quote->markApproved();
|
||||
}
|
||||
|
||||
if ($account->auto_archive_quote) {
|
||||
$this->invoiceRepo->archive($quote);
|
||||
}
|
||||
|
||||
return $invitation->invitation_key;
|
||||
}
|
||||
|
||||
|
@ -137,9 +137,11 @@ class PaymentService extends BaseService
|
||||
try {
|
||||
return $paymentDriver->completeOnsitePurchase(false, $paymentMethod);
|
||||
} catch (Exception $exception) {
|
||||
if (! Auth::check()) {
|
||||
$subject = trans('texts.auto_bill_failed', ['invoice_number' => $invoice->invoice_number]);
|
||||
$message = sprintf('%s: %s', ucwords($paymentDriver->providerName()), $exception->getMessage());
|
||||
//$message .= $exception->getTraceAsString();
|
||||
Utils::logError($message, 'PHP', true);
|
||||
if (! Auth::check()) {
|
||||
$mailer = app('App\Ninja\Mailers\UserMailer');
|
||||
$mailer->sendMessage($invoice->user, $subject, $message, [
|
||||
'invoice' => $invoice
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user