소스 검색

feat: history stock_product

부모
커밋
bcc7f01781

+ 75
- 0
app/Exports/HistoryStockProductExport.php 파일 보기

@@ -0,0 +1,75 @@
1
+<?php
2
+
3
+namespace App\Exports;
4
+
5
+use Illuminate\Contracts\View\View;
6
+use Maatwebsite\Excel\Concerns\FromView;
7
+use Maatwebsite\Excel\Concerns\Exportable;
8
+use Maatwebsite\Excel\Concerns\WithStyles;
9
+use Illuminate\Contracts\Support\Responsable;
10
+use Maatwebsite\Excel\Concerns\ShouldAutoSize;
11
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
12
+use Maatwebsite\Excel\Concerns\WithPreCalculateFormulas;
13
+
14
+class HistoryStockProductExport implements
15
+    FromView,
16
+    Responsable,
17
+    WithStyles,
18
+    ShouldAutoSize,
19
+    WithPreCalculateFormulas
20
+{
21
+    use Exportable;
22
+
23
+    private $fileName = "history-stock-product.xlsx";
24
+
25
+    public function __construct(private array $data)
26
+    {
27
+    }
28
+
29
+    public function view(): View
30
+    {
31
+        ["historyStockProducts" => $historyStockProducts] = $this->data;
32
+
33
+        return view(
34
+            "Excel.StockProducts.Export",
35
+            compact("historyStockProducts")
36
+        );
37
+    }
38
+
39
+    public function styles(Worksheet $sheet)
40
+    {
41
+        $lastRow = $sheet->getHighestDataRow();
42
+
43
+        $lastContent = $lastRow - 1;
44
+
45
+        $sheet->setCellValue("G$lastRow", "=SUM(G5:G$lastContent)");
46
+
47
+        $sheet
48
+            ->getStyle("G")
49
+            ->getNumberFormat()
50
+            ->setFormatCode("#,###");
51
+
52
+        return [
53
+            1 => [
54
+                "font" => ["bold" => true, "size" => 12],
55
+                "alignment" => [
56
+                    "vertical" => "center",
57
+                    "horizontal" => "center",
58
+                ],
59
+            ],
60
+            2 => [
61
+                "font" => ["bold" => true, "size" => 12],
62
+                "alignment" => [
63
+                    "vertical" => "center",
64
+                    "horizontal" => "center",
65
+                ],
66
+            ],
67
+            4 => [
68
+                "font" => ["bold" => true],
69
+            ],
70
+            $lastRow => [
71
+                "font" => ["bold" => true, "size" => 12],
72
+            ],
73
+        ];
74
+    }
75
+}

+ 11
- 0
app/Http/Controllers/PurchaseController.php 파일 보기

@@ -16,6 +16,7 @@ use Illuminate\Database\QueryException;
16 16
 use App\Http\Requests\Purchase\StorePurchaseRequest;
17 17
 use App\Http\Requests\Purchase\UpdatePurchaseRequest;
18 18
 use App\Models\Company;
19
+use App\Models\HistoryStockProduct;
19 20
 use App\Models\User;
20 21
 use App\Services\FunctionService;
21 22
 use App\Services\PurchaseService;
@@ -291,6 +292,16 @@ class PurchaseController extends Controller
291 292
 
292 293
                         StockProduct::create($validated);
293 294
                     }
295
+
296
+                    $validated = [
297
+                        "price" => $product["price"],
298
+                        "ppn" => $request->ppn ? true : false,
299
+                        "qty" => $product["qty"],
300
+                        "product_number" => $product["number"],
301
+                        "purchase_number" => $purchase->number,
302
+                    ];
303
+
304
+                    HistoryStockProduct::create($validated);
294 305
                 }
295 306
             }
296 307
 

+ 12
- 0
app/Http/Controllers/SalesController.php 파일 보기

@@ -13,6 +13,8 @@ use Illuminate\Database\QueryException;
13 13
 use App\Http\Requests\Sales\StoreSaleRequest;
14 14
 use App\Http\Requests\Sales\UpdateSaleRequest;
15 15
 use App\Models\Company;
16
+use App\Models\HistoryStock;
17
+use App\Models\HistoryStockProduct;
16 18
 use App\Models\SaleDetail;
