Invoice functionnality

This commit is contained in:
Jérémy Munsch 2022-08-26 18:24:23 +02:00
parent 86aa480c04
commit 88e5379610
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
21 changed files with 1664 additions and 214 deletions

View file

@ -2,9 +2,282 @@
namespace App\Http\Controllers;
use App\Enums\Invoice\TaskType;
use App\Http\Requests\Invoice\InvoiceSetDateRequest;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Package;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
use Symfony\Component\HttpFoundation\Response;
/**
* * Because update is denied when invoice is sent to the client
* ForceUpdate On signed date
* ForceUpdate On compta sent date
*/
class InvoiceController extends Controller
{
//
use CrudController {
index as crudIndex;
show as crudShow;
create as crudCreate;
edit as crudEdit;
}
/** @var \App\Models\Invoice */
public static $modelClass = Invoice::class;
/**
* Crud Layout
*
* @var string
*/
public static $layout = 'layout.app';
/**
* Crud index view
*
* @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
if (($models = $view->getData()['models'])->count() === 1) {
return \redirect()->route('invoices.edit', $models->first());
}
return view('invoices.index', $view->getData());
}
/**
* Index query middleware
*
* @param Builder $query
* @return Builder
* @throws \ErrorException If account is missing in url.
*/
public function indexQuery(Builder $query): Builder
{
$query->orderBy('created_at', 'DESC');
return $query->orWhere(function (Builder $q) {
$q->whereHas('site', function ($q) {
$q->where('name', 'LIKE', '%' . \filter_var(request()->search, \FILTER_SANITIZE_STRING) . '%');
})->orWhereHas('company', function ($q) {
$q->where('name', 'LIKE', '%' . \filter_var(request()->search, \FILTER_SANITIZE_STRING) . '%');
})->orWhereHas('companyInvoiced', function ($q) {
$q->where('name', 'LIKE', '%' . \filter_var(request()->search, \FILTER_SANITIZE_STRING) . '%');
});
});
}
/**
* Show model action
*
* @param \Illuminate\Http\Request $request
* @param mixed ...$params
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory|\Illuminate\Http\JsonResponse
*/
public function show(Request $request, ...$params)
{
/** @var \Illuminate\View\View|\Illuminate\Http\JsonResponse */
$view = call_user_func_array([$this, 'crudShow'], \array_merge([$request], $params));
if ($view instanceof \Illuminate\Http\JsonResponse) {
return $view;
}
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\Package> */
$packages = Package::get()->keyBy('id')->all();
return view('invoices.show', \array_merge($view->getData(), ['packages' => $packages]));
}
/**
* Crud create view
*
* @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function create(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudCreate($request);
$companies = Company::get();
$packages = Package::get();
$taskRowModel = Invoice::getTaskStructure();
return view('invoices.create', \array_merge($view->getData(), [
'companies' => $companies,
'packages' => $packages,
'taskRowModel' => $taskRowModel,
]));
}
/**
* Before store model middleware
*
* @param \App\Models\Invoice $model
* @return void
*/
public function storeModel(Invoice &$model)
{
$this->validateTasks($model);
}
/**
* Before update model middleware
*
* @param \App\Models\Invoice $model
* @return void
* @throws \Illuminate\Validation\ValidationException If invoice is signed.
*/
public function updateModel(Invoice &$model)
{
if ($model->signed_date) {
throw ValidationException::withMessages([
'erreur' => trans('Il n\'est pas psossible de mettre a jour une facture qui est signée')
]);
}
$this->validateTasks($model);
}
/**
* Edit model action
*
* @param mixed ...$params
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function edit(...$params)
{
/** @var \Illuminate\View\View */
$view = call_user_func_array([$this, 'crudEdit'], $params);
$companies = Company::get();
$packages = Package::get();
$taskRowModel = Invoice::getTaskStructure();
return view('invoices.edit', \array_merge($view->getData(), [
'companies' => $companies,
'packages' => $packages,
'taskRowModel' => $taskRowModel,
]));
}
/**
* Validate Tasks
*
* @param \App\Models\Invoice $invoice
* @return void
* @throws \Illuminate\Validation\ValidationException If tasks are not valid.
*/
private function validateTasks(Invoice $invoice): void
{
$tasks = \collect(\json_decode($invoice->tasks, true))
->map(function ($task) {
if (\is_array($task)) {
return \array_intersect_key($task, Invoice::getTaskStructure());
}
});
$invoice->tasks = Validator::make(['tasks' => $tasks->all()], [
'tasks' => 'required|array|min:1',
'tasks.*' => 'required|array',
'tasks.*.days' => 'required|numeric|min:0.125',
'tasks.*.amount' => 'required|numeric|min:0.125',
'tasks.*.desc' => 'required|string|min:10',
'tasks.*.type' => [
'required', new EnumIsValidRule(TaskType::class)
],
'tasks.*.package' => 'required_if:tasks.*.type,' .
TaskType::package()->value . '|nullable|exists:packages,id',
], [], [
'tasks' => trans('Tâches')
])->validated()['tasks'];
}
/**
* Download Invoice
*
* @param \App\Models\Invoice $invoice
* @return \Symfony\Component\HttpFoundation\Response
*/
public function download(Invoice $invoice): Response
{
$this->authorize($invoice, 'download');
return $invoice->generateInvoice()
->stream(Str::slug(trans('Facture :appname :date - ref :ref', [
'appname' => config('app.name'),
'date' => $invoice->created_at->isoFormat('YYYY-MM-DD'),
'ref' => $invoice->ref,
])) . '.pdf');
}
/**
* Set signed date
*
* @param \App\Http\Requests\Invoice\InvoiceSetDateRequest $request
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Validation\ValidationException If already signed.
*/
public function setSignedDate(InvoiceSetDateRequest $request, Invoice $invoice)
{
if ($invoice->signed_date) {
throw ValidationException::withMessages([
'erreur' => trans('La date de signature est déjà actée.')
]);
}
$invoice->signed_date = $request->date;
$invoice->saveOrFail();
return \back()->with('success', trans('La date de signature à bien été enregistrée'));
}
/**
* Set client signed date
*
* @param \App\Http\Requests\Invoice\InvoiceSetDateRequest $request
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Validation\ValidationException If already signed.
*/
public function setClientSentDate(InvoiceSetDateRequest $request, Invoice $invoice)
{
if ($invoice->sent_client_date) {
throw ValidationException::withMessages([
'erreur' => trans('La date de signature client est déjà actée.')
]);
}
$invoice->sent_client_date = $request->date;
$invoice->saveOrFail();
return \back()->with('success', trans('La date de signature client à bien été enregistrée'));
}
/**
* Set compta signed date
*
* @param \App\Http\Requests\Invoice\InvoiceSetDateRequest $request
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Validation\ValidationException If already signed.
*/
public function setComptaSentDate(InvoiceSetDateRequest $request, Invoice $invoice)
{
if ($invoice->sent_accounting_date) {
throw ValidationException::withMessages([
'erreur' => trans('La date de signature compta est déjà actée.')
]);
}
$invoice->sent_accounting_date = $request->date;
$invoice->saveOrFail();
return \back()->with('success', trans('La date de signature compta à bien été enregistrée'));
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Invoice;
use Illuminate\Foundation\Http\FormRequest;
class InvoiceSetDateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
/** @var \App\Models\User|null $user */
$user = auth()->user();
return auth()->check() && $user->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'date' => 'required|date'
];
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace App\Lib;
use Illuminate\Container\Container;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
class CollectionHelper
{
/**
* Paginate a collection
*
* @param \Illuminate\Support\Collection $results
* @param integer $pageSize
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginate(Collection $results, int $pageSize): LengthAwarePaginator
{
$page = Paginator::resolveCurrentPage('page');
return self::paginateManually($results, $pageSize, $page);
}
/**
* Paginate a collection
*
* @param \Illuminate\Support\Collection $results
* @param integer $pageSize
* @param integer $page
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginateManually(Collection $results, int $pageSize, int $page = 1): LengthAwarePaginator
{
$total = $results->count();
return self::paginator($results->forPage($page, $pageSize), $total, $pageSize, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
]);
}
/**
* Create a new length-aware paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param integer $total
* @param integer $perPage
* @param integer $currentPage
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected static function paginator(Collection $items, int $total, int $perPage, int $currentPage, array $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $items,
'total' => $total,
'perPage' => $perPage,
'currentPage' => $currentPage,
'options' => $options
]);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Enums\Invoice\TaskType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
@ -9,6 +10,7 @@ use Illuminate\Support\Str;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
use Meneses\LaravelMpdf\LaravelMpdf;
class Invoice extends Model
{
@ -48,7 +50,10 @@ class Invoice extends Model
* @var array
*/
protected $casts = [
'tasks' => 'array'
'tasks' => 'array',
'signed_date' => 'date',
'sent_client_date' => 'date',
'sent_accounting_date' => 'date',
];
/**
@ -114,20 +119,18 @@ class Invoice extends Model
'label' => trans('Montant total'),
'type' => CrudType::float(),
'nullable' => false,
'readonly' => true,
'actions' => [
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
'total_days' => [
'label' => trans('Montant total jours'),
'type' => CrudType::float(),
'nullable' => false,
'readonly' => true,
'actions' => [
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
'comment' => [
@ -154,33 +157,30 @@ class Invoice extends Model
'label' => trans('Date de signature'),
'type' => CrudType::datetime(),
'nullable' => true,
'readonly' => true,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
'sent_client_date' => [
'label' => trans('Date d\'envoi client'),
'type' => CrudType::datetime(),
'nullable' => true,
'readonly' => true,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
'sent_accounting_date' => [
'label' => trans('Date d\'envoi compta'),
'type' => CrudType::datetime(),
'nullable' => true,
'readonly' => true,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
'site_id' => [
@ -209,7 +209,7 @@ class Invoice extends Model
]
],
'invoiced_to_id' => [
'label' => trans('models.classes.companies'),
'label' => trans('Entité à facturer'),
'type' => CrudType::belongsTo(),
'belongsTo' => Company::class,
'belongsToLabel' => 'entité',
@ -249,39 +249,40 @@ class Invoice extends Model
protected static function booted()
{
static::creating(function (self $model) {
$model->ref = \sprintf(
$model->ref = \sprintf(
'%s_%s_%s',
Carbon::now()->isoFormat('Y-m'),
Str::padLeft(Invoice::whereYear('created_at', Carbon::now()->isoFormat('Y'))
->whereMonth('created_at', Carbon::now()->isoFormat('M'))
->count() ?: 1, 4, '0'),
Str::substr(Str::upper(Str::slug(User::query()->inRandomOrder()->first()->name)), 0, 2)
Carbon::now()->isoFormat('YYYY'),
Str::padLeft((Invoice::whereYear('created_at', Carbon::now()->isoFormat('YYYY'))
->whereMonth('created_at', Carbon::now()->isoFormat('MM'))
->count() + 1) ?: 1, 4, '0'),
Str::substr(Str::upper(Str::slug(User::query()->inRandomOrder()->first()->name)), 0, 1)
);
$model->total_days = collect($model->tasks)
/** @var null|float $previous */
->reduce(function ($previous, array $task) {
return \floatval($task['days']) + \floatval($previous);
});
$model->total = collect($model->tasks)
/** @var null|float $previous */
->reduce(function ($previous, array $task) {
return \floatval($task['amount']) + \floatval($previous);
});
});
static::updating(function (self $model) {
$model->total_days = collect($model->tasks)
/** @var null|float $previous */
->reduce(function ($previous, array $task) {
return \floatval($task['days']) + \floatval($previous);
});
$model->total = collect($model->tasks)
/** @var null|float $previous */
->reduce(function ($previous, array $task) {
return \floatval($task['amount']) + \floatval($previous);
});
});
}
// * METHODS
/**
* Get the tasks line structure skel
*
* @return array<string,string|float>
*/
public static function getTaskStructure(): array
{
return [
// * string
'desc' => '',
// * usigned float
'days' => '',
// * Invoice\TaskType
'type' => '',
// * usigned float
'amount' => '',
];
}
// * RELATIONSHIPS
// * RELATIONSHIPS
/**
* BelongsTo Site
@ -311,4 +312,79 @@ class Invoice extends Model
{
return $this->BelongsTo(Company::class, 'invoiced_to_id');
}
// * METHODS
/**
* Get the tasks line structure skel
*
* @return array<string,string|float>
*/
public static function getTaskStructure(): array
{
return [
// * string
'desc' => '',
// * usigned float
'days' => 0,
// * Invoice\TaskType value
'type' => TaskType::web()->value,
// * usigned float
'amount' => 0,
// * package ID if task type package or null
'package' => null
];
}
/**
* Generate Invoice
*
* @return LaravelMpdf
*/
public function generateInvoice(): LaravelMpdf
{
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\Package> */
$packages = Package::get()->keyBy('id')->all();
/** @var \Meneses\LaravelMpdf\LaravelMpdfWrapper */
$mpdf = app('mpdf.wrapper');
/** @var \Meneses\LaravelMpdf\LaravelMpdf */
$pdf = $mpdf->loadView(
'documents.invoice',
$this->toArray(),
[
'site' => $this->site,
'company' => $this->company,
'companyInvoiced' => $this->companyInvoiced,
'packages' => $packages,
],
[
'mode' => '',
'format' => 'A4',
'default_font_size' => '12',
'default_font' => 'sans-serif',
'margin_left' => 0,
'margin_right' => 0,
'margin_top' => 0,
'margin_bottom' => 0,
'margin_header' => 0,
'margin_footer' => 0,
'orientation' => 'P',
'title' => __('Facture Service Web :ref', ['ref' => $this->ref]),
'author' => 'Equipe Web',
'watermark' => '',
'show_watermark' => false,
'watermark_font' => 'sans-serif',
'display_mode' => 'fullpage',
'watermark_text_alpha' => 0.1,
'custom_font_dir' => '',
'custom_font_data' => [],
'auto_language_detection' => false,
'temp_dir' => rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR),
'pdfa' => false,
'pdfaauto' => false,
]
);
return $pdf;
}
}

