diff --git a/app/Exceptions/InputBackDateForbiddenException.php b/app/Exceptions/InputBackDateForbiddenException.php index d2a4092a1..33f825a57 100644 --- a/app/Exceptions/InputBackDateForbiddenException.php +++ b/app/Exceptions/InputBackDateForbiddenException.php @@ -2,14 +2,16 @@ namespace App\Exceptions; +use App\Model\Inventory\InventoryAudit\InventoryAudit; use Exception; class InputBackDateForbiddenException extends Exception { public function __construct($audit, $item) { + $form = $audit->form ?? InventoryAudit::find($audit->id)->form; parent::__construct('Input error because'. - $item->label.' already audited in '.$audit->form->number.' on '. - date('d F Y H:i', strtotime($audit->form->date)), 422); + $item->label.' already audited in '.$form->number.' on '. + date('d F Y H:i', strtotime($form->date)), 422); } } diff --git a/app/Exports/Inventory/InventoryUsageExport.php b/app/Exports/Inventory/InventoryUsageExport.php new file mode 100755 index 000000000..23d8bee6f --- /dev/null +++ b/app/Exports/Inventory/InventoryUsageExport.php @@ -0,0 +1,177 @@ +tenantName = $tenantName; + $this->filters = $filters; + } + + /** + * @return \Illuminate\Database\Eloquent\Builder + */ + public function query() + { + $inventoryUsages = InventoryUsage::from(InventoryUsage::getTableName().' as '.InventoryUsage::$alias) + ->eloquentFilter($this->filters); + + $inventoryUsages = InventoryUsage::joins($inventoryUsages, $this->filters->get('join')); + + return $inventoryUsages; + } + + public function columnFormats(): array + { + return [ + 'I' => NumberFormat::FORMAT_NUMBER, + ]; + } + + /** + * @return array + */ + public function headings(): array + { + $dateExport = Carbon::now()->timezone(config()->get('project.timezone')); + $periodExport = $this->_getPeriodExport(); + + return [ + ['Date Export', ': ' . $dateExport->format('d M Y H:i')], + ['Period Export', ': ' . $periodExport], + [$this->tenantName], + ['Inventory Usage'], + [ + 'Date Form', + 'Form Number', + 'Warehouse', + 'Employee', + 'Account', + 'Item', + 'Production Number', + 'Expiry Date', + 'Quantity Usage', + 'Notes', + 'Allocation', + 'Notes', + 'Created By', + 'Approved By', + 'Approval Status', + 'Form Status', + 'Created At', + 'Updated At', + 'Deleted At' + ] + ]; + } + + /** + * @param mixed $row + * @return array + */ + public function map($row): array + { + $form = Form::where('formable_id', $row->inventory_usage_id) + ->with(['createdBy', 'approvalBy']) + ->where('formable_type', InventoryUsage::$morphName) + ->first(); + + $usageItem = InventoryUsageItem::where('id', $row->id) + ->with(['item', 'account', 'allocation']) + ->first(); + + return [ + date('d F Y', strtotime($form->date)), + $form->number, + $row->warehouse->name, + $row->employee->name, + $usageItem->account->name, + $usageItem->item->name, + $row->production_number, + $row->expiry_date, + round($row->quantity, 2) . ' ' . $row->unit, + $row->notes, + $usageItem->allocation->name, + $form->notes, + $form->createdBy->getFullNameAttribute(), + optional($form->approvalBy)->getFullNameAttribute(), + $form->approval_status, + $form->done, + date('d F Y', strtotime($form->created_at)), + date('d F Y', strtotime($form->updated_at)), + date('d F Y', strtotime($form->updated_at)), + ]; + } + + public function registerEvents(): array + { + return [ + AfterSheet::class => function(AfterSheet $event) { + $event->sheet->getDelegate()->getStyle('F6:F100') + ->getAlignment() + ->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT); + $event->sheet->getColumnDimension('B') + ->setAutoSize(false) + ->setWidth(18); + $tenanNameColl = 'A3:M3'; // All headers + $event->sheet->mergeCells($tenanNameColl); + $event->sheet->getDelegate()->getStyle($tenanNameColl)->getFont()->setBold(true); + $event->sheet->getDelegate()->getStyle($tenanNameColl) + ->getAlignment() + ->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER); + $titleColl = 'A4:M4'; // All headers + $event->sheet->mergeCells($titleColl); + $event->sheet->getDelegate()->getStyle($titleColl)->getFont()->setBold(true); + $event->sheet->getDelegate()->getStyle($titleColl) + ->getAlignment() + ->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER); + }, + + ]; + } + + private function _getPeriodExport() + { + $dateMin = $this->filters->filter_date_min; + $dateMin = !is_null($dateMin) && !is_array($dateMin) + ? json_decode($dateMin, true) + : []; + + $dateMax = $this->filters->filter_date_max; + $dateMax = !is_null($dateMax) && !is_array($dateMax) + ? json_decode($dateMax, true) + : []; + + $periodExport = ''; + if (isset($dateMin['form.date'])) { + $periodExport .= Carbon::createFromFormat('Y-m-d H:i:s', $dateMin['form.date'])->format('d M Y'); + } + if (isset($dateMax['form.date'])) { + if($periodExport !== '') $periodExport .= ' - '; + $periodExport .= Carbon::createFromFormat('Y-m-d H:i:s', $dateMax['form.date'])->format('d M Y'); + } + + return $periodExport; + } +} diff --git a/app/Helpers/Inventory/InventoryHelper.php b/app/Helpers/Inventory/InventoryHelper.php index 204122938..a3f771c20 100644 --- a/app/Helpers/Inventory/InventoryHelper.php +++ b/app/Helpers/Inventory/InventoryHelper.php @@ -177,7 +177,7 @@ public static function auditExists($item, $date, $warehouse, $options) $inventoryExist = InventoryAuditItem::join(InventoryAudit::getTableName(), InventoryAudit::getTableName('id'), '=', InventoryAuditItem::getTableName('inventory_audit_id')) ->join(Form::getTableName(), function ($query) { $query->on(Form::getTableName('formable_id'), '=', InventoryAudit::getTableName('id')) - ->where(Form::getTableName('formable_type'), '=', InventoryAudit::class); + ->where(Form::getTableName('formable_type'), '=', InventoryAudit::$morphName); }) ->where(Form::getTableName('date'), '>=', $date) // check if form is not canceled diff --git a/app/Http/Controllers/Api/Inventory/InventoryUsage/InventoryUsageController.php b/app/Http/Controllers/Api/Inventory/InventoryUsage/InventoryUsageController.php index 6d2881c51..433fcc9a2 100644 --- a/app/Http/Controllers/Api/Inventory/InventoryUsage/InventoryUsageController.php +++ b/app/Http/Controllers/Api/Inventory/InventoryUsage/InventoryUsageController.php @@ -2,14 +2,20 @@ namespace App\Http\Controllers\Api\Inventory\InventoryUsage; +use App\Exports\Inventory\InventoryUsageExport; use App\Http\Controllers\Controller; use App\Http\Requests\Inventory\Usage\StoreRequest; use App\Http\Requests\Inventory\Usage\UpdateRequest; use App\Http\Resources\ApiCollection; use App\Http\Resources\ApiResource; +use App\Model\CloudStorage; use App\Model\Inventory\InventoryUsage\InventoryUsage; +use App\Model\Project\Project; +use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; +use Maatwebsite\Excel\Facades\Excel; class InventoryUsageController extends Controller { @@ -126,4 +132,36 @@ public function destroy(Request $request, $id) return response()->json([], 204); } + + public function export(Request $request) + { + try { + $tenant = strtolower($request->header('Tenant')); + $key = Str::random(16); + $fileName = strtoupper($tenant).' - Inventory Usage'; + $fileExt = 'xlsx'; + $path = 'tmp/'.$tenant.'/'.$key.'.'.$fileExt; + + Excel::store(new InventoryUsageExport($tenant, $request), $path, env('STORAGE_DISK')); + + $cloudStorage = new CloudStorage(); + $cloudStorage->file_name = $fileName; + $cloudStorage->file_ext = $fileExt; + $cloudStorage->feature = 'Inventory Usage Export'; + $cloudStorage->key = $key; + $cloudStorage->path = $path; + $cloudStorage->disk = env('STORAGE_DISK'); + $cloudStorage->project_id = Project::where('code', strtolower($tenant))->first()->id; + $cloudStorage->owner_id = auth()->user()->id; + $cloudStorage->expired_at = Carbon::now()->addDay(1); + $cloudStorage->download_url = env('API_URL').'/download?key='.$key; + $cloudStorage->save(); + + return response()->json([ + 'data' => [ 'url' => $cloudStorage->download_url ], + ], 200); + } catch (\Throwable $th) { + return response_error($th); + } + } } diff --git a/app/Http/Requests/Inventory/Usage/UpdateRequest.php b/app/Http/Requests/Inventory/Usage/UpdateRequest.php index 87d19049e..0c46f93f4 100644 --- a/app/Http/Requests/Inventory/Usage/UpdateRequest.php +++ b/app/Http/Requests/Inventory/Usage/UpdateRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Inventory\Usage; use App\Http\Requests\ValidationRule; +use App\Model\Inventory\InventoryUsage\InventoryUsage; use Illuminate\Foundation\Http\FormRequest; class UpdateRequest extends FormRequest @@ -24,15 +25,24 @@ public function authorize() */ public function rules() { - return [ - 'warehouse_id' => 'required', - 'request_approval_to' => 'required', + $inventoryUsage = InventoryUsage::find($this->id); + + $rulesForm = ValidationRule::form(); + $rulesForm['date'] = 'required|date|after_or_equal:'.optional($inventoryUsage->form)->date; + $rulesUpdate = [ + 'warehouse_id' => ValidationRule::foreignKey('warehouses'), + 'employee_id' => ValidationRule::foreignKey('employees'), + 'request_approval_to' => 'required', + 'notes' => 'nullable|string|max:255', 'items.*.item_id' => ValidationRule::foreignKey('items'), 'items.*.quantity' => ValidationRule::quantity(), 'items.*.unit' => ValidationRule::unit(), 'items.*.converter' => ValidationRule::converter(), - 'items.*.chart_of_account_id' => 'required', + 'items.*.chart_of_account_id' => ValidationRule::foreignKey('chart_of_accounts'), + 'items.*.allocation_id' => ValidationRule::foreignKey('allocations'), ]; + + return array_merge($rulesForm, $rulesUpdate); } } diff --git a/app/Mail/CustomEmail.php b/app/Mail/CustomEmail.php index 70ec19e64..4f7b59533 100644 --- a/app/Mail/CustomEmail.php +++ b/app/Mail/CustomEmail.php @@ -38,7 +38,7 @@ public function build() $user = $this->user; $request = $this->request; - $this->from($user->email, $user->getFullNameAttribute()); + $this->replyTo($user->email, $user->getFullNameAttribute()); $this->to($request->to); if (optional($request)->cc) { @@ -47,9 +47,6 @@ public function build() if (optional($request)->bcc) { $this->bcc($request->bcc); } - if (optional($request)->reply_to) { - $this->replyTo($request->reply_to, $request->reply_to_name); - } $this->subject($request->subject); $this->view('emails.custom-email', ['body' => $request->body]); diff --git a/app/Model/Inventory/InventoryUsage/InventoryUsage.php b/app/Model/Inventory/InventoryUsage/InventoryUsage.php index a98fa811e..0e9ae1e0e 100644 --- a/app/Model/Inventory/InventoryUsage/InventoryUsage.php +++ b/app/Model/Inventory/InventoryUsage/InventoryUsage.php @@ -4,6 +4,8 @@ use Exception; use App\Exceptions\StockNotEnoughException; +use App\Exceptions\ExpiryDateNotFoundException; +use App\Exceptions\ProductionNumberNotFoundException; use App\Helpers\Inventory\InventoryHelper; use App\Mail\Inventory\InventoryUsageApprovalMail; use App\Model\TransactionModel; @@ -174,6 +176,14 @@ private static function mapItems($items, $inventoryUsage) if ($itemModel->require_production_number || $itemModel->require_expiry_date) { if ($item['dna']) { foreach ($item['dna'] as $dna) { + if ($itemModel->require_production_number && !isset($dna['production_number'])) { + throw new ProductionNumberNotFoundException($itemModel); + } + + if ($itemModel->require_expiry_date && !isset($dna['expiry_date'])) { + throw new ExpiryDateNotFoundException($itemModel); + } + if ($dna['quantity'] > 0) { $options = [ 'expiry_date' => $dna['expiry_date'], diff --git a/routes/api/inventory.php b/routes/api/inventory.php index 43fb283d7..1b07ee219 100644 --- a/routes/api/inventory.php +++ b/routes/api/inventory.php @@ -23,6 +23,7 @@ Route::get('usages/{id}/histories', 'InventoryUsageHistoryController@index'); + Route::get('usages/export', 'InventoryUsageController@export'); Route::apiResource('usages', 'InventoryUsageController'); }); }); diff --git a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageApprovalTest.php b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageApprovalTest.php index 781f57474..77ab05dbf 100644 --- a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageApprovalTest.php +++ b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageApprovalTest.php @@ -4,7 +4,13 @@ use Tests\TestCase; +use App\Model\SettingJournal; +use App\Model\Master\Item; +use App\Model\Master\ItemUnit; +use App\Model\Accounting\ChartOfAccount; +use App\Model\Inventory\InventoryAudit\InventoryAudit; use App\Model\Inventory\InventoryUsage\InventoryUsage; +use Carbon\Carbon; class InventoryUsageApprovalTest extends TestCase { @@ -14,7 +20,65 @@ class InventoryUsageApprovalTest extends TestCase private $previousInventoryUsageData; - /** @test */ + public function success_create_inventory_audit() + { + $chartOfAccount = ChartOfAccount::where('name', 'FACTORY DIFFERENCE STOCK EXPENSE')->first(); + $settingJournal = SettingJournal::where('feature', 'inventory audit')->where('name', 'difference stock expense')->first(); + $settingJournal->chart_of_account_id = $chartOfAccount->id; + $settingJournal->save(); + + $quantity = $this->initialItemQuantity; + + $usage = InventoryUsage::first(); + $usageItem = $usage->items()->first(); + + $warehouse = $usage->warehouse; + + $form = $usage->form; + $approver = $form->requestApprovalTo; + + $item = $usageItem->item; + $item->chart_of_account_id = $usageItem->chart_of_account_id; + $item->save(); + + $unit = ItemUnit::where('label', $usageItem->unit)->first(); + + $allocation = $usageItem->allocation; + $chartOfAccount = $usageItem->account; + + $data = [ + "increment_group" => date("Ym"), + "date" => Carbon::now()->addDays(1)->format("Y-m-d H:i:s"), + "warehouse_id" => $warehouse->id, + "request_approval_to" => $approver->id, + "approver_name" => $approver->name, + "approver_email" => $approver->email, + "notes" => null, + "items" => [ + [ + "item_id" => $item->id, + "item_name" => $item->name, + "item_label" => "[{$item->code}] - {$item->name}", + "chart_of_account_id" => $chartOfAccount->id, + "chart_of_account_name" => $chartOfAccount->alias, + "require_expiry_date" => 0, + "require_production_number" => 0, + "unit" => $unit->name, + "converter" => $unit->converter, + "quantity" => $quantity, + "allocation_id" => $allocation->id, + "allocation_name" => $allocation->name, + "notes" => null, + "more" => false + ] + ] + ]; + + $response = $this->json('POST', '/api/v1/inventory/audits', $data, $this->headers); + + $response->assertStatus(201); + } + public function success_create_inventory_usage($isFirstCreate = true) { $data = $this->getDummyData(); @@ -52,7 +116,31 @@ public function unauthorized_approve_inventory_usage() } /** @test */ - public function success_approve_inventory_usage() + public function has_audit_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $this->success_create_inventory_audit(); + + $audit = InventoryAudit::first(); + + $usage = InventoryUsage::orderBy('id', 'asc')->first(); + $usageItem = $usage->items()->first(); + + $approver = $usage->form->requestApprovalTo; + $this->changeActingAs($approver, $usage); + + $response = $this->json('POST', self::$path . '/' . $usage->id . '/approve', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => 'Input error because'. $usageItem->item->label.' already audited in '.$audit->form->number.' on '. date('d F Y H:i', strtotime($audit->form->date)) + ]); + } + + /** @test */ + public function invalid_journal_approve_inventory_usage() { $this->success_create_inventory_usage(); @@ -61,9 +149,85 @@ public function success_approve_inventory_usage() $approver = $inventoryUsage->form->requestApprovalTo; $this->changeActingAs($approver, $inventoryUsage); + SettingJournal::where('feature', 'inventory usage') + ->where('name', 'difference stock expense') + ->update([ + "chart_of_account_id" => null, + ]); + $response = $this->json('POST', self::$path . '/' . $inventoryUsage->id . '/approve', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Journal inventory usage account - difference stock expense not found" + ]); + } - $response->assertStatus(200); + /** @test */ + public function success_approve_inventory_usage() + { + $this->success_create_inventory_usage(); + + $usage = InventoryUsage::orderBy('id', 'asc')->first(); + $usageItem = $usage->items()->first(); + $usageItemAmount = $usageItem->item->cogs($usageItem->item_id) * $usageItem->quantity; + + $approver = $usage->form->requestApprovalTo; + $this->changeActingAs($approver, $usage); + + $response = $this->json('POST', self::$path . '/' . $usage->id . '/approve', [], $this->headers); + + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + "id", + "warehouse_id", + "employee_id", + "form" => [ + "id", + "branch_id", + "approval_status", + "date", + "done", + "notes", + "number", + "request_approval_to", + "updated_by", + ], + "items" => [ + [ + "expiry_date", + "id", + "chart_of_account_id", + "item_id", + "allocation_id", + "notes", + "production_number", + "quantity", + "unit", + ] + ] + ], + ]); + + // check balance and match amount + $this->assertDatabaseHas('journals', [ + 'form_id' => $usage->form->id, + 'journalable_type' => Item::$morphName, + 'journalable_id' => $usageItem->item_id, + 'chart_of_account_id' => $usageItem->chart_of_account_id, + 'debit' => $usageItemAmount, + ], 'tenant'); + $this->assertDatabaseHas('journals', [ + 'form_id' => $usage->form->id, + 'journalable_type' => Item::$morphName, + 'journalable_id' => $usageItem->item_id, + 'chart_of_account_id' => get_setting_journal('inventory usage', 'difference stock expense'), + 'credit' => $usageItemAmount, + ], 'tenant'); + + // change form status changed and logged $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -76,6 +240,23 @@ public function success_approve_inventory_usage() 'table_type' => 'InventoryUsage', 'activity' => 'Approved' ], 'tenant'); + + $responseRecap = $this->json('GET', '/api/v1/inventory/inventory-recapitulations', [ + 'includes' => 'account', + 'sort_by' => 'code;name', + 'limit' => 10, + 'page' => 1, + 'date_from' => date('Y-m-01') . ' 00:00:00', + 'filter_to' => date('Y-m-31') . ' 23:59:59', + 'filter_like' => '{}', + ], $this->headers); + $responseRecap->assertStatus(200) + ->assertJsonFragment([ + "name" => $usageItem->item->name, + "stock_in" => number_format((float) $this->initialItemQuantity, 30, '.', ''), + "stock_out" => number_format((float) $this->initialUsageItemQuantity * -1, 30, '.', ''), + "ending_balance" => number_format((float) $this->initialItemQuantity + ($this->initialUsageItemQuantity * -1), 30, '.', '') + ]); } /** @test */ @@ -95,36 +276,45 @@ public function not_found_form_approve_inventory_usage() } /** @test */ - public function unauthorized_reject_inventory_usage() + public function invalid_reject_inventory_usage() { $this->success_create_inventory_usage(); $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); - $data['reason'] = $this->faker->text(200); - - $response = $this->json('POST', self::$path . '/' . $inventoryUsage->id . '/reject', $data, $this->headers); + $approver = $inventoryUsage->form->requestApprovalTo; + $this->changeActingAs($approver, $inventoryUsage); + $response = $this->json('POST', self::$path . '/' . $inventoryUsage->id . '/reject', [], $this->headers); + $response->assertStatus(422) ->assertJson([ "code" => 422, - "message" => "Unauthorized" + "message" => "The given data was invalid.", + "errors" => [ + "reason" => [ + "The reason field is required." + ] + ] ]); } /** @test */ - public function invalid_reject_inventory_usage() + public function unauthorized_reject_inventory_usage() { $this->success_create_inventory_usage(); $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); - $approver = $inventoryUsage->form->requestApprovalTo; - $this->changeActingAs($approver, $inventoryUsage); + $data['reason'] = $this->faker->text(200); - $response = $this->json('POST', self::$path . '/' . $inventoryUsage->id . '/reject', [], $this->headers); + $response = $this->json('POST', self::$path . '/' . $inventoryUsage->id . '/reject', $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); } /** @test */ @@ -133,6 +323,8 @@ public function success_reject_inventory_usage() $this->success_create_inventory_usage(); $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + $inventoryUsageItem = $inventoryUsage->items()->first(); + $usageItemAmount = $inventoryUsageItem->item->cogs($inventoryUsageItem->item_id) * $inventoryUsageItem->quantity; $approver = $inventoryUsage->form->requestApprovalTo; $this->changeActingAs($approver, $inventoryUsage); @@ -141,7 +333,41 @@ public function success_reject_inventory_usage() $response = $this->json('POST', self::$path . '/' . $inventoryUsage->id . '/reject', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + "id", + "warehouse_id", + "employee_id", + "form" => [ + "id", + "branch_id", + "approval_status", + "date", + "done", + "notes", + "number", + "request_approval_to", + "updated_by", + ], + ], + ]); + + $this->assertDatabaseMissing('journals', [ + 'form_id' => $inventoryUsage->form->id, + 'journalable_type' => Item::$morphName, + 'journalable_id' => $inventoryUsageItem->item_id, + 'chart_of_account_id' => $inventoryUsageItem->chart_of_account_id, + 'debit' => $usageItemAmount, + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $inventoryUsage->form->id, + 'journalable_type' => Item::$morphName, + 'journalable_id' => $inventoryUsageItem->item_id, + 'chart_of_account_id' => get_setting_journal('inventory usage', 'difference stock expense'), + 'credit' => $usageItemAmount, + ], 'tenant'); + $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -154,6 +380,21 @@ public function success_reject_inventory_usage() 'table_type' => 'InventoryUsage', 'activity' => 'Rejected' ], 'tenant'); + + $responseRecap = $this->json('GET', '/api/v1/inventory/inventory-recapitulations', [ + 'includes' => 'account', + 'sort_by' => 'code;name', + 'limit' => 10, + 'page' => 1, + 'date_from' => date('Y-m-01') . ' 00:00:00', + 'filter_to' => date('Y-m-31') . ' 23:59:59', + 'filter_like' => '{}', + ], $this->headers); + $responseRecap->assertStatus(200) + ->assertJsonMissing([ + "stock_out" => number_format((float) $this->initialUsageItemQuantity * -1, 30, '.', ''), + "ending_balance" => number_format((float) $this->initialItemQuantity + ($this->initialUsageItemQuantity * -1), 30, '.', '') + ]); } /** @test */ diff --git a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageHistoryTest.php b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageHistoryTest.php index 8167feb66..d9f2045c8 100644 --- a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageHistoryTest.php +++ b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageHistoryTest.php @@ -12,7 +12,6 @@ class InventoryUsageHistoryTest extends TestCase public static $path = '/api/v1/inventory/usages'; - /** @test */ public function success_create_inventory_usage() { $this->setRole(); diff --git a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageSetup.php b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageSetup.php index a216db53b..2414d8618 100644 --- a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageSetup.php +++ b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageSetup.php @@ -16,6 +16,7 @@ use App\Model\SettingJournal; use App\Model\Accounting\ChartOfAccount; use App\Model\HumanResource\Employee\Employee; +use App\Model\Master\Branch; use App\User; trait InventoryUsageSetup { @@ -23,6 +24,9 @@ trait InventoryUsageSetup { private $branchDefault; private $warehouseSelected; + private $initialItemQuantity = 500; + private $initialUsageItemQuantity = 5; + public function setUp(): void { parent::setUp(); @@ -61,7 +65,16 @@ protected function unsetUserRole() ->delete(); } - private function createWarehouse($branch = null) + protected function createBranch() + { + $branch = new Branch(); + $branch->name = 'Test branch'; + $branch->save(); + + return $branch; + } + + protected function createWarehouse($branch = null) { $warehouse = new Warehouse(); $warehouse->name = 'Test warehouse'; @@ -104,19 +117,51 @@ private function createItemWithStocks($unit) $options['quantity_reference'] = $item->quantity; $options['unit_reference'] = $unit->label; $options['converter_reference'] = $unit->converter; + $options['expiry_date'] = null; + $options['production_number'] = null; - if ($item->require_expiry_date) { - $options['expiry_date'] = $item->expiry_date; - } - if ($item->require_production_number) { - $options['production_number'] = $item->production_number; - } - - InventoryHelper::increase($form, $this->warehouseSelected, $item, 500, $unit->label, 1, $options); + InventoryHelper::increase($form, $this->warehouseSelected, $item, $this->initialItemQuantity, $unit->label, 1, $options); return $item; } + private function createItemDnaWithStocks($unit) + { + $item = factory(Item::class)->create([ + "require_expiry_date" => true, + "require_production_number" => true + ]); + $item->units()->save($unit); + + $form = new Form; + $form->date = now()->toDateTimeString(); + $form->created_by = $this->tenantUser->id; + $form->updated_by = $this->tenantUser->id; + $form->save(); + + $options = []; + $options['quantity_reference'] = $item->quantity; + $options['unit_reference'] = $unit->label; + $options['converter_reference'] = $unit->converter; + $options['expiry_date'] = date('Y-m-31 23:59:59'); + $options['production_number'] = 'TEST001'; + + InventoryHelper::increase($form, $this->warehouseSelected, $item, $this->initialItemQuantity, $unit->label, 1, $options); + + return [$item, $options]; + } + + private function changeUserDefaultBranch($newBranch = null) + { + $this->tenantUser->branches()->syncWithoutDetaching($newBranch); + foreach ($this->tenantUser->branches as $branch) { + if ($newBranch->id === $branch->id) { + $branch->pivot->is_default = true; + $branch->pivot->save(); + } + } + } + private function changeActingAs($tenantUser, $inventoryUsage) { $tenantUser->branches()->syncWithoutDetaching($inventoryUsage->form->branch_id); @@ -136,42 +181,106 @@ private function changeActingAs($tenantUser, $inventoryUsage) $user->save(); $this->actingAs($user, 'api'); } - - private function getDummyData($itemUnit = 'pcs') - { - $warehouse = $this->warehouseSelected; + private function getDummyDataItem($isItemDna = false) + { $allocation = factory(Allocation::class)->create(); - $employee = factory(Employee::class)->create(); - $chartOfAccount = ChartOfAccount::whereHas('type', function ($query) { return $query->whereIn('alias', ['BEBAN OPERASIONAL', 'BEBAN NON OPERASIONAL']); })->first(); + $unit = new ItemUnit(['label' => 'pcs', 'name' => 'pcs', 'converter' => 1]); + + $quantity = $this->initialItemQuantity; + + if ($isItemDna) { + $createdItemDna = $this->createItemDnaWithStocks($unit); + $item = $createdItemDna[0]; + $itemDna = [ + "quantity" => $quantity, + "expiry_date" => convert_to_server_timezone($createdItemDna[1]["expiry_date"], 'UTC', 'asia/jakarta'), + "production_number" => $createdItemDna[1]["production_number"], + ]; + } else { + $item = $this->createItemWithStocks($unit); + } + + $usageItem = [ + "item_id" => $item->id, + "item_name" => $item->name, + "item_label" => "[{$item->code}] - {$item->name}", + "chart_of_account_id" => $chartOfAccount->id, + "chart_of_account_name" => $chartOfAccount->alias, + "require_expiry_date" => 0, + "require_production_number" => 0, + "unit" => $unit->name, + "converter" => $unit->converter, + "quantity" => $quantity, + "allocation_id" => $allocation->id, + "allocation_name" => $allocation->name, + "notes" => null, + "more" => false, + ]; + if ($isItemDna) { + $usageItem['dna'] = [$itemDna]; + } + + return $usageItem; + } + + private function getDummyData($inventoryUsage = null, $itemUnit = 'pcs', $isItemDna = false) + { + $warehouse = $this->warehouseSelected; $unit = new ItemUnit([ 'label' => $itemUnit, 'name' => $itemUnit, 'converter' => 1, ]); - $item = $this->createItemWithStocks($unit); + $quantity = $this->initialUsageItemQuantity; - $role = Role::createIfNotExists('super admin'); - $approver = factory(TenantUser::class)->create(); - $approver->assignRole($role); + if ($inventoryUsage) { + $inventoryUsageItem = $inventoryUsage->items()->first(); - return [ - "increment_group" => date("Ym"), - "date" => date("Y-m-d H:i:s"), - "warehouse_id" => $warehouse->id, - "warehouse_name" => $warehouse->name, - "employee_id" => $employee->id, - "employee_name" => $employee->name, - "request_approval_to" => $approver->id, - "approver_name" => $approver->name, - "approver_email" => $approver->email, - "notes" => null, - "items" => [ - [ + $employee = $inventoryUsage->employee; + + $allocation = $inventoryUsageItem->allocation; + $chartOfAccount = $inventoryUsageItem->account; + $item = $inventoryUsageItem->item; + + if ($itemUnit === 'pcs') { + $unit = ItemUnit::where('label', $inventoryUsageItem->unit)->first(); + } + + $quantity = $inventoryUsageItem->quantity; + + $approver = $inventoryUsage->form->requestApprovalTo; + } else { + $allocation = factory(Allocation::class)->create(); + $employee = factory(Employee::class)->create(); + + $chartOfAccount = ChartOfAccount::whereHas('type', function ($query) { + return $query->whereIn('alias', ['BEBAN OPERASIONAL', 'BEBAN NON OPERASIONAL']); + })->first(); + + if ($isItemDna) { + $createdItemDna = $this->createItemDnaWithStocks($unit); + $item = $createdItemDna[0]; + $itemDna = [ + "quantity" => $quantity, + "expiry_date" => convert_to_server_timezone($createdItemDna[1]["expiry_date"], 'UTC', 'asia/jakarta'), + "production_number" => $createdItemDna[1]["production_number"], + ]; + + } else { + $item = $this->createItemWithStocks($unit); + } + + $role = Role::createIfNotExists('super admin'); + $approver = factory(TenantUser::class)->create(); + $approver->assignRole($role); + } + + $usageItem = [ "item_id" => $item->id, "item_name" => $item->name, "item_label" => "[{$item->code}] - {$item->name}", @@ -181,13 +290,28 @@ private function getDummyData($itemUnit = 'pcs') "require_production_number" => 0, "unit" => $unit->name, "converter" => $unit->converter, - "quantity" => 5, + "quantity" => $quantity, "allocation_id" => $allocation->id, "allocation_name" => $allocation->name, "notes" => null, "more" => false, - ] - ] + ]; + if ($isItemDna) { + $usageItem['dna'] = [$itemDna]; + } + + return [ + "increment_group" => date("Ym"), + "date" => date("Y-m-d H:i:s"), + "warehouse_id" => $warehouse->id, + "warehouse_name" => $warehouse->name, + "employee_id" => $employee->id, + "employee_name" => $employee->name, + "request_approval_to" => $approver->id, + "approver_name" => $approver->name, + "approver_email" => $approver->email, + "notes" => null, + "items" => [$usageItem] ]; } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageTest.php b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageTest.php index 670ff38d8..44d95ccd3 100644 --- a/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageTest.php +++ b/tests/Feature/Http/Inventory/InventoryUsage/InventoryUsageTest.php @@ -2,13 +2,12 @@ namespace Tests\Feature\Http\Inventory\InventoryUsage; +use Illuminate\Support\Facades\Mail; use Tests\TestCase; -use App\Model\Form; +use App\Mail\Inventory\InventoryUsageApprovalMail; use App\Model\Inventory\InventoryUsage\InventoryUsage; -use App\Model\Sales\DeliveryOrder\DeliveryOrder; -use App\Model\Sales\DeliveryNote\DeliveryNote; -use App\Model\Sales\DeliveryNote\DeliveryNoteItem; +use App\Model\Master\Item; class InventoryUsageTest extends TestCase { @@ -16,6 +15,47 @@ class InventoryUsageTest extends TestCase public static $path = '/api/v1/inventory/usages'; + /** @test */ + public function unauthorized_branch_create_inventory_usage() + { + $this->setRole('inventory'); + $this->setPermission('create inventory usage'); + + $this->branchDefault = null; + foreach ($this->tenantUser->branches as $branch) { + $branch->pivot->is_default = false; + $branch->pivot->save(); + } + + $data = $this->getDummyData(); + + $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 unauthorized_warehouse_create_inventory_usage() + { + $this->setRole('inventory'); + $this->setPermission('create inventory usage'); + + // make warehouse request difference with use default warehouse + $this->warehouseSelected = $this->createWarehouse($this->branchDefault); + + $data = $this->getDummyData(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Warehouse Test warehouse not set as default" + ]); + } /** @test */ public function unauthorized_create_inventory_usage() { @@ -45,23 +85,52 @@ public function overquantity_create_inventory_usage() ]); } /** @test */ - public function invalid_create_inventory_usage() + public function invalid_required_create_inventory_usage() { $this->setRole(); $data = $this->getDummyData(); - $data = data_set($data, 'items.0.chart_of_account_id', null); + $data = data_set($data, 'employee_id', null, true); + $data = data_set($data, 'request_approval_to', null, true); + $data = data_set($data, 'items.0.item_id', null, true); + $data = data_set($data, 'items.0.unit', null, true); + $data = data_set($data, 'items.0.chart_of_account_id', null, true); + $data = data_set($data, 'items.0.allocation_id', null, true); $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid.", + "errors" => [ + "employee_id" => [ + "The employee id field is required." + ], + "request_approval_to" => [ + "The request approval to field is required." + ], + "items.0.item_id" => [ + "The items.0.item_id field is required." + ], + "items.0.unit" => [ + "The items.0.unit field is required." + ], + "items.0.chart_of_account_id" => [ + "The items.0.chart_of_account_id field is required." + ], + "items.0.allocation_id" => [ + "The items.0.allocation_id field is required." + ] + ] + ]); } /** @test */ public function invalid_unit_create_inventory_usage() { $this->setRole(); - $data = $this->getDummyData($itemUnit = 'box'); + $data = $this->getDummyData(null, $itemUnit = 'box'); $response = $this->json('POST', self::$path, $data, $this->headers); $response->assertStatus(422) @@ -78,8 +147,39 @@ public function success_create_inventory_usage() $data = $this->getDummyData(); $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(201) + ->assertJsonStructure([ + "data" => [ + "id", + "warehouse_id", + "employee_id", + "form" => [ + "id", + "branch_id", + "approval_status", + "date", + "done", + "notes", + "number", + "request_approval_to", + ], + "items" => [ + [ + "expiry_date", + "id", + "chart_of_account_id", + "item_id", + "allocation_id", + "notes", + "production_number", + "quantity", + "unit", + ] + ] + ], + ]); - $response->assertStatus(201); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -110,7 +210,7 @@ public function success_approve_inventory_usage() /** @test */ public function read_all_inventory_usage() { - $this->setRole(); + $this->success_create_inventory_usage(); $data = [ 'join' => 'form,warehouse,items,item', @@ -122,13 +222,42 @@ public function read_all_inventory_usage() 'filter_date_min' => '{"form.date":"' . date('Y-m-01') . ' 00:00:00"}', 'filter_date_max' => '{"form.date":"' . date('Y-m-30') . ' 23:59:59"}', 'limit' => 10, - 'includes' => 'form', + 'includes' => 'form;warehouse;items;items.item', 'page' => 1 ]; $response = $this->json('GET', self::$path, $data, $this->headers); - - $response->assertStatus(200); + + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + [ + "id", + "form" => [ + "approval_status", + "date", + "done", + "notes", + "number", + ], + "items" => [ + [ + "expiry_date", + "id", + "item" => [ + "id", + "name" + ], + "item_id", + "notes", + "production_number", + "quantity", + "unit", + ] + ] + ], + ] + ]); } /** @test */ public function read_inventory_usage() @@ -140,100 +269,449 @@ public function read_inventory_usage() $data = [ 'with_archives' => 'true', 'with_origin' => 'true', - 'includes' => 'form' + 'includes' => 'warehouse;items.account;items.item;items.allocation;form.createdBy;form.requestApprovalTo;form.requestCancellationTo;employee' ]; $response = $this->json('GET', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + "employee" => [ + "id", + "name", + ], + "employee_id", + "form" => [ + "approval_at", + "approval_status", + "created_by" => [ + "first_name", + "full_name", + "id", + "last_name", + ], + "date", + "done", + "id", + "notes", + "number", + "request_approval_at", + "request_approval_to" => [ + "email", + "first_name", + "full_name", + "id", + "last_name", + ], + ], + "id", + "items" => [ + [ + "account" => [ + "id", + "alias", + "label", + "number" + ], + "allocation" => [ + "id", + "name", + ], + "allocation_id", + "expiry_date", + "id", + "item" => [ + "code", + "id", + "label", + "name" + ], + "item_id", + "notes", + "production_number", + "quantity", + "unit", + ] + ], + "warehouse" => [ + "id", + "name", + ], + "warehouse_id" + ], + ]); } /** @test */ - // public function unauthorized_update_inventory_usage() - // { - // $this->success_create_inventory_usage(); + public function unauthorized_branch_update_inventory_usage() + { + $this->success_create_inventory_usage(); - // $this->unsetUserRole(); + $this->setRole('inventory'); + $this->setPermission('update inventory usage'); - // $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); - // $data = $this->getDummyData($inventoryUsage); + $this->branchDefault = null; + foreach ($this->tenantUser->branches as $branch) { + $branch->pivot->is_default = false; + $branch->pivot->save(); + } + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($inventoryUsage); - // $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); - // $response->assertStatus(500) - // ->assertJson([ - // "code" => 0, - // "message" => "There is no permission named `update inventory usage` for guard `api`." - // ]); - // } + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "please set default branch to update this form" + ]); + } /** @test */ - // public function overquantity_update_inventory_usage() - // { - // $this->success_create_inventory_usage(); + public function unauthorized_warehouse_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $this->setRole('inventory'); + $this->setPermission('update inventory usage'); - // $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + // make warehouse request difference with use default warehouse + $this->warehouseSelected = $this->createWarehouse($this->branchDefault); - // $data = $this->getDummyData($inventoryUsage); - // $data = data_set($data, 'id', $inventoryUsage->id, false); - // $data = data_set($data, 'items.0.quantity', 2000); + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($inventoryUsage); - // $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); - // $response->assertStatus(422) - // ->assertJson([ - // "code" => 422, - // "message" => "Stock {$data['items'][0]['item_name']} not enough" - // ]); - // } + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Warehouse Test warehouse not set as default" + ]); + } /** @test */ - // public function invalid_update_inventory_usage() - // { - // $this->success_create_inventory_usage(); + public function unauthorized_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $this->unsetUserRole(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($inventoryUsage); - // $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(500) + ->assertJson([ + "code" => 0, + "message" => "There is no permission named `update inventory usage` for guard `api`." + ]); + } + /** @test */ + public function invalid_date_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); - // $data = $this->getDummyData($inventoryUsage); - // $data = data_set($data, 'id', $inventoryUsage->id, false); - // $data = data_set($data, 'request_approval_to', null); + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'date', '1970-01-01', true); // date less than create - // $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); - // $response->assertStatus(422); - // } + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid.", + "errors" => [ + "date" => ["The date must be a date after or equal to {$inventoryUsage->form->date}."], + ] + ]); + } /** @test */ - // public function invalid_unit_update_inventory_usage() - // { - // $this->setRole(); + public function invalid_approver_update_inventory_usage() + { + $this->success_create_inventory_usage(); - // $data = $this->getDummyData($itemUnit = 'box'); + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'request_approval_to', null); - // $response = $this->json('POST', self::$path, $data, $this->headers); - // $response->assertStatus(422) - // ->assertJson([ - // "code" => 422, - // "message" => "there are some item not in 'pcs' unit" - // ]); - // } + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid.", + "errors" => [ + "request_approval_to" => ["The request approval to field is required."], + ] + ]); + } /** @test */ - // public function success_update_inventory_usage() - // { - // $this->success_create_inventory_usage(); + public function overquantity_update_inventory_usage() + { + $this->success_create_inventory_usage(); - // $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); - // $data = $this->getDummyData($inventoryUsage); - // $data = data_set($data, 'id', $inventoryUsage->id, false); - - // $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->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' => 'InventoryUsage', - // 'activity' => 'Update - 1' - // ], 'tenant'); - // } + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'items.0.quantity', 2000); + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Stock {$data['items'][0]['item_name']} not enough" + ]); + } + /** @test */ + public function invalid_unit_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage, $itemUnit = 'box'); + $data = data_set($data, 'id', $inventoryUsage->id, false); + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "there are some item not in 'pcs' unit" + ]); + } + /** @test */ + public function invalid_productionnumber_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + + $dataUsageItem = $this->getDummyDataItem($isItemDna = true); + $dataUsageItem = data_set($dataUsageItem, 'dna.0.production_number', null, true); + + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'items.1', $dataUsageItem, false); // item dna without production number + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => 'Production Number for Item '.$dataUsageItem['item_name'].' not found' + ]); + } + /** @test */ + public function invalid_expirydate_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + + $dataUsageItem = $this->getDummyDataItem($isItemDna = true); + $dataUsageItem = data_set($dataUsageItem, 'dna.0.expiry_date', null, true); + + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'items.1', $dataUsageItem, false); // item dna without expry date + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Expiry Date for Item -{$dataUsageItem['item_name']} not found" + ]); + } + /** @test */ + public function invalid_notes_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'notes', str_pad('', 500, 'X', STR_PAD_LEFT), true); // over maximum length of notes + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid.", + "errors" => [ + "notes" => ["The notes may not be greater than 255 characters."], + ] + ]); + } + /** @test */ + public function invalid_warehouse_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'warehouse_id', 99, true); // random warehouse_id + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Warehouse Test warehouse not set as default" + ]); + } + /** @test */ + public function invalid_item_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'items.0.item_id', 99, true); // random item_id + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid.", + "errors" => [ + "items.0.item_id" => ["The selected items.0.item_id is invalid."], + ] + ]); + } + /** @test */ + public function invalid_account_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $inventoryUsage = InventoryUsage::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($inventoryUsage); + $data = data_set($data, 'id', $inventoryUsage->id, false); + $data = data_set($data, 'items.0.chart_of_account_id', 99, true); // random item_id + + $response = $this->json('PATCH', self::$path . '/' . $inventoryUsage->id, $data, $this->headers); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid.", + "errors" => [ + "items.0.chart_of_account_id" => ["The selected items.0.chart_of_account_id is invalid."], + ] + ]); + } + /** @test */ + public function success_update_inventory_usage() + { + $this->success_create_inventory_usage(); + + $usage = InventoryUsage::orderBy('id', 'asc')->first(); + $usageItem = $usage->items()->first(); + $usageItemAmount = $usageItem->item->cogs($usageItem->item_id) * $usageItem->quantity; + + $data = $this->getDummyData($usage); + $data = data_set($data, 'id', $usage->id, false); + + Mail::fake(); + + $response = $this->json('PATCH', self::$path . '/' . $usage->id, $data, $this->headers); + + $response->assertStatus(201) + ->assertJsonStructure([ + "data" => [ + "id", + "warehouse_id", + "employee_id", + "form" => [ + "id", + "branch_id", + "approval_status", + "date", + "done", + "notes", + "number", + "request_approval_to", + "updated_by", + ], + "items" => [ + [ + "expiry_date", + "id", + "chart_of_account_id", + "item_id", + "allocation_id", + "notes", + "production_number", + "quantity", + "unit", + ] + ] + ], + ]); + + $formNumberFormat = $usage->defaultNumberPrefix.date('ym')."000"; + $formNumber = $response->json('data.form.number'); + $this->assertStringContainsString($formNumberFormat, $formNumber, "not expected form number format") ; + + $this->assertDatabaseHas('forms', [ + 'number' => $response->json('data.form.number'), + 'request_approval_to' => $response->json('data.form.request_approval_to'), + 'approval_status' => 0, + ], 'tenant'); + $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' => 'InventoryUsage', + 'activity' => 'Update - 1' + ], 'tenant'); + + $this->assertDatabaseHas('inventory_usages', [ + 'id' => $response->json('data.id'), + 'warehouse_id' => $response->json('data.warehouse_id'), + 'employee_id' => $response->json('data.employee_id'), + ], 'tenant'); + $this->assertDatabaseHas('inventory_usage_items', [ + 'inventory_usage_id' => $response->json('data.id'), + 'item_id' => $response->json('data.items.0.item_id'), + 'chart_of_account_id' => $response->json('data.items.0.chart_of_account_id'), + 'allocation_id' => $response->json('data.items.0.allocation_id'), + 'quantity' => $response->json('data.items.0.quantity'), + ], 'tenant'); + + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'journalable_type' => Item::$morphName, + 'journalable_id' => $response->json('data.items.0.item_id'), + 'chart_of_account_id' => $response->json('data.items.0.chart_of_account_id'), + 'debit' => $usageItemAmount, + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'journalable_type' => Item::$morphName, + 'journalable_id' => $response->json('data.items.0.item_id'), + 'chart_of_account_id' => get_setting_journal('inventory usage', 'difference stock expense'), + 'credit' => $usageItemAmount, + ], 'tenant'); + + Mail::assertQueued(InventoryUsageApprovalMail::class); + } /** @test */ // public function unauthorized_delete_delivery_order() // { diff --git a/tests/TestCase.php b/tests/TestCase.php index 3abc80670..1a871071c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -143,9 +143,9 @@ protected function createSampleChartAccount($chartOfAccountType) $this->account = $chartOfAccount; } - protected function setRole() + protected function setRole($roleName = 'super admin') { - $role = \App\Model\Auth\Role::createIfNotExists('super admin'); + $role = \App\Model\Auth\Role::createIfNotExists($roleName); $hasRole = new \App\Model\Auth\ModelHasRole(); $hasRole->role_id = $role->id; $hasRole->model_type = 'App\Model\Master\User'; @@ -153,9 +153,9 @@ protected function setRole() $hasRole->save(); } - protected function setPermission() + protected function setPermission($permissionName = 'read pin point sales visitation form') { - $permission = \App\Model\Auth\Permission::createIfNotExists('read pin point sales visitation form'); + $permission = \App\Model\Auth\Permission::createIfNotExists($permissionName); $hasPermission = new \App\Model\Auth\ModelHasPermission(); $hasPermission->permission_id = $permission->id; $hasPermission->model_type = 'App\Model\Master\User';