1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Allow tasks to be associated with products, and pass that data through to the invoice. (#3205)

This commit is contained in:
Rob Peck 2020-01-21 15:28:45 -06:00 committed by David Bomba
parent 7c8d9f5c24
commit 1d1c650648
6 changed files with 113 additions and 3 deletions

View File

@ -7,6 +7,7 @@ use App\Http\Requests\TaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use App\Models\Client;
use App\Models\Project;
use App\Models\Product;
use App\Models\Task;
use App\Models\TaskStatus;
use App\Ninja\Datatables\TaskDatatable;
@ -128,6 +129,7 @@ class TaskController extends BaseController
'task' => null,
'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0),
'projectPublicId' => Input::old('project_id') ? Input::old('project_id') : ($request->project_id ?: 0),
'productPublicId' => Input::old('product_id') ? Input::old('product_id') : ($request->product_id ?: 0),
'method' => 'POST',
'url' => 'tasks',
'title' => trans('texts.new_task'),
@ -205,6 +207,7 @@ class TaskController extends BaseController
'entity' => $task,
'clientPublicId' => $task->client ? $task->client->public_id : 0,
'projectPublicId' => $task->project ? $task->project->public_id : 0,
'productPublicId' => $task->product ? $task->product->public_id : 0,
'method' => $method,
'url' => $url,
'title' => trans('texts.edit_task'),
@ -241,6 +244,7 @@ class TaskController extends BaseController
'clients' => Client::scope()->withActiveOrSelected($task ? $task->client_id : false)->with('contacts')->orderBy('name')->get(),
'account' => Auth::user()->account,
'projects' => Project::scope()->withActiveOrSelected($task ? $task->project_id : false)->with('client.contacts')->orderBy('name')->get(),
'products' => Product::scope()->withActiveOrSelected($task ? $task->product_id : false)->orderBy('product_key')->get(),
];
}
@ -330,12 +334,20 @@ class TaskController extends BaseController
$account = Auth::user()->account;
$showProject = $lastProjectId != $task->project_id;
$data[] = [
$item_data = [
'publicId' => $task->public_id,
'description' => $task->present()->invoiceDescription($account, $showProject),
'duration' => $task->getHours(),
'cost' => $task->getRate(),
'productKey' => null,
];
if (!empty($task->product_id)) {
$item_data['productKey'] = $task->product->product_key;
}
$data[] = $item_data;
$lastProjectId = $task->project_id;
}

View File

@ -81,6 +81,14 @@ class Task extends EntityModel
return $this->belongsTo('App\Models\Project')->withTrashed();
}
/**
* @return mixed
*/
public function product()
{
return $this->belongsTo('App\Models\Product')->withTrashed();
}
/**
* @return mixed
*/
@ -180,7 +188,9 @@ class Task extends EntityModel
{
$value = 0;
if ($this->project && floatval($this->project->task_rate)) {
if ($this->product && $this->product->cost) {
$value = $this->product->cost;
} elseif ($this->project && floatval($this->project->task_rate)) {
$value = $this->project->task_rate;
} elseif ($this->client && floatval($this->client->task_rate)) {
$value = $this->client->task_rate;

View File

@ -4,6 +4,7 @@ namespace App\Ninja\Repositories;
use App\Models\Client;
use App\Models\Project;
use App\Models\Product;
use App\Models\Task;
use App\Models\TaskStatus;
use Auth;
@ -25,6 +26,7 @@ class TaskRepository extends BaseRepository
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'tasks.invoice_id')
->leftJoin('projects', 'projects.id', '=', 'tasks.project_id')
->leftJoin('products', 'products.id', '=', 'tasks.product_id')
->leftJoin('task_statuses', 'task_statuses.id', '=', 'tasks.task_status_id')
->where('tasks.account_id', '=', Auth::user()->account_id)
->where(function ($query) { // handle when client isn't set
@ -173,6 +175,11 @@ class TaskRepository extends BaseRepository
}
}
if (!empty($data['product_id'])) {
$product = Product::scope($data['product_id'])->firstOrFail();
$task->product_id = $product->id;
}
if (isset($data['description'])) {
$task->description = trim($data['description']);
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTaskProducts extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function ($table) {
$table->integer('product_id')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function ($table) {
$table->dropColumn('product_id');
});
}
}

View File

@ -936,7 +936,8 @@
item.notes(task.description);
item.qty(task.duration);
item.cost(task.cost);
item.task_public_id(task.publicId);
item.task_public_id(task.publicId);
item.product_key(task.productKey);
}
model.invoice().has_tasks(true);
NINJA.formIsChanged = true;

View File

@ -58,12 +58,23 @@
->label('project')
->value($task->present()->project) !!}
@endif
@if ($task->product)
{!! Former::plaintext()
->label('product')
->value($task->present()->product) !!}
@endif
@else
{!! Former::select('client')->addOption('', '')->addGroupClass('client-select') !!}
{!! Former::select('project_id')
->addOption('', '')
->addGroupClass('project-select')
->label(trans('texts.project')) !!}
{!! Former::select('product_id')
->addOption('', '')
->addGroupClass('product-select')
->label(trans('texts.product')) !!}
@endif
@include('partials/custom_fields', ['entityType' => ENTITY_TASK])
@ -240,6 +251,7 @@
var clients = {!! $clients !!};
var projects = {!! $projects !!};
var products = {!! $products !!};
var timeLabels = {};
@foreach (['hour', 'minute', 'second'] as $period)
@ -552,9 +564,11 @@
// setup clients and project comboboxes
var clientId = {{ $clientPublicId }};
var projectId = {{ $projectPublicId }};
var productId = {{ $productPublicId }};
var clientMap = {};
var projectMap = {};
var productMap = {};
var projectsForClientMap = {};
var projectsForAllClients = [];
var $clientSelect = $('select#client');
@ -638,7 +652,33 @@
}
});
var $productSelect = $('select#product_id').on('change', function(e) {
var productId = $('input[name=product_id]').val();
if (productId == '-1') {
$('input[name=product_name]').val('');
}
$('select#project_id').combobox('refresh');
});
$productSelect.append(new Option('', ''));
for (var i=0; i<products.length; i++) {
var product = products[i];
var productName = product.product_key;
productMap[product.public_id] = product;
if (!productName) {
continue;
}
$productSelect.append(new Option(productName, product.public_id));
}
$productSelect.trigger('change');
@include('partials/entity_combobox', ['entityType' => ENTITY_PROJECT])
@include('partials/entity_combobox', ['entityType' => ENTITY_PRODUCT])
if (projectId) {
var project = projectMap[projectId];
@ -650,6 +690,14 @@
$clientSelect.trigger('change');
}
if (productId) {
var product = productMap[productId];
if (product) {
setComboboxValue($('.product-select'), product.public_id, product.product_key);
$productSelect.trigger('change');
}
}
@if (!$task)
var taskType = localStorage.getItem('last:task_type');
if (taskType) {