View file

@ -4,6 +4,7 @@ namespace App\Policies;
use App\Models\User;
use App\Policies\Rules\UserRoleRules as URR;
use App\Policies\Rules\InvoiceStaticRules as ISR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
@ -29,9 +30,17 @@ class InvoicePolicy extends RootPolicy
$this->policies = [
'invoice' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'update' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsAdmin', $user)
'update' => [
URR::make('userIsAdmin', $user),
ISR::make('invoiceHasNotBeenSentTotheClient', $user),
],
'delete' => URR::make('denyEveryOne'),
'download' => [
URR::make('userIsAdmin', $user),
ISR::make('invoiceHasTasks', $user),
]
]
];
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Policies\Rules;
use App\Models\Invoice;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
/**
* Invoice Policiy Static Rules
* ! Rules has to return boolean value
*/
abstract class InvoiceStaticRules extends StaticRules
{
/**
* Invoice has not been sent ot the client
*
* @param \App\Models\Invoice $model
* @return boolean
*/
protected static function invoiceHasNotBeenSentTotheClient(Invoice $model): bool
{
return \is_null($model->sent_client_date);
}
/**
* Invoice has some tasks
*
* @param \App\Models\Invoice $model
* @return boolean
*/
protected static function invoiceHasTasks(Invoice $model): bool
{
return (bool)\count($model->tasks);
}
}

