diff --git a/app/Factory/RecurringExpenseFactory.php b/app/Factory/RecurringExpenseFactory.php new file mode 100644 index 000000000000..08562266f9c8 --- /dev/null +++ b/app/Factory/RecurringExpenseFactory.php @@ -0,0 +1,45 @@ +user_id = $user_id; + $recurring_expense->company_id = $company_id; + $recurring_expense->is_deleted = false; + $recurring_expense->should_be_invoiced = false; + $recurring_expense->tax_name1 = ''; + $recurring_expense->tax_rate1 = 0; + $recurring_expense->tax_name2 = ''; + $recurring_expense->tax_rate2 = 0; + $recurring_expense->tax_name3 = ''; + $recurring_expense->tax_rate3 = 0; + $recurring_expense->date = null; + $recurring_expense->payment_date = null; + $recurring_expense->amount = 0; + $recurring_expense->foreign_amount = 0; + $recurring_expense->private_notes = ''; + $recurring_expense->public_notes = ''; + $recurring_expense->transaction_reference = ''; + $recurring_expense->custom_value1 = ''; + $recurring_expense->custom_value2 = ''; + $recurring_expense->custom_value3 = ''; + $recurring_expense->custom_value4 = ''; + + return $recurring_expense; + } +} diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index 8992424d59f6..11d4ee46ae26 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -38,11 +38,6 @@ class ExpenseFilters extends QueryFilters return $this->builder->where(function ($query) use ($filter) { $query->where('expenses.name', 'like', '%'.$filter.'%') ->orWhere('expenses.id_number', 'like', '%'.$filter.'%') - ->orWhereHas('contacts', function ($query) use($filter){ - $query->where('expense_contacts.first_name', 'like', '%'.$filter.'%'); - $query->orWhere('expense_contacts.last_name', 'like', '%'.$filter.'%'); - $query->orWhere('expense_contacts.email', 'like', '%'.$filter.'%'); - }) ->orWhere('expenses.custom_value1', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value2', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value3', 'like', '%'.$filter.'%') diff --git a/app/Filters/RecurringExpenseFilters.php b/app/Filters/RecurringExpenseFilters.php new file mode 100644 index 000000000000..e8c3c4740436 --- /dev/null +++ b/app/Filters/RecurringExpenseFilters.php @@ -0,0 +1,148 @@ +builder; + } + + return $this->builder->where(function ($query) use ($filter) { + $query->where('recurring_expenses.name', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.id_number', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value1', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value2', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value3', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value4', 'like', '%'.$filter.'%'); + }); + } + + /** + * Filters the list based on the status + * archived, active, deleted. + * + * @param string filter + * @return Builder + */ + public function status(string $filter = '') : Builder + { + if (strlen($filter) == 0) { + return $this->builder; + } + + $table = 'expenses'; + $filters = explode(',', $filter); + + return $this->builder->where(function ($query) use ($filters, $table) { + $query->whereNull($table.'.id'); + + if (in_array(parent::STATUS_ACTIVE, $filters)) { + $query->orWhereNull($table.'.deleted_at'); + } + + if (in_array(parent::STATUS_ARCHIVED, $filters)) { + $query->orWhere(function ($query) use ($table) { + $query->whereNotNull($table.'.deleted_at'); + + if (! in_array($table, ['users'])) { + $query->where($table.'.is_deleted', '=', 0); + } + }); + } + + if (in_array(parent::STATUS_DELETED, $filters)) { + $query->orWhere($table.'.is_deleted', '=', 1); + } + }); + } + + /** + * Sorts the list based on $sort. + * + * @param string sort formatted as column|asc + * @return Builder + */ + public function sort(string $sort) : Builder + { + $sort_col = explode('|', $sort); + + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + } + + /** + * Returns the base query. + * + * @param int company_id + * @param User $user + * @return Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + $query = DB::table('recurring_expenses') + ->join('companies', 'companies.id', '=', 'recurring_expenses.company_id') + ->where('recurring_expenses.company_id', '=', $company_id) + ->select( + DB::raw('COALESCE(recurring_expenses.country_id, companies.country_id) country_id'), + 'recurring_expenses.id', + 'recurring_expenses.private_notes', + 'recurring_expenses.custom_value1', + 'recurring_expenses.custom_value2', + 'recurring_expenses.custom_value3', + 'recurring_expenses.custom_value4', + 'recurring_expenses.created_at', + 'recurring_expenses.created_at as expense_created_at', + 'recurring_expenses.deleted_at', + 'recurring_expenses.is_deleted', + 'recurring_expenses.user_id', + ); + + /* + * If the user does not have permissions to view all invoices + * limit the user to only the invoices they have created + */ + if (Gate::denies('view-list', RecurringExpense::class)) { + $query->where('recurring_expenses.user_id', '=', $user->id); + } + + return $query; + } + + /** + * Filters the query by the users company ID. + * + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + return $this->builder->company(); + } +} diff --git a/app/Http/Controllers/RecurringExpenseController.php b/app/Http/Controllers/RecurringExpenseController.php new file mode 100644 index 000000000000..23e79186c1b9 --- /dev/null +++ b/app/Http/Controllers/RecurringExpenseController.php @@ -0,0 +1,578 @@ +recurring_expense_repo = $recurring_expense_repo; + } + + /** + * @OA\Get( + * path="/api/v1/recurring_expenses", + * operationId="getRecurringExpenses", + * tags={"recurring_expenses"}, + * summary="Gets a list of recurring_expenses", + * description="Lists recurring_expenses, search and filters allow fine grained lists to be generated. + + Query parameters can be added to performed more fine grained filtering of the recurring_expenses, these are handled by the RecurringExpenseFilters class which defines the methods available", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\Response( + * response=200, + * description="A list of recurring_expenses", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + * @param RecurringExpenseFilters $filters + * @return Response|mixed + */ + public function index(RecurringExpenseFilters $filters) + { + $recurring_expenses = RecurringExpense::filter($filters); + + return $this->listResponse($recurring_expenses); + } + + /** + * Display the specified resource. + * + * @param ShowRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * @OA\Get( + * path="/api/v1/recurring_expenses/{id}", + * operationId="showRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Shows a client", + * description="Displays a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the recurring_expense object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function show(ShowRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + return $this->itemResponse($recurring_expense); + } + + /** + * Show the form for editing the specified resource. + * + * @param EditRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * @OA\Get( + * path="/api/v1/recurring_expenses/{id}/edit", + * operationId="editRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Shows a client for editting", + * description="Displays a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function edit(EditRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + return $this->itemResponse($recurring_expense); + } + + /** + * Update the specified resource in storage. + * + * @param UpdateRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/recurring_expenses/{id}", + * operationId="updateRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Updates a client", + * description="Handles the updating of a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function update(UpdateRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + if ($request->entityIsDeleted($recurring_expense)) { + return $request->disallowUpdate(); + } + + $recurring_expense = $this->recurring_expense_repo->save($request->all(), $recurring_expense); + + $this->uploadLogo($request->file('company_logo'), $recurring_expense->company, $recurring_expense); + + event(new RecurringExpenseWasUpdated($recurring_expense, $recurring_expense->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + + return $this->itemResponse($recurring_expense->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @param CreateRecurringExpenseRequest $request + * @return Response + * + * + * + * @OA\Get( + * path="/api/v1/recurring_expenses/create", + * operationId="getRecurringExpensesCreate", + * tags={"recurring_expenses"}, + * summary="Gets a new blank client object", + * description="Returns a blank object with default values", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A blank client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function create(CreateRecurringExpenseRequest $request) + { + $recurring_expense = RecurringExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($recurring_expense); + } + + /** + * Store a newly created resource in storage. + * + * @param StoreRecurringExpenseRequest $request + * @return Response + * + * + * + * @OA\Post( + * path="/api/v1/recurring_expenses", + * operationId="storeRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Adds a client", + * description="Adds an client to a company", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="Returns the saved client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function store(StoreRecurringExpenseRequest $request) + { + $recurring_expense = $this->recurring_expense_repo->save($request->all(), RecurringExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + event(new RecurringExpenseWasCreated($recurring_expense, $recurring_expense->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + + return $this->itemResponse($recurring_expense); + } + + /** + * Remove the specified resource from storage. + * + * @param DestroyRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * @throws \Exception + * @OA\Delete( + * path="/api/v1/recurring_expenses/{id}", + * operationId="deleteRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Deletes a client", + * description="Handles the deletion of a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns a HTTP status", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function destroy(DestroyRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + $this->recurring_expense_repo->delete($recurring_expense); + + return $this->itemResponse($recurring_expense->fresh()); + } + + /** + * Perform bulk actions on the list view. + * + * @return Response + * + * + * @OA\Post( + * path="/api/v1/recurring_expenses/bulk", + * operationId="bulkRecurringExpenses", + * tags={"recurring_expenses"}, + * summary="Performs bulk actions on an array of recurring_expenses", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="User credentials", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The RecurringExpense User response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + $action = request()->input('action'); + + $ids = request()->input('ids'); + $recurring_expenses = RecurringExpense::withTrashed()->find($this->transformKeys($ids)); + + $recurring_expenses->each(function ($recurring_expense, $key) use ($action) { + if (auth()->user()->can('edit', $recurring_expense)) { + $this->recurring_expense_repo->{$action}($recurring_expense); + } + }); + + return $this->listResponse(RecurringExpense::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + /** + * Returns a client statement. + * + * @return void [type] [description] + */ + public function statement() + { + //todo + } + + /** + * Update the specified resource in storage. + * + * @param UploadRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/recurring_expenses/{id}/upload", + * operationId="uploadRecurringExpense", + * tags={"recurring_expense"}, + * summary="Uploads a document to a recurring_expense", + * description="Handles the uploading of a document to a recurring_expense", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the RecurringExpense object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/RecurringExpense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function upload(UploadRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + + if(!$this->checkFeature(Account::FEATURE_DOCUMENTS)) + return $this->featureFailure(); + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $recurring_expense); + + return $this->itemResponse($recurring_expense->fresh()); + + } +} diff --git a/app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php new file mode 100644 index 000000000000..75f89a155b70 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php @@ -0,0 +1,57 @@ +has('action')) { + return false; + } + + if (! in_array($this->action, $this->getBulkOptions(), true)) { + return false; + } + + return auth()->user()->can(auth()->user()->isAdmin(), RecurringExpense::class); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = $this->getGlobalRules(); + + /* We don't require IDs on bulk storing. */ + if ($this->action !== self::$STORE_METHOD) { + $rules['ids'] = ['required']; + } + + return $rules; + } +} diff --git a/app/Http/Requests/RecurringExpense/CreateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/CreateRecurringExpenseRequest.php new file mode 100644 index 000000000000..2f720a322d0d --- /dev/null +++ b/app/Http/Requests/RecurringExpense/CreateRecurringExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('create', RecurringExpense::class); + } +} diff --git a/app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php new file mode 100644 index 000000000000..ed7e7fa795bb --- /dev/null +++ b/app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php @@ -0,0 +1,27 @@ +user()->can('edit', $this->recurring_expense); + } +} diff --git a/app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php new file mode 100644 index 000000000000..e24e11b19c58 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('edit', $this->recurring_expense); + } + +} diff --git a/app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php new file mode 100644 index 000000000000..500c4ae13f9f --- /dev/null +++ b/app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php @@ -0,0 +1,27 @@ +user()->can('view', $this->recurring_expense); + } +} diff --git a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php new file mode 100644 index 000000000000..706962e7aa77 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php @@ -0,0 +1,73 @@ +user()->can('create', RecurringExpense::class); + } + + public function rules() + { + $rules = []; + + if ($this->number) + $rules['number'] = Rule::unique('recurring_expenses')->where('company_id', auth()->user()->company()->id); + + if(!empty($this->client_id)) + $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; + + return $this->globalRules($rules); + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $input = $this->decodePrimaryKeys($input); + + if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { + $input['category_id'] = $this->decodePrimaryKey($input['category_id']); + } + + if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { + $input['currency_id'] = (string)auth()->user()->company()->settings->currency_id; + } + + if(array_key_exists('color', $input) && is_null($input['color'])) + $input['color'] = ''; + + $this->replace($input); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + ]; + } +} diff --git a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php new file mode 100644 index 000000000000..c1be81d1053a --- /dev/null +++ b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php @@ -0,0 +1,79 @@ +user()->can('edit', $this->recurring_expense); + } + + public function rules() + { + /* Ensure we have a client name, and that all emails are unique*/ + + $rules['country_id'] = 'integer|nullable'; + + $rules['contacts.*.email'] = 'nullable|distinct'; + + if (isset($this->number)) { + $rules['number'] = Rule::unique('recurring_expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_expense->id); + } + + return $this->globalRules($rules); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + 'email' => ctrans('validation.email', ['attribute' => 'email']), + 'name.required' => ctrans('validation.required', ['attribute' => 'name']), + 'required' => ctrans('validation.required', ['attribute' => 'email']), + ]; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $input = $this->decodePrimaryKeys($input); + + if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { + $input['category_id'] = $this->decodePrimaryKey($input['category_id']); + } + + if (array_key_exists('documents', $input)) { + unset($input['documents']); + } + + if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { + $input['currency_id'] = (string)auth()->user()->company()->settings->currency_id; + } + + $this->replace($input); + } +} diff --git a/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php new file mode 100644 index 000000000000..e5fa858da8f7 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->recurring_expense); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Models/RecurringExpense.php b/app/Models/RecurringExpense.php new file mode 100644 index 000000000000..b1bdce29b7c8 --- /dev/null +++ b/app/Models/RecurringExpense.php @@ -0,0 +1,104 @@ + 'boolean', + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + ]; + + protected $touches = []; + + public function getEntityType() + { + return self::class; + } + + public function documents() + { + return $this->morphMany(Document::class, 'documentable'); + } + + public function user() + { + return $this->belongsTo(User::class)->withTrashed(); + } + + public function assigned_user() + { + return $this->belongsTo(User::class, 'assigned_user_id', 'id'); + } + + public function company() + { + return $this->belongsTo(Company::class); + } + + public function vendor() + { + return $this->belongsTo(Vendor::class); + } + + public function client() + { + return $this->belongsTo(Client::class); + } +} diff --git a/app/Policies/RecurringExpensePolicy.php b/app/Policies/RecurringExpensePolicy.php new file mode 100644 index 000000000000..032bf869fa19 --- /dev/null +++ b/app/Policies/RecurringExpensePolicy.php @@ -0,0 +1,31 @@ +isAdmin() || $user->hasPermission('create_recurring_expense') || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index a73ed4842e07..b2a4560ba93b 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -12,7 +12,6 @@ namespace App\Providers; use App\Models\Activity; -use App\Models\Subscription; use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; @@ -29,8 +28,10 @@ use App\Models\PaymentTerm; use App\Models\Product; use App\Models\Project; use App\Models\Quote; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Subscription; use App\Models\Task; use App\Models\TaskStatus; use App\Models\TaxRate; @@ -38,7 +39,6 @@ use App\Models\User; use App\Models\Vendor; use App\Models\Webhook; use App\Policies\ActivityPolicy; -use App\Policies\SubscriptionPolicy; use App\Policies\ClientPolicy; use App\Policies\CompanyGatewayPolicy; use App\Policies\CompanyPolicy; @@ -55,8 +55,10 @@ use App\Policies\PaymentTermPolicy; use App\Policies\ProductPolicy; use App\Policies\ProjectPolicy; use App\Policies\QuotePolicy; +use App\Policies\RecurringExpensePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; +use App\Policies\SubscriptionPolicy; use App\Policies\TaskPolicy; use App\Policies\TaskStatusPolicy; use App\Policies\TaxRatePolicy; @@ -92,6 +94,7 @@ class AuthServiceProvider extends ServiceProvider Product::class => ProductPolicy::class, Project::class => ProjectPolicy::class, Quote::class => QuotePolicy::class, + RecurringExpense::class => RecurringExpensePolicy::class, RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class, Webhook::class => WebhookPolicy::class, diff --git a/app/Repositories/RecurringExpenseRepository.php b/app/Repositories/RecurringExpenseRepository.php new file mode 100644 index 000000000000..ead280cab40b --- /dev/null +++ b/app/Repositories/RecurringExpenseRepository.php @@ -0,0 +1,60 @@ +fill($data); + $recurring_expense->number = empty($recurring_expense->number) ? $this->getNextRecurringExpenseNumber($recurring_expense) : $recurring_expense->number; + $recurring_expense->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $recurring_expense); + } + + return $recurring_expense; + } + + /** + * Store recurring_expenses in bulk. + * + * @param array $recurring_expense + * @return \App\Models\RecurringExpense|null + */ + public function create($recurring_expense): ?RecurringExpense + { + return $this->save( + $recurring_expense, + RecurringExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id) + ); + } +} diff --git a/app/Transformers/RecurringExpenseTransformer.php b/app/Transformers/RecurringExpenseTransformer.php new file mode 100644 index 000000000000..f2da8f6a0831 --- /dev/null +++ b/app/Transformers/RecurringExpenseTransformer.php @@ -0,0 +1,101 @@ +serializer); + + return $this->includeCollection($recurring_expense->documents, $transformer, Document::class); + } + + /** + * @param RecurringExpense $recurring_expense + * + * @return array + */ + public function transform(RecurringExpense $recurring_expense) + { + return [ + 'id' => $this->encodePrimaryKey($recurring_expense->id), + 'user_id' => $this->encodePrimaryKey($recurring_expense->user_id), + 'assigned_user_id' => $this->encodePrimaryKey($recurring_expense->assigned_user_id), + 'vendor_id' => $this->encodePrimaryKey($recurring_expense->vendor_id), + 'invoice_id' => $this->encodePrimaryKey($recurring_expense->invoice_id), + 'client_id' => $this->encodePrimaryKey($recurring_expense->client_id), + 'bank_id' => (string) $recurring_expense->bank_id ?: '', + 'invoice_currency_id' => (string) $recurring_expense->invoice_currency_id ?: '', + 'recurring_expense_currency_id' => '', //todo remove redundant in 5.0.25 + 'currency_id' => (string) $recurring_expense->currency_id ?: '', + 'category_id' => $this->encodePrimaryKey($recurring_expense->category_id), + 'payment_type_id' => (string) $recurring_expense->payment_type_id ?: '', + 'recurring_recurring_expense_id' => (string) $recurring_expense->recurring_recurring_expense_id ?: '', + 'is_deleted' => (bool) $recurring_expense->is_deleted, + 'should_be_invoiced' => (bool) $recurring_expense->should_be_invoiced, + 'invoice_documents' => (bool) $recurring_expense->invoice_documents, + 'amount' => (float) $recurring_expense->amount ?: 0, + 'foreign_amount' => (float) $recurring_expense->foreign_amount ?: 0, + 'exchange_rate' => (float) $recurring_expense->exchange_rate ?: 0, + 'tax_name1' => $recurring_expense->tax_name1 ? $recurring_expense->tax_name1 : '', + 'tax_rate1' => (float) $recurring_expense->tax_rate1, + 'tax_name2' => $recurring_expense->tax_name2 ? $recurring_expense->tax_name2 : '', + 'tax_rate2' => (float) $recurring_expense->tax_rate2, + 'tax_name3' => $recurring_expense->tax_name3 ? $recurring_expense->tax_name3 : '', + 'tax_rate3' => (float) $recurring_expense->tax_rate3, + 'private_notes' => (string) $recurring_expense->private_notes ?: '', + 'public_notes' => (string) $recurring_expense->public_notes ?: '', + 'transaction_reference' => (string) $recurring_expense->transaction_reference ?: '', + 'transaction_id' => (string) $recurring_expense->transaction_id ?: '', + 'date' => $recurring_expense->date ?: '', + //'recurring_expense_date' => $recurring_expense->date ?: '', + 'number' => (string)$recurring_expense->number ?: '', + 'payment_date' => $recurring_expense->payment_date ?: '', + 'custom_value1' => $recurring_expense->custom_value1 ?: '', + 'custom_value2' => $recurring_expense->custom_value2 ?: '', + 'custom_value3' => $recurring_expense->custom_value3 ?: '', + 'custom_value4' => $recurring_expense->custom_value4 ?: '', + 'updated_at' => (int) $recurring_expense->updated_at, + 'archived_at' => (int) $recurring_expense->deleted_at, + 'created_at' => (int) $recurring_expense->created_at, + 'project_id' => $this->encodePrimaryKey($recurring_expense->project_id), + 'tax_amount1' => (float) $recurring_expense->tax_amount1, + 'tax_amount2' => (float) $recurring_expense->tax_amount2, + 'tax_amount3' => (float) $recurring_expense->tax_amount3, + 'uses_inclusive_taxes' => (bool) $recurring_expense->uses_inclusive_taxes, + 'calculate_tax_by_amount' => (bool) $recurring_expense->calculate_tax_by_amount, + ]; + } +} diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index e478b4cbedba..c92d7675f139 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -19,6 +19,7 @@ use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; use App\Models\Quote; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\Task; use App\Models\Timezone; @@ -312,6 +313,27 @@ trait GeneratesCounter return $expense_number; } + /** + * Gets the next expense number. + * + * @param RecurringExpense $expense The expense + * @return string The next expense number. + */ + public function getNextRecurringExpenseNumber(RecurringExpense $expense) :string + { + $this->resetCompanyCounters($expense->company); + + $counter = $expense->company->settings->recurring_expense_number_counter; + $setting_entity = $expense->company->settings->recurring_expense_number_counter; + + $expense_number = $this->checkEntityNumber(RecurringExpense::class, $expense, $counter, $expense->company->settings->counter_padding, $expense->company->settings->recurring_expense_number_pattern); + + $this->incrementCounter($expense->company, 'recurring_expense_number_counter'); + + return $expense_number; + } + + /** * Determines if it has shared counter. *