17 19
 use App\Services\FunctionService;
18 20
 use App\Services\SaleService;
@@ -126,6 +128,16 @@ class SalesController extends Controller
126 128
                         "product_number",
127 129
                         $product["number"]
128 130
                     )->decrement("qty", $product["qty"]);
131
+
132
+                    $validated = [
133
+                        "price" => $product["price"],
134
+                        "qty" => $product["qty"],
135
+                        "ppn" => $request->ppn ? true : false,
136
+                        "product_number" => $product["number"],
137
+                        "sale_number" => $sale->number,
138
+                    ];
139
+
140
+                    HistoryStockProduct::create($validated);
129 141
                 }
130 142
             }
131 143
 

+ 80
- 3
app/Http/Controllers/StockProductController.php 파일 보기

@@ -2,9 +2,13 @@
2 2
 
3 3
 namespace App\Http\Controllers;
4 4
 
5
+use App\Models\Ppn;
6
+use App\Models\Product;
5 7
 use App\Models\StockProduct;
6
-use App\Services\FunctionService;
7 8
 use Illuminate\Http\Request;
9
+use App\Services\FunctionService;
10
+use App\Models\HistoryStockProduct;
11
+use App\Exports\HistoryStockProductExport;
8 12
 
9 13
 class StockProductController extends Controller