View file

@ -10,12 +10,13 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"carlos-meneses/laravel-mpdf": "^2.1",
"doctrine/dbal": "^3.1",
"erusev/parsedown": "^1.7",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"kwaadpepper/crud-policies": "^0.4.5",
"kwaadpepper/crud-policies": "^0.4.7",
"kwaadpepper/enum": "^2.1",
"kwaadpepper/exception-handler": "^0.0.9",
"laravel-lang/lang": "~8.0",

435
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "47a64c62b7e388c80216db44f86f0d44",
"content-hash": "d6431e8c4b000923f948206977031782",
"packages": [
{
"name": "asm89/stack-cors",
@ -122,6 +122,56 @@
],
"time": "2021-08-15T20:50:18+00:00"
},
{
"name": "carlos-meneses/laravel-mpdf",
"version": "2.1.5",
"source": {
"type": "git",
"url": "https://github.com/mccarlosen/laravel-mpdf.git",
"reference": "810008e1a4b82fb6c8c74cdf32fd130195756027"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mccarlosen/laravel-mpdf/zipball/810008e1a4b82fb6c8c74cdf32fd130195756027",
"reference": "810008e1a4b82fb6c8c74cdf32fd130195756027",
"shasum": ""
},
"require": {
"mpdf/mpdf": "^8.0",
"php": ">=7.0.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Meneses\\LaravelMpdf\\LaravelMpdfServiceProvider"
],
"aliases": {
"PDF": "Meneses\\LaravelMpdf\\Facades\\LaravelMpdf"
}
}
},
"autoload": {
"psr-4": {
"Meneses\\LaravelMpdf\\": "src/LaravelMpdf"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Laravel Mpdf: Using Mpdf in Laravel 5 for generate Pdfs.",
"keywords": [
"laravel",
"mpdf",
"pdf"
],
"support": {
"issues": "https://github.com/mccarlosen/laravel-mpdf/issues",
"source": "https://github.com/mccarlosen/laravel-mpdf/tree/2.1.5"
},
"time": "2021-03-30T21:17:19+00:00"
},
{
"name": "doctrine/cache",
"version": "2.2.0",
@ -1502,16 +1552,16 @@
},
{
"name": "kwaadpepper/crud-policies",
"version": "0.4.6",
"version": "0.4.7",
"source": {
"type": "git",
"url": "https://github.com/Kwaadpepper/crud-policies.git",
"reference": "8f83c66385409ce98955b1b3c65aa2da414acdc8"
"reference": "89e35c6969da683b0b5ed440539b45b5109871b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/8f83c66385409ce98955b1b3c65aa2da414acdc8",
"reference": "8f83c66385409ce98955b1b3c65aa2da414acdc8",
"url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/89e35c6969da683b0b5ed440539b45b5109871b0",
"reference": "89e35c6969da683b0b5ed440539b45b5109871b0",
"shasum": ""
},
"require": {
@ -1562,9 +1612,9 @@
],
"support": {
"issues": "https://github.com/Kwaadpepper/crud-policies/issues",
"source": "https://github.com/Kwaadpepper/crud-policies/tree/0.4.6"
"source": "https://github.com/Kwaadpepper/crud-policies/tree/0.4.7"
},
"time": "2022-06-17T08:48:10+00:00"
"time": "2022-08-26T14:55:14+00:00"
},
{
"name": "kwaadpepper/enum",
@ -2690,6 +2740,142 @@
],
"time": "2022-07-24T11:55:47+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.1.2",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
"reference": "a8a22f4874157e490d41b486053a20bec42e182c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/a8a22f4874157e490d41b486053a20bec42e182c",
"reference": "a8a22f4874157e490d41b486053a20bec42e182c",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0",
"php-http/message-factory": "^1.0",
"psr/http-message": "^1.0",
"psr/log": "^1.0 || ^2.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "^2.4",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Matěj Humpál",
"role": "Developer, maintainer"
},
{
"name": "Ian Back",
"role": "Developer (retired)"
}
],
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
"homepage": "https://mpdf.github.io",
"keywords": [
"pdf",
"php",
"utf-8"
],
"support": {
"docs": "http://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
"funding": [
{
"url": "https://www.paypal.me/mpdf",
"type": "custom"
}
],
"time": "2022-08-15T08:15:09+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2022-03-03T13:19:32+00:00"
},
{
"name": "nesbot/carbon",
"version": "2.61.0",
@ -2913,6 +3099,110 @@
},
"time": "2022-01-27T09:35:39+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "php-http/message-factory",
"version": "v1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-http/message-factory.git",
"reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1",
"reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1",
"shasum": ""
},
"require": {
"php": ">=5.4",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "Factory interfaces for PSR-7 HTTP Message",
"homepage": "http://php-http.org",
"keywords": [
"factory",
"http",
"message",
"stream",
"uri"
],
"support": {
"issues": "https://github.com/php-http/message-factory/issues",
"source": "https://github.com/php-http/message-factory/tree/master"
},
"time": "2015-12-19T14:08:53+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.0",
@ -3693,6 +3983,78 @@
],
"time": "2021-09-25T23:10:38+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.3.6",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/6231e315f73e4f62d72b73f3d6d78ff0eed93c31",
"reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^5.6 || ^7.0 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "~5.7",
"setasign/fpdf": "~1.8",
"setasign/tfpdf": "1.31",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "~6.2"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "jan.slabon@setasign.com",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "maximilian.kresse@setasign.com",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.3.6"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2021-02-11T11:37:01+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.3.0",
@ -6720,65 +7082,6 @@
},
"time": "2022-01-20T13:18:17+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2022-03-03T13:19:32+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v5.11.0",

