Polish the manual invoice feature with 4 improvements: 1. Admin invoice form: numeric fields (unit_price, discount_amount) now display values with thousand-separators (e.g. 1,000,000 instead of 1000000) via formatStateUsing/dehydrateStateUsing for easier reading of large Rial amounts. The placeholder/subtotal/total calculations also strip commas before arithmetic. 2. Fix 'total amount payable = 0' bug in the form. The placeholder had a typo: 'foreach ( as )' overwrote the loop variable and made the inner calculation use the array instead of each item, so the sum was always 0. Now correctly iterates with . 3. Customer dashboard widget: add an 'Invoices pending payment' card showing the count, with a link to /my-account/?tab=invoices if > 0. Relabel the existing 'Pending payment' card to 'Pending payment (orders)' to clarify it's for orders, not invoices. 4. Customer profile financial status: include manual invoices in the accounting_balance and pending_payments_total. Show a breakdown list (orders vs invoices) under the balance card, plus two new stat cards (pending invoices count/total, paid invoices count/total) with a link to the invoices tab.
162 lines
4.4 KiB
PHP
162 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
use App\Traits\Auditable;
|
|
|
|
/**
|
|
* ManualInvoice — فاکتور دستی صادر شده توسط مدیر
|
|
*
|
|
* سناریو:
|
|
* - مشتری خدماتی دریافت کرده (مثلاً مشاوره تلفنی، انبارداری اضافی)
|
|
* - مدیر برای او فاکتور صادر میکند با اقلام + مبالغ
|
|
* - مشتری در پورتالش فاکتور را میبیند
|
|
* - مشتری از کیف پول یا درگاه پرداخت میکند
|
|
* - پس از پرداخت، مبلغ در تراکنشهای کیف پول ثبت میشود
|
|
*
|
|
* همه مبالغ به ریال ایران (IRR) ذخیره میشوند.
|
|
*/
|
|
class ManualInvoice extends Model
|
|
{
|
|
use Auditable;
|
|
|
|
protected $table = 'manual_invoices';
|
|
|
|
protected $fillable = [
|
|
'user_id',
|
|
'issued_by',
|
|
'invoice_number',
|
|
'subtotal',
|
|
'discount_amount',
|
|
'total_amount',
|
|
'items',
|
|
'notes_to_customer',
|
|
'admin_notes',
|
|
'status',
|
|
'due_date',
|
|
'paid_at',
|
|
'wallet_transaction_id',
|
|
'payment_id',
|
|
'cancelled_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'items' => 'array', // اقلام بهصورت JSON
|
|
'subtotal' => 'decimal:2',
|
|
'discount_amount' => 'decimal:2',
|
|
'total_amount' => 'decimal:2',
|
|
'due_date' => 'date',
|
|
'paid_at' => 'datetime',
|
|
];
|
|
|
|
// ─── Relationships ───────────────────────────────
|
|
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'user_id');
|
|
}
|
|
|
|
public function issuedBy(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'issued_by');
|
|
}
|
|
|
|
public function cancelledBy(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'cancelled_by');
|
|
}
|
|
|
|
public function walletTransaction(): BelongsTo
|
|
{
|
|
return $this->belongsTo(WalletTransaction::class);
|
|
}
|
|
|
|
public function payment(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Payment::class);
|
|
}
|
|
|
|
// ─── Helpers ─────────────────────────────────────
|
|
|
|
public function isPending(): bool
|
|
{
|
|
return $this->status === 'pending_payment';
|
|
}
|
|
|
|
public function isPaid(): bool
|
|
{
|
|
return $this->status === 'paid';
|
|
}
|
|
|
|
public function isCancelled(): bool
|
|
{
|
|
return $this->status === 'cancelled';
|
|
}
|
|
|
|
public function isExpired(): bool
|
|
{
|
|
return $this->status === 'expired';
|
|
}
|
|
|
|
/**
|
|
* آیا فاکتور منقضی شده است؟ (بررسی بر اساس due_date)
|
|
*/
|
|
public function checkExpired(): bool
|
|
{
|
|
if ($this->status !== 'pending_payment') {
|
|
return false;
|
|
}
|
|
if (! $this->due_date) {
|
|
return false;
|
|
}
|
|
return $this->due_date->isPast();
|
|
}
|
|
|
|
/**
|
|
* تولید شماره فاکتور بعدی.
|
|
* فرمت: INV-2026-000001
|
|
*/
|
|
public static function generateInvoiceNumber(): string
|
|
{
|
|
$year = now()->year;
|
|
$prefix = "INV-{$year}-";
|
|
|
|
$last = self::where('invoice_number', 'like', "{$prefix}%")
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$next = 1;
|
|
if ($last) {
|
|
$parts = explode('-', $last->invoice_number);
|
|
$next = (int) end($parts) + 1;
|
|
}
|
|
|
|
return $prefix . str_pad($next, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
/**
|
|
* محاسبه مبلغ نهایی بر اساس اقلام و تخفیف.
|
|
*/
|
|
public static function calculateTotal(array $items, float $discount = 0): array
|
|
{
|
|
$subtotal = 0;
|
|
foreach ($items as $item) {
|
|
$qty = (float) ($item['quantity'] ?? 1);
|
|
$unit = (float) ($item['unit_price'] ?? 0);
|
|
$subtotal += $qty * $unit;
|
|
}
|
|
|
|
$discount = max(0, $discount);
|
|
$total = max(0, $subtotal - $discount);
|
|
|
|
return [
|
|
'subtotal' => round($subtotal, 2),
|
|
'discount_amount' => round($discount, 2),
|
|
'total_amount' => round($total, 2),
|
|
];
|
|
}
|
|
}
|