10 14
 {
@@ -21,14 +25,18 @@ class StockProductController extends Controller
21 25
     public function index()
22 26
     {
23 27
         return inertia("StockProducts/Index", [
24
-            "initialFilters" => request()->only("search"),
25
-            "stockProducts" => StockProduct::search(request()->only("search"))
28
+            "initialFilters" => request()->only("search", "category"),
29
+            "stockProducts" => StockProduct::filter(
30
+                request()->only("search", "category")
31
+            )
26 32
                 ->latest()
27 33
                 ->paginate(10)
28 34
                 ->withQueryString()
29 35
                 ->through(
30 36
                     fn($stockProduct) => [
31 37
                         "id" => $stockProduct->id,
38
+                        "productId" => $stockProduct->product->id,
39
+                        "productNumber" => $stockProduct->product->number,
32 40
                         "updatedAt" => $stockProduct->updated_at,
33 41
                         "name" => $stockProduct->product->name,
34 42
                         "price" => FunctionService::rupiahFormat(
@@ -106,4 +114,73 @@ class StockProductController extends Controller
106 114
     {
107 115
         //
108 116
     }
117
+
118
+    public function history(Product $product)
119
+    {
120
+        $ppn = Ppn::first()->ppn;
121
+
122
+        return inertia("StockProducts/Show", [
123
+            "initialFilters" => request()->only(
124
+                "start_date",
125
+                "end_date",
126
+                "category"
127
+            ),
128
+            "productId" => $product->id,
129
+            "productNumber" => $product->number,
130
+            "historyStockProducts" => HistoryStockProduct::where(
131
+                "product_number",
132
+                $product->number
133
+            )
134
+                ->filter(request()->only("start_date", "end_date", "category"))
135
+                ->latest()
136
+                ->paginate(10)
137
+                ->withQueryString()
138
+                ->through(
139
+                    fn($historyStockProduct) => [
140
+                        "createdAt" => $historyStockProduct->created_at,
141
+                        "name" => $historyStockProduct->product->name,
142
+                        "qty" => $historyStockProduct->qty,
143
+                        "ppn" => $historyStockProduct->ppn ? $ppn : 0,
144
+                        "unit" => $historyStockProduct->product->unit,
145
+                        "price" => FunctionService::rupiahFormat(
146
+                            $historyStockProduct->price *
147
+                                $historyStockProduct->qty
148
+                        ),
149
+                        "category" => $historyStockProduct->purchase_number
150
+                            ? __("words.addition")
151
+                            : __("words.reduction"),
152
+                    ]
153
+                ),
154
+        ]);
155
+    }
156
+
157
+    public function historyExcel()
158
+    {
159
+        $ppn = Ppn::first()->ppn;
160
+
161
+        return new HistoryStockProductExport([
162
+            "historyStockProducts" => HistoryStockProduct::where(
163
+                "product_number",
164
+                request("product_number")
165
+            )
166
+                ->filter(request()->only("start_date", "end_date", "category"))
167
+                ->latest()
168
+                ->get()
169
+                ->map(
170
+                    fn($historyStockProduct) => [
171
+                        "createdAt" => $historyStockProduct->created_at,
172
+                        "name" => $historyStockProduct->product->name,
173
+                        "qty" => $historyStockProduct->qty,
174
+                        "ppn" => $historyStockProduct->ppn ? $ppn : 0,
175
+                        "unit" => $historyStockProduct->product->unit,
176
+                        "price" =>
177
+                            $historyStockProduct->price *
178
+                            $historyStockProduct->qty,
179
+                        "category" => $historyStockProduct->purchase_number
180
+                            ? __("words.addition")
181
+                            : __("words.reduction"),
182
+                    ]
183
+                ),
184
+        ]);
185
+    }
109 186
 }

+ 65
- 0
app/Models/HistoryStockProduct.php 파일 보기

@@ -0,0 +1,65 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Carbon\Carbon;
6
+use App\Models\Product;
7
+use App\Services\FunctionService;
8
+use Illuminate\Database\Eloquent\Model;
9
+use Illuminate\Database\Eloquent\Casts\Attribute;
10
+use Illuminate\Database\Eloquent\Factories\HasFactory;
11
+
12
+class HistoryStockProduct extends Model
13
+{
14
+    use HasFactory;
15
+
16
+    protected $fillable = [
17
+        "price",
18
+        "qty",
19
+        "ppn",
20
+        "product_number",
21
+        "sale_number",
22
+        "purchase_number",
23
+    ];
24
+
25
+    protected function createdAt(): Attribute
26
+    {
27
+        return Attribute::make(
28
+            get: fn($value) => Carbon::parse($value)->translatedFormat(
29
+                "l d/m/y"
30
+            )
31
+        );
32
+    }
33
+
34
+    protected function price(): Attribute
35
+    {
36
+        return Attribute::make(
37
+            get: function ($value) {
38
+                $ppn = Ppn::first()->ppn;
39
+
40
+                return $this->ppn ? FunctionService::ppn($value, $ppn) : $value;
41
+            }
42
+        );
43
+    }
44
+
45
+    public function product()
46
+    {
47
+        return $this->belongsTo(Product::class, "product_number", "number");
48
+    }
49
+
50
+    public function scopeFilter($query, array $filters)
51
+    {
52
+        $query
53
+            ->when($filters["start_date"] ?? null, function ($query, $search) {
54
+                $query->whereDate("created_at", ">=", $search);
55
+            })
56
+            ->when($filters["end_date"] ?? null, function ($query, $search) {
57
+                $query->whereDate("created_at", "<=", $search);
58
+            })
59
+            ->when($filters["category"] ?? null, function ($query, $search) {
60
+                $query
61
+                    ->where("sale_number", "like", $search . "%")
62
+                    ->orWhere("purchase_number", "like", $search . "%");
63
+            });
64
+    }
65
+}

+ 4
- 2
app/Models/StockProduct.php 파일 보기

@@ -40,7 +40,7 @@ class StockProduct extends Model
40 40
         return $this->belongsTo(Product::class, "product_number", "number");
41 41
     }
42 42
 
43
-    public function scopeSearch($query, array $filters)
43
+    public function scopeFilter($query, array $filters)
44 44
     {
45 45
         $query
46 46
             ->when($filters["search"] ?? null, function ($query, $search) {
@@ -50,6 +50,8 @@ class StockProduct extends Model
50 50
                         ->orWhere("name", "like", "%" . $search . "%");
51 51
                 });
52 52
             })
53
-            ->where("qty", ">", 0);
53
+            ->when($filters["category"] ?? null, function ($query, $search) {
54
+                $query->where("qty", ">", 0);
55
+            });
54 56
     }
55 57
 }

+ 48
- 0
database/migrations/2022_08_11_100907_create_history_stock_products_table.php 파일 보기

@@ -0,0 +1,48 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration {
8
+    /**
9
+     * Run the migrations.
10
+     *
11
+     * @return void
12
+     */
13
+    public function up()
14
+    {
15
+        Schema::create("history_stock_products", function (Blueprint $table) {
16
+            $table->id();
17
+            $table->unsignedInteger("price");
18
+            $table->integer("qty");
19
+            $table->unsignedTinyInteger("ppn");
20
+            $table->string("product_number");
21
+            $table
22
+                ->foreign("product_number")
23
+                ->references("number")
24
+                ->on("products");
25
+            $table->string("sale_number")->nullable();
26
+            $table
27
+                ->foreign("sale_number")
28
+                ->references("number")
29
+                ->on("sales");
30
+            $table->string("purchase_number")->nullable();
31
+            $table
32
+                ->foreign("purchase_number")
33
+                ->references("number")
34
+                ->on("purchases");
35
+            $table->timestamps();
36
+        });
37
+    }
38
+
39
+    /**
40
+     * Reverse the migrations.
41
+     *
42
+     * @return void
43
+     */
44
+    public function down()
45
+    {
46
+        Schema::dropIfExists("history_stocks");
47
+    }
48
+};

+ 5
- 0
lang/en/words.php 파일 보기

@@ -53,4 +53,9 @@ return [
53 53
     "number" => "Number",
54 54
     "product_name" => "Product Name",
55 55
     "report_history_purchase" => "Report History Purchase",
56
+    "addition" => "Addition",
57
+    "reduction" => "Reduction",
58
+    "report_history_stock_product" => "Report History Stock Product",
59
+    "unit" => "Unit",
60
+    "category" => "Category",
56 61
 ];

+ 5
- 0
lang/id/words.php 파일 보기

@@ -53,4 +53,9 @@ return [
53 53
     "number" => "Nomor",
54 54
     "name_product" => "Nama Produk",
55 55
     "report_history_purchase" => "Laporan History Pembelian",
56
+    "addition" => "Penambahan",
57
+    "reduction" => "Pengurangan",
58
+    "report_history_stock_product" => "Laporan History Stok Produk",
59
+    "unit" => "Satuan",
60
+    "category" => "Kategory",
56 61
 ];

+ 37
- 6
resources/js/pages/StockProducts/Index.vue 파일 보기

@@ -1,7 +1,10 @@
1 1
 <script setup>
2
-import { indexTable } from './config'
2
+import { indexTable, stockOptionCategory } from './config'
3 3
 import AppSearchFilter from '@/components/AppSearchFilter.vue'
4
+import AppButtonLink from '@/components/AppButtonLink.vue'
4 5
 import AppPagination from '@/components/AppPagination.vue'
6
+import AppDropdownFilter from '@/components/AppDropdownFilter.vue'
7
+import AppResetFilter from '@/components/AppResetFilter.vue'
5 8
 import DashboardLayout from '@/layouts/Dashboard/DashboardLayout.vue'
6 9
 
7 10
 defineProps({
@@ -22,11 +25,28 @@ defineProps({
22 25
       <template #header>
23 26
         <h1>Stok Produk</h1>
24 27
 
25
-        <AppSearchFilter
26
-          placeholder="nama"
27
-          name-param="search"
28
-          :initial-search="initialFilters"
29
-        />
28
+        <div class="grid">
29
+          <div class="col-12 sm:col-6 lg:col-4">
30
+            <AppDropdownFilter
31
+              placeholder="category"
32
+              name-param="category"
33
+              :initial-dropdown="initialFilters"
34
+              :options="stockOptionCategory"
35
+            />
36
+          </div>
37
+
38
+          <div class="col-12 sm:col-6 lg:col-4">
39
+            <AppSearchFilter
40
+              placeholder="nama"
41
+              name-param="search"
42
+              :initial-search="initialFilters"
43
+            />
44
+          </div>
45
+
46
+          <div class="col-12 sm:col-6 lg:col-4">
47
+            <AppResetFilter :url="route('stock-products.index')" />
48
+          </div>
49
+        </div>
30 50
       </template>
31 51
 
32 52
       <Column
@@ -35,6 +55,17 @@ defineProps({
35 55
         :header="value.header"
36 56
         :key="value.field"
37 57
       />
58
+
59
+      <Column>
60
+        <template #body="{ data }">
61
+          <AppButtonLink
62
+            icon="pi pi-chevron-right"
63
+            class="p-button-icon-only p-button-rounded p-button-text"
64
+            v-tooltip.bottom="'Lihat History'"
65
+            :href="route('stock-products.history', data.productId)"
66
+          />
67
+        </template>
68
+      </Column>
38 69
     </DataTable>
39 70
 
40 71
     <AppPagination :links="stockProducts.links" />

+ 111
- 0
resources/js/pages/StockProducts/Show.vue 파일 보기

@@ -0,0 +1,111 @@
1
+<script setup>
2
+import { detailTable, filterOptionCategory } from './config'
3
+import AppPagination from '@/components/AppPagination.vue'
4
+import AppDateRangeFilter from '@/components/AppDateRangeFilter.vue'
5
+import AppDropdownFilter from '@/components/AppDropdownFilter.vue'
6
+import AppButtonLink from '@/components/AppButtonLink.vue'
7
+import AppResetFilter from '@/components/AppResetFilter.vue'
8
+import DashboardLayout from '@/layouts/Dashboard/DashboardLayout.vue'
9
+
10
+const props = defineProps({
11
+  initialFilters: Object,
12
+  productId: Number,
13
+  productNumber: String,
14
+  historyStockProducts: {
15
+    type: Object,
16
+    default: {
17
+      data: [],
18
+      links: [],
19
+      total: 0,
20
+    },
21
+  },
22
+})
23
+
24
+const exportExcel = () => {
25
+  if (location.search.length) {
26
+    return (
27
+      '/stock-products/history/excel' +
28
+      location.search +
29
+      `&product_number=${props.productNumber}`
30
+    )
31
+  } else {
32
+    return `/stock-products/history/excel?product_number=${props.productNumber}`
33
+  }
34
+}
35
+</script>
36
+
37
+<template>
38
+  <DashboardLayout title="History Stok Produk">
39
+    <DataTable
40
+      responsiveLayout="scroll"
41
+      :value="historyStockProducts.data"
42
+      :rowHover="true"
43
+      :stripedRows="true"
44
+    >
45
+      <template #header>
46
+        <h1>History Stok Produk</h1>
47
+
48
+        <div class="grid">
49
+          <div class="col-12 sm:col-6 lg:col-4">
50
+            <AppDateRangeFilter
51
+              placeholder="filter waktu..."
52
+              :name-param="['start_date', 'end_date']"
53
+              :initial-date-rage="initialFilters"
54
+            />
55
+          </div>
56
+
57
+          <div class="col-12 sm:col-6 lg:col-4">
58
+            <AppDropdownFilter
59
+              placeholder="category"
60
+              name-param="category"
61
+              :initial-dropdown="initialFilters"
62
+              :options="filterOptionCategory"
63
+            />
64
+          </div>
65
+
66
+          <div class="col-12 sm:col-6 lg:col-4">
67
+            <AppResetFilter :url="route('stock-products.history', productId)" />
68
+          </div>
69
+
70
+          <div class="col-12 flex flex-column sm:flex-row">
71
+            <AppButtonLink
72
+              v-if="historyStockProducts.total"
73
+              label="Export excel"
74
+              class-button="p-button-outlined md:w-16rem"
75
+              icon="pi pi-file-excel"
76
+              :inertia-link="false"
77
+              :href="exportExcel()"
78
+            />
79
+          </div>
80
+        </div>
81
+      </template>
82
+
83
+      <Column
84
+        v-for="value in detailTable"
85
+        :key="value.field"
86
+        :field="value.field"
87
+        :header="value.header"
88
+      />
89
+
90
+      <Column>
91
+        <template #body="{ data }">
92
+          <Badge
93
+            v-if="data.category === 'Penambahan'"
94
+            :value="data.category"
95
+            severity="success"
96
+            class="mr-2"
97
+          ></Badge>
98
+
99
+          <Badge
100
+            v-if="data.category === 'Pengurangan'"
101
+            :value="data.category"
102
+            severity="danger"
103
+            class="mr-2"
104
+          ></Badge>
105
+        </template>
106
+      </Column>
107
+    </DataTable>
108
+
109
+    <AppPagination :links="historyStockProducts.links" />
110
+  </DashboardLayout>
111
+</template>

+ 30
- 0
resources/js/pages/StockProducts/config.js 파일 보기

@@ -1,7 +1,37 @@
1 1
 export const indexTable = [
2 2
   { field: 'updatedAt', header: 'Terakhir Diperbaharui' },
3
+  { field: 'productNumber', header: 'Nomor Produk' },
3 4
   { field: 'name', header: 'Nama Produk' },
4 5
   { field: 'price', header: 'Harga' },
5 6
   { field: 'qty', header: 'Kuantitas' },
6 7
   { field: 'unit', header: 'Satuan' },
7 8
 ]
9
+
10
+export const detailTable = [
11
+  { field: 'createdAt', header: 'Tanggal' },
12
+  { field: 'name', header: 'Nama Produk' },
13
+  { field: 'price', header: 'Harga' },
14
+  { field: 'qty', header: 'Kuantitas' },
15
+  { field: 'ppn', header: 'PPN' },
16
+  { field: 'unit', header: 'Satuan' },
17
+]
18
+
19
+export const stockOptionCategory = [
20
+  null,
21
+  {
22
+    label: 'Kuantitas lebih dari satu',
23
+    value: 'not null',
24
+  },
25
+]
26
+
27
+export const filterOptionCategory = [
28
+  null,
29
+  {
30
+    label: 'Penambahan Stok',
31
+    value: 'PBN',
32
+  },
33
+  {
34
+    label: 'Pengurangan Stok',
35
+    value: 'PJN',
36
+  },
37
+]

+ 41
- 0
resources/views/Excel/StockProducts/Export.blade.php 파일 보기

@@ -0,0 +1,41 @@
1
+<table>
2
+    <thead>
3
+        <tr>
4
+            <th colspan="8">{{ __('words.report_history_stock_product') }}</th>
5
+        </tr>
6
+        <tr>
7
+            <th colspan="8" rowspan="2">{{ __('words.period', ['number' => '']) }}
8
+                {{ $historyStockProducts->first()['createdAt'] }}
9
+                -
10
+                {{ $historyStockProducts->last()['createdAt'] }} </th>
11
+        </tr>
12
+        <tr></tr>
13
+        <tr>
14
+            <th>#</th>
15
+            <th>{{ __('words.date') }}</th>
16
+            <th>{{ __('words.name_product') }}</th>
17
+            <th>{{ __('words.quantity') }}</th>
18
+            <th>PPN</th>
19
+            <th>{{ __('words.unit') }}</th>
20
+            <th>{{ __('words.total_price') }}</th>
21
+            <th>{{ __('words.category') }}</th>
22
+        </tr>
23
+    </thead>
24
+    <tbody>
25
+        @foreach ($historyStockProducts as $index => $historyStockProduct)
26
+            <tr>
27
+                <td>{{ ++$index }}</td>
28
+                <td>{{ $historyStockProduct['createdAt'] }}</td>
29
+                <td>{{ $historyStockProduct['name'] }}</td>
30
+                <td>{{ $historyStockProduct['qty'] }}</td>
31
+                <td>{{ $historyStockProduct['ppn'] }}</td>
32
+                <td>{{ $historyStockProduct['unit'] }}</td>
33
+                <td>{{ $historyStockProduct['price'] }}</td>
34
+                <td>{{ $historyStockProduct['category'] }}</td>
35
+            </tr>
36
+        @endforeach
37
+        <tr>
38
+            <td colspan="6">{{ __('words.total') }}</td>
39
+        </tr>
40
+    </tbody>
41
+</table>

+ 10
- 0
routes/web.php 파일 보기

@@ -109,6 +109,16 @@ Route::middleware(["auth", "verified", "checkBlocked"])->group(function () {
109 109
 
110 110
     Route::resource("/suppliers", SupplierController::class);
111 111
 
112
+    Route::get("/stock-products/history/excel", [
113
+        StockProductController::class,
114
+        "historyExcel",
115
+    ])->name("stock-products.history.excel");
116
+
117
+    Route::get("/stock-products/history/{product}", [
118
+        StockProductController::class,
119
+        "history",
120
+    ])->name("stock-products.history");
121
+
112 122
     Route::resource("/stock-products", StockProductController::class);
113 123
 
114 124
     Route::resource("/products", ProductController::class);