Andrew Wallo 5 месяцев назад
Родитель
Сommit
543687d085

+ 12
- 0
app/Enums/Accounting/CreditNoteStatus.php Просмотреть файл

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+enum CreditNoteStatus: string
6
+{
7
+    case Draft = 'draft';
8
+    case Open = 'open';
9
+    case Closed = 'closed';
10
+    case Applied = 'applied';
11
+    case Partial = 'partial';
12
+}

+ 12
- 2
app/Enums/Accounting/DocumentType.php Просмотреть файл

@@ -13,6 +13,7 @@ enum DocumentType: string implements HasIcon, HasLabel
13 13
     case Bill = 'bill';
14 14
     case Estimate = 'estimate';
15 15
     case RecurringInvoice = 'recurring_invoice';
16
+    case CreditNote = 'credit_note';
16 17
 
17 18
     public const DEFAULT = self::Invoice->value;
18 19
 
@@ -21,6 +22,7 @@ enum DocumentType: string implements HasIcon, HasLabel
21 22
         return match ($this) {
22 23
             self::Invoice, self::Bill, self::Estimate => $this->name,
23 24
             self::RecurringInvoice => 'Recurring Invoice',
25
+            self::CreditNote => 'Credit Note',
24 26
         };
25 27
     }
26 28
 
@@ -41,7 +43,7 @@ enum DocumentType: string implements HasIcon, HasLabel
41 43
     public function getTaxKey(): string
42 44
     {
43 45
         return match ($this) {
44
-            self::Invoice, self::RecurringInvoice, self::Estimate => 'salesTaxes',
46
+            self::Invoice, self::RecurringInvoice, self::Estimate, self::CreditNote => 'salesTaxes',
45 47
             self::Bill => 'purchaseTaxes',
46 48
         };
47 49
     }
@@ -49,7 +51,7 @@ enum DocumentType: string implements HasIcon, HasLabel
49 51
     public function getDiscountKey(): string
50 52
     {
51 53
         return match ($this) {
52
-            self::Invoice, self::RecurringInvoice, self::Estimate => 'salesDiscounts',
54
+            self::Invoice, self::RecurringInvoice, self::Estimate, self::CreditNote => 'salesDiscounts',
53 55
             self::Bill => 'purchaseDiscounts',
54 56
         };
55 57
     }
@@ -65,6 +67,14 @@ enum DocumentType: string implements HasIcon, HasLabel
65 67
                 dueDate: 'Payment Due',
66 68
                 amountDue: 'Amount Due',
67 69
             ),
