Merge branch 'release-3.5.0'

This commit is contained in:
Hillel Coren 2017-07-17 20:21:26 +03:00
commit cbef2425f2
160 changed files with 5012 additions and 1669 deletions

View File

@ -87,7 +87,6 @@ WEPAY_CLIENT_ID=
WEPAY_CLIENT_SECRET=
WEPAY_ENVIRONMENT=production # production or stage
WEPAY_AUTO_UPDATE=true # Requires permission from WePay
WEPAY_ENABLE_CANADA=true
WEPAY_FEE_PAYER=payee
WEPAY_APP_FEE_CC_MULTIPLIER=0
WEPAY_APP_FEE_ACH_MULTIPLIER=0

View File

@ -14,14 +14,14 @@ Watch this [YouTube video](https://www.youtube.com/watch?v=xHGKvadapbA) for an o
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $20 per year white-label license to remove our branding.
The [self-host zip](https://www.invoiceninja.com/self-host/) includes all third party libraries whereas downloading the code from GitHub requires using Composer to install the dependencies.
The self-host zip includes all third party libraries whereas downloading the code from GitHub requires using Composer to install the dependencies.
## Affiliates Programs
* Referral program (we pay you): $100 per sign up paid over 3 years - [Learn more](https://www.invoiceninja.com/referral-program/)
* White-label reseller (you pay us): $500 sign up fee and either 10% of revenue or $1 per user per month
### Installation Options
* [Self-Host Zip](https://www.invoiceninja.com/knowledgebase/self-host/)
* [Self-Host Zip](http://docs.invoiceninja.com/en/latest/install.html)
* [Docker File](https://github.com/invoiceninja/dockerfiles)
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
@ -54,9 +54,7 @@ The [self-host zip](https://www.invoiceninja.com/self-host/) includes all third
* [D3.js](http://d3js.org/) visualizations
## Documentation
* [Self Host Guide](https://www.invoiceninja.com/self-host)
* [User Guide](http://docs.invoiceninja.com/en/latest/)
* [Developer Guide](https://www.invoiceninja.com/knowledgebase/developer-guide/)
* [Support Forum](https://www.invoiceninja.com/forums/forum/support/)
* [Feature Roadmap](https://trello.com/b/63BbiVVe/)

View File

@ -3,6 +3,7 @@
namespace App\Console\Commands;
use Carbon;
use App\Libraries\CurlUtils;
use DB;
use Exception;
use Illuminate\Console\Command;
@ -75,6 +76,7 @@ class CheckData extends Command
$this->checkDraftSentInvoices();
}
$this->checkInvoices();
$this->checkBalances();
$this->checkContacts();
$this->checkUserAccounts();
@ -131,6 +133,38 @@ class CheckData extends Command
}
}
private function checkInvoices()
{
if (! env('PHANTOMJS_BIN_PATH')) {
return;
}
$date = new Carbon();
$date = $date->subDays(1)->format('Y-m-d');
$invoices = Invoice::with('invitations')
->where('created_at', '>', $date)
->orderBy('id')
->get(['id', 'balance']);
foreach ($invoices as $invoice) {
$link = $invoice->getInvitationLink('view', true, true);
$this->logMessage('Checking invoice: ' . $invoice->id . ' - ' . $invoice->balance);
$result = CurlUtils::phantom('GET', $link . '?phantomjs=true&phantomjs_balances=true&phantomjs_secret=' . env('PHANTOMJS_SECRET'));
$result = floatval(strip_tags($result));
$this->logMessage('Result: ' . $result);
if ($result && $result != $invoice->balance) {
$this->logMessage("Amounts do not match {$link} - PHP: {$invoice->balance}, JS: {$result}");
$this->isValid = false;
}
}
if ($this->isValid) {
$this->logMessage('0 invoices with mismatched PHP/JS balances');
}
}
private function checkOAuth()
{
// check for duplicate oauth ids

View File

@ -8,6 +8,10 @@ use App\Ninja\Repositories\ExpenseRepository;
use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\VendorRepository;
use App\Models\Client;
use App\Models\TaxRate;
use App\Models\Project;
use App\Models\ExpenseCategory;
use Auth;
use Faker\Factory;
use Illuminate\Console\Command;
@ -94,6 +98,7 @@ class CreateTestData extends Command
$this->createClients();
$this->createVendors();
$this->createOtherObjects();
$this->info('Done');
}
@ -210,6 +215,50 @@ class CreateTestData extends Command
}
}
private function createOtherObjects()
{
$this->createTaxRate('Tax 1', 10, 1);
$this->createTaxRate('Tax 2', 20, 2);
$this->createCategory('Category 1', 1);
$this->createCategory('Category 1', 2);
$this->createProject('Project 1', 1);
$this->createProject('Project 2', 2);
}
private function createTaxRate($name, $rate, $publicId)
{
$taxRate = new TaxRate();
$taxRate->name = $name;
$taxRate->rate = $rate;
$taxRate->account_id = 1;
$taxRate->user_id = 1;
$taxRate->public_id = $publicId;
$taxRate->save();
}
private function createCategory($name, $publicId)
{
$category = new ExpenseCategory();
$category->name = $name;
$category->account_id = 1;
$category->user_id = 1;
$category->public_id = $publicId;
$category->save();
}
private function createProject($name, $publicId)
{
$project = new Project();
$project->name = $name;
$project->account_id = 1;
$project->client_id = 1;
$project->user_id = 1;
$project->public_id = $publicId;
$project->save();
}
/**
* @return array
*/

View File

@ -4,13 +4,17 @@ 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 DateTime;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Auth;
use Exception;
use Utils;
/**
* Class SendRecurringInvoices.
@ -49,25 +53,34 @@ class SendRecurringInvoices extends Command
* @param InvoiceRepository $invoiceRepo
* @param PaymentService $paymentService
*/
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, PaymentService $paymentService)
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, PaymentService $paymentService, RecurringExpenseRepository $recurringExpenseRepo)
{
parent::__construct();
$this->mailer = $mailer;
$this->invoiceRepo = $invoiceRepo;
$this->paymentService = $paymentService;
$this->recurringExpenseRepo = $recurringExpenseRepo;
}
public function fire()
{
$this->info(date('Y-m-d H:i:s') . ' Running SendRecurringInvoices...');
$today = new DateTime();
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// check for counter resets
$this->resetCounters();
$this->createInvoices();
$this->billInvoices();
$this->createExpenses();
$this->info(date('Y-m-d H:i:s') . ' Done');
}
private function resetCounters()
{
$accounts = Account::where('reset_counter_frequency_id', '>', 0)
->orderBy('id', 'asc')
->get();
@ -75,6 +88,11 @@ class SendRecurringInvoices extends Command
foreach ($accounts as $account) {
$account->checkCounterReset();
}
}
private function createInvoices()
{
$today = new DateTime();
$invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
@ -94,14 +112,25 @@ class SendRecurringInvoices extends Command
$account = $recurInvoice->account;
$account->loadLocalizationSettings($recurInvoice->client);
Auth::loginUsingId($recurInvoice->user_id);
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
try {
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice && ! $invoice->isPaid()) {
$this->info('Sending Invoice');
$this->mailer->sendInvoice($invoice);
}
} catch (Exception $exception) {
$this->info('Error: ' . $exception->getMessage());
Utils::logError($exception);
}
Auth::logout();
}
}
private function billInvoices()
{
$today = new DateTime();
$delayedAutoBillInvoices = Invoice::with('account.timezone', 'recurring_invoice', 'invoice_items', 'client', 'user')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS FALSE AND is_public IS TRUE
@ -124,8 +153,28 @@ class SendRecurringInvoices extends Command
Auth::logout();
}
}
}
$this->info(date('Y-m-d H:i:s') . ' Done');
private function createExpenses()
{
$today = new DateTime();
$expenses = RecurringExpense::with('client')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc')
->get();
$this->info(count($expenses).' recurring expenses(s) found');
foreach ($expenses as $expense) {
$shouldSendToday = $expense->shouldSendToday();
if (! $shouldSendToday) {
continue;
}
$this->info('Processing Expense: '. $expense->id);
$this->recurringExpenseRepo->createRecurringExpense($expense);
}
}
/**

View File

@ -38,7 +38,7 @@ class $CLASS$ extends BaseController
public function datatable(DatatableService $datatableService)
{
$search = request()->input('test');
$search = request()->input('sSearch');
$userId = Auth::user()->filterId();
$datatable = new $STUDLY_NAME$Datatable();

View File

@ -37,6 +37,7 @@ if (! defined('APP_NAME')) {
define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount');
define('ENTITY_EXPENSE_CATEGORY', 'expense_category');
define('ENTITY_PROJECT', 'project');
define('ENTITY_RECURRING_EXPENSE', 'recurring_expense');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
@ -153,6 +154,9 @@ if (! defined('APP_NAME')) {
define('DEFAULT_BODY_FONT', 1); // Roboto
define('DEFAULT_SEND_RECURRING_HOUR', 8);
define('DEFAULT_BANK_OFX_VERSION', 102);
define('DEFAULT_BANK_APP_VERSION', 2500);
define('IMPORT_CSV', 'CSV');
define('IMPORT_JSON', 'JSON');
define('IMPORT_FRESHBOOKS', 'FreshBooks');
@ -303,7 +307,7 @@ 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', '3.4.2' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '3.5.0' . env('NINJA_VERSION_SUFFIX'));
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'));
@ -510,7 +514,6 @@ if (! defined('APP_NAME')) {
define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET'));
define('WEPAY_AUTO_UPDATE', env('WEPAY_AUTO_UPDATE', false));
define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION));
define('WEPAY_ENABLE_CANADA', env('WEPAY_ENABLE_CANADA', false));
define('WEPAY_THEME', env('WEPAY_THEME', '{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}'));
define('SKYPE_CARD_RECEIPT', 'message/card.receipt');

View File

@ -61,7 +61,7 @@ class Handler extends ExceptionHandler
}
// Log 404s to a separate file
$errorStr = date('Y-m-d h:i:s') . ' ' . request()->url() . "\n" . json_encode(Utils::prepareErrorData('PHP')) . "\n\n";
@file_put_contents(storage_path('logs/not_found.log'), $errorStr, FILE_APPEND);
@file_put_contents(storage_path('logs/not-found.log'), $errorStr, FILE_APPEND);
return false;
} elseif ($e instanceof HttpResponseException) {
return false;

View File

@ -455,7 +455,7 @@ class AccountController extends BaseController
if ($accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) {
if (! $accountGateway->getPublishableStripeKey()) {
Session::flash('warning', trans('texts.missing_publishable_key'));
Session::now('warning', trans('texts.missing_publishable_key'));
}
}
@ -876,7 +876,10 @@ class AccountController extends BaseController
$rules["{$entityType}_number_pattern"] = 'has_counter';
}
}
if (Input::get('credit_number_enabled')) {
$rules['credit_number_prefix'] = 'required_without:credit_number_pattern';
$rules['credit_number_pattern'] = 'required_without:credit_number_prefix';
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
@ -915,6 +918,9 @@ class AccountController extends BaseController
$account->client_number_prefix = trim(Input::get('client_number_prefix'));
$account->client_number_pattern = trim(Input::get('client_number_pattern'));
$account->client_number_counter = Input::get('client_number_counter');
$account->credit_number_counter = Input::get('credit_number_counter');
$account->credit_number_prefix = trim(Input::get('credit_number_prefix'));
$account->credit_number_pattern = trim(Input::get('credit_number_pattern'));
$account->reset_counter_frequency_id = Input::get('reset_counter_frequency_id');
$account->reset_counter_date = $account->reset_counter_frequency_id ? Utils::toSqlDate(Input::get('reset_counter_date')) : null;
@ -974,22 +980,7 @@ class AccountController extends BaseController
$account->page_size = Input::get('page_size');
$labels = [];
foreach ([
'item',
'description',
'unit_cost',
'quantity',
'line_total',
'terms',
'balance_due',
'partial_due',
'subtotal',
'paid_to_date',
'discount',
'tax',
'po_number',
'due_date',
] as $field) {
foreach (Account::$customLabels as $field) {
$labels[$field] = Input::get("labels_{$field}");
}
$account->invoice_labels = json_encode($labels);
@ -1143,6 +1134,7 @@ class AccountController extends BaseController
$user->username = $email;
$user->email = $email;
$user->phone = trim(Input::get('phone'));
$user->dark_mode = Input::get('dark_mode');
if (! Auth::user()->is_admin) {
$user->notify_sent = Input::get('notify_sent');

View File

@ -84,14 +84,14 @@ class AccountGatewayController extends BaseController
public function create()
{
if (! \Request::secure() && ! Utils::isNinjaDev()) {
Session::flash('warning', trans('texts.enable_https'));
Session::now('warning', trans('texts.enable_https'));
}
$account = Auth::user()->account;
$accountGatewaysIds = $account->gatewayIds();
$otherProviders = Input::get('other_providers');
if (! Utils::isNinja() || ! env('WEPAY_CLIENT_ID') || Gateway::hasStandardGateway($accountGatewaysIds)) {
if (! env('WEPAY_CLIENT_ID') || Gateway::hasStandardGateway($accountGatewaysIds)) {
$otherProviders = true;
}
@ -254,7 +254,7 @@ class AccountGatewayController extends BaseController
if ($oldConfig && $value && $value === str_repeat('*', strlen($value))) {
$value = $oldConfig->$field;
}
if (! $value && ($field == 'testMode' || $field == 'developerMode')) {
if (! $value && in_array($field, ['testMode', 'developerMode', 'sandbox'])) {
// do nothing
} elseif ($gatewayId == GATEWAY_CUSTOM) {
$config->$field = strip_tags($value);
@ -378,12 +378,9 @@ class AccountGatewayController extends BaseController
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|email',
'country' => 'required|in:US,CA,GB',
];
if (WEPAY_ENABLE_CANADA) {
$rules['country'] = 'required|in:US,CA';
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
@ -428,15 +425,14 @@ class AccountGatewayController extends BaseController
'theme_object' => json_decode(WEPAY_THEME),
'callback_uri' => $accountGateway->getWebhookUrl(),
'rbits' => $account->present()->rBits,
'country' => Input::get('country'),
];
if (WEPAY_ENABLE_CANADA) {
$accountDetails['country'] = Input::get('country');
if (Input::get('country') == 'CA') {
$accountDetails['currencies'] = ['CAD'];
$accountDetails['country_options'] = ['debit_opt_in' => boolval(Input::get('debit_cards'))];
}
} elseif (Input::get('country') == 'GB') {
$accountDetails['currencies'] = ['GBP'];
}
$wepayAccount = $wepay->request('account/create/', $accountDetails);
@ -461,7 +457,7 @@ class AccountGatewayController extends BaseController
'accountId' => $wepayAccount->account_id,
'state' => $wepayAccount->state,
'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGE,
'country' => WEPAY_ENABLE_CANADA ? Input::get('country') : 'US',
'country' => Input::get('country'),
]);
if ($confirmationRequired) {

View File

@ -97,10 +97,18 @@ class BankAccountController extends BaseController
$username = Crypt::decrypt($username);
$bankId = $bankAccount->bank_id;
} else {
$bankId = Input::get('bank_id');
$bankAccount = new BankAccount;
$bankAccount->bank_id = Input::get('bank_id');
}
return json_encode($this->bankAccountService->loadBankAccounts($bankId, $username, $password, $publicId));
$bankAccount->app_version = Input::get('app_version');
$bankAccount->ofx_version = Input::get('ofx_version');
if ($publicId) {
$bankAccount->save();
}
return json_encode($this->bankAccountService->loadBankAccounts($bankAccount, $username, $password, $publicId));
}
public function store(CreateBankAccountRequest $request)
@ -111,7 +119,7 @@ class BankAccountController extends BaseController
$username = trim(Input::get('bank_username'));
$password = trim(Input::get('bank_password'));
return json_encode($this->bankAccountService->loadBankAccounts($bankId, $username, $password, true));
return json_encode($this->bankAccountService->loadBankAccounts($bankAccount, $username, $password, true));
}
public function importExpenses($bankId)
@ -131,7 +139,7 @@ class BankAccountController extends BaseController
try {
$data = $this->bankAccountService->parseOFX($file);
} catch (\Exception $e) {
Session::flash('error', trans('texts.ofx_parse_failed'));
Session::now('error', trans('texts.ofx_parse_failed'));
Utils::logError($e);
return view('accounts.import_ofx');

View File

@ -121,6 +121,10 @@ class BaseAPIController extends Controller
protected function itemResponse($item)
{
if (! $item) {
return $this->errorResponse('Record not found', 404);
}
$transformerClass = EntityModel::getTransformerName($this->entityType);
$transformer = new $transformerClass(Auth::user()->account, Input::get('serializer'));
@ -206,6 +210,8 @@ class BaseAPIController extends Controller
$data[] = 'clients.contacts';
} elseif ($include == 'vendors') {
$data[] = 'vendors.vendor_contacts';
} elseif ($include == 'documents' && $this->entityType == ENTITY_INVOICE) {
$data[] = 'documents.expense';
} elseif ($include) {
$data[] = $include;
}

View File

@ -105,13 +105,17 @@ class ExpenseController extends BaseController
$actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans('texts.invoice_expense')];
// check for any open invoices
$invoices = $expense->client_id ? $this->invoiceRepo->findOpenInvoices($expense->client_id, ENTITY_EXPENSE) : [];
$invoices = $expense->client_id ? $this->invoiceRepo->findOpenInvoices($expense->client_id) : [];
foreach ($invoices as $invoice) {
$actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans('texts.add_to_invoice', ['invoice' => $invoice->invoice_number])];
}
}
if ($expense->recurring_expense_id) {
$actions[] = ['url' => URL::to("recurring_expenses/{$expense->recurring_expense->public_id}/edit"), 'label' => trans('texts.view_recurring_expense')];
}
$actions[] = \DropdownButton::DIVIDER;
if (! $expense->trashed()) {
$actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_expense')];
@ -266,6 +270,7 @@ class ExpenseController extends BaseController
'customLabel2' => Auth::user()->account->custom_vendor_label2,
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(),
'isRecurring' => false,
];
}

View File

@ -202,7 +202,10 @@ class InvoiceApiController extends BaseAPIController
if ($payment) {
app('App\Ninja\Mailers\ContactMailer')->sendPaymentConfirmation($payment);
//$this->dispatch(new SendPaymentEmail($payment));
} elseif (! $invoice->is_recurring) {
} else {
if ($invoice->is_recurring && $recurringInvoice = $this->invoiceRepo->createRecurringInvoice($invoice)) {
$invoice = $recurringInvoice;
}
app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);
//$this->dispatch(new SendInvoiceEmail($invoice));
}
@ -238,6 +241,10 @@ class InvoiceApiController extends BaseAPIController
'custom_value2' => 0,
'custom_taxes1' => false,
'custom_taxes2' => false,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'partial' => 0,
];
@ -314,6 +321,10 @@ class InvoiceApiController extends BaseAPIController
{
$invoice = $request->entity();
if ($invoice->is_recurring && $recurringInvoice = $this->invoiceRepo->createRecurringInvoice($invoice)) {
$invoice = $recurringInvoice;
}
//$this->dispatch(new SendInvoiceEmail($invoice));
$result = app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);

View File

@ -101,6 +101,7 @@ class InvoiceController extends BaseController
$invoice->id = $invoice->public_id = null;
$invoice->is_public = false;
$invoice->invoice_number = $account->getNextNumber($invoice);
$invoice->due_date = null;
$invoice->balance = $invoice->amount;
$invoice->invoice_status_id = 0;
$invoice->invoice_date = date_create()->format('Y-m-d');
@ -536,7 +537,7 @@ class InvoiceController extends BaseController
$versionsJson = [];
$versionsSelect = [];
$lastId = false;
//dd($activities->toArray());
foreach ($activities as $activity) {
if ($backup = json_decode($activity->json_backup)) {
$backup->invoice_date = Utils::fromSqlDate($backup->invoice_date);

View File

@ -75,7 +75,7 @@ class OnlinePaymentController extends BaseController
]);
}
if (! $invitation->invoice->canBePaid()) {
if (! $invitation->invoice->canBePaid() && ! request()->update) {
return redirect()->to('view/' . $invitation->invitation_key);
}
@ -120,14 +120,16 @@ class OnlinePaymentController extends BaseController
$gatewayTypeId = Session::get($invitation->id . 'gateway_type');
$paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId);
if (! $invitation->invoice->canBePaid()) {
if (! $invitation->invoice->canBePaid() && ! request()->update) {
return redirect()->to('view/' . $invitation->invitation_key);
}
try {
$paymentDriver->completeOnsitePurchase($request->all());
if ($paymentDriver->isTwoStep()) {
if (request()->update) {
return redirect('/client/dashboard')->withMessage(trans('texts.updated_payment_details'));
} elseif ($paymentDriver->isTwoStep()) {
Session::flash('warning', trans('texts.bank_account_verification_next_steps'));
} else {
Session::flash('message', trans('texts.applied_payment'));

View File

@ -0,0 +1,168 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateRecurringExpenseRequest;
use App\Http\Requests\RecurringExpenseRequest;
use App\Http\Requests\UpdateRecurringExpenseRequest;
use App\Models\Client;
use App\Models\ExpenseCategory;
use App\Models\TaxRate;
use App\Models\Vendor;
use App\Ninja\Datatables\RecurringExpenseDatatable;
use App\Ninja\Repositories\RecurringExpenseRepository;
use App\Services\RecurringExpenseService;
use Auth;
use Input;
use Session;
use View;
use Cache;
class RecurringExpenseController extends BaseController
{
protected $recurringExpenseRepo;
protected $recurringExpenseService;
protected $entityType = ENTITY_RECURRING_EXPENSE;
public function __construct(RecurringExpenseRepository $recurringExpenseRepo, RecurringExpenseService $recurringExpenseService)
{
$this->recurringExpenseRepo = $recurringExpenseRepo;
$this->recurringExpenseService = $recurringExpenseService;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list_wrapper', [
'entityType' => ENTITY_RECURRING_EXPENSE,
'datatable' => new RecurringExpenseDatatable(),
'title' => trans('texts.recurring_expenses'),
]);
}
public function getDatatable($expensePublicId = null)
{
$search = Input::get('sSearch');
$userId = Auth::user()->filterId();
return $this->recurringExpenseService->getDatatable($search, $userId);
}
public function create(RecurringExpenseRequest $request)
{
if ($request->vendor_id != 0) {
$vendor = Vendor::scope($request->vendor_id)->with('vendor_contacts')->firstOrFail();
} else {
$vendor = null;
}
$data = [
'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $request->vendor_id,
'expense' => null,
'method' => 'POST',
'url' => 'recurring_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,
];
$data = array_merge($data, self::getViewModel());
return View::make('expenses.edit', $data);
}
public function edit(RecurringExpenseRequest $request)
{
$expense = $request->entity();
$actions = [];
if (! $expense->trashed()) {
$actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_expense')];
$actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans('texts.delete_expense')];
} else {
$actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_expense')];
}
$data = [
'vendor' => null,
'expense' => $expense,
'entity' => $expense,
'method' => 'PUT',
'url' => 'recurring_expenses/'.$expense->public_id,
'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());
return View::make('expenses.edit', $data);
}
private static function getViewModel()
{
return [
'data' => Input::old('data'),
'account' => Auth::user()->account,
'sizes' => Cache::get('sizes'),
'paymentTerms' => Cache::get('paymentTerms'),
'industries' => Cache::get('industries'),
'currencies' => Cache::get('currencies'),
'languages' => Cache::get('languages'),
'countries' => Cache::get('countries'),
'customLabel1' => Auth::user()->account->custom_vendor_label1,
'customLabel2' => Auth::user()->account->custom_vendor_label2,
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(),
'isRecurring' => true,
];
}
public function store(CreateRecurringExpenseRequest $request)
{
$recurringExpense = $this->recurringExpenseService->save($request->input());
Session::flash('message', trans('texts.created_recurring_expense'));
return redirect()->to($recurringExpense->getRoute());
}
public function update(UpdateRecurringExpenseRequest $request)
{
$recurringExpense = $this->recurringExpenseService->save($request->input(), $request->entity());
Session::flash('message', trans('texts.updated_recurring_expense'));
if (in_array(Input::get('action'), ['archive', 'delete', 'restore'])) {
return self::bulk();
}
return redirect()->to($recurringExpense->getRoute());
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->recurringExpenseService->bulk($ids, $action);
if ($count > 0) {
$field = $count == 1 ? "{$action}d_recurring_expense" : "{$action}d_recurring_expenses";
$message = trans("texts.$field", ['count' => $count]);
Session::flash('message', $message);
}
return $this->returnBulk($this->entityType, $action, $ids);
}
}

View File

@ -158,7 +158,7 @@ class TaskController extends BaseController
$actions[] = ['url' => 'javascript:submitAction("invoice")', 'label' => trans('texts.invoice_task')];
// check for any open invoices
$invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id, ENTITY_TASK) : [];
$invoices = $task->client_id ? $this->invoiceRepo->findOpenInvoices($task->client_id) : [];
foreach ($invoices as $invoice) {
$actions[] = ['url' => 'javascript:submitAction("add_to_invoice", '.$invoice->public_id.')', 'label' => trans('texts.add_to_invoice', ['invoice' => $invoice->invoice_number])];
@ -314,7 +314,7 @@ class TaskController extends BaseController
{
if (! Auth::user()->account->timezone) {
$link = link_to('/settings/localization?focus=timezone_id', trans('texts.click_here'), ['target' => '_blank']);
Session::flash('warning', trans('texts.timezone_unset', ['link' => $link]));
Session::now('warning', trans('texts.timezone_unset', ['link' => $link]));
}
}
}

View File

@ -28,7 +28,6 @@ class DatabaseLookup
LookupUser::setServerByField('email', $email);
} else {
Auth::logout();
return redirect('/login');
}
} elseif ($guard == 'api') {
if ($token = $request->header('X-Ninja-Token')) {

View File

@ -18,7 +18,9 @@ class DuplicateSubmissionCheck
*/
public function handle(Request $request, Closure $next)
{
if ($request->is('api/v1/*') || $request->is('documents')) {
if ($request->is('api/v1/*')
|| $request->is('save_sidebar_state')
|| $request->is('documents')) {
return $next($request);
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class CreateRecurringExpenseRequest extends RecurringExpenseRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_RECURRING_EXPENSE);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'amount' => 'numeric',
];
}
}

View File

@ -14,7 +14,7 @@ class ExpenseRequest extends EntityRequest
$expense = parent::entity();
// eager load the documents
if ($expense && ! $expense->relationLoaded('documents')) {
if ($expense && method_exists($expense, 'documents') && ! $expense->relationLoaded('documents')) {
$expense->load('documents');
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class RecurringExpenseRequest extends ExpenseRequest
{
protected $entityType = ENTITY_RECURRING_EXPENSE;
}

View File

@ -11,7 +11,7 @@ class UpdateClientRequest extends ClientRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
@ -21,6 +21,10 @@ class UpdateClientRequest extends ClientRequest
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
$rules = [];
if ($this->user()->account->client_number_counter) {

View File

@ -11,7 +11,7 @@ class UpdateContactRequest extends ContactRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateCreditRequest extends CreditRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateDocumentRequest extends DocumentRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateExpenseCategoryRequest extends ExpenseCategoryRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
@ -21,6 +21,10 @@ class UpdateExpenseCategoryRequest extends ExpenseCategoryRequest
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
return [
'name' => 'required',
'name' => sprintf('required|unique:expense_categories,name,%s,id,account_id,%s', $this->entity()->id, $this->user()->account_id),

View File

@ -11,7 +11,7 @@ class UpdateExpenseRequest extends ExpenseRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -13,7 +13,7 @@ class UpdateInvoiceAPIRequest extends InvoiceRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
@ -23,6 +23,10 @@ class UpdateInvoiceAPIRequest extends InvoiceRequest
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
if ($this->action == ACTION_ARCHIVE) {
return [];
}

View File

@ -13,7 +13,7 @@ class UpdateInvoiceRequest extends InvoiceRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
@ -23,6 +23,10 @@ class UpdateInvoiceRequest extends InvoiceRequest
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
$invoiceId = $this->entity()->id;
$rules = [

View File

@ -11,7 +11,7 @@ class UpdatePaymentRequest extends PaymentRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateProductRequest extends ProductRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateProjectRequest extends ProjectRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**
@ -21,6 +21,10 @@ class UpdateProjectRequest extends ProjectRequest
*/
public function rules()
{
if (! $this->entity()) {
return [];
}
return [
'name' => sprintf('required|unique:projects,name,%s,id,account_id,%s', $this->entity()->id, $this->user()->account_id),
];

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class UpdateRecurringExpenseRequest extends RecurringExpenseRequest
{
/**
* 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()
{
return [
'amount' => 'numeric',
];
}
}

View File

@ -11,7 +11,7 @@ class UpdateTaskRequest extends TaskRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateTaxRateRequest extends TaxRateRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -11,7 +11,7 @@ class UpdateVendorRequest extends VendorRequest
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
return $this->entity() && $this->user()->can('edit', $this->entity());
}
/**

View File

@ -37,8 +37,6 @@ Route::group(['middleware' => ['lookup:contact', 'auth:client']], function () {
Route::get('bank/{routing_number}', 'OnlinePaymentController@getBankInfo');
Route::get('client/payment_methods', 'ClientPortalController@paymentMethods');
Route::post('client/payment_methods/verify', 'ClientPortalController@verifyPaymentMethod');
//Route::get('client/payment_methods/add/{gateway_type}/{source_id?}', 'ClientPortalController@addPaymentMethod');
//Route::post('client/payment_methods/add/{gateway_type}', 'ClientPortalController@postAddPaymentMethod');
Route::post('client/payment_methods/default', 'ClientPortalController@setDefaultPaymentMethod');
Route::post('client/payment_methods/{source_id}/remove', 'ClientPortalController@removePaymentMethod');
Route::get('client/quotes', 'ClientPortalController@quoteIndex');
@ -171,10 +169,20 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
Route::get('recurring_invoices/create/{client_id?}', 'InvoiceController@createRecurring');
Route::get('recurring_invoices', 'RecurringInvoiceController@index');
Route::get('recurring_invoices/{invoices}/edit', 'InvoiceController@edit');
Route::get('recurring_invoices/{invoices}', 'InvoiceController@edit');
Route::get('invoices/{invoices}/clone', 'InvoiceController@cloneInvoice');
Route::post('invoices/bulk', 'InvoiceController@bulk');
Route::post('recurring_invoices/bulk', 'InvoiceController@bulk');
Route::get('recurring_expenses', 'RecurringExpenseController@index');
Route::get('api/recurring_expenses', 'RecurringExpenseController@getDatatable');
Route::get('recurring_expenses/create/{vendor_id?}/{client_id?}/{category_id?}', 'RecurringExpenseController@create');
Route::post('recurring_expenses', 'RecurringExpenseController@store');
Route::put('recurring_expenses/{recurring_expenses}', 'RecurringExpenseController@update');
Route::get('recurring_expenses/{recurring_expenses}/edit', 'RecurringExpenseController@edit');
Route::get('recurring_expenses/{recurring_expenses}', 'RecurringExpenseController@edit');
Route::post('recurring_expenses/bulk', 'RecurringExpenseController@bulk');
Route::get('documents/{documents}/{filename?}', 'DocumentController@get');
Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS');
Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview');

View File

@ -11,6 +11,7 @@ use App\Ninja\Mailers\UserMailer;
use App\Models\User;
use Auth;
use App;
use Utils;
/**
* Class SendInvoiceEmail.
@ -67,6 +68,7 @@ class ImportData extends Job implements ShouldQueue
$this->user->account->loadLocalizationSettings();
}
try {
if ($this->type === IMPORT_JSON) {
$includeData = $this->settings['include_data'];
$includeSettings = $this->settings['include_settings'];
@ -85,6 +87,12 @@ class ImportData extends Job implements ShouldQueue
$subject = trans('texts.import_complete');
$message = $importService->presentResults($results, $includeSettings);
} catch (Exception $exception) {
$subject = trans('texts.import_failed');
$message = $exception->getMessage();
Utils::logError($subject . ':' . $message);
}
$userMailer->sendMessage($this->user, $subject, $message);
if (App::runningInConsole()) {

View File

@ -38,6 +38,7 @@ class PurgeAccountData extends Job
'credits',
'expense_categories',
'expenses',
'recurring_expenses',
'invoice_items',
'payments',
'invoices',
@ -56,6 +57,7 @@ class PurgeAccountData extends Job
$account->invoice_number_counter = 1;
$account->quote_number_counter = 1;
$account->credit_number_counter = $account->credit_number_counter > 0 ? 1 : 0;
$account->client_number_counter = $account->client_number_counter > 0 ? 1 : 0;
$account->save();

View File

@ -80,6 +80,7 @@ class HistoryUtils
ENTITY_QUOTE,
ENTITY_TASK,
ENTITY_EXPENSE,
//ENTITY_RECURRING_EXPENSE,
];
if (! in_array($entityType, $trackedTypes)) {

View File

@ -35,7 +35,7 @@ class OFX
$this->response = curl_exec($c);
if (Utils::isNinjaDev()) {
Log::info(print_r($this->response, true));
//Log::info(print_r($this->response, true));
}
curl_close($c);
@ -90,6 +90,8 @@ class Login
public $bank;
public $id;
public $pass;
public $ofxVersion;
public $appVersion;
public function __construct($bank, $id, $pass)
{
@ -103,7 +105,7 @@ class Login
$ofxRequest =
"OFXHEADER:100\n".
"DATA:OFXSGML\n".
"VERSION:102\n".
"VERSION:" . $this->ofxVersion . "\n".
"SECURITY:NONE\n".
"ENCODING:USASCII\n".
"CHARSET:1252\n".
@ -124,7 +126,7 @@ class Login
'<FID>'.$this->bank->fid."\n".
"</FI>\n".
"<APPID>QWIN\n".
"<APPVER>2500\n".
"<APPVER>" . $this->appVersion . "\n".
"</SONRQ>\n".
"</SIGNONMSGSRQV1>\n".
"<SIGNUPMSGSRQV1>\n".
@ -173,7 +175,7 @@ class Account
$ofxRequest =
"OFXHEADER:100\n".
"DATA:OFXSGML\n".
"VERSION:102\n".
"VERSION:" . $this->login->ofxVersion . "\n".
"SECURITY:NONE\n".
"ENCODING:USASCII\n".
"CHARSET:1252\n".
@ -193,7 +195,7 @@ class Account
'<FID>'.$this->login->bank->fid."\n".
"</FI>\n".
"<APPID>QWIN\n".
"<APPVER>2500\n".
"<APPVER>" . $this->login->appVersion . "\n".
"</SONRQ>\n".
"</SIGNONMSGSRQV1>\n";
if ($this->type == 'BANK') {

View File

@ -690,7 +690,7 @@ class Utils
}
}
public static function processVariables($str)
public static function processVariables($str, $client = false)
{
if (! $str) {
return '';
@ -718,7 +718,8 @@ class Utils
$offset = intval($minArray[1]) * -1;
}
$val = self::getDatePart($variable, $offset);
$locale = $client && $client->language_id ? $client->language->locale : null;
$val = self::getDatePart($variable, $offset, $locale);
$str = str_replace($match, $val, $str);
}
}
@ -726,11 +727,11 @@ class Utils
return $str;
}
private static function getDatePart($part, $offset)
private static function getDatePart($part, $offset, $locale)
{
$offset = intval($offset);
if ($part == 'MONTH') {
return self::getMonth($offset);
return self::getMonth($offset, $locale);
} elseif ($part == 'QUARTER') {
return self::getQuarter($offset);
} elseif ($part == 'YEAR') {
@ -751,7 +752,7 @@ class Utils
return $months;
}
private static function getMonth($offset)
private static function getMonth($offset, $locale)
{
$months = static::$months;
$month = intval(date('n')) - 1;
@ -763,7 +764,7 @@ class Utils
$month += 12;
}
return trans('texts.' . $months[$month]);
return trans('texts.' . $months[$month], [], null, $locale);
}
private static function getQuarter($offset)

View File

@ -43,7 +43,7 @@ class HandleUserLoggedIn
{
$account = Auth::user()->account;
if (empty($account->last_login)) {
if (! Utils::isNinja() && empty($account->last_login)) {
event(new UserSignedUp());
}
@ -77,6 +77,13 @@ class HandleUserLoggedIn
if (! $gateway || $gateway->name !== 'Custom') {
Session::flash('error', trans('texts.error_incorrect_gateway_ids'));
}
/*
if (! env('APP_KEY')) {
Session::flash('error', trans('texts.error_app_key_not_set'));
} elseif (strstr(env('APP_KEY'), 'SomeRandomString')) {
Session::flash('error', trans('texts.error_app_key_set_to_default'));
}
*/
}
}
}

View File

@ -171,6 +171,9 @@ class Account extends Eloquent
'custom_contact_label2',
'domain_id',
'analytics_key',
'credit_number_counter',
'credit_number_prefix',
'credit_number_pattern',
];
/**
@ -219,6 +222,28 @@ class Account extends Eloquent
'outstanding' => 4,
];
public static $customLabels = [
'balance_due',
'description',
'discount',
'due_date',
'hours',
'id_number',
'item',
'line_total',
'paid_to_date',
'partial_due',
'po_number',
'quantity',
'rate',
'service',
'subtotal',
'tax',
'terms',
'unit_cost',
'vat_number',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -944,6 +969,14 @@ class Account extends Eloquent
return strpos($this->account_key, 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx7') === 0;
}
/**
* @return bool
*/
public function isNinjaOrLicenseAccount()
{
return $this->isNinjaAccount() || $this->account_key == NINJA_LICENSE_ACCOUNT_KEY;
}
/**
* @param $plan
*/
@ -1570,6 +1603,7 @@ class Account extends Eloquent
return true;
}
// note: single & checks bitmask match
return $this->enabled_modules & static::$modules[$entityType];
}

View File

@ -10,11 +10,21 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class BankAccount extends EntityModel
{
use SoftDeletes;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $fillable = [
'bank_id',
'app_version',
'ofx_version',
];
/**
* @return mixed
*/

View File

@ -305,6 +305,7 @@ class Client extends EntityModel
$contact->fill($data);
$contact->is_primary = $isPrimary;
$contact->email = trim($contact->email);
return $this->contacts()->save($contact);
}
@ -559,6 +560,15 @@ class Client extends EntityModel
{
return $this->payment_terms == -1 ? 0 : $this->payment_terms;
}
public function firstInvitationKey()
{
if ($invoice = $this->invoices->first()) {
if ($invitation = $invoice->invitations->first()) {
return $invitation->invitation_key;
}
}
}
}
Client::creating(function ($client) {

View File

@ -312,6 +312,7 @@ class EntityModel extends Eloquent
'invoices' => 'file-pdf-o',
'payments' => 'credit-card',
'recurring_invoices' => 'files-o',
'recurring_expenses' => 'files-o',
'credits' => 'credit-card',
'quotes' => 'file-text-o',
'tasks' => 'clock-o',

View File

@ -51,6 +51,7 @@ class Expense extends EntityModel
'payment_type_id',
'transaction_reference',
'invoice_documents',
'should_be_invoiced',
];
public static function getImportColumns()
@ -141,6 +142,15 @@ class Expense extends EntityModel
return $this->belongsTo('App\Models\PaymentType');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function recurring_expense()
{
return $this->belongsTo('App\Models\RecurringExpense');
}
/**
* @return mixed
*/

View File

@ -171,6 +171,8 @@ class Gateway extends Eloquent
$link = 'https://www.dwolla.com/register';
} elseif ($this->id == GATEWAY_SAGE_PAY_DIRECT || $this->id == GATEWAY_SAGE_PAY_SERVER) {
$link = 'https://applications.sagepay.com/apply/2C02C252-0F8A-1B84-E10D-CF933EFCAA99';
} elseif ($this->id == GATEWAY_STRIPE) {
$link = 'https://dashboard.stripe.com/account/apikeys';
}
$key = 'texts.gateway_help_'.$this->id;

View File

@ -10,12 +10,13 @@ use App\Events\InvoiceInvitationWasEmailed;
use App\Events\QuoteInvitationWasEmailed;
use App\Libraries\CurlUtils;
use App\Models\Activity;
use App\Models\Credit;
use App\Models\Traits\ChargesFees;
use App\Models\Traits\HasRecurrence;
use DateTime;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
use Utils;
use Carbon;
/**
* Class Invoice.
@ -25,6 +26,7 @@ class Invoice extends EntityModel implements BalanceAffecting
use PresentableTrait;
use OwnedByClientTrait;
use ChargesFees;
use HasRecurrence;
use SoftDeletes {
SoftDeletes::trashed as parentTrashed;
}
@ -446,6 +448,10 @@ class Invoice extends EntityModel implements BalanceAffecting
public function markSent()
{
if ($this->is_deleted) {
return;
}
if (! $this->isSent()) {
$this->invoice_status_id = INVOICE_STATUS_SENT;
}
@ -462,6 +468,10 @@ class Invoice extends EntityModel implements BalanceAffecting
*/
public function markInvitationsSent($notify = false, $reminder = false)
{
if ($this->is_deleted) {
return;
}
if (! $this->relationLoaded('invitations')) {
$this->load('invitations');
}
@ -494,6 +504,10 @@ class Invoice extends EntityModel implements BalanceAffecting
*/
public function markInvitationSent($invitation, $messageId = false, $notify = true, $notes = false)
{
if ($this->is_deleted) {
return;
}
if (! $this->isSent()) {
$this->is_public = true;
$this->invoice_status_id = INVOICE_STATUS_SENT;
@ -531,7 +545,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$statusId = false;
if ($this->amount != 0 && $this->balance == 0) {
$statusId = INVOICE_STATUS_PAID;
} elseif ($this->balance > 0 && $this->balance < $this->amount) {
} elseif ($this->isSent() && $this->balance > 0 && $this->balance < $this->amount) {
$statusId = INVOICE_STATUS_PARTIAL;
} elseif ($this->isPartial() && $this->balance > 0) {
$statusId = ($this->balance == $this->amount ? INVOICE_STATUS_SENT : INVOICE_STATUS_PARTIAL);
@ -685,13 +699,13 @@ class Invoice extends EntityModel implements BalanceAffecting
return self::calcLink($this);
}
public function getInvitationLink($type = 'view', $forceOnsite = false)
public function getInvitationLink($type = 'view', $forceOnsite = false, $forcePlain = false)
{
if (! $this->relationLoaded('invitations')) {
$this->load('invitations');
}
return $this->invitations[0]->getLink($type, $forceOnsite);
return $this->invitations[0]->getLink($type, $forceOnsite, $forcePlain);
}
/**
@ -897,6 +911,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'tax_rate1',
'tax_name2',
'tax_rate2',
'invoice_item_type_id',
]);
}
@ -1123,122 +1138,6 @@ class Invoice extends EntityModel implements BalanceAffecting
return implode('<br/>', $dates);
}
/**
* @return string
*/
private function getRecurrenceRule()
{
$rule = '';
switch ($this->frequency_id) {
case FREQUENCY_WEEKLY:
$rule = 'FREQ=WEEKLY;';
break;
case FREQUENCY_TWO_WEEKS:
$rule = 'FREQ=WEEKLY;INTERVAL=2;';
break;
case FREQUENCY_FOUR_WEEKS:
$rule = 'FREQ=WEEKLY;INTERVAL=4;';
break;
case FREQUENCY_MONTHLY:
$rule = 'FREQ=MONTHLY;';
break;
case FREQUENCY_TWO_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=2;';
break;
case FREQUENCY_THREE_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=3;';
break;
case FREQUENCY_SIX_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=6;';
break;
case FREQUENCY_ANNUALLY:
$rule = 'FREQ=YEARLY;';
break;
}
if ($this->end_date) {
$rule .= 'UNTIL=' . $this->getOriginal('end_date');
}
return $rule;
}
/*
public function shouldSendToday()
{
if (!$nextSendDate = $this->getNextSendDate()) {
return false;
}
return $this->account->getDateTime() >= $nextSendDate;
}
*/
/**
* @return bool
*/
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 {
$date1 = new DateTime($this->last_sent_date);
$date2 = new DateTime();
$diff = $date2->diff($date1);
$daysSinceLastSent = $diff->format('%a');
$monthsSinceLastSent = ($diff->format('%y') * 12) + $diff->format('%m');
// check we don't send a few hours early due to timezone difference
if (Carbon::now()->format('Y-m-d') != Carbon::now($timezone)->format('Y-m-d')) {
return false;
}
// check we never send twice on one day
if ($daysSinceLastSent == 0) {
return false;
}
}
switch ($this->frequency_id) {
case FREQUENCY_WEEKLY:
return $daysSinceLastSent >= 7;
case FREQUENCY_TWO_WEEKS:
return $daysSinceLastSent >= 14;
case FREQUENCY_FOUR_WEEKS:
return $daysSinceLastSent >= 28;
case FREQUENCY_MONTHLY:
return $monthsSinceLastSent >= 1;
case FREQUENCY_TWO_MONTHS:
return $monthsSinceLastSent >= 2;
case FREQUENCY_THREE_MONTHS:
return $monthsSinceLastSent >= 3;
case FREQUENCY_SIX_MONTHS:
return $monthsSinceLastSent >= 6;
case FREQUENCY_ANNUALLY:
return $monthsSinceLastSent >= 12;
default:
return false;
}
return false;
}
/**
* @return bool|string
*/
@ -1255,17 +1154,18 @@ class Invoice extends EntityModel implements BalanceAffecting
$invitation = $this->invitations[0];
$link = $invitation->getLink('view', true);
$pdfString = false;
$phantomjsSecret = env('PHANTOMJS_SECRET');
try {
if (env('PHANTOMJS_BIN_PATH')) {
$pdfString = CurlUtils::phantom('GET', $link . '?phantomjs=true&phantomjs_secret=' . env('PHANTOMJS_SECRET'));
$pdfString = CurlUtils::phantom('GET', $link . "?phantomjs=true&phantomjs_secret={$phantomjsSecret}");
}
if (! $pdfString && ($key = env('PHANTOMJS_CLOUD_KEY'))) {
if (Utils::isNinjaDev()) {
$link = env('TEST_LINK');
}
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true&phantomjs_secret={$phantomjsSecret}%22,renderType:%22html%22%7D";
$pdfString = CurlUtils::get($url);
}
@ -1526,8 +1426,13 @@ class Invoice extends EntityModel implements BalanceAffecting
}
Invoice::creating(function ($invoice) {
if (! $invoice->is_recurring && $invoice->amount >= 0) {
$invoice->account->incrementCounter($invoice);
if (! $invoice->is_recurring) {
$account = $invoice->account;
if ($invoice->amount >= 0) {
$account->incrementCounter($invoice);
} elseif ($account->credit_number_counter > 0) {
$account->incrementCounter(new Credit());
}
}
});

View File

@ -0,0 +1,154 @@
<?php
namespace App\Models;
//use App\Events\ExpenseWasCreated;
//use App\Events\ExpenseWasUpdated;
use App\Models\Traits\HasRecurrence;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
use Utils;
/**
* Class Expense.
*/
class RecurringExpense extends EntityModel
{
// Expenses
use SoftDeletes;
use PresentableTrait;
use HasRecurrence;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var string
*/
protected $presenter = 'App\Ninja\Presenters\ExpensePresenter';
/**
* @var array
*/
protected $fillable = [
'client_id',
'vendor_id',
'expense_currency_id',
//'invoice_currency_id',
//'exchange_rate',
'amount',
'private_notes',
'public_notes',
'expense_category_id',
'tax_rate1',
'tax_name1',
'tax_rate2',
'tax_name2',
'should_be_invoiced',
//'start_date',
//'end_date',
'frequency_id',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function expense_category()
{
return $this->belongsTo('App\Models\ExpenseCategory')->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
/**
* @return mixed
*/
public function user()
{
return $this->belongsTo('App\Models\User')->withTrashed();
}
/**
* @return mixed
*/
public function vendor()
{
return $this->belongsTo('App\Models\Vendor')->withTrashed();
}
/**
* @return mixed
*/
public function client()
{
return $this->belongsTo('App\Models\Client')->withTrashed();
}
/**
* @return mixed
*/
public function getName()
{
if ($this->public_notes) {
return Utils::truncateString($this->public_notes, 16);
} else {
return '#' . $this->public_id;
}
}
/**
* @return mixed
*/
public function getDisplayName()
{
return $this->getName();
}
/**
* @return string
*/
public function getRoute()
{
return "/recurring_expenses/{$this->public_id}/edit";
}
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_RECURRING_EXPENSE;
}
public function amountWithTax()
{
return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2);
}
}
RecurringExpense::creating(function ($expense) {
$expense->setNullValues();
});
RecurringExpense::created(function ($expense) {
//event(new ExpenseWasCreated($expense));
});
RecurringExpense::updating(function ($expense) {
$expense->setNullValues();
});
RecurringExpense::updated(function ($expense) {
//event(new ExpenseWasUpdated($expense));
});
RecurringExpense::deleting(function ($expense) {
$expense->setNullValues();
});

View File

@ -67,6 +67,11 @@ trait GeneratesNumbers
$this->client_number_counter += $counterOffset - 1;
$this->save();
}
} elseif ($entity->isEntityType(ENTITY_CREDIT)) {
if ($this->creditNumbersEnabled()) {
$this->credit_number_counter += $counterOffset - 1;
$this->save();
}
} elseif ($entity->isType(INVOICE_TYPE_QUOTE)) {
if (! $this->share_counter) {
$this->quote_number_counter += $counterOffset - 1;
@ -227,6 +232,8 @@ trait GeneratesNumbers
{
if ($entityType == ENTITY_CLIENT) {
return $this->client_number_counter;
} elseif ($entityType == ENTITY_CREDIT) {
return $this->credit_number_counter;
} elseif ($entityType == ENTITY_QUOTE && ! $this->share_counter) {
return $this->quote_number_counter;
} else {
@ -254,11 +261,17 @@ trait GeneratesNumbers
public function incrementCounter($entity)
{
if ($entity->isEntityType(ENTITY_CLIENT)) {
if ($this->client_number_counter) {
if ($this->client_number_counter > 0) {
$this->client_number_counter += 1;
}
$this->save();
return;
} elseif ($entity->isEntityType(ENTITY_CREDIT)) {
if ($this->credit_number_counter > 0) {
$this->credit_number_counter += 1;
}
$this->save();
return;
}
if ($this->usesClientInvoiceCounter()) {
@ -295,6 +308,11 @@ trait GeneratesNumbers
return $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $this->client_number_counter > 0;
}
public function creditNumbersEnabled()
{
return $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $this->credit_number_counter > 0;
}
public function checkCounterReset()
{
if (! $this->reset_counter_frequency_id || ! $this->reset_counter_date) {
@ -338,6 +356,7 @@ trait GeneratesNumbers
$this->reset_counter_date = $resetDate->format('Y-m-d');
$this->invoice_number_counter = 1;
$this->quote_number_counter = 1;
$this->credit_number_counter = 1;
$this->save();
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Models\Traits;
use Carbon;
use DateTime;
/**
* Class HasRecurrence
*/
trait HasRecurrence
{
/**
* @return bool
*/
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 {
$date1 = new DateTime($this->last_sent_date);
$date2 = new DateTime();
$diff = $date2->diff($date1);
$daysSinceLastSent = $diff->format('%a');
$monthsSinceLastSent = ($diff->format('%y') * 12) + $diff->format('%m');
// check we don't send a few hours early due to timezone difference
if (Carbon::now()->format('Y-m-d') != Carbon::now($timezone)->format('Y-m-d')) {
return false;
}
// check we never send twice on one day
if ($daysSinceLastSent == 0) {
return false;
}
}
switch ($this->frequency_id) {
case FREQUENCY_WEEKLY:
return $daysSinceLastSent >= 7;
case FREQUENCY_TWO_WEEKS:
return $daysSinceLastSent >= 14;
case FREQUENCY_FOUR_WEEKS:
return $daysSinceLastSent >= 28;
case FREQUENCY_MONTHLY:
return $monthsSinceLastSent >= 1;
case FREQUENCY_TWO_MONTHS:
return $monthsSinceLastSent >= 2;
case FREQUENCY_THREE_MONTHS:
return $monthsSinceLastSent >= 3;
case FREQUENCY_SIX_MONTHS:
return $monthsSinceLastSent >= 6;
case FREQUENCY_ANNUALLY:
return $monthsSinceLastSent >= 12;
default:
return false;
}
return false;
}
/**
* @return string
*/
private function getRecurrenceRule()
{
$rule = '';
switch ($this->frequency_id) {
case FREQUENCY_WEEKLY:
$rule = 'FREQ=WEEKLY;';
break;
case FREQUENCY_TWO_WEEKS:
$rule = 'FREQ=WEEKLY;INTERVAL=2;';
break;
case FREQUENCY_FOUR_WEEKS:
$rule = 'FREQ=WEEKLY;INTERVAL=4;';
break;
case FREQUENCY_MONTHLY:
$rule = 'FREQ=MONTHLY;';
break;
case FREQUENCY_TWO_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=2;';
break;
case FREQUENCY_THREE_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=3;';
break;
case FREQUENCY_SIX_MONTHS:
$rule = 'FREQ=MONTHLY;INTERVAL=6;';
break;
case FREQUENCY_ANNUALLY:
$rule = 'FREQ=YEARLY;';
break;
}
if ($this->end_date) {
$rule .= 'UNTIL=' . $this->getOriginal('end_date');
}
return $rule;
}
/*
public function shouldSendToday()
{
if (!$nextSendDate = $this->getNextSendDate()) {
return false;
}
return $this->account->getDateTime() >= $nextSendDate;
}
*/
}

View File

@ -263,13 +263,16 @@ trait PresentsInvoice
'outstanding',
'invoice_due_date',
'quote_due_date',
'service',
];
foreach ($fields as $field) {
$translated = $this->isEnglish() ? uctrans("texts.$field") : trans("texts.$field");
if (isset($custom[$field]) && $custom[$field]) {
$data[$field] = $custom[$field];
$data[$field . '_orig'] = $translated;
} else {
$data[$field] = $this->isEnglish() ? uctrans("texts.$field") : trans("texts.$field");
$data[$field] = $translated;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace App\Ninja\Datatables;
use App\Models\Expense;
use Auth;
use URL;
use Utils;
class RecurringExpenseDatatable extends EntityDatatable
{
public $entityType = ENTITY_RECURRING_EXPENSE;
public $sortCol = 3;
public function columns()
{
return [
[
'vendor_name',
function ($model) {
if ($model->vendor_public_id) {
if (! Auth::user()->can('viewByOwner', [ENTITY_VENDOR, $model->vendor_user_id])) {
return $model->vendor_name;
}
return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml();
} else {
return '';
}
},
! $this->hideClient,
],
[
'client_name',
function ($model) {
if ($model->client_public_id) {
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
return Utils::getClientDisplayName($model);
}
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
} else {
return '';
}
},
! $this->hideClient,
],
/*
[
'expense_date',
function ($model) {
if (! Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])) {
return Utils::fromSqlDate($model->expense_date_sql);
}
return link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date_sql))->toHtml();
},
],
*/
[
'amount',
function ($model) {
$amount = Utils::calculateTaxes($model->amount, $model->tax_rate1, $model->tax_rate2);
$str = Utils::formatMoney($amount, $model->expense_currency_id);
/*
// show both the amount and the converted amount
if ($model->exchange_rate != 1) {
$converted = round($amount * $model->exchange_rate, 2);
$str .= ' | ' . Utils::formatMoney($converted, $model->invoice_currency_id);
}
*/
return $str;
},
],
[
'category',
function ($model) {
$category = $model->category != null ? substr($model->category, 0, 100) : '';
if (! Auth::user()->can('editByOwner', [ENTITY_EXPENSE_CATEGORY, $model->category_user_id])) {
return $category;
}
return $model->category_public_id ? link_to("expense_categories/{$model->category_public_id}/edit", $category)->toHtml() : '';
},
],
[
'public_notes',
function ($model) {
return $model->public_notes != null ? substr($model->public_notes, 0, 100) : '';
},
],
[
'frequency',
function ($model) {
$frequency = strtolower($model->frequency);
$frequency = preg_replace('/\s/', '_', $frequency);
return link_to("recurring_expenses/{$model->public_id}/edit", trans('texts.freq_'.$frequency))->toHtml();
},
],
];
}
public function actions()
{
return [
[
trans('texts.edit_recurring_expense'),
function ($model) {
return URL::to("recurring_expenses/{$model->public_id}/edit");
},
function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_RECURRING_EXPENSE, $model->user_id]);
},
],
];
}
}

View File

@ -17,10 +17,15 @@ class RecurringInvoiceDatatable extends EntityDatatable
[
'frequency',
function ($model) {
if ($model->frequency) {
$frequency = strtolower($model->frequency);
$frequency = preg_replace('/\s/', '_', $frequency);
$label = trans('texts.freq_' . $frequency);
} else {
$label = trans('texts.freq_inactive');
}
return link_to("recurring_invoices/{$model->public_id}/edit", trans('texts.freq_'.$frequency))->toHtml();
return link_to("recurring_invoices/{$model->public_id}/edit", $label)->toHtml();
},
],
[
@ -76,8 +81,12 @@ class RecurringInvoiceDatatable extends EntityDatatable
$class = Invoice::calcStatusClass($model->invoice_status_id, $model->balance, $model->due_date_sql, $model->is_recurring);
$label = Invoice::calcStatusLabel($model->invoice_status_name, $class, $this->entityType, $model->quote_invoice_id);
if ($model->invoice_status_id == INVOICE_STATUS_SENT && (! $model->last_sent_date_sql || $model->last_sent_date_sql == '0000-00-00')) {
if ($model->invoice_status_id == INVOICE_STATUS_SENT) {
if (! $model->last_sent_date_sql || $model->last_sent_date_sql == '0000-00-00') {
$label = trans('texts.pending');
} else {
$label = trans('texts.active');
}
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";

View File

@ -5,6 +5,7 @@ namespace App\Ninja\Import;
use Carbon;
use League\Fractal\TransformerAbstract;
use Utils;
use Exception;
/**
* Class BaseTransformer.
@ -107,6 +108,30 @@ class BaseTransformer extends TransformerAbstract
return isset($this->maps[ENTITY_PRODUCT][$name]) ? $this->maps[ENTITY_PRODUCT][$name] : null;
}
/**
* @param $name
*
* @return null
*/
public function getProductNotes($name)
{
$name = strtolower(trim($name));
return isset($this->maps['product_notes'][$name]) ? $this->maps['product_notes'][$name] : null;
}
/**
* @param $name
*
* @return null
*/
public function getProductCost($name)
{
$name = strtolower(trim($name));
return isset($this->maps['product_cost'][$name]) ? $this->maps['product_cost'][$name] : null;
}
/**
* @param $name
*
@ -158,6 +183,7 @@ class BaseTransformer extends TransformerAbstract
$date = new Carbon($date);
} catch (Exception $e) {
// if we fail to parse return blank
$date = false;
}
}

View File

@ -38,8 +38,8 @@ class InvoiceTransformer extends BaseTransformer
'invoice_items' => [
[
'product_key' => $this->getString($data, 'product'),
'notes' => $this->getString($data, 'notes'),
'cost' => $this->getFloat($data, 'amount'),
'notes' => $this->getString($data, 'notes') ?: $this->getProductNotes($this->getString($data, 'product')),
'cost' => $this->getFloat($data, 'amount') ?: $this->getProductCost($this->getString($data, 'product')),
'qty' => $this->getFloat($data, 'quantity') ?: 1,
],
],

View File

@ -29,6 +29,7 @@ class Mailer
return true;
}
/*
if (isset($_ENV['POSTMARK_API_TOKEN'])) {
$views = 'emails.'.$view.'_html';
} else {
@ -37,6 +38,12 @@ class Mailer
'emails.'.$view.'_text',
];
}
*/
$views = [
'emails.'.$view.'_html',
'emails.'.$view.'_text',
];
try {
$response = Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) {

View File

@ -10,6 +10,7 @@ use App\Models\GatewayType;
use App\Models\License;
use App\Models\Payment;
use App\Models\PaymentMethod;
use Omnipay\Common\Item;
use CreditCard;
use DateTime;
use Exception;
@ -157,6 +158,11 @@ class BasePaymentDriver
return redirect()->to('view/' . $this->invitation->invitation_key);
}
$url = 'payment/' . $this->invitation->invitation_key;
if (request()->update) {
$url .= '?update=true';
}
$data = [
'details' => ! empty($input['details']) ? json_decode($input['details']) : false,
'accountGateway' => $this->accountGateway,
@ -164,7 +170,7 @@ class BasePaymentDriver
'gateway' => $gateway,
'showAddress' => $this->accountGateway->show_address,
'showBreadcrumbs' => false,
'url' => 'payment/' . $this->invitation->invitation_key,
'url' => $url,
'amount' => $this->invoice()->getRequestedAmount(),
'invoiceNumber' => $this->invoice()->invoice_number,
'client' => $this->client(),
@ -293,13 +299,16 @@ class BasePaymentDriver
}
}
if ($this->isTwoStep()) {
if ($this->isTwoStep() || request()->update) {
return;
}
// prepare and process payment
$data = $this->paymentDetails($paymentMethod);
$response = $gateway->purchase($data)->send();
$items = $this->paymentItems();
$response = $gateway->purchase($data)
->setItems($items)
->send();
$this->purchaseResponse = (array) $response->getData();
// parse the transaction reference
@ -332,6 +341,38 @@ class BasePaymentDriver
}
}
private function paymentItems()
{
$invoice = $this->invoice();
$items = [];
$total = 0;
foreach ($invoice->invoice_items as $invoiceItem) {
$item = new Item([
'name' => $invoiceItem->product_key,
'description' => $invoiceItem->notes,
'price' => $invoiceItem->cost,
'quantity' => $invoiceItem->qty,
]);
$items[] = $item;
$total += $invoiceItem->cost * $invoiceItem->qty;
}
if ($total != $invoice->getRequestedAmount()) {
$item = new Item([
'name' => trans('texts.taxes_and_fees'),
'description' => '',
'price' => $invoice->getRequestedAmount() - $total,
'quantity' => 1,
]);
$items[] = $item;
}
return $items;
}
private function updateClient()
{
if (! $this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) {
@ -381,7 +422,7 @@ class BasePaymentDriver
'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}",
'transactionId' => $invoice->invoice_number,
'transactionType' => 'Purchase',
'ip' => Request::getClientIp(),
'clientIp' => Request::getClientIp(),
];
if ($paymentMethod) {

View File

@ -4,6 +4,7 @@ namespace App\Ninja\PaymentDrivers;
use Exception;
use App\Models\Invitation;
use App\Models\Payment;
class MolliePaymentDriver extends BasePaymentDriver
{
@ -19,42 +20,38 @@ class MolliePaymentDriver extends BasePaymentDriver
public function completeOffsitePurchase($input)
{
$details = $this->paymentDetails();
$details['transactionReference'] = $this->invitation->transaction_reference;
$response = $this->gateway()->fetchTransaction($details)->send();
if ($response->isCancelled() || ! $response->isSuccessful()) {
// payment is created by the webhook
return false;
}
return $this->createPayment($response->getTransactionReference());
}
public function handleWebHook($input)
{
$ref = array_get($input, 'id');
$invitation = Invitation::whereAccountId($this->accountGateway->account_id)
->whereTransactionReference($ref)
->first();
if ($invitation) {
$this->invitation = $invitation;
} else {
return false;
}
$data = [
'transactionReference' => $ref
];
$response = $this->gateway()->fetchTransaction($data)->send();
if ($response->isCancelled() || ! $response->isSuccessful()) {
if ($response->isPaid() || $response->isPaidOut()) {
$invitation = Invitation::whereAccountId($this->accountGateway->account_id)
->whereTransactionReference($ref)
->first();
if ($invitation) {
$this->invitation = $invitation;
$this->createPayment($ref);
}
} else {
// check if payment has failed
$payment = Payment::whereAccountId($this->accountGateway->account_id)
->whereTransactionReference($ref)
->first();
if ($payment) {
$payment->markFailed($response->getStatus());
}
return false;
}
$this->createPayment($ref);
return RESULT_SUCCESS;
}

View File

@ -151,10 +151,10 @@ class WePayPaymentDriver extends BasePaymentDriver
switch ($source->state) {
case 'new':
case 'pending':
$paymentMethod->status = 'new';
$paymentMethod->status = PAYMENT_METHOD_STATUS_NEW;
break;
case 'authorized':
$paymentMethod->status = 'verified';
$paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED;
break;
}
} else {

View File

@ -11,7 +11,7 @@ class CompanyPresenter extends EntityPresenter
}
return trans('texts.promo_message', [
'expires' => $this->entity->promo_expires->format('M dS, Y'),
'expires' => $this->entity->promo_expires->format('M jS, Y'),
'amount' => (int) ($this->discount * 100) . '%',
]);
}

View File

@ -31,8 +31,8 @@ class BankAccountRepository extends BaseRepository
public function save($input)
{
$bankAccount = BankAccount::createNew();
$bankAccount->bank_id = $input['bank_id'];
$bankAccount->username = Crypt::encrypt(trim($input['bank_username']));
$bankAccount->fill($input);
$account = \Auth::user()->account;
$account->bank_accounts()->save($bankAccount);

View File

@ -176,8 +176,6 @@ class ExpenseRepository extends BaseRepository
$expense->payment_date = Utils::toSqlDate($input['payment_date']);
}
$expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false;
if (! $expense->expense_currency_id) {
$expense->expense_currency_id = \Auth::user()->account->getCurrencyId();
}
@ -195,7 +193,7 @@ class ExpenseRepository extends BaseRepository
// Documents
$document_ids = ! empty($input['document_ids']) ? array_map('intval', $input['document_ids']) : [];
;
foreach ($document_ids as $document_id) {
// check document completed upload before user submitted form
if ($document_id) {

View File

@ -144,7 +144,7 @@ class InvoiceRepository extends BaseRepository
->join('accounts', 'accounts.id', '=', 'invoices.account_id')
->join('clients', 'clients.id', '=', 'invoices.client_id')
->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id')
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->leftJoin('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('invoices.account_id', '=', $accountId)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD)
@ -217,6 +217,7 @@ class InvoiceRepository extends BaseRepository
->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', true)
->where('invoices.is_public', '=', true)
->where('invoices.deleted_at', '=', null)
//->where('invoices.start_date', '>=', date('Y-m-d H:i:s'))
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
@ -431,7 +432,7 @@ class InvoiceRepository extends BaseRepository
$invoice->last_sent_date = null;
}
$invoice->frequency_id = array_get($data, 'frequency_id', 0);
$invoice->frequency_id = array_get($data, 'frequency_id', FREQUENCY_MONTHLY);
$invoice->start_date = Utils::toSqlDate(array_get($data, 'start_date'));
$invoice->end_date = Utils::toSqlDate(array_get($data, 'end_date'));
$invoice->client_enable_auto_bill = isset($data['client_enable_auto_bill']) && $data['client_enable_auto_bill'] ? true : false;
@ -447,7 +448,9 @@ class InvoiceRepository extends BaseRepository
$invoice->due_date = $data['due_date'];
}
} else {
if (! empty($data['due_date']) || ! empty($data['due_date_sql'])) {
if ($isNew && empty($data['due_date']) && empty($data['due_date_sql'])) {
// do nothing
} elseif (isset($data['due_date']) || isset($data['due_date_sql'])) {
$invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']);
}
$invoice->frequency_id = 0;
@ -976,7 +979,7 @@ class InvoiceRepository extends BaseRepository
*
* @return mixed
*/
public function findOpenInvoices($clientId, $entityType = false)
public function findOpenInvoices($clientId)
{
$query = Invoice::scope()
->invoiceType(INVOICE_TYPE_STANDARD)
@ -985,12 +988,6 @@ class InvoiceRepository extends BaseRepository
->whereDeletedAt(null)
->where('balance', '>', 0);
if ($entityType == ENTITY_TASK) {
$query->whereHasTasks(true);
} elseif ($entityType == ENTITY_EXPENSE) {
$query->whereHasTasks(false);
}
return $query->where('invoice_status_id', '<', INVOICE_STATUS_PAID)
->select(['public_id', 'invoice_number'])
->get();
@ -1004,8 +1001,9 @@ class InvoiceRepository extends BaseRepository
public function createRecurringInvoice(Invoice $recurInvoice)
{
$recurInvoice->load('account.timezone', 'invoice_items', 'client', 'user');
$client = $recurInvoice->client;
if ($recurInvoice->client->deleted_at) {
if ($client->deleted_at) {
return false;
}
@ -1028,9 +1026,9 @@ class InvoiceRepository extends BaseRepository
$invoice->invoice_date = date_create()->format('Y-m-d');
$invoice->discount = $recurInvoice->discount;
$invoice->po_number = $recurInvoice->po_number;
$invoice->public_notes = Utils::processVariables($recurInvoice->public_notes);
$invoice->terms = Utils::processVariables($recurInvoice->terms ?: $recurInvoice->account->invoice_terms);
$invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer ?: $recurInvoice->account->invoice_footer);
$invoice->public_notes = Utils::processVariables($recurInvoice->public_notes, $client);
$invoice->terms = Utils::processVariables($recurInvoice->terms ?: $recurInvoice->account->invoice_terms, $client);
$invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer ?: $recurInvoice->account->invoice_footer, $client);
$invoice->tax_name1 = $recurInvoice->tax_name1;
$invoice->tax_rate1 = $recurInvoice->tax_rate1;
$invoice->tax_name2 = $recurInvoice->tax_name2;
@ -1040,8 +1038,8 @@ class InvoiceRepository extends BaseRepository
$invoice->custom_value2 = $recurInvoice->custom_value2 ?: 0;
$invoice->custom_taxes1 = $recurInvoice->custom_taxes1 ?: 0;
$invoice->custom_taxes2 = $recurInvoice->custom_taxes2 ?: 0;
$invoice->custom_text_value1 = Utils::processVariables($recurInvoice->custom_text_value1);
$invoice->custom_text_value2 = Utils::processVariables($recurInvoice->custom_text_value2);
$invoice->custom_text_value1 = Utils::processVariables($recurInvoice->custom_text_value1, $client);
$invoice->custom_text_value2 = Utils::processVariables($recurInvoice->custom_text_value2, $client);
$invoice->is_amount_discount = $recurInvoice->is_amount_discount;
$invoice->due_date = $recurInvoice->getDueDate();
$invoice->save();
@ -1051,14 +1049,14 @@ class InvoiceRepository extends BaseRepository
$item->product_id = $recurItem->product_id;
$item->qty = $recurItem->qty;
$item->cost = $recurItem->cost;
$item->notes = Utils::processVariables($recurItem->notes);
$item->product_key = Utils::processVariables($recurItem->product_key);
$item->notes = Utils::processVariables($recurItem->notes, $client);
$item->product_key = Utils::processVariables($recurItem->product_key, $client);
$item->tax_name1 = $recurItem->tax_name1;
$item->tax_rate1 = $recurItem->tax_rate1;
$item->tax_name2 = $recurItem->tax_name2;
$item->tax_rate2 = $recurItem->tax_rate2;
$item->custom_value1 = Utils::processVariables($recurItem->custom_value1);
$item->custom_value2 = Utils::processVariables($recurItem->custom_value2);
$item->custom_value1 = Utils::processVariables($recurItem->custom_value1, $client);
$item->custom_value2 = Utils::processVariables($recurItem->custom_value2, $client);
$invoice->invoice_items()->save($item);
}

View File

@ -0,0 +1,194 @@
<?php
namespace App\Ninja\Repositories;
use App\Models\RecurringExpense;
use App\Models\Expense;
use App\Models\Vendor;
use Auth;
use DB;
use Utils;
class RecurringExpenseRepository extends BaseRepository
{
// Expenses
public function getClassName()
{
return 'App\Models\RecurringExpense';
}
public function all()
{
return RecurringExpense::scope()
->with('user')
->withTrashed()
->where('is_deleted', '=', false)
->get();
}
public function find($filter = null)
{
$accountid = \Auth::user()->account_id;
$query = DB::table('recurring_expenses')
->join('accounts', 'accounts.id', '=', 'recurring_expenses.account_id')
->leftjoin('clients', 'clients.id', '=', 'recurring_expenses.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftjoin('vendors', 'vendors.id', '=', 'recurring_expenses.vendor_id')
->join('frequencies', 'frequencies.id', '=', 'recurring_expenses.frequency_id')
->leftJoin('expense_categories', 'recurring_expenses.expense_category_id', '=', 'expense_categories.id')
->where('recurring_expenses.account_id', '=', $accountid)
->where('contacts.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);
})
->select(
'recurring_expenses.account_id',
'recurring_expenses.amount',
'recurring_expenses.deleted_at',
'recurring_expenses.id',
'recurring_expenses.is_deleted',
'recurring_expenses.private_notes',
'recurring_expenses.public_id',
'recurring_expenses.public_notes',
'recurring_expenses.should_be_invoiced',
'recurring_expenses.vendor_id',
'recurring_expenses.expense_currency_id',
'recurring_expenses.invoice_currency_id',
'recurring_expenses.user_id',
'recurring_expenses.tax_rate1',
'recurring_expenses.tax_rate2',
'frequencies.name as frequency',
'expense_categories.name as category',
'expense_categories.user_id as category_user_id',
'expense_categories.public_id as category_public_id',
'vendors.name as vendor_name',
'vendors.public_id as vendor_public_id',
'vendors.user_id as vendor_user_id',
DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"),
'clients.public_id as client_public_id',
'clients.user_id as client_user_id',
'contacts.first_name',
'contacts.email',
'contacts.last_name',
'clients.country_id as client_country_id'
);
$this->applyFilters($query, ENTITY_RECURRING_EXPENSE);
if ($filter) {
$query->where(function ($query) use ($filter) {
$query->where('recurring_expenses.public_notes', 'like', '%'.$filter.'%')
->orWhere('clients.name', 'like', '%'.$filter.'%')
->orWhere('vendors.name', 'like', '%'.$filter.'%')
->orWhere('expense_categories.name', 'like', '%'.$filter.'%');
;
});
}
return $query;
}
public function save($input, $expense = null)
{
$publicId = isset($input['public_id']) ? $input['public_id'] : false;
if ($expense) {
// do nothing
} elseif ($publicId) {
$expense = RecurringExpense::scope($publicId)->firstOrFail();
if (Utils::isNinjaDev()) {
\Log::warning('Entity not set in expense repo save');
}
} else {
$expense = RecurringExpense::createNew();
}
if ($expense->is_deleted) {
return $expense;
}
// First auto fill
$expense->fill($input);
if (isset($input['start_date'])) {
if ($expense->exists && $expense->start_date && $expense->start_date != Utils::toSqlDate($input['start_date'])) {
$expense->last_sent_date = null;
}
$expense->start_date = Utils::toSqlDate($input['start_date']);
}
if (isset($input['end_date'])) {
$expense->end_date = Utils::toSqlDate($input['end_date']);
}
if (! $expense->expense_currency_id) {
$expense->expense_currency_id = \Auth::user()->account->getCurrencyId();
}
/*
if (! $expense->invoice_currency_id) {
$expense->invoice_currency_id = \Auth::user()->account->getCurrencyId();
}
$rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1;
$expense->exchange_rate = round($rate, 4);
if (isset($input['amount'])) {
$expense->amount = round(Utils::parseFloat($input['amount']), 2);
}
*/
$expense->save();
return $expense;
}
public function createRecurringExpense(RecurringExpense $recurringExpense)
{
if ($recurringExpense->client && $recurringExpense->client->deleted_at) {
return false;
}
if (! $recurringExpense->user->confirmed) {
return false;
}
if (! $recurringExpense->shouldSendToday()) {
return false;
}
$account = $recurringExpense->account;
$expense = Expense::createNew($recurringExpense);
$fields = [
'vendor_id',
'client_id',
'amount',
'public_notes',
'private_notes',
'invoice_currency_id',
'expense_currency_id',
'should_be_invoiced',
'expense_category_id',
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
];
foreach ($fields as $field) {
$expense->$field = $recurringExpense->$field;
}
$expense->expense_date = $account->getDateTime()->format('Y-m-d');
$expense->exchange_rate = 1;
$expense->invoice_currency_id = $recurringExpense->expense_currency_id;
$expense->recurring_expense_id = $recurringExpense->id;
$expense->save();
$recurringExpense->last_sent_date = $account->getDateTime()->format('Y-m-d');
$recurringExpense->save();
return $expense;
}
}

View File

@ -35,6 +35,7 @@ class ActivityTransformer extends EntityTransformer
'expense_id' => $activity->expense_id ? $activity->expense->public_id : null,
'is_system' => $activity->is_system ? (bool) $activity->is_system : null,
'contact_id' => $activity->contact_id ? $activity->contact->public_id : null,
'task_id' => $activity->task_id ? $activity->task->public_id : null,
];
}
}

View File

@ -11,8 +11,9 @@ class DocumentTransformer extends EntityTransformer
{
/**
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="name", type="string", example="Test")
* @SWG\Property(property="type", type="string", example="CSV")
* @SWG\Property(property="name", type="string", example="sample.png")
* @SWG\Property(property="type", type="string", example="png")
* @SWG\Property(property="path", type="string", example="abc/sample.png")
* @SWG\Property(property="invoice_id", type="integer", example=1)
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
@ -23,8 +24,9 @@ class DocumentTransformer extends EntityTransformer
'id' => (int) $document->public_id,
'name' => $document->name,
'type' => $document->type,
'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null,
'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null,
'path' => $document->path,
'invoice_id' => $document->invoice_id && $document->invoice ? (int) $document->invoice->public_id : null,
'expense_id' => $document->expense_id && $document->expense ? (int) $document->expense->public_id : null,
'updated_at' => $this->getTimestamp($document->updated_at),
]);
}

View File

@ -34,6 +34,10 @@ class ExpenseTransformer extends EntityTransformer
* @SWG\Property(property="vendor_id", type="integer", example=1)
*/
protected $availableIncludes = [
'documents',
];
public function __construct($account = null, $serializer = null, $client = null)
{
parent::__construct($account, $serializer);
@ -41,6 +45,18 @@ class ExpenseTransformer extends EntityTransformer
$this->client = $client;
}
public function includeDocuments(Expense $expense)
{
$transformer = new DocumentTransformer($this->account, $this->serializer);
$expense->documents->each(function ($document) use ($expense) {
$document->setRelation('expense', $expense);
$document->setRelation('invoice', $expense->invoice);
});
return $this->includeCollection($expense->documents, $transformer, ENTITY_DOCUMENT);
}
public function transform(Expense $expense)
{
return array_merge($this->getDefaults($expense), [

View File

@ -77,6 +77,10 @@ class InvoiceTransformer extends EntityTransformer
{
$transformer = new DocumentTransformer($this->account, $this->serializer);
$invoice->documents->each(function ($document) use ($invoice) {
$document->setRelation('invoice', $invoice);
});
return $this->includeCollection($invoice->documents, $transformer, ENTITY_DOCUMENT);
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Policies;
use App\Models\User;
class RecurringExpensePolicy extends EntityPolicy
{
/**
* @param User $user
* @param mixed $item
*
* @return bool
*/
public static function create(User $user, $item)
{
if (! parent::create($user, $item)) {
return false;
}
return $user->hasFeature(FEATURE_EXPENSES);
}
}

View File

@ -18,6 +18,7 @@ class AuthServiceProvider extends ServiceProvider
\App\Models\Credit::class => \App\Policies\CreditPolicy::class,
\App\Models\Document::class => \App\Policies\DocumentPolicy::class,
\App\Models\Expense::class => \App\Policies\ExpensePolicy::class,
\App\Models\RecurringExpense::class => \App\Policies\RecurringExpensePolicy::class,
\App\Models\ExpenseCategory::class => \App\Policies\ExpenseCategoryPolicy::class,
\App\Models\Invoice::class => \App\Policies\InvoicePolicy::class,
\App\Models\Payment::class => \App\Policies\PaymentPolicy::class,

View File

@ -14,6 +14,7 @@ use App\Ninja\Repositories\VendorRepository;
use Hash;
use stdClass;
use Utils;
use Carbon;
/**
* Class BankAccountService.
@ -74,6 +75,7 @@ class BankAccountService extends BaseService
$expenses = Expense::scope()
->bankId($bankId)
->where('transaction_id', '!=', '')
->where('expense_date', '>=', Carbon::now()->subYear()->format('Y-m-d'))
->withTrashed()
->get(['transaction_id'])
->toArray();
@ -92,12 +94,13 @@ class BankAccountService extends BaseService
*
* @return array|bool
*/
public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true)
public function loadBankAccounts($bankAccount, $username, $password, $includeTransactions = true)
{
if (! $bankId || ! $username || ! $password) {
if (! $bankAccount || ! $username || ! $password) {
return false;
}
$bankId = $bankAccount->bank_id;
$expenses = $this->getExpenses();
$vendorMap = $this->createVendorMap();
$bankAccounts = BankSubaccount::scope()
@ -112,11 +115,18 @@ class BankAccountService extends BaseService
try {
$finance = new Finance();
$finance->banks[$bankId] = $bank->getOFXBank($finance);
$finance->banks[$bankId]->logins[] = new Login($finance->banks[$bankId], $username, $password);
$login = new Login($finance->banks[$bankId], $username, $password);
$login->appVersion = $bankAccount->app_version;
$login->ofxVersion = $bankAccount->ofx_version;
$finance->banks[$bankId]->logins[] = $login;
foreach ($finance->banks as $bank) {
foreach ($bank->logins as $login) {
$login->setup();
if (! is_array($login->accounts)) {
return false;
}
foreach ($login->accounts as $account) {
$account->setup($includeTransactions);
if ($account = $this->parseBankAccount($account, $bankAccounts, $expenses, $includeTransactions, $vendorMap)) {

View File

@ -30,6 +30,7 @@ use parsecsv;
use Session;
use stdClass;
use Utils;
use Carbon;
/**
* Class ImportService.
@ -183,45 +184,58 @@ class ImportService
if ($transformer->hasProduct($jsonProduct['product_key'])) {
continue;
}
if (EntityModel::validate($jsonProduct, ENTITY_PRODUCT) === true) {
$productValidate = EntityModel::validate($jsonProduct, ENTITY_PRODUCT);
if ($productValidate === true) {
$product = $this->productRepo->save($jsonProduct);
$this->addProductToMaps($product);
$this->addSuccess($product);
} else {
$jsonProduct['type'] = ENTITY_PRODUCT;
$jsonProduct['error'] = $productValidate;
$this->addFailure(ENTITY_PRODUCT, $jsonProduct);
continue;
}
}
foreach ($json['clients'] as $jsonClient) {
if (EntityModel::validate($jsonClient, ENTITY_CLIENT) === true) {
$clientValidate = EntityModel::validate($jsonClient, ENTITY_CLIENT);
if ($clientValidate === true) {
$client = $this->clientRepo->save($jsonClient);
$this->addClientToMaps($client);
$this->addSuccess($client);
} else {
$jsonClient['type'] = ENTITY_CLIENT;
$jsonClient['error'] = $clientValidate;
$this->addFailure(ENTITY_CLIENT, $jsonClient);
continue;
}
foreach ($jsonClient['invoices'] as $jsonInvoice) {
$jsonInvoice['client_id'] = $client->id;
if (EntityModel::validate($jsonInvoice, ENTITY_INVOICE) === true) {
$invoiceValidate = EntityModel::validate($jsonInvoice, ENTITY_INVOICE);
if ($invoiceValidate === true) {
$invoice = $this->invoiceRepo->save($jsonInvoice);
$this->addInvoiceToMaps($invoice);
$this->addSuccess($invoice);
} else {
$jsonInvoice['type'] = ENTITY_INVOICE;
$jsonInvoice['error'] = $invoiceValidate;
$this->addFailure(ENTITY_INVOICE, $jsonInvoice);
continue;
}
foreach ($jsonInvoice['payments'] as $jsonPayment) {
$jsonPayment['invoice_id'] = $invoice->public_id;
if (EntityModel::validate($jsonPayment, ENTITY_PAYMENT) === true) {
$paymentValidate = EntityModel::validate($jsonPayment, ENTITY_PAYMENT);
if ($paymentValidate === true) {
$jsonPayment['client_id'] = $client->id;
$jsonPayment['invoice_id'] = $invoice->id;
$payment = $this->paymentRepo->save($jsonPayment);
$this->addSuccess($payment);
} else {
$jsonPayment['type'] = ENTITY_PAYMENT;
$jsonPayment['error'] = $paymentValidate;
$this->addFailure(ENTITY_PAYMENT, $jsonPayment);
continue;
}
@ -520,7 +534,18 @@ class ImportService
// Lookup field translations
foreach ($columns as $key => $value) {
unset($columns[$key]);
$columns[$value] = trans("texts.{$value}");
$label = $value;
// disambiguate some of the labels
if ($entityType == ENTITY_INVOICE) {
if ($label == 'name') {
$label = 'client_name';
} elseif ($label == 'notes') {
$label = 'product_notes';
} elseif ($label == 'terms') {
$label = 'invoice_terms';
}
}
$columns[$value] = trans("texts.{$label}");
}
array_unshift($columns, ' ');
@ -581,8 +606,24 @@ class ImportService
'hasHeaders' => $hasHeaders,
'columns' => $columns,
'mapped' => $mapped,
'warning' => false,
];
// check that dates are valid
if (count($data['data']) > 1) {
$row = $data['data'][1];
foreach ($mapped as $index => $field) {
if (! strstr($field, 'date')) {
continue;
}
try {
$date = new Carbon($row[$index]);
} catch(Exception $e) {
$data['warning'] = 'invalid_date';
}
}
}
return $data;
}
@ -881,6 +922,8 @@ class ImportService
{
if ($key = strtolower(trim($product->product_key))) {
$this->maps['product'][$key] = $product->id;
$this->maps['product_notes'][$key] = $product->notes;
$this->maps['product_cost'][$key] = $product->cost;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Services;
use Utils;
use App\Models\Client;
use App\Models\Vendor;
use App\Ninja\Datatables\RecurringExpenseDatatable;
use App\Ninja\Repositories\RecurringExpenseRepository;
/**
* Class RecurringExpenseService.
*/
class RecurringExpenseService extends BaseService
{
/**
* @var RecurringExpenseRepository
*/
protected $recurringExpenseRepo;
/**
* @var DatatableService
*/
protected $datatableService;
/**
* CreditService constructor.
*
* @param RecurringExpenseRepository $creditRepo
* @param DatatableService $datatableService
*/
public function __construct(RecurringExpenseRepository $recurringExpenseRepo, DatatableService $datatableService)
{
$this->recurringExpenseRepo = $recurringExpenseRepo;
$this->datatableService = $datatableService;
}
/**
* @return CreditRepository
*/
protected function getRepo()
{
return $this->recurringExpenseRepo;
}
/**
* @param $data
* @param mixed $recurringExpense
*
* @return mixed|null
*/
public function save($data, $recurringExpense = false)
{
if (isset($data['client_id']) && $data['client_id']) {
$data['client_id'] = Client::getPrivateId($data['client_id']);
}
if (isset($data['vendor_id']) && $data['vendor_id']) {
$data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']);
}
return $this->recurringExpenseRepo->save($data, $recurringExpense);
}
/**
* @param $clientPublicId
* @param $search
* @param mixed $userId
*
* @return \Illuminate\Http\JsonResponse
*/
public function getDatatable($search, $userId)
{
$query = $this->recurringExpenseRepo->find($search);
if (! Utils::hasPermission('view_all')) {
$query->where('recurring_expenses.user_id', '=', Auth::user()->id);
}
return $this->datatableService->createDatatable(new RecurringExpenseDatatable(), $query);
}
}

View File

@ -30,7 +30,7 @@
"cerdic/css-tidy": "~v1.5",
"chumper/datatable": "dev-develop#04ef2bf",
"codedge/laravel-selfupdater": "5.x-dev",
"collizo4sky/omnipay-wepay": "^1.3",
"collizo4sky/omnipay-wepay": "dev-address-fix",
"delatbabel/omnipay-fatzebra": "dev-master",
"dercoder/omnipay-ecopayz": "~1.0",
"dercoder/omnipay-paysafecard": "dev-master",
@ -45,7 +45,7 @@
"fruitcakestudio/omnipay-sisow": "~2.0",
"fzaninotto/faker": "^1.5",
"gatepay/FedACHdir": "dev-master@dev",
"google/apiclient": "^1.0",
"google/apiclient": "^2.0",
"guzzlehttp/guzzle": "~6.0",
"incube8/omnipay-multicards": "dev-master",
"intervention/image": "dev-master",
@ -82,13 +82,13 @@
"turbo124/laravel-push-notification": "2.*",
"vink/omnipay-komoju": "~1.0",
"webpatser/laravel-countries": "dev-master",
"websight/l5-google-cloud-storage": "^1.0",
"websight/l5-google-cloud-storage": "dev-master",
"wepay/php-sdk": "^0.2",
"wildbit/laravel-postmark-provider": "3.0"
},
"require-dev": {
"codeception/c3": "~2.0",
"codeception/codeception": "*",
"codeception/codeception": "2.3.3",
"phpspec/phpspec": "~2.1",
"phpunit/phpunit": "~4.0",
"symfony/dom-crawler": "~3.0"
@ -152,6 +152,14 @@
"reference": "origin/master"
}
}
},
{
"type": "vcs",
"url": "https://github.com/hillelcoren/omnipay-wepay"
},
{
"type": "vcs",
"url": "https://github.com/hillelcoren/l5-google-cloud-storage"
}
]
}

1112
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -78,10 +78,12 @@ return [
'gcs' => [
'driver' => 'gcs',
'service_account' => env('GCS_USERNAME', ''),
'service_account_certificate' => storage_path() . '/credentials.p12',
'service_account_certificate_password' => env('GCS_PASSWORD', ''),
'bucket' => env('GCS_BUCKET', 'cloud-storage-bucket'),
//'service_account' => env('GCS_USERNAME', ''),
//'service_account_certificate' => storage_path() . '/credentials.p12',
//'service_account_certificate_password' => env('GCS_PASSWORD', ''),
'project_id' => env('GCS_PROJECT_ID'),
'credentials' => storage_path() . '/gcs-credentials.json',
],
],

View File

@ -54,7 +54,7 @@ class SimplifyTasks extends Migration
$table->integer('break_duration')->nullable();
});
if (Schema::hasColumn('accounts', 'dark_mode')) {
if (Schema::hasColumn('users', 'dark_mode')) {
Schema::table('users', function ($table) {
$table->dropColumn('dark_mode');
});

View File

@ -0,0 +1,107 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateDarkMode extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function ($table) {
$table->boolean('dark_mode')->default(true)->change();
});
Schema::table('accounts', function ($table) {
$table->integer('credit_number_counter')->default(0)->nullable();
$table->text('credit_number_prefix')->nullable();
$table->text('credit_number_pattern')->nullable();
});
DB::statement('update users set dark_mode = 1');
// update invoice_item_type_id for task invoice items
DB::statement('update invoice_items
left join invoices on invoices.id = invoice_items.invoice_id
set invoice_item_type_id = 2
where invoices.has_tasks = 1
and invoice_item_type_id = 1');
Schema::create('recurring_expenses', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->unsignedInteger('account_id')->index();
$table->unsignedInteger('vendor_id')->nullable();
$table->unsignedInteger('user_id');
$table->unsignedInteger('client_id')->nullable();
$table->boolean('is_deleted')->default(false);
$table->decimal('amount', 13, 2);
$table->text('private_notes');
$table->text('public_notes');
$table->unsignedInteger('invoice_currency_id')->nullable()->index();
$table->unsignedInteger('expense_currency_id')->nullable()->index();
$table->boolean('should_be_invoiced')->default(true);
$table->unsignedInteger('expense_category_id')->nullable()->index();
$table->string('tax_name1')->nullable();
$table->decimal('tax_rate1', 13, 3);
$table->string('tax_name2')->nullable();
$table->decimal('tax_rate2', 13, 3);
$table->unsignedInteger('frequency_id');
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->timestamp('last_sent_date')->nullable();
// Relations
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('invoice_currency_id')->references('id')->on('currencies');
$table->foreign('expense_currency_id')->references('id')->on('currencies');
$table->foreign('expense_category_id')->references('id')->on('expense_categories')->onDelete('cascade');
// Indexes
$table->unsignedInteger('public_id')->index();
$table->unique(['account_id', 'public_id']);
});
Schema::table('expenses', function ($table) {
$table->unsignedInteger('recurring_expense_id')->nullable();
});
Schema::table('bank_accounts', function ($table) {
$table->mediumInteger('app_version')->default(DEFAULT_BANK_APP_VERSION);
$table->mediumInteger('ofx_version')->default(DEFAULT_BANK_OFX_VERSION);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('recurring_expenses');
Schema::table('expenses', function ($table) {
$table->dropColumn('recurring_expense_id');
});
Schema::table('accounts', function ($table) {
$table->dropColumn('credit_number_counter');
$table->dropColumn('credit_number_prefix');
$table->dropColumn('credit_number_pattern');
});
Schema::table('bank_accounts', function ($table) {
$table->dropColumn('app_version');
$table->dropColumn('ofx_version');
});
}
}

View File

@ -1,5 +1,7 @@
<?php
use App\Models\Timezone;
class DatabaseSeeder extends Seeder
{
/**
@ -11,6 +13,11 @@ class DatabaseSeeder extends Seeder
{
$this->command->info('Running DatabaseSeeder');
if (Timezone::count()) {
$this->command->info('Skipping: already run');
return;
}
Eloquent::unguard();
$this->call('ConstantsSeeder');

View File

@ -41,6 +41,7 @@ class IndustrySeeder extends Seeder
['name' => 'Other'],
['name' => 'Photography'],
['name' => 'Construction'],
['name' => 'Restaurant & Catering'],
];
foreach ($industries as $industry) {

View File

@ -71,6 +71,7 @@ class PaymentLibrariesSeeder extends Seeder
['name' => 'WePay', 'provider' => 'WePay', 'is_offsite' => false],
['name' => 'Braintree', 'provider' => 'Braintree', 'sort_order' => 2],
['name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 8],
['name' => 'FirstData Payeezy', 'provider' => 'FirstData_Payeezy'],
];
foreach ($gateways as $gateway) {

View File

@ -36,9 +36,9 @@ class UserTableSeeder extends Seeder
'invoice_terms' => $faker->text($faker->numberBetween(50, 300)),
'work_phone' => $faker->phoneNumber,
'work_email' => $faker->safeEmail,
'invoice_design_id' => InvoiceDesign::where('id', '<', CUSTOM_DESIGN1)->get()->random()->id,
'header_font_id' => min(Font::all()->random()->id, 17),
'body_font_id' => min(Font::all()->random()->id, 17),
//'invoice_design_id' => InvoiceDesign::where('id', '<', CUSTOM_DESIGN1)->get()->random()->id,
//'header_font_id' => min(Font::all()->random()->id, 17),
//'body_font_id' => min(Font::all()->random()->id, 17),
'primary_color' => $faker->hexcolor,
'timezone_id' => 58,
'company_id' => $company->id,

File diff suppressed because one or more lines are too long

View File

@ -57,9 +57,9 @@ author = u'Invoice Ninja'
# built documents.
#
# The short X.Y version.
version = u'3.4'
version = u'3.5'
# The full version, including alpha/beta/rc tags.
release = u'3.4.2'
release = u'3.5.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -5,6 +5,8 @@ Update
To update the app you just need to copy over the latest code. The app tracks the current version in a file called version.txt, if it notices a change it loads ``/update`` to run the database migrations.
.. TIP:: You can use this `shell script <https://pastebin.com/j657uv9A>`_ to automate the update process, consider running it as a daily cron to automatically keep your app up to date.
If you're moving servers make sure to copy over the .env file.
If the auto-update fails you can manually run the update with the following commands. Once completed add ``?clear_cache=true`` to the end of the URL to clear the application cache.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -39,38 +39,68 @@ thead th {
border-left: 1px solid #999;
}
.sidebar-nav {
.sidebar-nav-dark {
background-color: #313131;
}
.sidebar-nav li {
.sidebar-nav-dark li {
border-bottom:solid 1px #444444;
}
.sidebar-nav i.fa {
color: white;
.sidebar-nav-dark li > a {
color: #aaa;
}
.menu-toggle i,
.sidebar-nav li > a {
color: #999999;
}
.menu-toggle:hover i,
.sidebar-nav li:hover > a,
.sidebar-nav li > a.active {
.sidebar-nav-dark li:hover > a,
.sidebar-nav-dark li > a.active {
color: #fff;
}
.sidebar-nav li:hover,
.sidebar-nav li.active {
.sidebar-nav-dark li:hover,
.sidebar-nav-dark li.active {
background: rgba(255,255,255,0.1);
}
.sidebar-nav-light {
background-color: #FFFFFF;
}
.sidebar-nav-light li {
border-bottom:solid 1px #DDD;
}
.sidebar-nav-light li > a {
color: #757575;
}
.sidebar-nav-light li:hover > a,
.sidebar-nav-light li > a.active {
color: #363636;
}
.sidebar-nav-light li:hover,
.sidebar-nav-light li.active {
background: rgba(140,140,140,0.1);
}
.menu-toggle i {
color: #999999;
}
.menu-toggle:hover {
color: #fff;
}
.menu-toggle {
color: #999 !important;
}
.sidebar-nav a.btn i.fa,
.navbar-header:hover i,
.menu-toggle:hover {
color: #fff !important;
}

View File

@ -32,10 +32,11 @@ html {
margin-right: 16px !important;
}
#footer,
.navbar {
x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
x-moz-box-shadow: 0 0 8px 2px rgba(0,0,0,.6);
x-webkit-box-shadow: 0 0 8px 2px rgba(0,0,0,.6);
box-shadow: 0 0 8px 2px rgba(0,0,0,.6);
border-width: 0;
border-radius: 0;
}

View File

@ -92,13 +92,23 @@
margin: 0;
list-style: none;
height: 100%;
}
.sidebar-nav-dark {
box-shadow: inset 0 0 6px #000000;
-moz-box-shadow: inset 0 0 6px #000000;
-webkit-box-shadow: inset 0 0 5px #000000;
}
.sidebar-nav-light {
height: 100%;
box-shadow: inset 0 0 4px #888;
-moz-box-shadow: inset 0 0 4px #888;
-webkit-box-shadow: inset 0 0 4px #888;
}
#left-sidebar-wrapper .sidebar-nav li {
text-indent: 20px;
text-indent: 14px;
line-height: 36px;
}

View File

@ -412,7 +412,12 @@ background-color: #9b9b9b;
xbackground-color: #337ab7;
}
.navbar,
.navbar {
x-moz-box-shadow: 0 0 8px 2px rgba(0,0,0,.6);
x-webkit-box-shadow: 0 0 8px 2px rgba(0,0,0,.6);
box-shadow: 0 0 8px 2px rgba(0,0,0,.6);
}
ul.dropdown-menu,
.twitter-typeahead .tt-menu {
x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05);
@ -1093,7 +1098,7 @@ div.panel-body div.panel-body {
}
.invoice-table #document-upload{
width:550px;
width: 100%;
}
#document-upload .dropzone{

Some files were not shown because too many files have changed in this diff Show More