diff --git a/app/Exceptions/BranchNotRightException.php b/app/Exceptions/BranchNotRightException.php new file mode 100644 index 000000000..de206e6be --- /dev/null +++ b/app/Exceptions/BranchNotRightException.php @@ -0,0 +1,13 @@ +eloquentFilter($request); - + $purchaseReceives = PurchaseReceive::joins($purchaseReceives, $request->get('join')); $purchaseReceives = pagination($purchaseReceives, $request->get('limit')); @@ -99,8 +104,26 @@ public function store(StorePurchaseReceiveRequest $request) */ public function show(Request $request, $id) { + $purchaseReceive = PurchaseReceive::eloquentFilter($request)->findOrFail($id); + //Check branches + $branches = tenant(auth()->user()->id)->branches; + $userBranch = null; + foreach ($branches as $branch) { + if ($branch->pivot->is_default) { + $userBranch = $branch->id; + break; + } + } + + if($userBranch == null) { + throw new BranchNullException(); + } + else if ($purchaseReceive->form->branch_id != $userBranch) { + throw new BranchNotRightException(); + } + return new ApiResource($purchaseReceive); } @@ -114,6 +137,21 @@ public function show(Request $request, $id) public function edit(Request $request, $id) { $purchaseReceive = PurchaseReceive::eloquentFilter($request)->findOrFail($id)->load('items'); + $branches = tenant(auth()->user()->id)->branches; + $userBranch = null; + foreach ($branches as $branch) { + if ($branch->pivot->is_default) { + $userBranch = $branch->id; + break; + } + } + + if($userBranch == null) { + throw new BranchNullException(); + } + else if ($purchaseReceive->form->branch_id != $userBranch) { + throw new BranchNotRightException(); + } $orderItems = optional($purchaseReceive->purchaseOrder)->items; @@ -147,11 +185,27 @@ public function edit(Request $request, $id) * @return ApiResource * @throws Throwable */ - public function update(Request $request, $id) + public function update(UpdatePurchaseReceiveRequest $request, $id) { $purchaseReceive = PurchaseReceive::findOrFail($id); $purchaseReceive->isAllowedToUpdate(); + $branches = tenant(auth()->user()->id)->branches; + $userBranch = null; + foreach ($branches as $branch) { + if ($branch->pivot->is_default) { + $userBranch = $branch->id; + break; + } + } + + if($userBranch == null) { + throw new BranchNullException(); + } + else if ($purchaseReceive->form->branch_id != $userBranch) { + throw new BranchNotRightException(); + } + $result = DB::connection('tenant')->transaction(function () use ($request, $purchaseReceive) { $purchaseReceive->form->archive(); @@ -186,6 +240,20 @@ public function destroy(Request $request, $id) $purchaseReceive = PurchaseReceive::findOrFail($id); $purchaseReceive->isAllowedToDelete(); + $branches = tenant(auth()->user()->id)->branches; + $userBranch = null; + foreach ($branches as $branch) { + if ($branch->pivot->is_default) { + $userBranch = $branch->id; + break; + } + } + if($userBranch == null) { + throw new BranchNullException(); + } + else if ($purchaseReceive->form->branch_id != $userBranch) { + throw new BranchNotRightException(); + } $purchaseReceive->requestCancel($request); DB::connection('tenant')->commit(); diff --git a/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/StorePurchaseReceiveRequest.php b/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/StorePurchaseReceiveRequest.php index db4b307d8..4472166ff 100644 --- a/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/StorePurchaseReceiveRequest.php +++ b/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/StorePurchaseReceiveRequest.php @@ -30,7 +30,7 @@ public function rules() 'supplier_id' => ValidationRule::foreignKey('suppliers'), 'supplier_name' => 'required|string', 'warehouse_id' => ValidationRule::foreignKey('warehouses'), - 'purchase_order_id' => ValidationRule::foreignKeyNullable('purchase_orders'), + 'purchase_order_id' => ValidationRule::foreignKey('purchase_orders'), 'items' => 'required_without:services|array', 'services' => 'required_without:items|array', ]; @@ -55,4 +55,25 @@ public function rules() return array_merge($ruleForm, $rulePurchaseReceive, $rulePurchaseReceiveItems, $rulePurchaseReceiveServices); } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + $sum = 0; + $items = $this->get('items'); + foreach($items as $item) { + if (isset($item['dna'])) { + foreach ($item['dna'] as $dna) { + $sum += $dna['quantity']; + } + } else { + $sum += $item['quantity']; + } + } + + if ($sum == 0) { + $validator->errors()->add("total_quantity", 'quantity must be filled in'); + } + }); + } } diff --git a/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/UpdatePurchaseReceiveRequest.php b/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/UpdatePurchaseReceiveRequest.php index 9cbdda925..99ed3e82c 100644 --- a/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/UpdatePurchaseReceiveRequest.php +++ b/app/Http/Requests/Purchase/PurchaseReceive/PurchaseReceive/UpdatePurchaseReceiveRequest.php @@ -30,7 +30,7 @@ public function rules() 'supplier_id' => ValidationRule::foreignKey('suppliers'), 'supplier_name' => 'required|string', 'warehouse_id' => ValidationRule::foreignKey('warehouses'), - 'purchase_order_id' => ValidationRule::foreignKeyNullable('purchase_orders'), + 'purchase_order_id' => ValidationRule::foreignKey('purchase_orders'), 'items' => 'required_without:services|array', 'services' => 'required_without:items|array', @@ -56,4 +56,25 @@ public function rules() return array_merge($ruleForm, $rulePurchaseReceive, $rulePurchaseReceiveItems, $rulePurchaseReceiveServices); } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + $sum = 0; + $items = $this->get('items'); + foreach($items as $item) { + if ($item['dna']) { + foreach ($item['dna'] as $dna) { + $sum += $dna['quantity']; + } + } else { + $sum += $item['quantity']; + } + } + + if ($sum == 0) { + $validator->errors()->add("total_quantity", 'quantity must be filled in'); + } + }); + } } diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index edc036dd0..ac52c03f9 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -68,7 +68,7 @@ 'numeric' => 'The :attribute must be a number.', 'present' => 'The :attribute field must be present.', 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', + 'required' => ':attribute field can\'t be null.', 'required_if' => 'The :attribute field is required when :other is :value.', 'required_unless' => 'The :attribute field is required unless :other is in :values.', 'required_with' => 'The :attribute field is required when :values is present.', @@ -100,8 +100,11 @@ */ 'custom' => [ - 'attribute-name' => [ - 'rule-name' => 'custom-message', + 'purchase_order_id' => [ + 'required' => "Purchase Order can't be null", + ], + 'warehouse_id' => [ + 'required' => "Warehouse can't be null", ], ], diff --git a/tests/Feature/Http/Purchase/PurchaseReceive/PurchaseReceiveSetup.php b/tests/Feature/Http/Purchase/PurchaseReceive/PurchaseReceiveSetup.php new file mode 100644 index 000000000..719fcd03e --- /dev/null +++ b/tests/Feature/Http/Purchase/PurchaseReceive/PurchaseReceiveSetup.php @@ -0,0 +1,309 @@ +signIn(); + $this->setProject(); + + $this->tenantUser = TenantUser::find($this->user->id); + $this->branchDefault = $this->tenantUser->branches() + ->where('is_default', true) + ->first(); + + $this->setUserWarehouse($this->branchDefault); + $this->createCustomerUnitItem(); + $this->setApprover(); + } + + public function tearDown(): void + { + parent::tearDown(); + } + + protected function unsetUserRole() + { + $role = Role::createIfNotExists('super admin'); + + ModelHasRole::where('role_id', $role->id) + ->where('model_type', 'App\Model\Master\User') + ->where('model_id', $this->user->id) + ->delete(); + } + + protected function unsetDefaultBranch() + { + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->save(); + + $this->tenantUser->branches()->detach($this->branchDefault->pivot->branch_id); + } + + private function setUserWarehouse($branch = null) + { + $warehouse = $this->createWarehouse($branch); + $this->tenantUser->warehouses()->syncWithoutDetaching($warehouse->id); + foreach ($this->tenantUser->warehouses as $warehouse) { + $warehouse->pivot->is_default = true; + $warehouse->pivot->save(); + + $this->warehouseSelected = $warehouse; + } + } + + private function createWarehouse($branch = null) + { + $warehouse = new Warehouse(); + $warehouse->name = 'Test warehouse'; + if ($branch) { + $warehouse->branch_id = $branch->id; + } + $warehouse->save(); + + return $warehouse; + } + + private function createCustomerUnitItem() + { + $this->customer = factory(Customer::class)->create(); + $this->supplier = factory(Supplier::class)->create(); + $this->unit = factory(ItemUnit::class)->make(); + $this->item = factory(Item::class)->create(); + $this->item->units()->save($this->unit); + + $this->generateChartOfAccount(); + $this->item->chart_of_account_id = $this->coa->id; + $this->item->save(); + } + + private function setApprover() + { + $role = Role::createIfNotExists('super admin'); + $this->approver = factory(TenantUser::class)->create(); + $this->approver->assignRole($role); + } + + private function setStock($quantity = 100) + { + $form = new Form(); + $form->date = date('Y-m-d H:i:s', time() - 3600); + $form->created_by = $this->user->id; + $form->updated_by = $this->user->id; + $form->save(); + + InventoryHelper::increase($form, $this->warehouseSelected, $this->item, $quantity, $this->unit, 1); + } + + private function generateChartOfAccount() + { + $coa = ChartOfAccount::where('name', 'COST OF SALES')->first(); + if ($coa) { + $this->coa = $coa; + } else { + $type = new ChartOfAccountType; + $type->name = 'COST OF SALES'; + $type->alias = 'BEBAN POKOK PENJUALAN'; + $type->is_debit = 1; + $type->save(); + + $this->coa = new ChartOfAccount; + $this->coa->type_id = $type->id; + $this->coa->position = 'DEBIT'; + $this->coa->is_locked = 1; + $this->coa->number = 41200; + $this->coa->name = 'COST OF SALES'; + $this->coa->alias = 'BEBAN POKOK PENJUALAN'; + $this->coa->created_by = $this->user->id; + $this->coa->updated_by = $this->user->id; + $this->coa->save(); + + $setting = new SettingJournal; + $setting->feature = 'sales'; + $setting->name = 'cost of sales'; + $setting->chart_of_account_id = $this->coa->id; + $setting->save(); + } + } + + private function getDummyData($purchaseReceive = null) + { + $purchase = $purchaseReceive->purchaseOrder ?? $this->createPurchaseOrder(); + $purchaseItem = $purchase->items()->first(); + + $quantityDelivered = $purchase->quantity_delivered; + $quantityRemaining = $purchase->quantity_delivered; + + return [ + 'supplier_id' => $this->supplier->id, + 'supplier_name' => $this->supplier->name, + 'supplier_address' => $this->supplier->address, + 'supplier_phone' => $this->supplier->phone, + 'warehouse_id' => $this->warehouseSelected->id, + 'warehouse_name' => $this->warehouseSelected->name, + 'purchase_order_id' => $purchase->id, + 'driver' => '', + 'license_plate' => '', + 'date' => now(), + 'increment_group' => 1, + 'items' => [ + [ + 'purchase_order_item_id' => $purchaseItem->id, + 'item_id' => $this->item->id, + 'item_name' => $this->item->name, + 'more' => false, + 'unit' => $this->unit->label, + 'converter' => $purchaseItem->converter, + 'quantity' => 10, + 'quantity_remaining' => $quantityRemaining, + 'require_expiry_date' => 0, + 'require_production_number' => 0, + 'is_quantity_over' => false, + 'price' => $purchaseItem->price, + 'discount_percent' => 0, + 'discount_value' => 0, + 'total' => $purchase->amount, + 'allocation_id' => null, + 'notes' => null, + 'warehouse_id' => $this->warehouseSelected->id, + 'warehouse_name' => $this->warehouseSelected->name, + 'dna' => [ + [ + 'quantity' => 10, + 'production_number' => 'prod1', + 'expiry_date' => '2022-05-13 10:38:42', + 'stock' => $quantityRemaining, + 'balance' => $quantityRemaining - $quantityDelivered + ] + ], + ], + ], + ]; + } + + private function createPurchaseOrder() + { + $purchaseRequest = $this->createPurchaseRequest(); + $orderItem = $purchaseRequest->items()->first(); + + $params = [ + 'purchase_request_id' => $purchaseRequest->id, + 'purchase_contract_id' => null, + 'supplier_id' => $this->supplier->id, + 'supplier_name' => $this->supplier->name, + 'supplier_address' => $this->supplier->address, + 'supplier_phone' => $this->supplier->phone, + 'warehouse_id' => $this->warehouseSelected->id, + 'request_approval_to' => $this->approver->id, + 'approver_name' => $this->approver->getFullNameAttribute(), + 'approver_email' => $this->approver->email, + 'items' => [ + [ + 'purchase_request_item_id' => $orderItem->id, + 'item_id' => $this->item->id, + 'item_name' => $this->item->name, + 'quantity' => 10, + 'price' => 1100, + 'discount_percent' => 0, + 'discount_value' => 0, + 'taxable' => false, + 'unit' => $this->unit->label, + 'converter' => 1, + 'allocation_id' => null, + 'notes' => null + ], + ], + ]; + + $deliveryOrder = PurchaseOrder::create($params); + $deliveryOrder->form->approval_by = $this->approver->id; + $deliveryOrder->form->approval_at = now(); + $deliveryOrder->form->approval_status = 1; + $deliveryOrder->form->save(); + + return $deliveryOrder; + } + + private function createPurchaseRequest() + { + $params = [ + 'required_date' => date('Y-m-d H:i:s'), + 'supplier_id' => $this->supplier->id, + 'supplier_name' => $this->supplier->name, + 'supplier_address' => $this->supplier->address, + 'supplier_phone' => $this->supplier->phone, + 'items' => [ + [ + 'item_id' => $this->item->id, + 'item_name' => $this->item->name, + 'quantity' => '220', + 'unit' => $this->unit->label, + 'converter' => 1, + 'price' => 5000, + 'notes' => null, + 'allocation_id' => null, + 'units' => [ + [ + 'id' => $this->unit->id, + 'label' => $this->unit->label, + 'name' => $this->unit->name, + 'converter' => 1, + 'disabled' => 0, + 'item_id' => $this->item->id, + 'created_by' => 1, + 'updated_by' => 1, + 'created_at' => '2022-05-13 10:38:42', + 'updated_at' => '2022-05-13 10:38:42', + 'prices' => [], + ], + ], + ], + ], + 'amount' => 1210000 + ]; + + $purchaseRequest = PurchaseRequest::create($params); + $purchaseRequest->form->approval_by = $this->approver->id; + $purchaseRequest->form->approval_at = now(); + $purchaseRequest->form->approval_status = 1; + $purchaseRequest->form->save(); + + return $purchaseRequest; + } +} \ No newline at end of file diff --git a/tests/Feature/Http/Purchase/PurchaseReceive/PurchaseReceiveTest.php b/tests/Feature/Http/Purchase/PurchaseReceive/PurchaseReceiveTest.php new file mode 100644 index 000000000..422e7ecd8 --- /dev/null +++ b/tests/Feature/Http/Purchase/PurchaseReceive/PurchaseReceiveTest.php @@ -0,0 +1,246 @@ +setStock(300); + $this->setRole(); + + $data = $this->getDummyData(); + + $this->unsetDefaultBranch(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to create this form', + ]); + } + + + /** @test */ + public function createPurchaseReceiveFailed() + { + $this->setRole(); + + $dummy = $this->getDummyData(); + $data = ['warehouse_id' => $dummy['warehouse_id'], 'items' => $dummy['items']]; + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + ]); + } + + + /** @test */ + public function createPurchaseReceiveQuantityZero() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data['items'][0] = data_set($data['items'][0], 'quantity', 0); + $data['items'][0]['dna'][0] = data_set($data['items'][0]['dna'][0], 'quantity', 0); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'total_quantity' => [ + 'quantity must be filled in', + ], + ], + ]); + } + + /** @test */ + public function createPurchaseReceive() + { + $this->setRole(); + + $data = $this->getDummyData(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + $response->assertStatus(201); + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'done' => 0, + ], 'tenant'); + + $this->assertDatabaseHas('purchase_receives', [ + "supplier_id" => $response->json('data.supplier_id'), + "supplier_name" => $response->json('data.supplier_name'), + "warehouse_id" => $response->json('data.warehouse_id'), + "warehouse_name" => $response->json('data.warehouse_name'), + "purchase_order_id" => $response->json('data.purchase_order_id'), + "id" => $response->json('data.id'), + ], 'tenant'); + + $item = $response->json('data.items')[0]; + $this->assertDatabaseHas('purchase_receive_items', [ + "id" => $item['id'], + "purchase_receive_id" => $item['purchase_receive_id'], + "purchase_order_item_id" => $item['purchase_order_item_id'], + "item_id" => $item['item_id'], + "item_name" => $item['item_name'], + "quantity" => $item['quantity'], + "price" => $item['price'], + "discount_value" => $item['discount_value'], + "taxable" => $item['taxable'], + "unit" => $item['unit'], + "converter" => $item['converter'], + ], 'tenant'); + } + + /** @test */ + public function updatePurchaseReceive() + { + $this->createPurchaseReceive(); + + $purchaseReceive = PurchaseReceive::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($purchaseReceive); + $data = data_set($data, 'id', $purchaseReceive->id, false); + $data['items'][0]['unit'] = 1; + + $response = $this->json('PATCH', self::$path.'/'.$purchaseReceive->id, $data, $this->headers); + + $response->assertStatus(201); + $this->assertDatabaseHas('forms', ['edited_number' => $response->json('data.form.number')], 'tenant'); + $this->assertDatabaseHas('user_activities', [ + 'number' => $response->json('data.form.number'), + 'table_id' => $response->json('data.id'), + 'table_type' => 'PurchaseReceive', + 'activity' => 'Update - 1', + ], 'tenant'); + } + + /** @test */ + public function deletePurchaseReceive() + { + $this->createPurchaseReceive(); + + $purchaseReceive = PurchaseReceive::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $response = $this->json('DELETE', self::$path.'/'.$purchaseReceive->id, $data, $this->headers); + + $response->assertStatus(204); + $this->assertDatabaseHas('forms', [ + 'number' => $purchaseReceive->form->number, + 'request_cancellation_reason' => $data['reason'], + 'cancellation_status' => 0, + ], 'tenant'); + } + + /** @test */ + public function approvalDeletionPurchaseReceiveFormStatusPending() + { + $this->deletePurchaseReceive(); + + $purchaseReceive = PurchaseReceive::orderBy('id', 'asc')->first(); + + $response = $this->json('POST', self::$path.'/'.$purchaseReceive->id.'/cancellation-approve', $this->headers); + + $response->assertStatus(200); + $this->assertDatabaseHas('forms', [ + 'number' => $purchaseReceive->form->number, + 'cancellation_status' => 1, + ], 'tenant'); + } + + public function createPurchaseInvoice() + { + $this->createCustomerUnitItem(); + $this->createPurchaseReceive(); + $purchaseReceive = PurchaseReceive::orderBy('id', 'asc')->first(); + $orderItem = $purchaseReceive->items()->first(); + + $invoiceData = [ + 'due_date' => now(), + 'date' => now(), + 'increment_group' => 1, + 'delivery_fee' => 0, + 'discount_percent' => 0, + 'discount_value' => 0, + 'type_of_tax' => 'exclude', + 'tax' => 0, + 'invoice_number', + 'supplier_id' => $this->supplier->id, + 'supplier_name' => $this->supplier->name, + 'supplier_address' => $this->supplier->address, + 'supplier_phone' => $this->supplier->phone, + 'items' => [ + [ + 'purchase_receive_id' => $purchaseReceive->id, + 'purchase_receive_item_id' => $orderItem->id, + 'item_id' => Item::first()->id, + 'item_name' => 'test', + 'quantity' => $orderItem->quantity, + 'unit' => $orderItem->unit, + 'price' => $orderItem->price, + 'converter' => $orderItem->converter + ], + ], + ]; + + $response = $this->json('POST', '/api/v1/purchase/invoices', $invoiceData, $this->headers); + + $response->assertStatus(201); + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'done' => 0, + ], 'tenant'); + } + + /** @test */ + public function markDonePurchaseReceive() + { + $this->createPurchaseInvoice(); + $purchaseReceive = PurchaseReceive::orderBy('id', 'asc')->first(); + $this->assertDatabaseHas('forms', [ + 'id' => $purchaseReceive->form->id, + 'number' => $purchaseReceive->form->number, + 'done' => 1, + ], 'tenant'); + } + + /** @test */ + public function deletePurchaseReceiveStatusDone() + { + $this->createPurchaseInvoice(); + $purchaseReceive = PurchaseReceive::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $response = $this->json('DELETE', self::$path.'/'.$purchaseReceive->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'Cannot edit form because referenced by purchase receive', + ]); + } +}