View file

@ -49,11 +49,14 @@ class InvoiceFactory extends Factory
}
$totalDays += $days;
$total += $amount;
$tasks[] = \array_merge(Invoice::getTaskStructure(), [
/** @var \App\Enums\Invoice\TaskType */
$taskType = \collect(TaskType::toArray())->random();
$tasks[] = \array_merge(Invoice::getTaskStructure(), [
'desc' => $this->faker->words(9, true),
'days' => $days,
'type' => \collect(TaskType::toArray())->random()->value,
'type' => $taskType->value,
'amount' => $amount,
'package' => 1
]);
} //end for

View file

@ -39,11 +39,11 @@ class InvoicingSeeder extends Seeder
Invoice::factory(45)->make()->each(function (Invoice $model) {
$model->ref = \sprintf(
'%s_%s_%s',
Carbon::now()->isoFormat('Y-m'),
Str::padLeft(Invoice::whereYear('created_at', Carbon::now()->isoFormat('Y'))
->whereMonth('created_at', Carbon::now()->isoFormat('M'))
->count() ?: 1, 4, '0'),
Str::substr(Str::upper(Str::slug(User::query()->inRandomOrder()->first()->name)), 0, 2)
Carbon::now()->isoFormat('YYYY-MM'),
Str::padLeft((Invoice::whereYear('created_at', Carbon::now()->isoFormat('YYYY'))
->whereMonth('created_at', Carbon::now()->isoFormat('MM'))
->count() + 1) ?: 1, 4, '0'),
Str::substr(Str::upper(Str::slug(User::query()->inRandomOrder()->first()->name)), 0, 1)
);
$model->site()->associate(Site::query()->inRandomOrder()->first());
$model->company()->associate(Company::query()->inRandomOrder()->first());

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -15,6 +15,6 @@ return [
'labels' => 'Labels',
'packages' => 'Forfaits',
'invoices' => 'Factures',
'companies' => 'Compagnies',
'companies' => 'Sociétés',
]
];

View file

@ -24,103 +24,110 @@
<th width="100">
{{ __("Jours") }}
</th>
<th width="100">
<th width="140">
{{ __("Montant") }}
</th>
<th />
</thead>
<tbody v-if="tasks.length">
<tr
v-for="(task, index) in tasks"
:key="index"
>
<td>
<textarea
v-model="task.desc"
rows="2"
class="form-control"
/>
</td>
<td>
<select
v-model="task.type"
@change="updateAmount(index)"
class="form-select"
>
<option
v-for="(typeEnum, index) in taskTypeEnums"
:value="typeEnum.value"
:key="index"
<tbody>
<template v-if="tasks.length">
<tr
v-for="(task, index) in tasks"
:key="index"
>
<td>
<textarea
v-model="task.desc"
rows="2"
class="form-control"
/>
</td>
<td>
<select
v-model="task.type"
@change="updateAmount(index)"
class="form-select"
>
{{ typeEnum.label }}
</option>
</select>
<select
v-if="task.type === getTaskTypeEnum('forfait').value"
v-model="task.package"
@change="updateAmount(index)"
class="form-select"
>
<option
v-for="(packageModel, index) in packages"
:value="packageModel"
:key="index"
:selected="task.package === undefined && index === 0"
<option
v-for="(typeEnum, typeIndex) in taskTypeEnums"
:value="typeEnum.value"
:key="typeIndex"
>
{{ typeEnum.label }}
</option>
</select>
<select
v-if="task.type === getTaskTypeEnum('forfait').value"
v-model="task.packageModel"
@change="updateAmount(index)"
class="form-select"
>
{{ packageModel.name }}
</option>
</select>
</td>
<td>
<input
class="inputNumber"
v-model="task.days"
@change="updateAmount(index)"
type="number"
step="0.125"
min="0"
>
</td>
<td>
<input
v-if="task.type === getTaskTypeEnum('libre').value"
class="inputNumber"
v-model="task.amount"
type="number"
min="0"
>
<template v-else>
{{ task.amount.toFixed(2) }}
</template>&nbsp;€
</td>
<td>
<div
class="btn-group"
role="group"
:aria-label="__('Ajouter ou enlever une ligne')"
>
<button
class="btn btn-sm btn-primary"
@click.prevent="addTasksRow(index)"
<option
v-for="(packageModel, packageIndex) in packages"
:value="packageModel"
:key="packageIndex"
:selected="!task.packageModel && index === 0"
>
{{ packageModel.name }}
</option>
</select>
</td>
<td>
<input
class="inputNumber form-control"
v-model="task.days"
@change="updateAmount(index)"
type="number"
step="0.125"
min="0"
>
<font-awesome-icon icon="fa-solid fa-plus" />
</button>
<button
class="btn btn-sm btn-danger"
@click.prevent="removeTasksRow(index)"
</td>
<td>
<div
v-if="task.type === getTaskTypeEnum('libre').value"
class="input-group"
>
<font-awesome-icon icon="fa-solid fa-minus" />
</button>
</div>
</td>
</tr>
<input
class="inputNumber form-control"
v-model="task.amount"
type="number"
min="0"
>
<span class="input-group-text">€</span>
</div>
<template v-else>
{{ task.amount.toFixed(2) }}&nbsp;€
</template>
</td>
<td>
<div
class="btn-group"
role="group"
:aria-label="__('Ajouter ou enlever une ligne')"
>
<button
class="btn btn-sm btn-primary"
@click.prevent="addTasksRow(index)"
>
<font-awesome-icon icon="fa-solid fa-plus" />
</button>
<button
class="btn btn-sm btn-danger"
@click.prevent="removeTasksRow(index)"
>
<font-awesome-icon icon="fa-solid fa-minus" />
</button>
</div>
</td>
</tr>
</template>
<tr>
<td
colspan="4"
colspan="5"
class="text-end"
>
<button
class="btn btn-primary"
class="btn btn-sm btn-primary"
@click.prevent="addTasksRow()"
>
<font-awesome-icon icon="fa-solid fa-plus" />
@ -131,11 +138,23 @@
<td colspan="2">
{{ __("Total") }}
</td>
<td>{{ totalDays }}</td>
<td>{{ Number.parseFloat(String(total)).toFixed(2) }}&nbsp;€</td>
<td>
<span class="mt-2">{{ totalDays }}</span>
</td>
<td>
<span class="mt-2">
{{ Number.parseFloat(String(total)).toFixed(2) }}&nbsp;€</span>
</td>
<td />
</tr>
</tbody>
</table>
<input
class="d-none"
name="tasks"
type="text"
:value="JSON.stringify(databaseTasks)"
>
</div>
</template>
@ -150,7 +169,8 @@ type TaskRow = {
days: number;
type: number;
amount: number;
package: LaravelModel | null;
package: number | null;
packageModel: LaravelModel | null;
};
type TaskType = Enum & { price: number };
@ -164,15 +184,25 @@ export default defineComponent({
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.companies = data.companies;
this.packages = data.packages;
this.taskTypeEnums = data.taskTypeEnums;
this.taskRowModel = data.taskRowModel;
const tasks = data.tasks;
tasks.map((task: TaskRow) => {
if (task.package) {
task.packageModel =
data.packages.find(
(packageModel: LaravelModel) => packageModel.id === task.package
) ?? null;
}
});
this.tasks = tasks;
// * Use first enum value as default
this.taskRowModel.type = this.getTaskTypeEnum("web").value as number;
this.taskRowModel.package = this._packages[0];
this.taskRowModel.packageModel = this.packages[0];
},
data(): {
tasks: TaskRow[];
@ -192,10 +222,20 @@ export default defineComponent({
type: 1,
amount: 0,
package: null,
packageModel: null,
},
};
},
computed__tasks: {
computed: {
databaseTasks(): TaskRow[] {
let tasks = [...this.tasks];
tasks.map((task: TaskRow) => {
task = Object.assign({}, task);
task.package = task.packageModel ? task.package : null;
task.packageModel = null;
});
return tasks;
},
total(): number {
let total = 0;
this.tasks.map((task) => (total += task.amount));
@ -207,7 +247,14 @@ export default defineComponent({
return total;
},
},
watch: {},
watch: {
tasks(tasks: TaskRow[]) {
tasks.map((task) => {
task.package = task.packageModel?.id as number | null;
});
this.tasks = tasks;
},
},
methods: {
/**
* Get the coresponding task type using value or label
@ -246,10 +293,8 @@ export default defineComponent({
task.amount = task.days * this.getTaskTypeEnum(task.type).price;
break;
case this.getTaskTypeEnum("forfait").value:
if (!task.package) {
throw new Error(`task ${task.desc} has undefined package`);
}
task.amount = task.days * (task.package.amount as number);
task.amount =
task.days * ((task.packageModel?.amount ?? 0) as number);
break;
case this.getTaskTypeEnum("libre").value:
// Do nothing, free input

View file

@ -0,0 +1,219 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
{{ __('Facture :appname - ref :ref', [
'appname' => config('app.name'),
'ref' => $ref,
]) }}
</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
html,
body {
position: relative;
width: 210mm;
height: 297mm;
margin: 0;
padding: 0;
}
body {
font-family: Tahoma;
font-size: 12px;
color: #333333;
background-color: #FFFFFF;
}
table {
border-collapse: collapse;
}
#mainTable {
margin: auto;
padding-top: 10mm;
height: 244mm;
width: 188mm;
}
table.bordered,
table.bordered tr,
table.bordered th,
table.bordered td {
border: 1px solid;
border-color: #CCC;
}
.with-bg {
background-color: #F2F2F2;
}
@page {
footer: page-footer;
}
</style>
</head>
<body>
<div id="mainTable" cellpadding="0" cellspacing="0">
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td colspan="2" style="text-align: right;padding-bottom: 3mm;"><b>{{ Str::upper($lastname) }} {{ $firstname }}</b></td>
</tr>
<tr>
<td style="text-align: right;padding-right: 3mm;"><b>{{ Str::singular(__('models.classes.companies')) }}</b></td>
<td width="auto" style="white-space: nowrap;">{{ $company->name }}</td>
</tr>
<tr>
<td style="text-align: right;padding-right: 3mm;"><b>{{ __('Entité à facturer') }}</b></td>
<td width="auto" style="white-space: nowrap;">{{ $companyInvoiced->name }}</td>
</tr>
</table>
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td valign="middle" width="12mm" style="height: 23mm;">
<img style="width: 12mm;" src="{{ resource_path('assets/documents/invoice/logo_ldc.jpg') }}" alt="{{ trans('Logo LDC') }}"/>
</td>
<td width="50%">
<span style="font-size: 16px;font-weight: bold;">{{ __('SERVICE WEB MARKETING GROUPE') }}</span>
</td>
</tr>
</table>
<br/>
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td width="25mm"><b>{{ __('Objet :') }}</b></td>
<td>{{ $object }}</td>
</tr>
<tr>
<td width="25mm"><b>{{ __('Site Internet :') }}</b></td>
<td><u>{{ $site->name }}</u></td>
</tr>
<tr>
<td width="25mm"><b>{{ __('Devis :') }}</b></td>
<td><strong><i>{{ $ref }}</i></strong></td>
</tr>
<tr>
<td width="25mm"><b>{{ __('Date :') }}</b></td>
<td><i>{{ Carbon::parse($updated_at)->isoFormat('DD/MM/YYYY HH:mm') }}</i></td>
</tr>
</table>
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td height="26mm" style="text-align: center;">
<span style="font-size: 16px;font-weight: bold;">{{ __('Prestation WEB') }}</span>
</td>
</tr>
</table>
@php
$tasks = collect($tasks);
$columns = 2 + ( $show_days ? 1 : 0);
$page = 1;
$perPage = 8;
$lastPage = App\Lib\CollectionHelper::paginateManually($tasks, $perPage)->lastPage();
@endphp
@while($lastPage >= $page)
@php $taskList = App\Lib\CollectionHelper::paginateManually($tasks, $perPage, $page); @endphp
@if($page !== 1)
<table width="100%" style="height: 10mm;">
<tr>
<td>&nbsp;</td>
</tr>
</table>
@endif
<table width="100%" cellspacing="0" cellpadding="2" class="bordered" @if($page !== 1) style="margin-top: 30mm;" @endif>
@if($page === 1)
<tr>
<td class="with-bg" style="padding-left: 2mm;"><strong>{{ __('Tâches') }}</strong></td>
@if($show_days)
<td width="22mm" class="with-bg" style="padding-left: 2mm;"><strong>{{ __('Jours') }}</strong></td>
@endif
<td width="22mm" class="with-bg" style="padding-left: 2mm;"><strong>{{ __('Montant') }}</strong></td>
</tr>
<tr style="display:none;"><td colspan="{{ $columns }}"></td></tr>
@endif
@foreach($taskList as $task)
<tr>
<td valign="top" style="padding: 3mm 2mm;">
@if($task['package'] ?? 0)
<b>{{ __('Forfait : :package', [
'package' => $packages[$task['package']]->name ?? __('non existant')
]) }}</b>
<br>
@endif
{{ $task['desc'] }}
</td>
@if($show_days)
<td style="text-align: center;">{{ number_format($task['days'], 2, ',', '') }}</td>
@endif
<td style="text-align: center;">{{ number_format($task['amount'], 2, ',', '') }}</td>
</tr>
@endforeach
@for ($i = $perPage - $taskList->count(); $i > 0; $i--)
<tr>
@for ($k = $columns; $k > 0; $k--)
<td valign="top">&nbsp;</td>
@endfor
</tr>
@endfor
</table>
@php $page++; @endphp
@if($taskList->currentPage() < $taskList->lastPage())
<pagebreak>
@endif
@endwhile
<table width="100%" cellspacing="0" cellpadding="2" class="bordered">
<tr>
<td style="padding-left: 2mm;"><span style="font-size: 14px;font-weight: bold;">{{ __('Total') }}</span></td>
@if($show_days)
<td width="22mm" align="center"><strong>{{ __(':total Jours', [
'total' => number_format($total_days, 2, ',', '')
]) }}</strong></td>
@endif
<td width="22mm" align="center"><strong>{{ __(':total €', [
'total' => number_format($total, 2, ',', '')
]) }}</strong></td>
</tr>
</table>
@if($comment)
<br>
<div>
<span style="font-size: 13px;font-weight: bold;">{{ __('Commentaire') }}</span>
</div>
<table width="100%" cellspacing="0" cellpadding="2" class="bordered">
<tr>
<td style="padding: 2mm;">
{{ Str::limit($comment, 2100) }}
</td>
</tr>
</table>
@endif
</div>
<htmlpagefooter name="page-footer">
<table width="90%" cellspacing="0" cellpadding="2" style="margin: auto;margin-bottom: 10mm;">
<tr>
<td width="33%" style="border-top:double medium #CCCCCC;padding-top: 2mm;" valign="top">
{{ __("LDC Web Service") }}<br/>
</td>
<td width="33%" style="text-align: center;border-top:double medium #CCCCCC;padding-top: 2mm;" valign="top">
</td>
<td valign="bottom" width="34%" style="text-align: right;border-top:double medium #CCCCCC;padding-top: 2mm;">
<span>{{ __('page') }}&nbsp;{PAGENO}</span>
</td>
</tr>
</table>
</htmlpagefooter>
</body>
</html>

View file

@ -0,0 +1,43 @@
@extends($viewLayout)
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.create')))
@section('description', sprintf('%s %s', __('crud-policies::crud.create'), class_basename($modelClass)))
@section('metaIndex', 'noindex,nofollow')
@section('content')
<div class="card">
@include('crud-policies::crud.modules.flashMessage')
<div class="card-header">
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::create()])
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::create()])
</div>
<div class="card-body">
<form action="{{ CrudController::getRoutePrefixed("$modelTable.store") }}" method="POST" @if($hasFile) enctype="multipart/form-data" @endif>
@csrf
<div class="container text-center">
<div class="row">
<div class="col-3 border border-secondary py-1 px-1">{{ __('crud-policies::crud.property') }}</div>
<div class="col-9 border border-secondary py-1 px-1">{{ __('crud-policies::crud.value') }}</div>
</div>
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
@php
// create form cannot accept readonly field
$prop['readonly'] = false;
@endphp
@if(in_array(CrudAction::create(), $prop['actions']))
<div class="row">
@include('crud-policies::crud.formfield')
</div>
@endif
@endforeach
<div class="row">
<div class="col text-center">
<button class="btn btn-info" type="submit">{{ __('crud-policies::crud.create') }}</button>
</div>
</div>
</div>
</form>
</div>
</div>
@include('crud-policies::crud.modules.scriptConstants')
@endsection

View file

@ -0,0 +1,114 @@
@extends($viewLayout)
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.edit')))
@section('description', sprintf('%s %s', __('crud-policies::crud.edit'), trans(class_basename($modelClass))))
@section('metaIndex', 'noindex,nofollow')
@section('content')
<div class="card">
@include('crud-policies::crud.modules.flashMessage')
<div class="card-header">
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::update()])
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::update()])
<div class="row text-end">
<div class="col">
@if(!$model->signed_date)
<form action="{{ route('invoices.sign.setSignedDate', $model) }}" method="POST">
@csrf
<div class="input-group">
<input name="date" type="date" class="form-control" aria-label="{{ __('Signé par le service') }}" aria-describedby="signed_date">
<button class="btn {{ !$model->signed_date ? 'btn-outline-warning' : 'btn-outline-secondary' }}" type="submit" id="signed_date">{{ __('Service Web') }}</button>
</div>
</form>
@else
<span class="d-block h5 fw-bold pt-2">{{ __('Signé le :date', [
'date' => $model->signed_date->isoFormat('DD/MM/YYYY')
]) }}</span>
@endif
</div>
<div class="col">
@if(!$model->sent_client_date)
<form action="{{ route('invoices.sign.setClientSentDate', $model) }}" method="POST">
@csrf
<div class="input-group">
<input name="date" type="date" class="form-control" aria-label="{{ __('Signé par le client') }}" aria-describedby="sent_client_date" @if($model->sent_client_date)readonly @endif>
<button class="btn {{ !$model->sent_client_date ? 'btn-outline-success' : 'btn-outline-secondary' }}" type="submit" id="sent_client_date">{{ __('Client') }}</button>
</div>
</form>
@else
<span class="d-block h5 fw-bold pt-2">{{ __('Signé le :date (client)', [
'date' => $model->sent_client_date->isoFormat('DD/MM/YYYY')
]) }}</span>
@endif
</div>
<div class="col">
@if(!$model->sent_accounting_date)
<form action="{{ route('invoices.sign.setComptaSentDate', $model) }}" method="POST">
@csrf
<div class="input-group">
<input name="date" type="date" class="form-control" aria-label="{{ __('Signé par la compta') }}" aria-describedby="sent_accounting_date" @if($model->sent_accounting_date)readonly @endif>
<button class="btn {{ !$model->sent_accounting_date ? 'btn-outline-info' : 'btn-outline-secondary' }}" type="submit" id="sent_accounting_date">{{ __('Compta') }}</button>
</div>
</form>
@else
<span class="d-block h5 fw-bold pt-2">{{ __('Signé le :date (compta)', [
'date' => $model->sent_accounting_date->isoFormat('DD/MM/YYYY')
]) }}</span>
@endif
</div>
</div>
</div>
<div class="card-body">
<form action="{{ CrudController::getRoutePrefixed("$modelTable.update", $model) }}" method="POST" @if($hasFile) enctype="multipart/form-data" @endif>
@method('put')
@csrf
<div class="container text-center">
<div class="row">
<div class="col-3 border border-secondary py-1 px-1">{{ __('crud-policies::crud.property') }}</div>
<div class="col-9 border border-secondary py-1 px-1">{{ __('crud-policies::crud.value') }}</div>
</div>
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
@if(in_array(CrudAction::update(), $prop['actions']))
@if($fieldName === 'tasks')
<div class="row">
<div class="col-3 border border-secondary py-1 px-1">
<label for="total" class="form-label">{{ $prop['label'] ?? $fieldName }}</label>
</div>
<div class="col-9 border border-secondary py-1 px-1">
@php
$taskTypeEnums = collect(App\Enums\Invoice\TaskType::toArray())->map(function($enum) {
return [
'value' => $enum->value,
'label' => $enum->label,
'price' => $enum->price,
];
});
$dataVue = [
'tasks'=> old($fieldName) ? json_decode(old($fieldName), true): $model->{$fieldName} ?? [],
'companies' => $companies,
'packages' => $packages,
'taskRowModel' => $taskRowModel,
'taskTypeEnums' => $taskTypeEnums,
];
@endphp
<div
id="invoice-tasks"
data-json='@json($dataVue)'></div>
</div>
</div>
@else
<div class="row">
@include('crud-policies::crud.formfield')
</div>
@endif
@endif
@endforeach
</div>
<div class="col text-center">
<button class="btn btn-info" type="submit" @if($model->signed_date) disabled @endif>{{ __('crud-policies::crud.update') }}</button>
</div>
</form>
</div>
</div>
@include('crud-policies::crud.modules.scriptConstants')
@endsection

View file

@ -0,0 +1,130 @@
@extends($viewLayout)
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s',
strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))),
__('crud-policies::crud.viewAny')))
@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'),
trans(Str::plural(class_basename($modelClass)))))
@section('metaIndex', 'noindex,nofollow')
@section('content')
<div class="card">
@include('crud-policies::crud.modules.flashMessage')
<div class="card-header">
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()])
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()])
</div>
<div class="card-body">
<div class="row justify-content-center">
<div class="col">
{{ $models->links() }}
</div>
<div class="col">
<form action="{{ url()->current() }}">
<div class="input-group mb-3">
<input type="search" name="search" class="form-control" value="{{ request()->search }}"
placeholder="Recherche.." />
<input type="text" name="sort_col" class="d-none" value="{{ request()->sort_col }}" />
<input type="text" name="sort_way" class="d-none" value="{{ request()->sort_way }}" />
<a class="btn btn-danger" href="{{ url()->current() }}">{{ __('crud-policies::crud.reset')
}}</a>
<button type="submit" class="btn btn-primary">
{{ __('crud-policies::crud.search') }}
</button>
</div>
</form>
</div>
</div>
<table class="table text-center">
<thead class="thead-dark">
@include('crud-policies::crud.modules.sorter')
</thead>
<tbody>
@if(count($models))
{{-- INIT QUERIES for OPTIMIZING --}}
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
@if($prop['type']->equals(CrudType::order()))
@php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp
@endif
@if($prop['type']->equals(CrudType::belongsTo()))
@php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e)
{} @endphp
@endif
@if($prop['type']->equals(CrudType::belongsToMany()))
@php $models->load("$fieldName"); @endphp
@endif
@if($prop['type']->equals(CrudType::hasMany()))
@php $models->load("$fieldName"); @endphp
@endif
@endforeach
{{-- DISPLAY MODELS --}}
@foreach($models as $model)
<tr>
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
@if(in_array(CrudAction::viewAny(), $prop['actions']))
@if($fieldName === 'object')
<td>{{ Str::limit($model->{$fieldName}, 25) }}</td>
@else
@switch($prop['type'])
@case(CrudType::password())
{{-- Ignore --}}
@break
@default
<td>@include('crud-policies::crud.printfield', [
'action' => CrudAction::viewAny(),
'field' => $model->{$fieldName},
'props' => $prop,
"$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null
])</td>
@endswitch
@endif
@endif
@endforeach
{{-- MODEL ACTIONS --}}
<td>
<div class="btn-group" role="group"
aria-label="{{ Str::plural(__('crud-policies::crud.action')) }}">
@can('view', $model)
<a href="{{ CrudController::getRoutePrefixed("$modelTable.show", $model) }}"
class="btn btn-sm btn-primary">{{ __('crud-policies::crud.view') }}</a>
@endcan
@can('update', $model)
<a href="{{ CrudController::getRoutePrefixed("$modelTable.edit", $model) }}"
class="btn btn-sm btn-info">{{ __('crud-policies::crud.edit') }}</a>
@endcan
@can('download', $model)
<a href="{{ CrudController::getRoutePrefixed("$modelTable.download", $model) }}"
class="btn btn-sm btn-secondary">{{ __('Télécharger') }}</a>
@endcan
@if(config('crud-policies.deleteOnIndex') and Auth::user()->can('delete', $model))
<form class="CrudConfirmDelete" action="{{ CrudController::getRoutePrefixed("
$modelTable.destroy", $model) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">{{ __('crud-policies::crud.delete')
}}</button>
</form>
@endcan
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="{{ count($modelClass::getEditableProperties()) }}">
{{ __('crud-policies::crud.nothingtoshow') }}
</td>
</tr>
@endif
</tbody>
</table>
{{ $models->links() }}
</div>
</div>
@include('crud-policies::crud.modules.scriptConstants')
@endsection

View file

@ -0,0 +1,87 @@
@extends($viewLayout)
@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.view')))
@section('description', sprintf('%s %s', __('crud-policies::crud.view'), trans(class_basename($modelClass))))
@section('metaIndex', 'noindex,nofollow')
@section('content')
<div class="card">
@include('crud-policies::crud.modules.flashMessage')
<div class="card-header">
@include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::view()])
@include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::view()])
</div>
<div class="card-body">
<table class="table table-sm table-bordered border-primary align-top table-striped text-center">
<thead class="thead-dark">
<tr>
<th scope="col" class="w-25">{{ __('crud-policies::crud.property') }}</th>
<th scope="col" class="w-75">{{ __('crud-policies::crud.value') }}</th>
</tr>
</thead>
<tbody>
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
@if(in_array(CrudAction::view(), $prop['actions']))
@switch($prop['type'])
@case(CrudType::password())
{{-- Ignore --}}
@break
@default
@if($fieldName === 'tasks')
<tr>
<td>{{ $prop['label'] }}</td>
<td>
@php $tasks = collect($model->{$fieldName}); @endphp
@if($tasks->count())
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ __('Commentaire') }}</th>
<th>{{ __('Type') }}</th>
<th>{{ __('Jours') }}</th>
<th>{{ __('Montant') }}</th>
</tr>
</thead>
<tbody>
@foreach ($tasks as $task)
<tr>
<td>{{ $task['desc'] }}</td>
<td>
{{ App\Enums\Invoice\TaskType::make($task['type'])->label }}
@if($task['package'] ?? 0)
<br>
{{ optional($packages[$task['package']] ?? null)->name }}
@endif
</td>
<td>{{ number_format($task['days'], 2, ',', '') }}</td>
<td>{{ number_format($task['amount'], 2, ',', '') }}&nbsp;€</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<span class="h5">{{ __('Pas de tâches') }}</span>
@endif
</td>
</tr>
@else
<tr>
<td>{{ $prop['label'] }}</td>
<td>@include('crud-policies::crud.printfield', [
'action' => CrudAction::view(),
'field' => $model->{$fieldName},
'props' => $prop
])</td>
</tr>
@endif
@endswitch
@endif
@endforeach
</tbody>
</table>
</div>
</div>
@include('crud-policies::crud.modules.scriptConstants')
@endsection

View file

@ -37,6 +37,9 @@
@can('viewAny', App\Models\Project::class)
<a class="dropdown-item @if(strpos(request()->route()->getName(), 'projects') === 0)active @endif" href="{{ route('projects.index') }}">{{ __('models.classes.projects') }}</a>
@endcan
@can('viewAny', App\Models\Invoice::class)
<a class="dropdown-item @if(strpos(request()->route()->getName(), 'invoices') === 0)active @endif" href="{{ route('invoices.index') }}">{{ __('models.classes.invoices') }}</a>
@endcan
@can('viewAny', App\Models\Company::class)
<a class="dropdown-item @if(strpos(request()->route()->getName(), 'companies') === 0)active @endif" href="{{ route('companies.index') }}">{{ __('models.classes.companies') }}</a>
@endcan

View file

@ -168,7 +168,7 @@ $userPr = Auth::user()->sites ?? collect([]);
</td>
@can('assignLabels', App\Models\Ticket::class)
<td>
<span class="ticketIndexLabels">
<span class="ticket-index-labels">
@foreach ($ticket->labels as $label)
<span class="badge label colorize" data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach

View file

@ -7,6 +7,7 @@ use App\Http\Controllers\Credit\CreditLogController;
use App\Http\Controllers\Credit\EstimationController;
use App\Http\Controllers\FileController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\LabelController;
use App\Http\Controllers\LogController;
use App\Http\Controllers\MarkerController;
@ -74,6 +75,7 @@ Route::middleware('auth:web')->group(function () {
Route::resource('users', UserController::class);
Route::resource('packages', PackageController::class)->except('show');
Route::resource('companies', CompanyController::class)->except('show');
Route::resource('invoices', InvoiceController::class);
Route::resource('labels', LabelController::class)->except('show');
Route::resource('brands', BrandController::class);
Route::resource('sites', SiteController::class)
@ -120,6 +122,16 @@ Route::middleware('auth:web')->group(function () {
Route::resource('sites.logs', LogController::class);
Route::resource('sites.files', FileController::class);
// * INVOICES
Route::get('/invoices/{invoice}/download', [InvoiceController::class, 'download'])
->name('invoices.download');
Route::post('/invoices/{invoice}/sign/service', [InvoiceController::class, 'setSignedDate'])
->name('invoices.sign.setSignedDate');
Route::post('/invoices/{invoice}/sign/client', [InvoiceController::class, 'setClientSentDate'])
->name('invoices.sign.setClientSentDate');
Route::post('/invoices/{invoice}/sign/compta', [InvoiceController::class, 'setComptaSentDate'])
->name('invoices.sign.setComptaSentDate');
// * Ticket
Route::prefix('/ticket')->name('ticket.')->group(function () {