mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
4244301b62
20
.env.example
20
.env.example
@ -20,11 +20,25 @@ MAIL_FROM_ADDRESS
|
|||||||
MAIL_FROM_NAME
|
MAIL_FROM_NAME
|
||||||
MAIL_PASSWORD
|
MAIL_PASSWORD
|
||||||
|
|
||||||
|
#POSTMARK_API_TOKEN=
|
||||||
|
|
||||||
PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'
|
PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'
|
||||||
LOG=single
|
LOG=single
|
||||||
REQUIRE_HTTPS=false
|
REQUIRE_HTTPS=false
|
||||||
API_SECRET=password
|
API_SECRET=password
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID
|
#TRUSTED_PROXIES=
|
||||||
GOOGLE_CLIENT_SECRET
|
|
||||||
GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google
|
#SESSION_DRIVER=
|
||||||
|
#SESSION_DOMAIN=
|
||||||
|
#SESSION_ENCRYPT=
|
||||||
|
#SESSION_SECURE=
|
||||||
|
|
||||||
|
#CACHE_DRIVER=
|
||||||
|
#CACHE_HOST=
|
||||||
|
#CACHE_PORT1=
|
||||||
|
#CACHE_PORT2=
|
||||||
|
|
||||||
|
#GOOGLE_CLIENT_ID=
|
||||||
|
#GOOGLE_CLIENT_SECRET=
|
||||||
|
#GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google
|
@ -189,7 +189,7 @@ class AccountRepository
|
|||||||
return $invitation;
|
return $invitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createNinjaInvoice($client, $account)
|
public function createNinjaInvoice($client, $clientAccount)
|
||||||
{
|
{
|
||||||
$account = $this->getNinjaAccount();
|
$account = $this->getNinjaAccount();
|
||||||
$lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first();
|
$lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first();
|
||||||
@ -201,7 +201,7 @@ class AccountRepository
|
|||||||
$invoice->public_id = $publicId;
|
$invoice->public_id = $publicId;
|
||||||
$invoice->client_id = $client->id;
|
$invoice->client_id = $client->id;
|
||||||
$invoice->invoice_number = $account->getNextInvoiceNumber($invoice);
|
$invoice->invoice_number = $account->getNextInvoiceNumber($invoice);
|
||||||
$invoice->invoice_date = $account->getRenewalDate();
|
$invoice->invoice_date = $clientAccount->getRenewalDate();
|
||||||
$invoice->amount = PRO_PLAN_PRICE;
|
$invoice->amount = PRO_PLAN_PRICE;
|
||||||
$invoice->balance = PRO_PLAN_PRICE;
|
$invoice->balance = PRO_PLAN_PRICE;
|
||||||
$invoice->save();
|
$invoice->save();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php namespace App\Ninja\Repositories;
|
<?php namespace App\Ninja\Repositories;
|
||||||
|
|
||||||
use DB;
|
use DB;
|
||||||
|
use Cache;
|
||||||
use App\Ninja\Repositories\BaseRepository;
|
use App\Ninja\Repositories\BaseRepository;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
@ -74,6 +75,17 @@ class ClientRepository extends BaseRepository
|
|||||||
$client = Client::scope($publicId)->with('contacts')->firstOrFail();
|
$client = Client::scope($publicId)->with('contacts')->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert currency code to id
|
||||||
|
if (isset($data['currency_code'])) {
|
||||||
|
$currencyCode = strtolower($data['currency_code']);
|
||||||
|
$currency = Cache::get('currencies')->filter(function($item) use ($currencyCode) {
|
||||||
|
return strtolower($item->code) == $currencyCode;
|
||||||
|
})->first();
|
||||||
|
if ($currency) {
|
||||||
|
$data['currency_id'] = $currency->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$client->fill($data);
|
$client->fill($data);
|
||||||
$client->save();
|
$client->save();
|
||||||
|
|
||||||
|
@ -30967,6 +30967,29 @@ function prettyJson(json) {
|
|||||||
return '<span class="' + cls + '">' + match + '</span>';
|
return '<span class="' + cls + '">' + match + '</span>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function searchData(data, key, fuzzy) {
|
||||||
|
return function findMatches(q, cb) {
|
||||||
|
var matches, substringRegex;
|
||||||
|
if (fuzzy) {
|
||||||
|
var options = {
|
||||||
|
keys: [key],
|
||||||
|
}
|
||||||
|
var fuse = new Fuse(data, options);
|
||||||
|
matches = fuse.search(q);
|
||||||
|
} else {
|
||||||
|
matches = [];
|
||||||
|
substrRegex = new RegExp(q, 'i');
|
||||||
|
$.each(data, function(i, obj) {
|
||||||
|
if (substrRegex.test(obj[key])) {
|
||||||
|
matches.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cb(matches);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var NINJA = NINJA || {};
|
var NINJA = NINJA || {};
|
||||||
|
|
||||||
NINJA.TEMPLATES = {
|
NINJA.TEMPLATES = {
|
||||||
|
2
public/css/built.css
vendored
2
public/css/built.css
vendored
@ -2060,7 +2060,7 @@ See http://bgrins.github.io/spectrum/themes/ for instructions.
|
|||||||
|
|
||||||
/*root typeahead class*/
|
/*root typeahead class*/
|
||||||
.twitter-typeahead {
|
.twitter-typeahead {
|
||||||
display: inherit !important;
|
/*display: inherit !important;*/
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
public/css/typeahead.js-bootstrap.css
vendored
2
public/css/typeahead.js-bootstrap.css
vendored
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
/*root typeahead class*/
|
/*root typeahead class*/
|
||||||
.twitter-typeahead {
|
.twitter-typeahead {
|
||||||
display: inherit !important;
|
/*display: inherit !important;*/
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1074,4 +1074,26 @@ function prettyJson(json) {
|
|||||||
match = snakeToCamel(match);
|
match = snakeToCamel(match);
|
||||||
return '<span class="' + cls + '">' + match + '</span>';
|
return '<span class="' + cls + '">' + match + '</span>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function searchData(data, key, fuzzy) {
|
||||||
|
return function findMatches(q, cb) {
|
||||||
|
var matches, substringRegex;
|
||||||
|
if (fuzzy) {
|
||||||
|
var options = {
|
||||||
|
keys: [key],
|
||||||
|
}
|
||||||
|
var fuse = new Fuse(data, options);
|
||||||
|
matches = fuse.search(q);
|
||||||
|
} else {
|
||||||
|
matches = [];
|
||||||
|
substrRegex = new RegExp(q, 'i');
|
||||||
|
$.each(data, function(i, obj) {
|
||||||
|
if (substrRegex.test(obj[key])) {
|
||||||
|
matches.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cb(matches);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -259,13 +259,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showSearch() {
|
function showSearch() {
|
||||||
console.log('showSearch..');
|
$('#search').typeahead('val', '');
|
||||||
//$('#search').typeahead('setQuery', '');
|
|
||||||
$('#navbar-options').hide();
|
$('#navbar-options').hide();
|
||||||
$('#search-form').show();
|
$('#search-form').show();
|
||||||
|
|
||||||
if (window.hasOwnProperty('loadedSearchData')) {
|
if (window.hasOwnProperty('loadedSearchData')) {
|
||||||
console.log('has data');
|
|
||||||
$('#search').focus();
|
$('#search').focus();
|
||||||
} else {
|
} else {
|
||||||
trackEvent('/activity', '/search');
|
trackEvent('/activity', '/search');
|
||||||
@ -275,68 +273,24 @@
|
|||||||
$('#search').typeahead({
|
$('#search').typeahead({
|
||||||
hint: true,
|
hint: true,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
},
|
}
|
||||||
{
|
@foreach (['clients', 'contacts', 'invoices', 'quotes', 'navigation'] as $type)
|
||||||
|
,{
|
||||||
name: 'data',
|
name: 'data',
|
||||||
display: 'value',
|
display: 'value',
|
||||||
source: searchData(data['clients']),
|
source: searchData(data['{{ $type }}'], 'value', true),
|
||||||
templates: {
|
templates: {
|
||||||
header: ' <b>{{ trans('texts.clients') }}</b>'
|
header: ' <span style="font-weight:600;font-size:16px">{{ trans("texts.{$type}") }}</span>'
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
@endforeach
|
||||||
name: 'data',
|
).on('typeahead:selected', function(element, datum, name) {
|
||||||
display: 'value',
|
|
||||||
source: searchData(data['contacts']),
|
|
||||||
templates: {
|
|
||||||
header: ' <b>{{ trans('texts.contacts') }}</b>'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'data',
|
|
||||||
display: 'value',
|
|
||||||
source: searchData(data['invoices']),
|
|
||||||
templates: {
|
|
||||||
header: ' <b>{{ trans('texts.contacts') }}</b>'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'data',
|
|
||||||
display: 'value',
|
|
||||||
source: searchData(data['quotes']),
|
|
||||||
templates: {
|
|
||||||
header: ' <b>{{ trans('texts.quotes') }}</b>'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'data',
|
|
||||||
display: 'value',
|
|
||||||
source: searchData(data['navigation']),
|
|
||||||
templates: {
|
|
||||||
header: ' <b>{{ trans('texts.navigation') }}</b>'
|
|
||||||
}
|
|
||||||
}).on('typeahead:selected', function(element, datum, name) {
|
|
||||||
window.location = datum.url;
|
window.location = datum.url;
|
||||||
}).focus();
|
}).focus();
|
||||||
|
|
||||||
//.typeahead('setQuery', $('#search').val());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchData(data) {
|
|
||||||
return function findMatches(q, cb) {
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
keys: ['value'],
|
|
||||||
}
|
|
||||||
var fuse = new Fuse(data, options);
|
|
||||||
var matches = fuse.search(q);
|
|
||||||
|
|
||||||
cb(matches);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function hideSearch() {
|
function hideSearch() {
|
||||||
$('#search-form').hide();
|
$('#search-form').hide();
|
||||||
$('#navbar-options').show();
|
$('#navbar-options').show();
|
||||||
|
@ -209,11 +209,7 @@
|
|||||||
$parent.invoice_items().length > 1" class="fa fa-sort"></i>
|
$parent.invoice_items().length > 1" class="fa fa-sort"></i>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{!! Former::text('product_key')->useDatalist($products->toArray(), 'product_key')
|
<input id="product_key" type="text" data-bind="typeahead: product_key, items: $root.products, key: 'product_key', valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][product_key]'}" class="form-control invoice-item handled"/>
|
||||||
->data_bind("value: product_key, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + \$index() + '][product_key]'}")
|
|
||||||
->addClass('datalist')
|
|
||||||
->raw()
|
|
||||||
!!}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
|
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
|
||||||
@ -915,6 +911,9 @@
|
|||||||
function applyComboboxListeners() {
|
function applyComboboxListeners() {
|
||||||
var selectorStr = '.invoice-table input, .invoice-table textarea';
|
var selectorStr = '.invoice-table input, .invoice-table textarea';
|
||||||
$(selectorStr).off('change').on('change', function(event) {
|
$(selectorStr).off('change').on('change', function(event) {
|
||||||
|
if ($(event.target).hasClass('handled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onItemChange();
|
onItemChange();
|
||||||
refreshPDF(true);
|
refreshPDF(true);
|
||||||
});
|
});
|
||||||
@ -930,38 +929,6 @@
|
|||||||
$(this).height($(this).height()+1);
|
$(this).height($(this).height()+1);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@if (Auth::user()->account->fill_products)
|
|
||||||
$('.datalist').off('input').on('input', function() {
|
|
||||||
var key = $(this).val();
|
|
||||||
for (var i=0; i<products.length; i++) {
|
|
||||||
var product = products[i];
|
|
||||||
if (product.product_key == key) {
|
|
||||||
var model = ko.dataFor(this);
|
|
||||||
if (model.expense_public_id()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (product.notes) {
|
|
||||||
model.notes(product.notes);
|
|
||||||
}
|
|
||||||
if (product.cost) {
|
|
||||||
model.cost(accounting.toFixed(product.cost, 2));
|
|
||||||
}
|
|
||||||
if (!model.qty()) {
|
|
||||||
model.qty(1);
|
|
||||||
}
|
|
||||||
@if ($account->invoice_item_taxes)
|
|
||||||
if (product.default_tax_rate) {
|
|
||||||
model.tax(self.model.getTaxRateById(product.default_tax_rate.public_id));
|
|
||||||
}
|
|
||||||
@endif
|
|
||||||
model.product_key(key);
|
|
||||||
onItemChange();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInvoiceModel() {
|
function createInvoiceModel() {
|
||||||
|
@ -9,7 +9,7 @@ function ViewModel(data) {
|
|||||||
self.expense_currency_id = ko.observable();
|
self.expense_currency_id = ko.observable();
|
||||||
self.tax_rates = ko.observableArray();
|
self.tax_rates = ko.observableArray();
|
||||||
self.tax_rates.push(new TaxRateModel()); // add blank row
|
self.tax_rates.push(new TaxRateModel()); // add blank row
|
||||||
|
self.products = {!! $products !!};
|
||||||
|
|
||||||
self.loadClient = function(client) {
|
self.loadClient = function(client) {
|
||||||
ko.mapping.fromJS(client, model.invoice().client().mapping, model.invoice().client);
|
ko.mapping.fromJS(client, model.invoice().client().mapping, model.invoice().client);
|
||||||
@ -807,4 +807,56 @@ function ItemModel(data) {
|
|||||||
this.onSelect = function() {}
|
this.onSelect = function() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
/* Custom binding for product key typeahead */
|
||||||
|
ko.bindingHandlers.typeahead = {
|
||||||
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||||
|
var $element = $(element);
|
||||||
|
var allBindings = allBindingsAccessor();
|
||||||
|
|
||||||
|
$element.typeahead({
|
||||||
|
highlight: true,
|
||||||
|
minLength: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
display: allBindings.key,
|
||||||
|
source: searchData(allBindings.items, allBindings.key)
|
||||||
|
}).on('typeahead:select', function(element, datum, name) {
|
||||||
|
@if (Auth::user()->account->fill_products)
|
||||||
|
var model = ko.dataFor(this);
|
||||||
|
if (model.expense_public_id()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (datum.notes) {
|
||||||
|
model.notes(datum.notes);
|
||||||
|
}
|
||||||
|
if (datum.cost) {
|
||||||
|
model.cost(accounting.toFixed(datum.cost, 2));
|
||||||
|
}
|
||||||
|
if (!model.qty()) {
|
||||||
|
model.qty(1);
|
||||||
|
}
|
||||||
|
@if ($account->invoice_item_taxes)
|
||||||
|
if (datum.default_tax_rate) {
|
||||||
|
model.tax(self.model.getTaxRateById(datum.default_tax_rate.public_id));
|
||||||
|
}
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
onItemChange();
|
||||||
|
}).on('typeahead:change', function(element, datum, name) {
|
||||||
|
var value = valueAccessor();
|
||||||
|
value(datum);
|
||||||
|
onItemChange();
|
||||||
|
refreshPDF(true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function (element, valueAccessor) {
|
||||||
|
var value = ko.utils.unwrapObservable(valueAccessor());
|
||||||
|
if (value) {
|
||||||
|
$(element).typeahead('val', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
@ -44,6 +44,7 @@ class CheckBalanceCest
|
|||||||
$I->amOnPage('/invoices/create');
|
$I->amOnPage('/invoices/create');
|
||||||
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
|
$I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable');
|
||||||
$I->click('Save');
|
$I->click('Save');
|
||||||
$I->wait(1);
|
$I->wait(1);
|
||||||
$I->see($clientEmail);
|
$I->see($clientEmail);
|
||||||
|
@ -53,6 +53,7 @@ class OnlinePaymentCest
|
|||||||
$I->amOnPage('/invoices/create');
|
$I->amOnPage('/invoices/create');
|
||||||
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
|
$I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable');
|
||||||
$I->click('Save');
|
$I->click('Save');
|
||||||
$I->see($clientEmail);
|
$I->see($clientEmail);
|
||||||
|
|
||||||
@ -88,6 +89,7 @@ class OnlinePaymentCest
|
|||||||
// create recurring invoice and auto-bill
|
// create recurring invoice and auto-bill
|
||||||
$I->amOnPage('/recurring_invoices/create');
|
$I->amOnPage('/recurring_invoices/create');
|
||||||
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
|
$I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable');
|
||||||
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
$I->checkOption('#auto_bill');
|
$I->checkOption('#auto_bill');
|
||||||
$I->executeJS('preparePdfData(\'email\')');
|
$I->executeJS('preparePdfData(\'email\')');
|
||||||
|
@ -40,6 +40,7 @@ class PaymentCest
|
|||||||
$I->amOnPage('/invoices/create');
|
$I->amOnPage('/invoices/create');
|
||||||
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
|
$I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable');
|
||||||
$I->click('Save');
|
$I->click('Save');
|
||||||
$I->see($clientEmail);
|
$I->see($clientEmail);
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@ class TaxRatesCest
|
|||||||
$I->amOnPage('/invoices/create');
|
$I->amOnPage('/invoices/create');
|
||||||
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
|
$I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable');
|
||||||
$I->selectOption('#taxRateSelect', $invoiceTaxName . ' ' . $invoiceTaxRate . '%');
|
$I->selectOption('#taxRateSelect', $invoiceTaxName . ' ' . $invoiceTaxRate . '%');
|
||||||
$I->wait(2);
|
$I->wait(2);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user