Added expense category

This commit is contained in:
Hillel Coren 2016-07-05 21:49:47 +03:00
parent f2cbfec926
commit 21a91ff0e1
11 changed files with 117 additions and 23 deletions

View File

@ -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

View File

@ -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()
]; ];
} }

View File

@ -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
*/ */

View File

@ -25,4 +25,4 @@ class ExpenseCategory extends EntityModel
return $this->belongsTo('App\Models\Expense'); return $this->belongsTo('App\Models\Expense');
} }
} }

View File

@ -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);
} }

View File

@ -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 = {

View File

@ -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, "\\$&");
} }

View File

@ -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;

View File

@ -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();

View File

@ -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>

View File

@ -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();