mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-23 20:00:33 -04:00
Merge branch 'release-3.5.0'
This commit is contained in:
commit
cbef2425f2
@ -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
|
||||
|
@ -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/)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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'));
|
||||
|
168
app/Http/Controllers/RecurringExpenseController.php
Normal file
168
app/Http/Controllers/RecurringExpenseController.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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')) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
28
app/Http/Requests/CreateRecurringExpenseRequest.php
Normal file
28
app/Http/Requests/CreateRecurringExpenseRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
|
8
app/Http/Requests/RecurringExpenseRequest.php
Normal file
8
app/Http/Requests/RecurringExpenseRequest.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class RecurringExpenseRequest extends ExpenseRequest
|
||||
{
|
||||
protected $entityType = ENTITY_RECURRING_EXPENSE;
|
||||
}
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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),
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 [];
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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),
|
||||
];
|
||||
|
28
app/Http/Requests/UpdateRecurringExpenseRequest.php
Normal file
28
app/Http/Requests/UpdateRecurringExpenseRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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');
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -80,6 +80,7 @@ class HistoryUtils
|
||||
ENTITY_QUOTE,
|
||||
ENTITY_TASK,
|
||||
ENTITY_EXPENSE,
|
||||
//ENTITY_RECURRING_EXPENSE,
|
||||
];
|
||||
|
||||
if (! in_array($entityType, $trackedTypes)) {
|
||||
|
@ -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') {
|
||||
|
@ -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)
|
||||
|
@ -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'));
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
154
app/Models/RecurringExpense.php
Normal file
154
app/Models/RecurringExpense.php
Normal 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();
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
129
app/Models/Traits/HasRecurrence.php
Normal file
129
app/Models/Traits/HasRecurrence.php
Normal 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;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
121
app/Ninja/Datatables/RecurringExpenseDatatable.php
Normal file
121
app/Ninja/Datatables/RecurringExpenseDatatable.php
Normal 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]);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -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>";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
],
|
||||
],
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) . '%',
|
||||
]);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
194
app/Ninja/Repositories/RecurringExpenseRepository.php
Normal file
194
app/Ninja/Repositories/RecurringExpenseRepository.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
@ -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), [
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
23
app/Policies/RecurringExpensePolicy.php
Normal file
23
app/Policies/RecurringExpensePolicy.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
82
app/Services/RecurringExpenseService.php
Normal file
82
app/Services/RecurringExpenseService.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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
1112
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
107
database/migrations/2017_06_19_111515_update_dark_mode.php
Normal file
107
database/migrations/2017_06_19_111515_update_dark_mode.php
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
@ -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');
|
||||
|
@ -41,6 +41,7 @@ class IndustrySeeder extends Seeder
|
||||
['name' => 'Other'],
|
||||
['name' => 'Photography'],
|
||||
['name' => 'Construction'],
|
||||
['name' => 'Restaurant & Catering'],
|
||||
];
|
||||
|
||||
foreach ($industries as $industry) {
|
||||
|
@ -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) {
|
||||
|
@ -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
@ -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.
|
||||
|
@ -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
4
public/css/built.css
vendored
4
public/css/built.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/css/built.public.css
vendored
2
public/css/built.public.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
58
resources/assets/css/colors.css
vendored
58
resources/assets/css/colors.css
vendored
@ -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;
|
||||
}
|
||||
|
7
resources/assets/css/public.style.css
vendored
7
resources/assets/css/public.style.css
vendored
@ -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;
|
||||
}
|
||||
|
12
resources/assets/css/sidebar.css
vendored
12
resources/assets/css/sidebar.css
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
9
resources/assets/css/style.css
vendored
9
resources/assets/css/style.css
vendored
@ -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
Loading…
x
Reference in New Issue
Block a user