feat: add pre-delivery item rejection tracking to invoices
- Add pre_delivery_status column to order_items table - Track approved/rejected items during buyer pre-delivery review - Display rejected items with strikethrough styling in invoices - Add isPreDeliveryRejected() and shouldBeProcessed() helper methods - Show rejection badges on invoice line items - Handle delivered_qty fallback to picked_qty for invoicing - Apply styling to buyer, seller, and PDF invoice views 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ class OrderItem extends Model implements AuditableContract
|
||||
'batch_number',
|
||||
'quantity',
|
||||
'picked_qty',
|
||||
'pre_delivery_status',
|
||||
'delivered_qty',
|
||||
'accepted_qty',
|
||||
'rejected_qty',
|
||||
'rejection_reason',
|
||||
@@ -45,6 +47,7 @@ class OrderItem extends Model implements AuditableContract
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'picked_qty' => 'integer',
|
||||
'delivered_qty' => 'integer',
|
||||
'accepted_qty' => 'integer',
|
||||
'rejected_qty' => 'integer',
|
||||
'unit_price' => 'decimal:2',
|
||||
@@ -132,4 +135,21 @@ class OrderItem extends Model implements AuditableContract
|
||||
|
||||
return ($this->accepted_qty / $this->quantity) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if item was rejected during pre-delivery review
|
||||
*/
|
||||
public function isPreDeliveryRejected(): bool
|
||||
{
|
||||
return $this->pre_delivery_status === 'rejected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if item should be included in picking, delivery, and invoicing
|
||||
* Items rejected pre-delivery are excluded from the fulfillment process
|
||||
*/
|
||||
public function shouldBeProcessed(): bool
|
||||
{
|
||||
return $this->pre_delivery_status !== 'rejected';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
// Add pre_delivery_status to track buyer's pre-delivery approval/rejection
|
||||
// NULL = not yet reviewed (backwards compatibility)
|
||||
// 'approved' = buyer approved item for delivery
|
||||
// 'rejected' = buyer rejected item before delivery (won't be picked/delivered/invoiced)
|
||||
$table->string('pre_delivery_status', 20)->nullable()->after('picked_qty');
|
||||
$table->index('pre_delivery_status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->dropIndex(['pre_delivery_status']);
|
||||
$table->dropColumn('pre_delivery_status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -109,12 +109,15 @@
|
||||
<!-- Order Items List -->
|
||||
<div class="space-y-2 mb-4">
|
||||
@foreach($invoice->order->items as $item)
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="flex justify-between text-sm {{ $item->isPreDeliveryRejected() ? 'opacity-50' : '' }}">
|
||||
<div class="flex-1">
|
||||
<span class="font-medium">{{ $item->product_name }}</span>
|
||||
<span class="font-medium {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_name }}</span>
|
||||
<span class="text-base-content/60 ml-2">× {{ $item->delivered_qty ?? $item->picked_qty }}</span>
|
||||
@if($item->isPreDeliveryRejected())
|
||||
<span class="badge badge-xs badge-ghost ml-2">Rejected</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="font-semibold">${{ number_format($item->line_total, 2) }}</span>
|
||||
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->line_total, 2) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -156,32 +159,58 @@
|
||||
<!-- Invoice Line Items -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Line Items</h2>
|
||||
<h2 class="card-title mb-4">
|
||||
<span class="icon-[lucide--package] size-5"></span>
|
||||
Line Items
|
||||
</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Brand</th>
|
||||
<th>Quantity</th>
|
||||
<th>Quantity (Fulfilled/Ordered)</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoice->order->items as $item)
|
||||
<tr>
|
||||
<tr class="{{ $item->isPreDeliveryRejected() ? 'opacity-60' : '' }}">
|
||||
<td>
|
||||
<div class="font-semibold">{{ $item->product_name }}</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if($item->product)
|
||||
<div class="avatar">
|
||||
<div class="w-12 h-12 rounded-lg bg-base-200">
|
||||
@if($item->product->image_path)
|
||||
<img src="{{ Storage::url($item->product->image_path) }}" alt="{{ $item->product_name }}">
|
||||
@else
|
||||
<span class="icon-[lucide--package] size-6 text-base-content/40"></span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
|
||||
{{ $item->product_name }}
|
||||
@if($item->isPreDeliveryRejected())
|
||||
<span class="badge badge-sm badge-ghost ml-2">Rejected</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="font-mono text-sm">{{ $item->product_sku }}</td>
|
||||
<td>{{ $item->brand_name }}</td>
|
||||
<td class="font-mono text-sm {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_sku }}</td>
|
||||
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->brand_name }}</td>
|
||||
<td>
|
||||
<span class="font-semibold">{{ $item->delivered_qty ?? $item->picked_qty }}</span>
|
||||
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
|
||||
{{ $item->delivered_qty ?? $item->picked_qty }}
|
||||
<span class="text-gray-400">/{{ $item->quantity }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->unit_price, 2) }}</td>
|
||||
<td class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
|
||||
${{ number_format($item->line_total, 2) }}
|
||||
</td>
|
||||
<td>${{ number_format($item->unit_price, 2) }}</td>
|
||||
<td class="font-semibold">${{ number_format($item->line_total, 2) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@@ -190,6 +219,12 @@
|
||||
<td colspan="5" class="text-right">Subtotal:</td>
|
||||
<td>${{ number_format($invoice->subtotal, 2) }}</td>
|
||||
</tr>
|
||||
@if($invoice->tax > 0)
|
||||
<tr>
|
||||
<td colspan="5" class="text-right">Tax:</td>
|
||||
<td>${{ number_format($invoice->tax, 2) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr class="font-bold text-lg">
|
||||
<td colspan="5" class="text-right">Total:</td>
|
||||
<td class="text-primary">${{ number_format($invoice->total, 2) }}</td>
|
||||
|
||||
@@ -331,13 +331,18 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($items as $item)
|
||||
<tr>
|
||||
<td>{{ $item->product_sku }}</td>
|
||||
<td>{{ $item->product_name }}</td>
|
||||
<td>{{ $item->brand_name }}</td>
|
||||
<td class="right">{{ number_format($item->delivered_qty ?? $item->picked_qty, 0) }}</td>
|
||||
<td class="right">${{ number_format($item->unit_price, 2) }}</td>
|
||||
<td class="right">${{ number_format($item->line_total, 2) }}</td>
|
||||
<tr @if($item->isPreDeliveryRejected()) style="opacity: 0.6;" @endif>
|
||||
<td @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>{{ $item->product_sku }}</td>
|
||||
<td @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>
|
||||
{{ $item->product_name }}
|
||||
@if($item->isPreDeliveryRejected())
|
||||
<span style="color: #6b7280; font-size: 9pt; font-weight: bold;"> [REJECTED]</span>
|
||||
@endif
|
||||
</td>
|
||||
<td @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>{{ $item->brand_name }}</td>
|
||||
<td class="right" @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>{{ number_format($item->delivered_qty ?? $item->picked_qty, 0) }}</td>
|
||||
<td class="right" @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>${{ number_format($item->unit_price, 2) }}</td>
|
||||
<td class="right" @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>${{ number_format($item->line_total, 2) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<span class="icon-[lucide--arrow-left] size-5"></span>
|
||||
Back to Order
|
||||
</a>
|
||||
<a href="{{ route('seller.business.invoices.pdf', [$business->slug, $invoice]) }}" class="btn btn-primary" target="_blank">
|
||||
<span class="icon-[lucide--download] size-5"></span>
|
||||
Download PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +78,7 @@
|
||||
<!-- Invoice Information Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<!-- Invoice Details -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
<span class="icon-[lucide--file-text] size-5 inline"></span>
|
||||
@@ -107,7 +111,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Company Information -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
<span class="icon-[lucide--building-2] size-5 inline"></span>
|
||||
@@ -153,47 +157,65 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Summary -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<!-- Order Summary -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
<span class="icon-[lucide--dollar-sign] size-5 inline"></span>
|
||||
Payment Summary
|
||||
</h3>
|
||||
<h3 class="font-bold text-lg mb-4">Order Summary</h3>
|
||||
|
||||
<!-- Order Items List -->
|
||||
<div class="space-y-2 mb-4">
|
||||
@foreach($invoice->order->items as $item)
|
||||
<div class="flex justify-between text-sm {{ $item->isPreDeliveryRejected() ? 'opacity-50' : '' }}">
|
||||
<div class="flex-1">
|
||||
<span class="font-medium {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_name }}</span>
|
||||
<span class="text-base-content/60 ml-2">× {{ $item->delivered_qty ?? $item->picked_qty }}</span>
|
||||
@if($item->isPreDeliveryRejected())
|
||||
<span class="badge badge-xs badge-ghost ml-2">Rejected</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->line_total, 2) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="divider my-2"></div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
@if($invoice->order && $invoice->order->surcharge > 0)
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Subtotal</span>
|
||||
<span class="font-semibold">${{ number_format($invoice->subtotal, 2) }}</span>
|
||||
</div>
|
||||
@if($invoice->order && $invoice->order->surcharge > 0)
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Payment Terms Surcharge ({{ \App\Models\Order::getSurchargePercentage($invoice->order->payment_terms) }}%)</span>
|
||||
<span class="font-semibold">${{ number_format($invoice->order->surcharge, 2) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="font-bold">Total</span>
|
||||
<span class="font-bold text-primary">${{ number_format($invoice->total, 2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Amount Paid</span>
|
||||
<span class="font-semibold text-success">${{ number_format($invoice->amount_paid, 2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="font-bold">Amount Due</span>
|
||||
<span class="font-bold text-error">${{ number_format($invoice->amount_due, 2) }}</span>
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Payment Terms</p>
|
||||
<p class="font-semibold">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Payment Terms</span>
|
||||
<span class="font-semibold">
|
||||
@if($invoice->order->payment_terms === 'cod')
|
||||
COD
|
||||
@else
|
||||
{{ ucfirst(str_replace('_', ' ', $invoice->order->payment_terms)) }}
|
||||
@endif
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Payment Status</span>
|
||||
<div>
|
||||
<span class="badge badge-sm {{ $invoice->payment_status === 'paid' ? 'badge-success' : ($invoice->isOverdue() ? 'badge-error' : 'badge-warning') }}">
|
||||
{{ ucfirst(str_replace('_', ' ', $invoice->payment_status)) }}
|
||||
@if($invoice->isOverdue())
|
||||
(Overdue)
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,7 +223,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Invoice Line Items -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
<span class="icon-[lucide--package] size-5"></span>
|
||||
@@ -221,7 +243,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoice->order->items as $item)
|
||||
<tr>
|
||||
<tr class="{{ $item->isPreDeliveryRejected() ? 'opacity-60' : '' }}">
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
@if($item->product)
|
||||
@@ -235,20 +257,25 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="font-semibold">{{ $item->product_name }}</div>
|
||||
<div class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
|
||||
{{ $item->product_name }}
|
||||
@if($item->isPreDeliveryRejected())
|
||||
<span class="badge badge-sm badge-ghost ml-2">Rejected</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="font-mono text-sm">{{ $item->product_sku }}</td>
|
||||
<td>{{ $item->brand_name }}</td>
|
||||
<td class="font-mono text-sm {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_sku }}</td>
|
||||
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->brand_name }}</td>
|
||||
<td>
|
||||
<span class="font-semibold">
|
||||
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
|
||||
{{ $item->delivered_qty ?? $item->picked_qty }}
|
||||
<span class="text-gray-400">/{{ $item->quantity }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>${{ number_format($item->unit_price, 2) }}</td>
|
||||
<td class="font-semibold">
|
||||
${{ number_format(($item->delivered_qty ?? $item->picked_qty) * $item->unit_price, 2) }}
|
||||
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->unit_price, 2) }}</td>
|
||||
<td class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
|
||||
${{ number_format($item->line_total, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -276,7 +303,7 @@
|
||||
|
||||
<!-- Change History -->
|
||||
@if(isset($invoice->changes) && $invoice->changes->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow-lg mt-6">
|
||||
<div class="card bg-base-100 shadow mt-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
<span class="icon-[lucide--history] size-5"></span>
|
||||
@@ -322,7 +349,7 @@
|
||||
|
||||
@if($invoice->notes)
|
||||
<!-- Notes Section -->
|
||||
<div class="card bg-base-100 shadow-lg mt-6">
|
||||
<div class="card bg-base-100 shadow mt-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
<span class="icon-[lucide--message-square] size-5"></span>
|
||||
|
||||
Reference in New Issue
Block a user