mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 15:54:31 -04:00
Added expense category
This commit is contained in:
parent
f2cbfec926
commit
21a91ff0e1
@ -7,11 +7,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
- Configuration for first day of the week #950
|
- Configuration for first day of the week #950
|
||||||
- StyleCI configuration #929
|
- StyleCI configuration #929
|
||||||
|
- Added expense category
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Removed `invoiceninja.komodoproject` from Git #932
|
- Removed `invoiceninja.komodoproject` from Git #932
|
||||||
- `APP_CIPHER` changed from `rinjdael-128` to `AES-256-CBC` #898
|
- `APP_CIPHER` changed from `rinjdael-128` to `AES-256-CBC` #898
|
||||||
- Improved options when exporting data
|
- Improved options when exporting data
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- "Manual entry" untranslatable #562
|
- "Manual entry" untranslatable #562
|
||||||
|
@ -10,6 +10,7 @@ use Redirect;
|
|||||||
use Cache;
|
use Cache;
|
||||||
use App\Models\Vendor;
|
use App\Models\Vendor;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
|
use App\Models\ExpenseCategory;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Services\ExpenseService;
|
use App\Services\ExpenseService;
|
||||||
use App\Ninja\Repositories\ExpenseRepository;
|
use App\Ninja\Repositories\ExpenseRepository;
|
||||||
@ -73,7 +74,7 @@ class ExpenseController extends BaseController
|
|||||||
} else {
|
} else {
|
||||||
$vendor = null;
|
$vendor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $request->vendor_id,
|
'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $request->vendor_id,
|
||||||
'expense' => null,
|
'expense' => null,
|
||||||
@ -94,7 +95,7 @@ class ExpenseController extends BaseController
|
|||||||
public function edit(ExpenseRequest $request)
|
public function edit(ExpenseRequest $request)
|
||||||
{
|
{
|
||||||
$expense = $request->entity();
|
$expense = $request->entity();
|
||||||
|
|
||||||
$expense->expense_date = Utils::fromSqlDate($expense->expense_date);
|
$expense->expense_date = Utils::fromSqlDate($expense->expense_date);
|
||||||
|
|
||||||
$actions = [];
|
$actions = [];
|
||||||
@ -140,7 +141,7 @@ class ExpenseController extends BaseController
|
|||||||
{
|
{
|
||||||
$data = $request->input();
|
$data = $request->input();
|
||||||
$data['documents'] = $request->file('documents');
|
$data['documents'] = $request->file('documents');
|
||||||
|
|
||||||
$expense = $this->expenseService->save($data, $request->entity());
|
$expense = $this->expenseService->save($data, $request->entity());
|
||||||
|
|
||||||
Session::flash('message', trans('texts.updated_expense'));
|
Session::flash('message', trans('texts.updated_expense'));
|
||||||
@ -157,7 +158,7 @@ class ExpenseController extends BaseController
|
|||||||
{
|
{
|
||||||
$data = $request->input();
|
$data = $request->input();
|
||||||
$data['documents'] = $request->file('documents');
|
$data['documents'] = $request->file('documents');
|
||||||
|
|
||||||
$expense = $this->expenseService->save($data);
|
$expense = $this->expenseService->save($data);
|
||||||
|
|
||||||
Session::flash('message', trans('texts.created_expense'));
|
Session::flash('message', trans('texts.created_expense'));
|
||||||
@ -176,7 +177,7 @@ class ExpenseController extends BaseController
|
|||||||
$expenses = Expense::scope($ids)->with('client')->get();
|
$expenses = Expense::scope($ids)->with('client')->get();
|
||||||
$clientPublicId = null;
|
$clientPublicId = null;
|
||||||
$currencyId = null;
|
$currencyId = null;
|
||||||
|
|
||||||
// Validate that either all expenses do not have a client or if there is a client, it is the same client
|
// Validate that either all expenses do not have a client or if there is a client, it is the same client
|
||||||
foreach ($expenses as $expense)
|
foreach ($expenses as $expense)
|
||||||
{
|
{
|
||||||
@ -232,6 +233,7 @@ class ExpenseController extends BaseController
|
|||||||
'countries' => Cache::get('countries'),
|
'countries' => Cache::get('countries'),
|
||||||
'customLabel1' => Auth::user()->account->custom_vendor_label1,
|
'customLabel1' => Auth::user()->account->custom_vendor_label1,
|
||||||
'customLabel2' => Auth::user()->account->custom_vendor_label2,
|
'customLabel2' => Auth::user()->account->custom_vendor_label2,
|
||||||
|
'categories' => ExpenseCategory::scope()->get()
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,8 +39,17 @@ class Expense extends EntityModel
|
|||||||
'public_notes',
|
'public_notes',
|
||||||
'bank_id',
|
'bank_id',
|
||||||
'transaction_id',
|
'transaction_id',
|
||||||
|
'expense_category_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function expense_category()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\ExpenseCategory');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
*/
|
*/
|
||||||
|
@ -25,4 +25,4 @@ class ExpenseCategory extends EntityModel
|
|||||||
return $this->belongsTo('App\Models\Expense');
|
return $this->belongsTo('App\Models\Expense');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ use Utils;
|
|||||||
use App\Ninja\Repositories\ExpenseRepository;
|
use App\Ninja\Repositories\ExpenseRepository;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Vendor;
|
use App\Models\Vendor;
|
||||||
|
use App\Models\ExpenseCategory;
|
||||||
use App\Ninja\Datatables\ExpenseDatatable;
|
use App\Ninja\Datatables\ExpenseDatatable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,6 +58,19 @@ class ExpenseService extends BaseService
|
|||||||
$data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']);
|
$data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! empty($data['category'])) {
|
||||||
|
$name = trim($data['category']);
|
||||||
|
$category = ExpenseCategory::scope()->whereName($name)->first();
|
||||||
|
if ( ! $category) {
|
||||||
|
$category = ExpenseCategory::createNew();
|
||||||
|
$category->name = $name;
|
||||||
|
$category->save();
|
||||||
|
}
|
||||||
|
$data['expense_category_id'] = $category->id;
|
||||||
|
} elseif (isset($data['category'])) {
|
||||||
|
$data['expense_category_id'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->expenseRepo->save($data, $expense);
|
return $this->expenseRepo->save($data, $expense);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30319,6 +30319,34 @@ if (window.ko) {
|
|||||||
trigger: "hover"
|
trigger: "hover"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
limit: 50,
|
||||||
|
source: searchData(allBindings.items, allBindings.key)
|
||||||
|
}).on('typeahead:change', function(element, datum, name) {
|
||||||
|
var value = valueAccessor();
|
||||||
|
value(datum);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function (element, valueAccessor) {
|
||||||
|
var value = ko.utils.unwrapObservable(valueAccessor());
|
||||||
|
if (value) {
|
||||||
|
$(element).typeahead('val', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContactDisplayName(contact)
|
function getContactDisplayName(contact)
|
||||||
@ -30450,7 +30478,7 @@ function formatAddress(city, state, zip, swap) {
|
|||||||
str += zip ? zip + ' ' : '';
|
str += zip ? zip + ' ' : '';
|
||||||
str += city ? city : '';
|
str += city ? city : '';
|
||||||
str += (city && state) ? ', ' : (city ? ' ' : '');
|
str += (city && state) ? ', ' : (city ? ' ' : '');
|
||||||
str += state;
|
str += state;
|
||||||
} else {
|
} else {
|
||||||
str += city ? city : '';
|
str += city ? city : '';
|
||||||
str += (city && state) ? ', ' : (state ? ' ' : '');
|
str += (city && state) ? ', ' : (state ? ' ' : '');
|
||||||
@ -30484,7 +30512,7 @@ function calculateAmounts(invoice) {
|
|||||||
var hasTaxes = false;
|
var hasTaxes = false;
|
||||||
var taxes = {};
|
var taxes = {};
|
||||||
invoice.has_product_key = false;
|
invoice.has_product_key = false;
|
||||||
|
|
||||||
// Bold designs currently breaks w/o the product column
|
// Bold designs currently breaks w/o the product column
|
||||||
if (invoice.invoice_design_id == 2) {
|
if (invoice.invoice_design_id == 2) {
|
||||||
invoice.has_product_key = true;
|
invoice.has_product_key = true;
|
||||||
@ -30532,7 +30560,7 @@ function calculateAmounts(invoice) {
|
|||||||
lineTotal -= roundToTwo(lineTotal * (invoice.discount/100));
|
lineTotal -= roundToTwo(lineTotal * (invoice.discount/100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var taxAmount1 = roundToTwo(lineTotal * taxRate1 / 100);
|
var taxAmount1 = roundToTwo(lineTotal * taxRate1 / 100);
|
||||||
if (taxAmount1) {
|
if (taxAmount1) {
|
||||||
var key = taxName1 + taxRate1;
|
var key = taxName1 + taxRate1;
|
||||||
@ -30609,7 +30637,7 @@ function calculateAmounts(invoice) {
|
|||||||
invoice.tax_amount1 = taxAmount1;
|
invoice.tax_amount1 = taxAmount1;
|
||||||
invoice.tax_amount2 = taxAmount2;
|
invoice.tax_amount2 = taxAmount2;
|
||||||
invoice.item_taxes = taxes;
|
invoice.item_taxes = taxes;
|
||||||
|
|
||||||
if (NINJA.parseFloat(invoice.partial)) {
|
if (NINJA.parseFloat(invoice.partial)) {
|
||||||
invoice.balance_amount = roundToTwo(invoice.partial);
|
invoice.balance_amount = roundToTwo(invoice.partial);
|
||||||
} else {
|
} else {
|
||||||
@ -30934,7 +30962,7 @@ function truncate(string, length){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show/hide the 'Select' option in the datalists
|
// Show/hide the 'Select' option in the datalists
|
||||||
function actionListHandler() {
|
function actionListHandler() {
|
||||||
$('tbody tr .tr-action').closest('tr').mouseover(function() {
|
$('tbody tr .tr-action').closest('tr').mouseover(function() {
|
||||||
$(this).closest('tr').find('.tr-action').show();
|
$(this).closest('tr').find('.tr-action').show();
|
||||||
@ -31000,11 +31028,12 @@ function searchData(data, key, fuzzy) {
|
|||||||
}
|
}
|
||||||
cb(matches);
|
cb(matches);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function escapeRegExp(str) {
|
function escapeRegExp(str) {
|
||||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
var NINJA = NINJA || {};
|
var NINJA = NINJA || {};
|
||||||
|
|
||||||
NINJA.TEMPLATES = {
|
NINJA.TEMPLATES = {
|
||||||
|
@ -425,6 +425,34 @@ if (window.ko) {
|
|||||||
trigger: "hover"
|
trigger: "hover"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
limit: 50,
|
||||||
|
source: searchData(allBindings.items, allBindings.key)
|
||||||
|
}).on('typeahead:change', function(element, datum, name) {
|
||||||
|
var value = valueAccessor();
|
||||||
|
value(datum);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function (element, valueAccessor) {
|
||||||
|
var value = ko.utils.unwrapObservable(valueAccessor());
|
||||||
|
if (value) {
|
||||||
|
$(element).typeahead('val', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContactDisplayName(contact)
|
function getContactDisplayName(contact)
|
||||||
@ -556,7 +584,7 @@ function formatAddress(city, state, zip, swap) {
|
|||||||
str += zip ? zip + ' ' : '';
|
str += zip ? zip + ' ' : '';
|
||||||
str += city ? city : '';
|
str += city ? city : '';
|
||||||
str += (city && state) ? ', ' : (city ? ' ' : '');
|
str += (city && state) ? ', ' : (city ? ' ' : '');
|
||||||
str += state;
|
str += state;
|
||||||
} else {
|
} else {
|
||||||
str += city ? city : '';
|
str += city ? city : '';
|
||||||
str += (city && state) ? ', ' : (state ? ' ' : '');
|
str += (city && state) ? ', ' : (state ? ' ' : '');
|
||||||
@ -590,7 +618,7 @@ function calculateAmounts(invoice) {
|
|||||||
var hasTaxes = false;
|
var hasTaxes = false;
|
||||||
var taxes = {};
|
var taxes = {};
|
||||||
invoice.has_product_key = false;
|
invoice.has_product_key = false;
|
||||||
|
|
||||||
// Bold designs currently breaks w/o the product column
|
// Bold designs currently breaks w/o the product column
|
||||||
if (invoice.invoice_design_id == 2) {
|
if (invoice.invoice_design_id == 2) {
|
||||||
invoice.has_product_key = true;
|
invoice.has_product_key = true;
|
||||||
@ -638,7 +666,7 @@ function calculateAmounts(invoice) {
|
|||||||
lineTotal -= roundToTwo(lineTotal * (invoice.discount/100));
|
lineTotal -= roundToTwo(lineTotal * (invoice.discount/100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var taxAmount1 = roundToTwo(lineTotal * taxRate1 / 100);
|
var taxAmount1 = roundToTwo(lineTotal * taxRate1 / 100);
|
||||||
if (taxAmount1) {
|
if (taxAmount1) {
|
||||||
var key = taxName1 + taxRate1;
|
var key = taxName1 + taxRate1;
|
||||||
@ -715,7 +743,7 @@ function calculateAmounts(invoice) {
|
|||||||
invoice.tax_amount1 = taxAmount1;
|
invoice.tax_amount1 = taxAmount1;
|
||||||
invoice.tax_amount2 = taxAmount2;
|
invoice.tax_amount2 = taxAmount2;
|
||||||
invoice.item_taxes = taxes;
|
invoice.item_taxes = taxes;
|
||||||
|
|
||||||
if (NINJA.parseFloat(invoice.partial)) {
|
if (NINJA.parseFloat(invoice.partial)) {
|
||||||
invoice.balance_amount = roundToTwo(invoice.partial);
|
invoice.balance_amount = roundToTwo(invoice.partial);
|
||||||
} else {
|
} else {
|
||||||
@ -1040,7 +1068,7 @@ function truncate(string, length){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show/hide the 'Select' option in the datalists
|
// Show/hide the 'Select' option in the datalists
|
||||||
function actionListHandler() {
|
function actionListHandler() {
|
||||||
$('tbody tr .tr-action').closest('tr').mouseover(function() {
|
$('tbody tr .tr-action').closest('tr').mouseover(function() {
|
||||||
$(this).closest('tr').find('.tr-action').show();
|
$(this).closest('tr').find('.tr-action').show();
|
||||||
@ -1106,8 +1134,8 @@ function searchData(data, key, fuzzy) {
|
|||||||
}
|
}
|
||||||
cb(matches);
|
cb(matches);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function escapeRegExp(str) {
|
function escapeRegExp(str) {
|
||||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
@ -2009,7 +2009,8 @@ $LANG = array(
|
|||||||
'vendor_contacts' => 'Vendor Contacts',
|
'vendor_contacts' => 'Vendor Contacts',
|
||||||
'all' => 'All',
|
'all' => 'All',
|
||||||
'selected' => 'Selected',
|
'selected' => 'Selected',
|
||||||
|
'category' => 'Category',
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return $LANG;
|
return $LANG;
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
@if ($expense)
|
@if ($expense)
|
||||||
{!! Former::populate($expense) !!}
|
{!! Former::populate($expense) !!}
|
||||||
{!! Former::populateField('should_be_invoiced', intval($expense->should_be_invoiced)) !!}
|
{!! Former::populateField('should_be_invoiced', intval($expense->should_be_invoiced)) !!}
|
||||||
|
{!! Former::populateField('category', $expense->expense_category ? $expense->expense_category->name : '') !!}
|
||||||
{!! Former::hidden('public_id') !!}
|
{!! Former::hidden('public_id') !!}
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@ -33,11 +34,16 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
||||||
{!! Former::select('vendor_id')->addOption('', '')
|
{!! Former::select('vendor_id')->addOption('', '')
|
||||||
->data_bind('combobox: vendor_id')
|
->data_bind('combobox: vendor_id')
|
||||||
->label(trans('texts.vendor'))
|
->label(trans('texts.vendor'))
|
||||||
->addGroupClass('vendor-select') !!}
|
->addGroupClass('vendor-select') !!}
|
||||||
|
|
||||||
|
{!! Former::text('category')
|
||||||
|
->data_bind("typeahead: category, items: categories, key: 'name', valueUpdate: 'afterkeydown'")
|
||||||
|
->label(trans('texts.category')) !!}
|
||||||
|
|
||||||
{!! Former::text('expense_date')
|
{!! Former::text('expense_date')
|
||||||
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))
|
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))
|
||||||
->addGroupClass('expense_date')
|
->addGroupClass('expense_date')
|
||||||
@ -279,6 +285,10 @@
|
|||||||
var ViewModel = function(data) {
|
var ViewModel = function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
self.categories = {!! $categories !!};
|
||||||
|
console.log(self.categories[0].name);
|
||||||
|
|
||||||
|
self.category = ko.observable();
|
||||||
self.expense_currency_id = ko.observable();
|
self.expense_currency_id = ko.observable();
|
||||||
self.invoice_currency_id = ko.observable();
|
self.invoice_currency_id = ko.observable();
|
||||||
self.documents = ko.observableArray();
|
self.documents = ko.observableArray();
|
||||||
|
@ -256,7 +256,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div id="scrollable-dropdown-menu">
|
<div id="scrollable-dropdown-menu">
|
||||||
<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"/>
|
<input id="product_key" type="text" data-bind="productTypeahead: product_key, items: $root.products, key: 'product_key', valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][product_key]'}" class="form-control invoice-item handled"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -822,7 +822,7 @@ var ExpenseModel = function(data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* Custom binding for product key typeahead */
|
/* Custom binding for product key typeahead */
|
||||||
ko.bindingHandlers.typeahead = {
|
ko.bindingHandlers.productTypeahead = {
|
||||||
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||||
var $element = $(element);
|
var $element = $(element);
|
||||||
var allBindings = allBindingsAccessor();
|
var allBindings = allBindingsAccessor();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user