diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 365241e..efe993e 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -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')); + } } diff --git a/app/Http/Requests/Invoice/InvoiceSetDateRequest.php b/app/Http/Requests/Invoice/InvoiceSetDateRequest.php new file mode 100644 index 0000000..ae8dff3 --- /dev/null +++ b/app/Http/Requests/Invoice/InvoiceSetDateRequest.php @@ -0,0 +1,33 @@ +user(); + + return auth()->check() && $user->isAdmin(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'date' => 'required|date' + ]; + } +} diff --git a/app/Lib/CollectionHelper.php b/app/Lib/CollectionHelper.php new file mode 100644 index 0000000..a665a2f --- /dev/null +++ b/app/Lib/CollectionHelper.php @@ -0,0 +1,64 @@ +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 + ]); + } +} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 5ba7c7b..0b68a2c 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -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 - */ - 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 + */ + 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; + } } diff --git a/app/Policies/InvoicePolicy.php b/app/Policies/InvoicePolicy.php index 70c06fb..46c5f79 100644 --- a/app/Policies/InvoicePolicy.php +++ b/app/Policies/InvoicePolicy.php @@ -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), + ] ] ]; } diff --git a/app/Policies/Rules/InvoiceStaticRules.php b/app/Policies/Rules/InvoiceStaticRules.php new file mode 100644 index 0000000..747d998 --- /dev/null +++ b/app/Policies/Rules/InvoiceStaticRules.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/composer.json b/composer.json index 2095906..338f720 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index ce363c3..b7be3ff 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/database/factories/InvoiceFactory.php b/database/factories/InvoiceFactory.php index a551c1a..393b3f5 100644 --- a/database/factories/InvoiceFactory.php +++ b/database/factories/InvoiceFactory.php @@ -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 diff --git a/database/seeders/InvoicingSeeder.php b/database/seeders/InvoicingSeeder.php index 9cd2f4c..166c42e 100644 --- a/database/seeders/InvoicingSeeder.php +++ b/database/seeders/InvoicingSeeder.php @@ -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()); diff --git a/resources/assets/documents/invoice/logo_ldc.jpg b/resources/assets/documents/invoice/logo_ldc.jpg new file mode 100644 index 0000000..d9ad13e Binary files /dev/null and b/resources/assets/documents/invoice/logo_ldc.jpg differ diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index b85e130..0406f0b 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -15,6 +15,6 @@ return [ 'labels' => 'Labels', 'packages' => 'Forfaits', 'invoices' => 'Factures', - 'companies' => 'Compagnies', + 'companies' => 'Sociétés', ] ]; diff --git a/resources/ts/components/InvoiceTasksComponent.vue b/resources/ts/components/InvoiceTasksComponent.vue index c699cde..cba0b49 100644 --- a/resources/ts/components/InvoiceTasksComponent.vue +++ b/resources/ts/components/InvoiceTasksComponent.vue @@ -24,103 +24,110 @@ {{ __("Jours") }} - + {{ __("Montant") }} - - - -