70
+            self::CreditNote => new DocumentLabelDTO(
71
+                title: self::CreditNote->getLabel(),
72
+                number: 'Credit Note Number',
73
+                referenceNumber: 'P.O/S.O Number',
74
+                date: 'Credit Note Date',
75
+                dueDate: 'Payment Due',
76
+                amountDue: 'Amount Due',
77
+            ),
68 78
             self::RecurringInvoice => new DocumentLabelDTO(
69 79
                 title: self::RecurringInvoice->getLabel(),
70 80
                 number: 'Invoice Number',

+ 533
- 0
app/Models/Accounting/CreditNote.php Просмотреть файл

@@ -0,0 +1,533 @@
1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
7
+use App\Collections\Accounting\DocumentCollection;
8
+use App\Enums\Accounting\AdjustmentComputation;
9
+use App\Enums\Accounting\CreditNoteStatus;
10
+use App\Enums\Accounting\DocumentDiscountMethod;
11
+use App\Enums\Accounting\DocumentType;
12
+use App\Enums\Accounting\JournalEntryType;
13
+use App\Enums\Accounting\TransactionType;
14
+use App\Filament\Company\Resources\Sales\CreditNoteResource;
15
+use App\Models\Common\Client;
16
+use App\Models\Company;
17
+use App\Models\Setting\DocumentDefault;
18
+use App\Utilities\Currency\CurrencyAccessor;
19
+use App\Utilities\Currency\CurrencyConverter;
20
+use Filament\Actions\Action;
21
+use Filament\Actions\MountableAction;
22
+use Filament\Actions\ReplicateAction;
23
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
24
+use Illuminate\Database\Eloquent\Casts\Attribute;
25
+use Illuminate\Database\Eloquent\Model;
26
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
27
+use Illuminate\Database\Eloquent\Relations\MorphMany;
28
+use Illuminate\Database\Eloquent\Relations\MorphOne;
29
+use Illuminate\Support\Carbon;
30
+use Illuminate\Support\Collection;
31
+
32
+#[CollectedBy(DocumentCollection::class)]
33
+class CreditNote extends Document
34
+{
35
+    protected $table = 'credit_notes';
36
+
37
+    protected $fillable = [
38
+        'company_id',
39
+        'client_id',
40
+        'logo',
41
+        'header',
42
+        'subheader',
43
+        'credit_note_number',
44
+        'reference_number',
45
+        'date',
46
+        'approved_at',
47
+        'last_sent_at',
48
+        'last_viewed_at',
49
+        'status',
50
+        'currency_code',
51
+        'discount_method',
52
+        'discount_computation',
53
+        'discount_rate',
54
+        'subtotal',
55
+        'tax_total',
56
+        'discount_total',
57
+        'total',
58
+        'amount_used',
59
+        'terms',
60
+        'footer',
61
+        'created_by',
62
+        'updated_by',
63
+    ];
64
+
65
+    protected $casts = [
66
+        'date' => 'date',
67
+        'approved_at' => 'datetime',
68
+        'last_sent_at' => 'datetime',
69
+        'last_viewed_at' => 'datetime',
70
+        'status' => CreditNoteStatus::class,
71
+        'discount_method' => DocumentDiscountMethod::class,
72
+        'discount_computation' => AdjustmentComputation::class,
73
+        'discount_rate' => RateCast::class,
74
+        'subtotal' => MoneyCast::class,
75
+        'tax_total' => MoneyCast::class,
76
+        'discount_total' => MoneyCast::class,
77
+        'total' => MoneyCast::class,
78
+        'amount_used' => MoneyCast::class,
79
+    ];
80
+
81
+    // Basic Relationships
82
+
83
+    public function client(): BelongsTo
84
+    {
85
+        return $this->belongsTo(Client::class);
86
+    }
87
+
88
+    public function company(): BelongsTo
89
+    {
90
+        return $this->belongsTo(Company::class);
91
+    }
92
+
93
+    // Transaction Relationships
94
+
95
+    public function transactions(): MorphMany
96
+    {
97
+        return $this->morphMany(Transaction::class, 'transactionable');
98
+    }
99
+
100
+    public function initialTransaction(): MorphOne
101
+    {
102
+        return $this->morphOne(Transaction::class, 'transactionable')
103
+            ->where('type', TransactionType::Journal);
104
+    }
105
+
106
+    // Track where this credit note has been applied
107
+
108
+    public function applications(): Collection
109
+    {
110
+        // Find all invoice transactions that reference this credit note
111
+        return Transaction::where('type', TransactionType::CreditNote)
112
+            ->where('is_payment', true)
113
+            ->whereJsonContains('meta->credit_note_id', $this->id)
114
+            ->get()
115
+            ->map(function ($transaction) {
116
+                return [
117
+                    'invoice' => $transaction->transactionable,
118
+                    'amount' => $transaction->amount,
119
+                    'date' => $transaction->posted_at,
120
+                    'transaction' => $transaction,
121
+                ];
122
+            })
123
+            ->filter(function ($item) {
124
+                return ! is_null($item['invoice']);
125
+            });
126
+    }
127
+
128
+    public function appliedInvoices(): Collection
129
+    {
130
+        return $this->applications()->pluck('invoice');
131
+    }
132
+
133
+    // Document Interface Implementation
134
+
135
+    public static function documentType(): DocumentType
136
+    {
137
+        return DocumentType::CreditNote;
138
+    }
139
+
140
+    public function documentNumber(): ?string
141
+    {
142
+        return $this->credit_note_number;
143
+    }
144
+
145
+    public function documentDate(): ?string
146
+    {
147
+        return $this->date?->toDefaultDateFormat();
148
+    }
149
+
150
+    public function dueDate(): ?string
151
+    {
152
+        return null;
153
+    }
154
+
155
+    public function amountDue(): ?string
156
+    {
157
+        return null;
158
+    }
159
+
160
+    public function referenceNumber(): ?string
161
+    {
162
+        return $this->reference_number;
163
+    }
164
+
165
+    // Computed Properties
166
+
167
+    protected function availableBalance(): Attribute
168
+    {
169
+        return Attribute::get(function () {
170
+            $totalCents = (int) $this->getRawOriginal('total');
171
+            $amountUsedCents = (int) $this->getRawOriginal('amount_used');
172
+
173
+            return CurrencyConverter::convertCentsToFormatSimple($totalCents - $amountUsedCents);
174
+        });
175
+    }
176
+
177
+    protected function availableBalanceCents(): Attribute
178
+    {
179
+        return Attribute::get(function () {
180
+            $totalCents = (int) $this->getRawOriginal('total');
181
+            $amountUsedCents = (int) $this->getRawOriginal('amount_used');
182
+
183
+            return $totalCents - $amountUsedCents;
184
+        });
185
+    }
186
+
187
+    // Status Methods
188
+
189
+    public function isFullyApplied(): bool
190
+    {
191
+        return $this->availableBalanceCents <= 0;
192
+    }
193
+
194
+    public function isPartiallyApplied(): bool
195
+    {
196
+        $amountUsedCents = (int) $this->getRawOriginal('amount_used');
197
+
198
+        return $amountUsedCents > 0 && ! $this->isFullyApplied();
199
+    }
200
+
201
+    public function isDraft(): bool
202
+    {
203
+        return $this->status === CreditNoteStatus::Draft;
204
+    }
205
+
206
+    public function wasApproved(): bool
207
+    {
208
+        return $this->approved_at !== null;
209
+    }
210
+
211
+    public function hasBeenSent(): bool
212
+    {
213
+        return $this->last_sent_at !== null;
214
+    }
215
+
216
+    public function hasBeenViewed(): bool
217
+    {
218
+        return $this->last_viewed_at !== null;
219
+    }
220
+
221
+    public function canBeAppliedToInvoice(): bool
222
+    {
223
+        return in_array($this->status->value, CreditNoteStatus::canBeApplied()) &&
224
+            $this->availableBalanceCents > 0;
225
+    }
226
+
227
+    // Application Methods
228
+
229
+    public function applyToInvoice(Invoice $invoice, string $amount): void
230
+    {
231
+        // Validate currencies match
232
+        if ($this->currency_code !== $invoice->currency_code) {
233
+            throw new \RuntimeException('Cannot apply credit note with different currency to invoice.');
234
+        }
235
+
236
+        // Validate available amount
237
+        $amountCents = CurrencyConverter::convertToCents($amount, $this->currency_code);
238
+
239
+        if ($amountCents > $this->availableBalanceCents) {
240
+            throw new \RuntimeException('Cannot apply more than the available credit note amount.');
241
+        }
242
+
243
+        // Create transaction on the invoice
244
+        $invoice->transactions()->create([
245
+            'company_id' => $this->company_id,
246
+            'type' => TransactionType::CreditNote,
247
+            'is_payment' => true,
248
+            'posted_at' => now(),
249
+            'amount' => $amount,
250
+            'account_id' => Account::getAccountsReceivableAccount()->id,
251
+            'description' => "Credit Note #{$this->credit_note_number} applied to Invoice #{$invoice->invoice_number}",
252
+            'meta' => [
253
+                'credit_note_id' => $this->id,
254
+            ],
255
+        ]);
256
+
257
+        // Update amount used on credit note
258
+        $this->amount_used = CurrencyConverter::convertCentsToFormatSimple(
259
+            (int) $this->getRawOriginal('amount_used') + $amountCents
260
+        );
261
+        $this->save();
262
+
263
+        // Update status if needed
264
+        $this->updateStatusBasedOnUsage();
265
+
266
+        // Update invoice payment status
267
+        $invoice->updatePaymentStatus();
268
+    }
269
+
270
+    protected function updateStatusBasedOnUsage(): void
271
+    {
272
+        if ($this->isFullyApplied()) {
273
+            $this->status = CreditNoteStatus::Applied;
274
+        } elseif ($this->isPartiallyApplied()) {
275
+            $this->status = CreditNoteStatus::Partial;
276
+        }
277
+
278
+        $this->save();
279
+    }
280
+
281
+    public function autoApplyToInvoices(): void
282
+    {
283
+        // Skip if no available amount
284
+        if ($this->availableBalanceCents <= 0) {
285
+            return;
286
+        }
287
+
288
+        // Find unpaid invoices for this client, ordered by due date (oldest first)
289
+        $unpaidInvoices = Invoice::where('client_id', $this->client_id)
290
+            ->where('currency_code', $this->currency_code)
291
+            ->unpaid()
292
+            ->orderBy('due_date')
293
+            ->get();
294
+
295
+        // Apply to invoices until amount is used up
296
+        foreach ($unpaidInvoices as $invoice) {
297
+            $invoiceAmountDueCents = (int) $invoice->getRawOriginal('amount_due');
298
+
299
+            if ($invoiceAmountDueCents <= 0 || $this->availableBalanceCents <= 0) {
300
+                continue;
301
+            }
302
+
303
+            // Calculate amount to apply to this invoice
304
+            $applyAmountCents = min($this->availableBalanceCents, $invoiceAmountDueCents);
305
+            $applyAmount = CurrencyConverter::convertCentsToFormatSimple($applyAmountCents);
306
+
307
+            // Apply to invoice
308
+            $this->applyToInvoice($invoice, $applyAmount);
309
+
310
+            if ($this->availableBalanceCents <= 0) {
311
+                break;
312
+            }
313
+        }
314
+    }
315
+
316
+    // Accounting Methods
317
+
318
+    public function createInitialTransaction(?Carbon $postedAt = null): void
319
+    {
320
+        $postedAt ??= $this->date;
321
+
322
+        $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
323
+
324
+        $transaction = $this->transactions()->create([
325
+            'company_id' => $this->company_id,
326
+            'type' => TransactionType::Journal,
327
+            'posted_at' => $postedAt,
328
+            'amount' => $total,
329
+            'description' => 'Credit Note Creation for Credit Note #' . $this->credit_note_number,
330
+        ]);
331
+
332
+        $baseDescription = "{$this->client->name}: Credit Note #{$this->credit_note_number}";
333
+
334
+        // Credit AR (opposite of invoice)
335
+        $transaction->journalEntries()->create([
336
+            'company_id' => $this->company_id,
337
+            'type' => JournalEntryType::Credit,
338
+            'account_id' => Account::getAccountsReceivableAccount()->id,
339
+            'amount' => $total,
340
+            'description' => $baseDescription,
341
+        ]);
342
+
343
+        // Handle line items - debit revenue accounts (reverse of invoice)
344
+        foreach ($this->lineItems as $lineItem) {
345
+            $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
346
+            $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
347
+
348
+            $transaction->journalEntries()->create([
349
+                'company_id' => $this->company_id,
350
+                'type' => JournalEntryType::Debit,
351
+                'account_id' => $lineItem->offering->income_account_id,
352
+                'amount' => $lineItemSubtotal,
353
+                'description' => $lineItemDescription,
354
+            ]);
355
+
356
+            // Handle adjustments
357
+            foreach ($lineItem->adjustments as $adjustment) {
358
+                $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
359
+
360
+                $transaction->journalEntries()->create([
361
+                    'company_id' => $this->company_id,
362
+                    'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
363
+                    'account_id' => $adjustment->account_id,
364
+                    'amount' => $adjustmentAmount,
365
+                    'description' => $lineItemDescription,
366
+                ]);
367
+            }
368
+        }
369
+    }
370
+
371
+    public function approveDraft(?Carbon $approvedAt = null): void
372
+    {
373
+        if (! $this->isDraft()) {
374
+            throw new \RuntimeException('Credit note is not in draft status.');
375
+        }
376
+
377
+        $this->createInitialTransaction();
378
+
379
+        $approvedAt ??= now();
380
+
381
+        $this->update([
382
+            'approved_at' => $approvedAt,
383
+            'status' => CreditNoteStatus::Sent,
384
+        ]);
385
+
386
+        // Auto-apply if configured in settings
387
+        if ($this->company->settings->auto_apply_credit_notes ?? true) {
388
+            $this->autoApplyToInvoices();
389
+        }
390
+    }
391
+
392
+    public function convertAmountToDefaultCurrency(int $amountCents): int
393
+    {
394
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
395
+        $needsConversion = $this->currency_code !== $defaultCurrency;
396
+
397
+        if ($needsConversion) {
398
+            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
399
+        }
400
+
401
+        return $amountCents;
402
+    }
403
+
404
+    public function formatAmountToDefaultCurrency(int $amountCents): string
405
+    {
406
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
407
+
408
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
409
+    }
410
+
411
+    // Other methods
412
+
413
+    public function markAsSent(?Carbon $sentAt = null): void
414
+    {
415
+        $sentAt ??= now();
416
+
417
+        $this->update([
418
+            'status' => CreditNoteStatus::Sent,
419
+            'last_sent_at' => $sentAt,
420
+        ]);
421
+    }
422
+
423
+    public function markAsViewed(?Carbon $viewedAt = null): void
424
+    {
425
+        $viewedAt ??= now();
426
+
427
+        $this->update([
428
+            'status' => CreditNoteStatus::Viewed,
429
+            'last_viewed_at' => $viewedAt,
430
+        ]);
431
+    }
432
+
433
+    // Utility Methods
434
+
435
+    public static function getNextDocumentNumber(?Company $company = null): string
436
+    {
437
+        $company ??= auth()->user()?->currentCompany;
438
+
439
+        if (! $company) {
440
+            throw new \RuntimeException('No current company is set for the user.');
441
+        }
442
+
443
+        $defaultSettings = $company->defaultCreditNote;
444
+
445
+        $numberPrefix = $defaultSettings->number_prefix ?? 'CN-';
446
+
447
+        $latestDocument = static::query()
448
+            ->whereNotNull('credit_note_number')
449
+            ->latest('credit_note_number')
450
+            ->first();
451
+
452
+        $lastNumberNumericPart = $latestDocument
453
+            ? (int) substr($latestDocument->credit_note_number, strlen($numberPrefix))
454
+            : DocumentDefault::getBaseNumber();
455
+
456
+        $numberNext = $lastNumberNumericPart + 1;
457
+
458
+        return $defaultSettings->getNumberNext(
459
+            prefix: $numberPrefix,
460
+            next: $numberNext
461
+        );
462
+    }
463
+
464
+    // Action Methods
465
+
466
+    public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
467
+    {
468
+        return $action::make()
469
+            ->excludeAttributes([
470
+                'status',
471
+                'amount_used',
472
+                'created_by',
473
+                'updated_by',
474
+                'created_at',
475
+                'updated_at',
476
+                'credit_note_number',
477
+                'date',
478
+                'approved_at',
479
+                'last_sent_at',
480
+                'last_viewed_at',
481
+            ])
482
+            ->modal(false)
483
+            ->beforeReplicaSaved(function (self $original, self $replica) {
484
+                $replica->status = CreditNoteStatus::Draft;
485
+                $replica->credit_note_number = self::getNextDocumentNumber();
486
+                $replica->date = now();
487
+                $replica->amount_used = 0;
488
+            })
489
+            ->databaseTransaction()
490
+            ->after(function (self $original, self $replica) {
491
+                $original->replicateLineItems($replica);
492
+            })
493
+            ->successRedirectUrl(static function (self $replica) {
494
+                return CreditNoteResource::getUrl('edit', ['record' => $replica]);
495
+            });
496
+    }
497
+
498
+    public static function getApproveAction(string $action = Action::class): MountableAction
499
+    {
500
+        return $action::make('approve')
501
+            ->label('Approve')
502
+            ->icon('heroicon-m-check-circle')
503
+            ->visible(fn (self $record) => $record->isDraft())
504
+            ->requiresConfirmation()
505
+            ->databaseTransaction()
506
+            ->successNotificationTitle('Credit note approved')
507
+            ->action(function (self $record) {
508
+                $record->approveDraft();
509
+            });
510
+    }
511
+
512
+    public function replicateLineItems(Model $target): void
513
+    {
514
+        $this->lineItems->each(function (DocumentLineItem $lineItem) use ($target) {
515
+            $replica = $lineItem->replicate([
516
+                'documentable_id',
517
+                'documentable_type',
518
+                'subtotal',
519
+                'total',
520
+                'created_by',
521
+                'updated_by',
522
+                'created_at',
523
+                'updated_at',
524
+            ]);
525
+
526
+            $replica->documentable_id = $target->id;
527
+            $replica->documentable_type = $target->getMorphClass();
528
+            $replica->save();
529
+
530
+            $replica->adjustments()->sync($lineItem->adjustments->pluck('id'));
531
+        });
532
+    }
533
+}

+ 12
- 0
app/Models/Accounting/DebitNote.php Просмотреть файл

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use Illuminate\Database\Eloquent\Factories\HasFactory;
6
+use Illuminate\Database\Eloquent\Model;
7
+
8
+class DebitNote extends Model
9
+{
10
+    /** @use HasFactory<\Database\Factories\Accounting\DebitNoteFactory> */
11
+    use HasFactory;
12
+}

+ 23
- 0
database/factories/Accounting/CreditNoteFactory.php Просмотреть файл

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace Database\Factories\Accounting;
4
+
5
+use Illuminate\Database\Eloquent\Factories\Factory;
6
+
7
+/**
8
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\CreditNote>
9
+ */
10
+class CreditNoteFactory extends Factory
11
+{
12
+    /**
13
+     * Define the model's default state.
14
+     *
15
+     * @return array<string, mixed>
16
+     */
17
+    public function definition(): array
18
+    {
19
+        return [
20
+            //
21
+        ];
22
+    }
23
+}

+ 23
- 0
database/factories/Accounting/DebitNoteFactory.php Просмотреть файл

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace Database\Factories\Accounting;
4
+
5
+use Illuminate\Database\Eloquent\Factories\Factory;
6
+
7
+/**
8
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\DebitNote>
9
+ */
10
+class DebitNoteFactory extends Factory
11
+{
12
+    /**
13
+     * Define the model's default state.
14
+     *
15
+     * @return array<string, mixed>
16
+     */
17
+    public function definition(): array
18
+    {
19
+        return [
20
+            //
21
+        ];
22
+    }
23
+}

+ 27
- 0
database/migrations/2025_04_22_014110_create_credit_notes_table.php Просмотреть файл

@@ -0,0 +1,27 @@
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
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('credit_notes', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->timestamps();
17
+        });
18
+    }
19
+
20
+    /**
21
+     * Reverse the migrations.
22
+     */
23
+    public function down(): void
24
+    {
25
+        Schema::dropIfExists('credit_notes');
26
+    }
27
+};

+ 27
- 0
database/migrations/2025_04_22_014129_create_debit_notes_table.php Просмотреть файл

@@ -0,0 +1,27 @@
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
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('debit_notes', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->timestamps();
17
+        });
18
+    }
19
+
20
+    /**
21
+     * Reverse the migrations.
22
+     */
23
+    public function down(): void
24
+    {
25
+        Schema::dropIfExists('debit_notes');
26
+    }
27
+};

Загрузка…
Отмена
Сохранить