mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 10:17:30 -04:00
Expense module
This commit is contained in:
parent
d8cb1b436d
commit
3e9e28f06f
@ -143,7 +143,51 @@ class ExpenseController extends BaseController
|
|||||||
{
|
{
|
||||||
$action = Input::get('action');
|
$action = Input::get('action');
|
||||||
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
|
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
|
||||||
|
|
||||||
|
switch($action)
|
||||||
|
{
|
||||||
|
case 'invoice':
|
||||||
|
$expenses = Expense::scope($ids)->get();
|
||||||
|
$clientPublicId = null;
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
if ($expense->client_id) {
|
||||||
|
if (!$clientPublicId) {
|
||||||
|
$clientPublicId = $expense->client_id;
|
||||||
|
} else if ($clientPublicId != $expense->client_id) {
|
||||||
|
Session::flash('error', trans('texts.expense_error_multiple_clients'));
|
||||||
|
return Redirect::to('expenses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expense->invoice_id) {
|
||||||
|
Session::flash('error', trans('texts.expense_error_invoiced'));
|
||||||
|
return Redirect::to('expenses');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expense->should_be_invoiced == 0) {
|
||||||
|
Session::flash('error', trans('texts.expense_error_should_not_be_invoiced'));
|
||||||
|
return Redirect::to('expenses');
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = Auth::user()->account;
|
||||||
|
$data[] = [
|
||||||
|
'publicId' => $expense->public_id,
|
||||||
|
'description' => $expense->public_notes,
|
||||||
|
'qty' => 1,
|
||||||
|
'cost' => $expense->amount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Redirect::to("invoices/create/{$clientPublicId}")->with('expenses', $data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
$count = $this->expenseService->bulk($ids, $action);
|
$count = $this->expenseService->bulk($ids, $action);
|
||||||
|
}
|
||||||
|
|
||||||
if ($count > 0) {
|
if ($count > 0) {
|
||||||
$message = Utils::pluralize($action.'d_expense', $count);
|
$message = Utils::pluralize($action.'d_expense', $count);
|
||||||
|
@ -380,6 +380,7 @@ class InvoiceController extends BaseController
|
|||||||
'recurringHelp' => $recurringHelp,
|
'recurringHelp' => $recurringHelp,
|
||||||
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
|
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
|
||||||
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
|
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
|
||||||
|
'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<?php namespace app\Listeners;
|
<?php namespace app\Listeners;
|
||||||
|
|
||||||
use Carbon;
|
use Carbon;
|
||||||
use App\Models\Credit;
|
use App\Models\Expense;
|
||||||
use App\Events\PaymentWasDeleted;
|
use App\Events\PaymentWasDeleted;
|
||||||
|
use App\Events\InvoiceWasDeleted;
|
||||||
use App\Ninja\Repositories\ExpenseRepository;
|
use App\Ninja\Repositories\ExpenseRepository;
|
||||||
|
|
||||||
class ExpenseListener
|
class ExpenseListener
|
||||||
@ -14,4 +15,11 @@ class ExpenseListener
|
|||||||
{
|
{
|
||||||
$this->expenseRepo = $expenseRepo;
|
$this->expenseRepo = $expenseRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deletedInvoice(InvoiceWasDeleted $event)
|
||||||
|
{
|
||||||
|
// Release any tasks associated with the deleted invoice
|
||||||
|
Expense::where('invoice_id', '=', $event->invoice->id)
|
||||||
|
->update(['invoice_id' => null]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
|||||||
'is_recurring' => 'boolean',
|
'is_recurring' => 'boolean',
|
||||||
'has_tasks' => 'boolean',
|
'has_tasks' => 'boolean',
|
||||||
'auto_bill' => 'boolean',
|
'auto_bill' => 'boolean',
|
||||||
|
'has_expenses' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
// used for custom invoice numbers
|
// used for custom invoice numbers
|
||||||
@ -358,6 +359,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
|||||||
'has_tasks',
|
'has_tasks',
|
||||||
'custom_text_value1',
|
'custom_text_value1',
|
||||||
'custom_text_value2',
|
'custom_text_value2',
|
||||||
|
'has_expenses',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->client->setVisible([
|
$this->client->setVisible([
|
||||||
|
@ -7,6 +7,7 @@ use App\Models\InvoiceItem;
|
|||||||
use App\Models\Invitation;
|
use App\Models\Invitation;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
|
use App\Models\Expense;
|
||||||
use App\Services\PaymentService;
|
use App\Services\PaymentService;
|
||||||
use App\Ninja\Repositories\BaseRepository;
|
use App\Ninja\Repositories\BaseRepository;
|
||||||
|
|
||||||
@ -204,6 +205,9 @@ class InvoiceRepository extends BaseRepository
|
|||||||
if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
|
if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
|
||||||
$invoice->has_tasks = true;
|
$invoice->has_tasks = true;
|
||||||
}
|
}
|
||||||
|
if (isset($data['has_expenses']) && filter_var($data['has_expenses'], FILTER_VALIDATE_BOOLEAN)) {
|
||||||
|
$invoice->has_expenses = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$invoice = Invoice::scope($publicId)->firstOrFail();
|
$invoice = Invoice::scope($publicId)->firstOrFail();
|
||||||
}
|
}
|
||||||
@ -387,6 +391,14 @@ class InvoiceRepository extends BaseRepository
|
|||||||
$task->save();
|
$task->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($item['expense_public_id']) && $item['expense_public_id']) {
|
||||||
|
$expense = Expense::scope($item['expense_public_id'])->where('invoice_id', '=', null)->firstOrFail();
|
||||||
|
$expense->invoice_id = $invoice->id;
|
||||||
|
$expense->invoice_client_id = $invoice->client_id;
|
||||||
|
$expense->is_invoiced = true;
|
||||||
|
$expense->save();
|
||||||
|
}
|
||||||
|
|
||||||
if ($item['product_key']) {
|
if ($item['product_key']) {
|
||||||
$productKey = trim($item['product_key']);
|
$productKey = trim($item['product_key']);
|
||||||
if (\Auth::user()->account->update_products && ! strtotime($productKey)) {
|
if (\Auth::user()->account->update_products && ! strtotime($productKey)) {
|
||||||
@ -395,7 +407,10 @@ class InvoiceRepository extends BaseRepository
|
|||||||
$product = Product::createNew();
|
$product = Product::createNew();
|
||||||
$product->product_key = trim($item['product_key']);
|
$product->product_key = trim($item['product_key']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$product->notes = $invoice->has_tasks ? '' : $item['notes'];
|
$product->notes = $invoice->has_tasks ? '' : $item['notes'];
|
||||||
|
$product->notes = $invoice->has_expenses ? '' : $item['notes'];
|
||||||
|
|
||||||
$product->cost = $item['cost'];
|
$product->cost = $item['cost'];
|
||||||
$product->save();
|
$product->save();
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@ class InvoiceTransformer extends EntityTransformer
|
|||||||
'custom_value2' => $invoice->custom_value2,
|
'custom_value2' => $invoice->custom_value2,
|
||||||
'custom_taxes1' => (bool) $invoice->custom_taxes1,
|
'custom_taxes1' => (bool) $invoice->custom_taxes1,
|
||||||
'custom_taxes2' => (bool) $invoice->custom_taxes2,
|
'custom_taxes2' => (bool) $invoice->custom_taxes2,
|
||||||
|
'has_expenses' => (bool) $invoice->has_expenses,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,6 +23,7 @@ class CreateExpensesTable extends Migration
|
|||||||
$table->unsignedInteger('account_id')->index();
|
$table->unsignedInteger('account_id')->index();
|
||||||
$table->unsignedInteger('vendor_id')->nullable();
|
$table->unsignedInteger('vendor_id')->nullable();
|
||||||
$table->unsignedInteger('user_id');
|
$table->unsignedInteger('user_id');
|
||||||
|
$table->unsignedInteger('invoice_id')->nullable();
|
||||||
$table->unsignedInteger('invoice_client_id')->nullable();
|
$table->unsignedInteger('invoice_client_id')->nullable();
|
||||||
$table->boolean('is_deleted')->default(false);
|
$table->boolean('is_deleted')->default(false);
|
||||||
$table->decimal('amount', 13, 2);
|
$table->decimal('amount', 13, 2);
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddInvoicesHasExpenses extends Migration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function(Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->boolean('has_expenses')->default(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
$invoices = DB::table('invoices')
|
||||||
|
->join('expenses', 'expenses.invoice_id', '=', 'invoices.id')
|
||||||
|
->selectRaw('DISTINCT invoices.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($invoices as $invoice) {
|
||||||
|
DB::table('invoices')
|
||||||
|
->where('id', $invoice->id)
|
||||||
|
->update(['has_tasks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function(Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->dropColumn('has_expenses');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1051,11 +1051,14 @@ return array(
|
|||||||
'view' => 'View',
|
'view' => 'View',
|
||||||
'restore_expense' => 'Restore expense',
|
'restore_expense' => 'Restore expense',
|
||||||
'invoice_expense' => 'Invoice',
|
'invoice_expense' => 'Invoice',
|
||||||
|
'expense_error_multiple_clients' =>'The expenses can\'t belong to different clients',
|
||||||
|
'expense_error_invoiced' => 'Expense have already been invoiced',
|
||||||
|
'expense_error_should_not_be_invoiced' => 'Expense maked not to be invoiced',
|
||||||
|
|
||||||
// Payment terms
|
// Payment terms
|
||||||
'num_days' => 'Number of days',
|
'num_days' => 'Number of days',
|
||||||
'create_payment_term' => 'Create payment term',
|
'create_payment_term' => 'Create payment term',
|
||||||
'edit_payment_terms' => 'Edit payment term',
|
'edit_payment_terms' => 'Edit payment term',
|
||||||
'edit_payment_term' => 'Edit payment term',
|
'edit_payment_term' => 'Edit payment term',
|
||||||
'archive_payment_term' => 'Archive payment term',
|
'archive_payment_term' => 'Archive payment term',
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -203,6 +203,7 @@
|
|||||||
<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]'}"
|
||||||
rows="1" cols="60" style="resize: vertical" class="form-control word-wrap"></textarea>
|
rows="1" cols="60" style="resize: vertical" class="form-control word-wrap"></textarea>
|
||||||
<input type="text" data-bind="value: task_public_id, attr: {name: 'invoice_items[' + $index() + '][task_public_id]'}" style="display: none"/>
|
<input type="text" data-bind="value: task_public_id, attr: {name: 'invoice_items[' + $index() + '][task_public_id]'}" style="display: none"/>
|
||||||
|
<input type="text" data-bind="value: expense_public_id, attr: {name: 'invoice_items[' + $index() + '][expense_public_id]'}" style="display: none"/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input data-bind="value: prettyCost, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][cost]'}"
|
<input data-bind="value: prettyCost, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][cost]'}"
|
||||||
@ -383,6 +384,7 @@
|
|||||||
{!! Former::text('is_quote')->data_bind('value: is_quote') !!}
|
{!! Former::text('is_quote')->data_bind('value: is_quote') !!}
|
||||||
{!! Former::text('has_tasks')->data_bind('value: has_tasks') !!}
|
{!! Former::text('has_tasks')->data_bind('value: has_tasks') !!}
|
||||||
{!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!}
|
{!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!}
|
||||||
|
{!! Former::text('has_expenses')->data_bind('value: has_expenses') !!}
|
||||||
{!! Former::text('pdfupload') !!}
|
{!! Former::text('pdfupload') !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -685,6 +687,24 @@
|
|||||||
model.invoice().invoice_items.push(blank);
|
model.invoice().invoice_items.push(blank);
|
||||||
model.invoice().has_tasks(true);
|
model.invoice().has_tasks(true);
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if (isset($expenses) && $expenses)
|
||||||
|
// move the blank invoice line item to the end
|
||||||
|
var blank = model.invoice().invoice_items.pop();
|
||||||
|
var expenses = {!! $expenses !!};
|
||||||
|
|
||||||
|
for (var i=0; i<expenses.length; i++) {
|
||||||
|
var expense = expenses[i];
|
||||||
|
var item = model.invoice().addItem();
|
||||||
|
item.notes(expense.description);
|
||||||
|
item.qty(expense.qty);
|
||||||
|
item.expense_public_id(expense.publicId);
|
||||||
|
item.cost(expense.cost);
|
||||||
|
}
|
||||||
|
model.invoice().invoice_items.push(blank);
|
||||||
|
model.invoice().has_expenses(true);
|
||||||
|
@endif
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
model.invoice().tax(model.getTaxRate(model.invoice().tax_name(), model.invoice().tax_rate()));
|
model.invoice().tax(model.getTaxRate(model.invoice().tax_name(), model.invoice().tax_rate()));
|
||||||
@ -806,6 +826,10 @@
|
|||||||
NINJA.formIsChanged = true;
|
NINJA.formIsChanged = true;
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if (isset($expenses) && $expenses)
|
||||||
|
NINJA.formIsChanged = true;
|
||||||
|
@endif
|
||||||
|
|
||||||
applyComboboxListeners();
|
applyComboboxListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -229,6 +229,7 @@ function InvoiceModel(data) {
|
|||||||
self.invoice_design_id = ko.observable(1);
|
self.invoice_design_id = ko.observable(1);
|
||||||
self.partial = ko.observable(0);
|
self.partial = ko.observable(0);
|
||||||
self.has_tasks = ko.observable();
|
self.has_tasks = ko.observable();
|
||||||
|
self.has_expenses = ko.observable();
|
||||||
|
|
||||||
self.custom_value1 = ko.observable(0);
|
self.custom_value1 = ko.observable(0);
|
||||||
self.custom_value2 = ko.observable(0);
|
self.custom_value2 = ko.observable(0);
|
||||||
@ -712,6 +713,7 @@ function ItemModel(data) {
|
|||||||
self.tax_name = ko.observable('');
|
self.tax_name = ko.observable('');
|
||||||
self.tax_rate = ko.observable(0);
|
self.tax_rate = ko.observable(0);
|
||||||
self.task_public_id = ko.observable('');
|
self.task_public_id = ko.observable('');
|
||||||
|
self.expense_public_id = ko.observable('');
|
||||||
self.actionsVisible = ko.observable(false);
|
self.actionsVisible = ko.observable(false);
|
||||||
|
|
||||||
self._tax = ko.observable();
|
self._tax = ko.observable();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user