diff --git a/04_Laravel/app/Http/Controllers/ShipmentPdfController.php b/04_Laravel/app/Http/Controllers/ShipmentPdfController.php index bd3d5b8..dcf9d6f 100644 --- a/04_Laravel/app/Http/Controllers/ShipmentPdfController.php +++ b/04_Laravel/app/Http/Controllers/ShipmentPdfController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Enums\ShipmentType; use App\Models\Shipment; use App\Services\PdfService; use Illuminate\Http\Request; @@ -11,49 +12,119 @@ class ShipmentPdfController extends Controller { public function __construct(protected PdfService $pdf) {} + /** + * تولید AWB PDF (برای همه نوع محموله‌ها) + */ public function awb(Shipment $shipment) { try { Log::info('Generating AWB PDF', ['shipment_id' => $shipment->id]); + $content = $this->pdf->awb($shipment); - Log::info('AWB PDF generated successfully'); + + Log::info('AWB PDF generated successfully', ['awb' => $shipment->awb_no]); + return response($content, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="AWB-' . $shipment->awb_no . '.pdf"', ]); } catch (\Throwable $e) { Log::error('AWB PDF generation failed', [ + 'shipment_id' => $shipment->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); + return response('PDF generation failed: ' . $e->getMessage(), 500); } } + /** + * تولید Invoice PDF — فقط برای محموله‌های دارای کالا (PARCEL) + * + * محموله‌های DOC (DOC_NORMAL/DOC_ECONOMY) که کالا ندارن، + * فاکتور صادراتی ندارن و این متد براشون ارور برمی‌گردونه. + */ public function invoice(Shipment $shipment) { try { + // بارگذاری items برای بررسی + $shipment->loadMissing(['items']); + + // بررسی نوع محموله + $isDocType = in_array($shipment->type, [ + ShipmentType::DocNormal, + ShipmentType::DocEconomy, + ]); + + if ($isDocType && $shipment->items->isEmpty()) { + Log::info('Invoice PDF not available for DOC shipment without items', [ + 'shipment_id' => $shipment->id, + 'type' => $shipment->type?->value, + ]); + + return response()->json([ + 'success' => false, + 'message' => 'فاکتور فقط برای محموله‌های دارای کالا (PARCEL) صادر می‌شود.', + 'hint' => 'محموله‌های DOC (مدارک) فاکتور صادراتی ندارند.', + ], 400); + } + $content = $this->pdf->invoice($shipment); + + Log::info('Invoice PDF generated successfully', [ + 'awb' => $shipment->awb_no, + 'type' => $shipment->type?->value, + 'items_count' => $shipment->items->count(), + ]); + return response($content, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="INVOICE-' . $shipment->awb_no . '.pdf"', ]); + } catch (\InvalidArgumentException $e) { + // خطای مربوط به نوع محموله + Log::info('Invoice PDF skipped', [ + 'shipment_id' => $shipment->id, + 'reason' => $e->getMessage(), + ]); + + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); } catch (\Throwable $e) { - Log::error('Invoice PDF generation failed', ['error' => $e->getMessage()]); + Log::error('Invoice PDF generation failed', [ + 'shipment_id' => $shipment->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return response('PDF generation failed: ' . $e->getMessage(), 500); } } + /** + * تولید Label PDF — اندازه استاندارد لیبل پستی (100x150mm) + */ public function label(Shipment $shipment) { try { $content = $this->pdf->label($shipment); + + Log::info('Label PDF generated successfully', ['awb' => $shipment->awb_no]); + return response($content, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="LABEL-' . $shipment->awb_no . '.pdf"', ]); } catch (\Throwable $e) { - Log::error('Label PDF generation failed', ['error' => $e->getMessage()]); + Log::error('Label PDF generation failed', [ + 'shipment_id' => $shipment->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return response('PDF generation failed: ' . $e->getMessage(), 500); } } diff --git a/04_Laravel/app/Services/PdfService.php b/04_Laravel/app/Services/PdfService.php index 3b6d57c..8320d53 100644 --- a/04_Laravel/app/Services/PdfService.php +++ b/04_Laravel/app/Services/PdfService.php @@ -2,30 +2,21 @@ namespace App\Services; +use App\Enums\ShipmentType; use App\Models\Shipment; +use App\Models\ShipmentItem; use Dompdf\Dompdf; use Dompdf\Options; use Illuminate\Support\Collection; -use Picqer\Barcode\BarcodeGeneratorHTML; +use Illuminate\Support\Facades\Log; +use Picqer\Barcode\BarcodeGeneratorPNG; class PdfService { + /** + * تولید AWB PDF (برای همه نوع محموله‌ها) + */ public function awb(Shipment $shipment): string - { - $shipment->loadMissing(['fromCountry', 'toCountry', 'items', 'packages']); - - $data = [ - 'shipment' => $shipment, - 'shipper' => $this->formatAddress($shipment, 'sender'), - 'receiver' => $this->formatAddress($shipment, 'receiver'), - 'items' => $shipment->items ?? collect(), - 'barcode' => $this->generateBarcode($shipment->awb_no), - ]; - - return $this->generatePdf(view('pdfs.awb', $data)->render(), 'A4', 'portrait'); - } - - public function invoice(Shipment $shipment): string { $shipment->loadMissing(['fromCountry', 'toCountry', 'items']); @@ -33,65 +24,156 @@ class PdfService 'shipment' => $shipment, 'shipper' => $this->formatAddress($shipment, 'sender'), 'receiver' => $this->formatAddress($shipment, 'receiver'), - 'items' => $shipment->isParcel() ? ($shipment->items ?? collect()) : collect(), - 'invoice_total_usd' => $shipment->invoice_total_usd, - 'barcode' => $this->generateBarcode($shipment->awb_no), + 'items' => $shipment->items ?? collect(), + 'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0, + 'barcode_base64' => $this->generateBarcode($shipment->awb_no), + 'service_label' => $this->getServiceLabel($shipment), + 'type_label' => $this->getTypeLabel($shipment), ]; - return $this->generatePdf(view('pdfs.invoice', $data)->render(), 'A4', 'portrait'); + $html = view('pdfs.awb', $data)->render(); + + return $this->generatePdf($html, 'A4', 'portrait'); } + /** + * تولید Invoice PDF — فقط برای PARCEL و DOC_NORMAL/DOC_ECONOMY + * (نه برای محموله‌های DOC خالص که کالا ندارن) + */ + public function invoice(Shipment $shipment): string + { + // اگه محموله DOC هست و آیتم نداره، invoice تولید نکن + if ($shipment->type === ShipmentType::DocNormal || $shipment->type === ShipmentType::DocEconomy) { + if ($shipment->items->isEmpty()) { + throw new \InvalidArgumentException( + 'فاکتور فقط برای محموله‌های دارای کالا (PARCEL) صادر می‌شود. محموله‌های DOC بدون کالا فاکتور ندارند.' + ); + } + } + + $shipment->loadMissing(['fromCountry', 'toCountry', 'items']); + + $data = [ + 'shipment' => $shipment, + 'shipper' => $this->formatAddress($shipment, 'sender'), + 'receiver' => $this->formatAddress($shipment, 'receiver'), + 'items' => $shipment->items ?? collect(), + 'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0, + 'barcode_base64' => $this->generateBarcode($shipment->awb_no), + ]; + + $html = view('pdfs.invoice', $data)->render(); + + return $this->generatePdf($html, 'A4', 'portrait'); + } + + /** + * تولید Label PDF — اندازه استاندارد لیبل پستی (100x150mm) + */ public function label(Shipment $shipment): string { $shipment->loadMissing(['fromCountry', 'toCountry']); $data = [ 'shipment' => $shipment, - 'shipper' => $this->formatAddress($shipment, 'sender'), - 'receiver' => $this->formatAddress($shipment, 'receiver'), - 'barcode' => $this->generateBarcode($shipment->awb_no), + 'barcode_base64' => $this->generateBarcode($shipment->awb_no), + 'origin_iso' => $shipment->fromCountry?->iso_code ?? 'IR', + 'dest_iso' => $shipment->toCountry?->iso_code ?? '', ]; - return $this->generatePdf(view('pdfs.label', $data)->render(), 'A4', 'portrait'); + $html = view('pdfs.label', $data)->render(); + + // A5 افقی (Landscape) — استاندارد لیبل پستی: 210×148 mm + return $this->generatePdf($html, 'A5', 'landscape'); } - private function generatePdf(string $html, string $paper, string $orientation): string + /** + * تولید بارکد از AWB number — base64 embed (مطمئن‌ترین روش) + */ + private function generateBarcode(string $awbNo): string + { + try { + $generator = new BarcodeGeneratorPNG(); + $barcode = $generator->getBarcode($awbNo, $generator::TYPE_CODE_128, 2, 40); + + // تبدیل به base64 و استفاده از data URI + return 'data:image/png;base64,' . base64_encode($barcode); + } catch (\Throwable $e) { + Log::error('Barcode generation failed', [ + 'awb' => $awbNo, + 'error' => $e->getMessage(), + ]); + return ''; + } + } + + /** + * برچسب Service برای AWB (Outbound/Inbound) + */ + private function getServiceLabel(Shipment $shipment): string + { + return match ($shipment->direction?->value) { + 'export' => 'Outbound', + 'import' => 'Inbound', + default => 'Outbound', + }; + } + + /** + * برچسب Type برای AWB (NON DOC / DOC) + */ + private function getTypeLabel(Shipment $shipment): string + { + return match ($shipment->type) { + ShipmentType::Parcel => 'NON DOC', + ShipmentType::DocNormal => 'DOC NORMAL', + ShipmentType::DocEconomy => 'DOC ECONOMY', + default => 'NON DOC', + }; + } + + /** + * تولید PDF از HTML + */ + private function generatePdf(string $html, array|string $paper, string $orientation): string { $options = new Options(); $options->set('isHtml5ParserEnabled', true); - $options->set('isRemoteEnabled', true); + $options->set('isRemoteEnabled', true); // مهم: برای data URI $options->set('defaultFont', 'DejaVu Sans'); + $options->set('dpi', 150); + $options->set('debugKeepTemp', false); + $options->set('debugCss', false); + $options->set('debugLayout', false); $dompdf = new Dompdf($options); $dompdf->loadHtml($html); - $dompdf->setPaper($paper, $orientation); + + if (is_array($paper)) { + $dompdf->setPaper($paper, $orientation); + } else { + $dompdf->setPaper($paper, $orientation); + } + $dompdf->render(); return $dompdf->output(); } + /** + * فرمت‌بندی آدرس فرستنده/گیرنده + */ private function formatAddress(Shipment $shipment, string $prefix): Collection { return collect([ - 'name' => $shipment->{"{$prefix}_name"}, - 'company' => $shipment->{"{$prefix}_company"}, - 'phone' => $shipment->{"{$prefix}_phone"}, - 'email' => $shipment->{"{$prefix}_email"}, - 'address' => $shipment->{"{$prefix}_address"}, - 'city' => $shipment->{"{$prefix}_city"}, - 'state' => $shipment->{"{$prefix}_state"}, - 'zip' => $shipment->{"{$prefix}_zip"}, - 'id_number' => $shipment->{"{$prefix}_id_number"}, + 'name' => $shipment->{$prefix . '_name'}, + 'company' => $shipment->{$prefix . '_company'}, + 'phone' => $shipment->{$prefix . '_phone'}, + 'email' => $shipment->{$prefix . '_email'}, + 'address' => $shipment->{$prefix . '_address'}, + 'city' => $shipment->{$prefix . '_city'}, + 'zip' => $shipment->{$prefix . '_zip'}, + 'id_number' => $shipment->{$prefix . '_id_number'}, ]); } - - private function generateBarcode(string $code): string - { - try { - $generator = new BarcodeGeneratorHTML(); - return $generator->getBarcode($code, $generator::TYPE_CODE_128, 2, 60); - } catch (\Throwable $e) { - return '
Barcode unavailable
'; - } - } } diff --git a/04_Laravel/composer.json b/04_Laravel/composer.json index bae38c1..e8e1c23 100644 --- a/04_Laravel/composer.json +++ b/04_Laravel/composer.json @@ -8,6 +8,7 @@ "require": { "php": "^8.2", "barryvdh/laravel-dompdf": "*", + "doctrine/dbal": "^4.4", "filament/filament": "3.3.*", "laravel/framework": "^11.0", "laravel/sanctum": "^4.0", diff --git a/04_Laravel/composer.lock b/04_Laravel/composer.lock index 0963828..da43df9 100644 --- a/04_Laravel/composer.lock +++ b/04_Laravel/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a22b2cc91f8cab3cca421c6ab381c544", + "content-hash": "318d369da85cca6a3cf11f3c2f6a55de", "packages": [ { "name": "anourvalar/eloquent-serialize", diff --git a/04_Laravel/database/migrations/2026_08_29_021506_fix_shipment_items_table.php b/04_Laravel/database/migrations/2026_08_29_021506_fix_shipment_items_table.php new file mode 100644 index 0000000..70eb8e6 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_29_021506_fix_shipment_items_table.php @@ -0,0 +1,41 @@ +unsignedTinyInteger('row_number')->default(1)->after('shipment_id'); + } + + // اضافه‌کردن unit_price اگر وجود نداره (متفاوت از unit_price_usd) + if (!Schema::hasColumn('shipment_items', 'unit_price')) { + $table->decimal('unit_price', 12, 2)->default(0)->after('quantity'); + } + }); + + // کپی داده‌ها از unit_price_usd به unit_price اگر مقدار داره + \DB::statement('UPDATE shipment_items SET unit_price = unit_price_usd WHERE unit_price_usd IS NOT NULL AND unit_price = 0'); + + // کپی داده‌ها از name به description اگر description خالیه + \DB::statement('UPDATE shipment_items SET description = name WHERE (description IS NULL OR description = "") AND name IS NOT NULL'); + } + + public function down(): void + { + Schema::table('shipment_items', function (Blueprint $table) { + if (Schema::hasColumn('shipment_items', 'row_number')) { + $table->dropColumn('row_number'); + } + if (Schema::hasColumn('shipment_items', 'unit_price')) { + $table->dropColumn('unit_price'); + } + }); + } +}; \ No newline at end of file diff --git a/04_Laravel/database/migrations/2026_08_29_021631_make_name_nullable_in_shipment_items.php b/04_Laravel/database/migrations/2026_08_29_021631_make_name_nullable_in_shipment_items.php new file mode 100644 index 0000000..6e74be9 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_29_021631_make_name_nullable_in_shipment_items.php @@ -0,0 +1,22 @@ +string('name')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('shipment_items', function (Blueprint $table) { + $table->string('name')->nullable(false)->change(); + }); + } +}; \ No newline at end of file diff --git a/04_Laravel/public/barcodes/barcode_IFN-2026-92701.png b/04_Laravel/public/barcodes/barcode_IFN-2026-92701.png new file mode 100644 index 0000000..3dc312a Binary files /dev/null and b/04_Laravel/public/barcodes/barcode_IFN-2026-92701.png differ diff --git a/04_Laravel/resources/views/pdfs/awb.blade.php b/04_Laravel/resources/views/pdfs/awb.blade.php index 01fb78a..b89455b 100644 --- a/04_Laravel/resources/views/pdfs/awb.blade.php +++ b/04_Laravel/resources/views/pdfs/awb.blade.php @@ -1,200 +1,358 @@ - + -
+
+
- -
-
Shipment Waybill
-
We Deliver Value
+
+ +
IFNEx
+
+
+

Shipment Waybill

+
We Deliver Value
+
+
+ Date: {{ $shipment->created_at?->format('m/d/Y') }}
-
AWB No: {{ $shipment->awb_no }}
- - - - - -
-
-
SHIPPER
-
From:{{ $shipment->fromCountry?->name }}
-
Company Name:{{ $shipper['company'] }}
-
Contact Name:{{ $shipper['name'] }}
-
Tel/Mob:{{ $shipper['phone'] }}
-
Email:{{ $shipper['email'] }}
-
Address:{{ $shipper['address'] }}
-
ID Number:{{ $shipper['id_number'] }}
-
Zip Code:{{ $shipper['zip'] }}
-
-
-
-
RECEIVER
-
To:{{ $shipment->toCountry?->name }}
-
Company Name:{{ $receiver['company'] }}
-
Contact Name:{{ $receiver['name'] }}
-
Tel/Mobile:{{ $receiver['phone'] }}
-
Email:{{ $receiver['email'] }}
-
Address:{{ $receiver['address'] }}
-
City:{{ $receiver['city'] }}
-
Zip Code:{{ $receiver['zip'] }}
-
-
- - - - - - -
-
-
SHIPMENT
-
Gross Weight:{{ $shipment->weight }} Kg
-
Dimensions:{{ $shipment->dimensions }} cm
-
Volumetric Weight:{{ $shipment->volumetric_weight }} Kg
-
Chargeable Weight:{{ $shipment->chargeable_weight }} Kg
-
Value:${{ number_format($shipment->declared_value ?? 0, 2) }} USD
-
Service:{{ $shipment->type?->label() }}
-
Type:{{ $shipment->direction?->value === 'export' ? 'Export' : 'Import' }}
-
Content:{{ $shipment->content_description ?: 'General Goods' }}
- - @if($shipment->isParcel() && $items->count() > 0) -
-
Shipment Details
- - - - - - - - - - - - - @foreach($items as $item) - - - - - - - - - @endforeach - -
RowDescriptionHS CodeQtyUnit Price (USD)Total (USD)
{{ $item->row_number }}{{ $item->description }}{{ $item->hs_code }}{{ $item->quantity }}{{ number_format($item->unit_price, 2) }}{{ number_format($item->total_usd, 2) }}
-
- @endif -
-
-
-
PAYMENT
-
Shipping Price:{{ number_format($shipment->shipping_price ?? 0, 2) }} IRR
-
Extra Service:{{ number_format($shipment->extra_service ?? 0) }} IRR
-
Domestic Pickup:{{ number_format($shipment->domestic_pickup ?? 0) }} IRR
-
Packing Cost:{{ number_format($shipment->packing_cost ?? 0) }} IRR
-
Domestic Delivery:{{ number_format($shipment->domestic_delivery ?? 0) }} IRR
-
Warehousing Cost:{{ number_format($shipment->warehousing_cost ?? 0) }} IRR
-
Discount:{{ number_format($shipment->discount ?? 0) }} IRR
-
Total Fee:{{ number_format($shipment->total_fee ?? 0) }} IRR
-
Cash on Delivery:{{ number_format($shipment->cod_amount ?? 0, 2) }} AED
-
-
- -
- Please note that: Door-to-door Services is only applicable when the shipment is general cargo. For special cargo, dangerous goods, or items requiring special handling, additional charges may apply. The receiver is responsible for customs clearance and any associated fees. Claims must be filed within 30 days of delivery. Shipper declares that the contents are accurately described and properly packed for transport. + +
+ @if($barcode_base64) + barcode + @endif +
{{ $shipment->awb_no }}
-
+ +
+
+
SHIPPER
+
From: {{ $shipper['city'] }}, {{ $shipment->fromCountry?->name }} ({{ $shipment->fromCountry?->iso_code }})
+
Company Name:{{ $shipper['company'] ?: '—' }}
+
Contact Name:{{ $shipper['name'] }}
+
Tel / Mob:{{ $shipper['phone'] }}
+
Email:{{ $shipper['email'] ?: '—' }}
+
Address:{{ $shipper['address'] }}
+
ID Number:{{ $shipper['id_number'] ?: '—' }}
+
Zip Code:{{ $shipper['zip'] ?: '—' }}
+
- @if($shipment->relationLoaded('packages') && $shipment->packages->count() > 1) -
-
PACKAGES ({{ $shipment->packages->count() }})
- +
+
RECEIVER
+
To: {{ $receiver['city'] }}, {{ $shipment->toCountry?->name }} ({{ $shipment->toCountry?->iso_code }})
+
Company Name:{{ $receiver['company'] ?: '—' }}
+
Contact Name:{{ $receiver['name'] }}
+
Tel / Mobile:{{ $receiver['phone'] }}
+
Email:{{ $receiver['email'] ?: '—' }}
+
Address:{{ $receiver['address'] }}
+
City:{{ $receiver['city'] ?: '—' }}
+
Zip Code:{{ $receiver['zip'] ?: '—' }}
+
+ + + +
+
+
SHIPMENT
+
Gross Weight:{{ $shipment->weight }} (Kg)
+
Dimensions:{{ $shipment->dimensions ?: '—' }} (cm)
+
Volumetric Weight:{{ $shipment->volumetric_weight }} (Kg)
+
Chargeable Weight:{{ $shipment->chargeable_weight ?: max($shipment->weight, $shipment->volumetric_weight) }} (Kg)
+
Value:{{ $invoice_total_usd > 0 ? number_format($invoice_total_usd, 0) . ' (USD)' : '—' }}
+
Service:{{ $service_label }}
+
Type:{{ $type_label }}
+
Content:{{ $shipment->content_description ?: $shipment->reason_for_export ?: 'General Goods' }}
+
+ +
+
PAYMENT
+
Shipping Price:{{ $shipment->shipping_price ? number_format($shipment->shipping_price, 2) : '—' }}
+
Extra Service:{{ number_format($shipment->extra_service ?? 0) }}
+
Domestic Pickup:{{ number_format($shipment->domestic_pickup ?? 0) }}
+
Packing Cost:{{ number_format($shipment->packing_cost ?? 0) }}
+
Domestic Delivery:{{ number_format($shipment->domestic_delivery ?? 0) }}
+
Warehousing:{{ number_format($shipment->warehousing_cost ?? 0) }}
+
Discount:{{ number_format($shipment->discount ?? 0) }}
+
Total Fee:{{ number_format($shipment->total_fee ?? 0) }} IRR
+
Cash on Delivery:{{ $shipment->cod_amount ? number_format($shipment->cod_amount, 2) . ' AED' : '—' }}
+
+
+ + + @if($items->isNotEmpty()) +
+
Shipment Details
+
- - - - - - - - + + + + + + + - @foreach($shipment->packages as $pkg) + @foreach($items as $item) - - - - - - - + + + + + + @endforeach + + + +
#Weight (kg)Vol. Wt (kg)Chg. Wt (kg)DimensionsValue (USD)Content
NoDescriptionH.S. CodeQtyUnit Price (USD)Total (USD)
{{ $pkg->package_no }}{{ number_format($pkg->weight, 2) }}{{ number_format($pkg->volumetric_weight, 2) }}{{ number_format($pkg->chargeable_weight, 2) }}{{ $pkg->dimensions }}{{ number_format($pkg->declared_value, 2) }}{{ $pkg->content_description }}{{ $item->row_number }}{{ $item->description }}{{ $item->hs_code }}{{ $item->quantity }}{{ number_format($item->unit_price, 2) }}{{ number_format($item->total_usd, 2) }}
Total Invoice Amount in USD:{{ number_format($invoice_total_usd, 2) }}
@endif -
+ +
+ Please note that:
+ Door-to-door Services is only applicable when the shipment is general cargo and does not require any special approvals or regulations in origin & destination.
+ Only Consignee is aware of the importing rules in the destination and consignee is the person who is in contact with the shipper.
+ The Courier Company cannot guarantee the clearance since All shipments are subjected to customs approval at Origin and Destination. +
+ + +
- Shipper Name & Signature
- Date: ....../....../........ + Shipper Name & Signature: +
- Track Your Shipment at:
- http://ifnex.net + Date: +
...../...../..........
+
diff --git a/04_Laravel/resources/views/pdfs/invoice.blade.php b/04_Laravel/resources/views/pdfs/invoice.blade.php index b35f00e..36a88ba 100644 --- a/04_Laravel/resources/views/pdfs/invoice.blade.php +++ b/04_Laravel/resources/views/pdfs/invoice.blade.php @@ -1,164 +1,362 @@ - + -
+
+

INVOICE

-
- DATE: {{ $shipment->created_at?->format('Y-m-d') }} - INVOICE NO: {{ $shipment->awb_no }} +
+ DATE: {{ $shipment->created_at?->format('m/d/Y') }} + INVOICE NO: {{ $shipment->awb_no }}
+
-
+
SHIPPER
-
Name:{{ $shipper['name'] }}
-
Company Name:{{ $shipper['company'] }}
-
Contact Person:{{ $shipper['name'] }}
-
Address:{{ $shipper['address'] }}
-
Phone:{{ $shipper['phone'] }}
-
Email:{{ $shipper['email'] }}
+
SHIPPER:{{ $shipper['city'] }}, {{ $shipment->fromCountry?->name }} ({{ $shipment->fromCountry?->iso_code }})
+
COMPANY NAME:{{ $shipper['company'] ?: '—' }}
+
CONTACT PERSON:{{ $shipper['name'] }}
+
ADDRESS:{{ $shipper['address'] }}
+
PHONE:{{ $shipper['phone'] }}
+
EMAIL:{{ $shipper['email'] ?: '—' }}
-
+
CONSIGNEE
-
Name:{{ $receiver['name'] }}
-
Company Name:{{ $receiver['company'] }}
-
Contact Person:{{ $receiver['name'] }}
-
Address:{{ $receiver['address'] }}
-
Zip Code & City:{{ $receiver['zip'] }} {{ $receiver['city'] }}
-
Phone:{{ $receiver['phone'] }}
-
Email:{{ $receiver['email'] }}
+
CONSIGNEE:{{ $receiver['city'] }}, {{ $shipment->toCountry?->name }} ({{ $shipment->toCountry?->iso_code }})
+
COMPANY NAME:{{ $receiver['company'] ?: '—' }}
+
CONTACT PERSON:{{ $receiver['name'] }}
+
ADDRESS:{{ $receiver['address'] }}
+
Zip Code & City:{{ $receiver['zip'] }} {{ $receiver['city'] }}
+
PHONE:{{ $receiver['phone'] }}
+
EMAIL:{{ $receiver['email'] ?: '—' }}
-
-
CONTENT
-

{{ $shipment->content_description ?: 'General Goods' }}

-
+ +
+
Content
+
{{ $shipment->content_description ?: 'General Goods' }}
- @if($shipment->isParcel() && $items->count() > 0) -
-
SHIPMENT ITEMS
- + - - - - + + + + - @foreach($items as $item) + @forelse($items as $item) - + - - - - + + + + + + @empty + + @endforelse + + {{-- Fill empty rows to match reference (9 rows) --}} + @for($i = $items->count() + 1; $i <= 9; $i++) + + + + + + + + + @endfor + + + + - @endforeach
NONO DESCRIPTIONH.S. CODEQUANTITYUNIT PRICE (USD)TOTAL IN USDH.S. CODEQUANTITYUNIT PRICETOTAL IN USD
{{ $item->row_number }}{{ $item->row_number }} {{ $item->description }}{{ $item->hs_code }}{{ $item->quantity }}{{ number_format($item->unit_price, 2) }}{{ number_format($item->total_usd, 2) }}{{ $item->hs_code }}{{ $item->quantity }}{{ number_format($item->unit_price, 2) }}{{ number_format($item->total_usd, 2) }}
No items
{{ $i }} 
TOTAL INVOICE AMOUNT IN USD{{ number_format($invoice_total_usd, 2) }}
-
- TOTAL INVOICE AMOUNT IN USD: {{ number_format($invoice_total_usd, 2) }} -
- @endif -
-
-
WEIGHT:{{ $shipment->weight }} KG
-
DIMENSION:{{ $shipment->dimensions }} CM
+ +
+
+
Gross Weight
+
{{ $shipment->weight }} KG
-
-
REASON FOR EXPORT:{{ $shipment->reason_for_export ?: 'N/A' }}
+
+
Volumetric Weight
+
{{ $shipment->volumetric_weight }} KG
+
+
+
W * L * H (cm)
+
{{ $shipment->dimensions ?: '—' }}
+ +
+
REASON FOR EXPORT
+
+ {{ strtoupper($shipment->reason_for_export ?: 'ITEM BEING SENT AS A SAMPLE, NOT FOR SALE') }} +
+
+ +
- I HEREBY STATE THAT THE ABOVE INFORMATION IS TRUE AND I, THE UNDERSIGNED, AM LIABLE FOR ANY CONSEQUENCES THAT MAY ARISE DUE TO NON-DISCLOSURE OF FACTS -
- -
-
- NAME - SIGNATURE -
-
- NAME - SIGNATURE + I HEREBY STATE THAT THE ABOVE INFORMATION IS TRUE AND CORRECT TO THE BEST OF MY KNOWLEDGE AND I, THE UNDERSIGNED, AM LIABLE FOR ANY CONSEQUENCES THAT MAY ARISE DUE TO NON-DISCLOSURE OF FACTS. +
+
+ NAME - SIGNATURE +
+
+
+ DATE +
...../...../..........
+
-
-
LABEL
-
- AWB: {{ $shipment->awb_no }} - Weight: {{ $shipment->weight }} KG -
-
- Gross Weight: {{ $shipment->weight }} KG - Volumetric Weight: {{ $shipment->volumetric_weight }} KG -
-
- W*L*H: {{ $shipment->dimensions }} -
-
- {!! $barcode !!} + +
+ @if($barcode_base64) + barcode + @endif +
+ {{ $shipment->awb_no }} + {{ $shipment->weight }} KG + {{ $shipment->created_at?->format('m/d/Y') }} + {{ $shipment->fromCountry?->name }} → {{ $shipment->toCountry?->name }}
+
diff --git a/04_Laravel/resources/views/pdfs/label.blade.php b/04_Laravel/resources/views/pdfs/label.blade.php index 050ff33..3496e0f 100644 --- a/04_Laravel/resources/views/pdfs/label.blade.php +++ b/04_Laravel/resources/views/pdfs/label.blade.php @@ -1,104 +1,249 @@ - + -
-
-
- -
IFNEX LOGISTICS
+
+ +
+
+
IFNEx LOGISTICS
+
We Deliver Value
+
+
Date: {{ $shipment->created_at?->format('m/d/Y') }}
+
+ + +
+ +
+ @if($barcode_base64) + barcode + @endif +
AWB Number
+
{{ $shipment->awb_no }}
-
- {{ $shipment->awb_no }} -
- -
- {!! $barcode !!} -
- -
-
-
SHIPPER
-
Name:{{ $shipper['name'] }}
-
Company:{{ $shipper['company'] }}
-
Phone:{{ $shipper['phone'] }}
-
Address:{{ $shipper['address'] }}
-
-
-
RECEIVER
-
Name:{{ $receiver['name'] }}
-
Company:{{ $receiver['company'] }}
-
Phone:{{ $receiver['phone'] }}
-
Address:{{ $receiver['address'] }}
-
-
- -
-
SHIPMENT DETAILS
-
-
-
Gross Weight:{{ $shipment->weight }} KG
-
Volumetric Weight:{{ $shipment->volumetric_weight }} KG
-
Dimensions:{{ $shipment->dimensions }} CM
+ +
+ +
+
+
ORIGIN
+
{{ $shipment->fromCountry?->name ?? '—' }}
+
{{ $origin_iso }}
-
-
From:{{ $shipment->fromCountry?->name }}
-
To:{{ $shipment->toCountry?->name }}
-
Service:{{ $shipment->type?->label() }}
+
+
+
DESTINATION
+
{{ $shipment->toCountry?->name ?? '—' }}
+
{{ $dest_iso }}
-
-
+ + +
diff --git a/04_Laravel/scripts/test_pdf_generation.php b/04_Laravel/scripts/test_pdf_generation.php new file mode 100644 index 0000000..3edfb42 --- /dev/null +++ b/04_Laravel/scripts/test_pdf_generation.php @@ -0,0 +1,157 @@ +make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + +use App\Models\Shipment; +use App\Models\ShipmentItem; +use App\Models\Country; +use App\Services\PdfService; +use Illuminate\Support\Facades\File; + +echo "═══════════════════════════════════════════\n"; +echo "🧪 IFNEX PDF Generation Test\n"; +echo "═══════════════════════════════════════════\n\n"; + +// ─── ۱. بررسی نصب بودن کتابخانه بارکد ─── +echo "1️⃣ Checking barcode library...\n"; +if (class_exists(\Picqer\Barcode\BarcodeGeneratorPNG::class)) { + echo " ✅ picqer/php-barcode-generator is installed\n\n"; +} else { + echo " ❌ picqer/php-barcode-generator is NOT installed\n"; + echo " Run: composer require picqer/php-barcode-generator\n\n"; + exit(1); +} + +// ─── ۱.۵. تست تولید بارکد ─── +echo "1.5️⃣ Testing barcode generation...\n"; +try { + $generator = new \Picqer\Barcode\BarcodeGeneratorPNG(); + $barcode = $generator->getBarcode('TEST123', $generator::TYPE_CODE_128, 3, 80); + $b64 = 'data:image/png;base64,' . base64_encode($barcode); + echo " ✅ Barcode generated, length: " . strlen($b64) . " chars\n"; + echo " Preview: " . substr($b64, 0, 50) . "...\n\n"; +} catch (\Throwable $e) { + echo " ❌ Barcode generation failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۲. ایجاد پوشه تست ─── +$testDir = storage_path('app/pdf-test'); +if (!File::exists($testDir)) { + File::makeDirectory($testDir, 0755, true); + echo "2️⃣ Created test directory: {$testDir}\n\n"; +} else { + echo "2️⃣ Test directory exists: {$testDir}\n\n"; +} + +// ─── ۳. پیدا کردن یه محموله نمونه ─── +echo "3️⃣ Finding sample shipment...\n"; +$shipment = Shipment::with(['fromCountry', 'toCountry', 'items'])->latest()->first(); + +if (!$shipment) { + echo " ❌ No shipment found in database. Please create one first.\n"; + exit(1); +} + +echo " ✅ Found shipment: {$shipment->awb_no}\n"; +echo " - Type: {$shipment->type?->value}\n"; +echo " - Direction: {$shipment->direction?->value}\n"; +echo " - From: {$shipment->fromCountry?->name}\n"; +echo " - To: {$shipment->toCountry?->name}\n"; +echo " - Items: {$shipment->items->count()}\n\n"; + +// ─── ۴. اگر آیتم نداره، یه آیتم تستی اضافه کن ─── +if ($shipment->items->isEmpty()) { + echo " ⚠️ Shipment has no items. Adding test item...\n"; + ShipmentItem::create([ + 'shipment_id' => $shipment->id, + 'row_number' => 1, + 'description' => 'Electronics PCB Board', + 'hs_code' => '8542390001', + 'quantity' => 104, + 'unit_price' => 1.10, + 'total_usd' => 114.40, + ]); + $shipment->load('items'); + echo " ✅ Added test item\n\n"; +} + +// ─── ۵. تولید AWB PDF ─── +echo "4️⃣ Generating AWB PDF...\n"; +try { + $pdfService = app(PdfService::class); + $awbContent = $pdfService->awb($shipment); + $awbPath = $testDir . '/AWB-' . $shipment->awb_no . '.pdf'; + File::put($awbPath, $awbContent); + echo " ✅ AWB PDF saved: {$awbPath}\n"; + echo " Size: " . number_format(strlen($awbContent) / 1024, 2) . " KB\n\n"; +} catch (\Throwable $e) { + echo " ❌ AWB PDF failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۶. تولید Invoice PDF ─── +echo "5️⃣ Generating Invoice PDF...\n"; +try { + $invoiceContent = $pdfService->invoice($shipment); + $invoicePath = $testDir . '/INVOICE-' . $shipment->awb_no . '.pdf'; + File::put($invoicePath, $invoiceContent); + echo " ✅ Invoice PDF saved: {$invoicePath}\n"; + echo " Size: " . number_format(strlen($invoiceContent) / 1024, 2) . " KB\n\n"; +} catch (\InvalidArgumentException $e) { + echo " ⚠️ Invoice skipped: " . $e->getMessage() . "\n"; + echo " (این طبیعی است اگر محموله DOC است)\n\n"; +} catch (\Throwable $e) { + echo " ❌ Invoice PDF failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۷. تولید Label PDF ─── +echo "6️⃣ Generating Label PDF...\n"; +try { + $labelContent = $pdfService->label($shipment); + $labelPath = $testDir . '/LABEL-' . $shipment->awb_no . '.pdf'; + File::put($labelPath, $labelContent); + echo " ✅ Label PDF saved: {$labelPath}\n"; + echo " Size: " . number_format(strlen($labelContent) / 1024, 2) . " KB\n\n"; +} catch (\Throwable $e) { + echo " ❌ Label PDF failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۸. تست محموله DOC (بدون فاکتور) ─── +echo "7️⃣ Testing DOC shipment (should skip invoice)...\n"; +$docShipment = Shipment::where('type', 'DOC_NORMAL')->first(); +if ($docShipment) { + try { + $docShipment->load('items'); + if ($docShipment->items->isEmpty()) { + $pdfService->invoice($docShipment); + echo " ❌ ERROR: Should have thrown exception for DOC shipment!\n\n"; + } else { + echo " ℹ️ DOC shipment has items, invoice would work\n\n"; + } + } catch (\InvalidArgumentException $e) { + echo " ✅ Correctly skipped invoice for DOC: " . $e->getMessage() . "\n\n"; + } +} else { + echo " ℹ️ No DOC shipment found for testing (skipped)\n\n"; +} + +echo "═══════════════════════════════════════════\n"; +echo "✅ Test completed!\n"; +echo "═══════════════════════════════════════════\n\n"; +echo "📁 Check the generated PDFs at:\n"; +echo " {$testDir}\n\n"; diff --git a/04_Laravel/test_pdf_generation.php b/04_Laravel/test_pdf_generation.php new file mode 100644 index 0000000..c1b4a37 --- /dev/null +++ b/04_Laravel/test_pdf_generation.php @@ -0,0 +1,145 @@ +make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + +use App\Models\Shipment; +use App\Models\ShipmentItem; +use App\Models\Country; +use App\Services\PdfService; +use Illuminate\Support\Facades\File; + +echo "═══════════════════════════════════════════\n"; +echo "🧪 IFNEX PDF Generation Test\n"; +echo "═══════════════════════════════════════════\n\n"; + +// ─── ۱. بررسی نصب بودن کتابخانه بارکد ─── +echo "1️⃣ Checking barcode library...\n"; +if (class_exists(\Picqer\Barcode\BarcodeGeneratorPNG::class)) { + echo " ✅ picqer/php-barcode-generator is installed\n\n"; +} else { + echo " ❌ picqer/php-barcode-generator is NOT installed\n"; + echo " Run: composer require picqer/php-barcode-generator\n\n"; + exit(1); +} + +// ─── ۲. ایجاد پوشه تست ─── +$testDir = storage_path('app/pdf-test'); +if (!File::exists($testDir)) { + File::makeDirectory($testDir, 0755, true); + echo "2️⃣ Created test directory: {$testDir}\n\n"; +} else { + echo "2️⃣ Test directory exists: {$testDir}\n\n"; +} + +// ─── ۳. پیدا کردن یه محموله نمونه ─── +echo "3️⃣ Finding sample shipment...\n"; +$shipment = Shipment::with(['fromCountry', 'toCountry', 'items'])->latest()->first(); + +if (!$shipment) { + echo " ❌ No shipment found in database. Please create one first.\n"; + exit(1); +} + +echo " ✅ Found shipment: {$shipment->awb_no}\n"; +echo " - Type: {$shipment->type?->value}\n"; +echo " - Direction: {$shipment->direction?->value}\n"; +echo " - From: {$shipment->fromCountry?->name}\n"; +echo " - To: {$shipment->toCountry?->name}\n"; +echo " - Items: {$shipment->items->count()}\n\n"; + +// ─── ۴. اگر آیتم نداره، یه آیتم تستی اضافه کن ─── +if ($shipment->items->isEmpty()) { + echo " ⚠️ Shipment has no items. Adding test item...\n"; + ShipmentItem::create([ + 'shipment_id' => $shipment->id, + 'row_number' => 1, + 'description' => 'Electronics PCB Board', + 'hs_code' => '8542390001', + 'quantity' => 104, + 'unit_price' => 1.10, + 'total_usd' => 114.40, + ]); + $shipment->load('items'); + echo " ✅ Added test item\n\n"; +} + +// ─── ۵. تولید AWB PDF ─── +echo "4️⃣ Generating AWB PDF...\n"; +try { + $pdfService = app(PdfService::class); + $awbContent = $pdfService->awb($shipment); + $awbPath = $testDir . '/AWB-' . $shipment->awb_no . '.pdf'; + File::put($awbPath, $awbContent); + echo " ✅ AWB PDF saved: {$awbPath}\n"; + echo " Size: " . number_format(strlen($awbContent) / 1024, 2) . " KB\n\n"; +} catch (\Throwable $e) { + echo " ❌ AWB PDF failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۶. تولید Invoice PDF ─── +echo "5️⃣ Generating Invoice PDF...\n"; +try { + $invoiceContent = $pdfService->invoice($shipment); + $invoicePath = $testDir . '/INVOICE-' . $shipment->awb_no . '.pdf'; + File::put($invoicePath, $invoiceContent); + echo " ✅ Invoice PDF saved: {$invoicePath}\n"; + echo " Size: " . number_format(strlen($invoiceContent) / 1024, 2) . " KB\n\n"; +} catch (\InvalidArgumentException $e) { + echo " ⚠️ Invoice skipped: " . $e->getMessage() . "\n"; + echo " (این طبیعی است اگر محموله DOC است)\n\n"; +} catch (\Throwable $e) { + echo " ❌ Invoice PDF failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۷. تولید Label PDF ─── +echo "6️⃣ Generating Label PDF...\n"; +try { + $labelContent = $pdfService->label($shipment); + $labelPath = $testDir . '/LABEL-' . $shipment->awb_no . '.pdf'; + File::put($labelPath, $labelContent); + echo " ✅ Label PDF saved: {$labelPath}\n"; + echo " Size: " . number_format(strlen($labelContent) / 1024, 2) . " KB\n\n"; +} catch (\Throwable $e) { + echo " ❌ Label PDF failed: " . $e->getMessage() . "\n\n"; +} + +// ─── ۸. تست محموله DOC (بدون فاکتور) ─── +echo "7️⃣ Testing DOC shipment (should skip invoice)...\n"; +$docShipment = Shipment::where('type', 'DOC_NORMAL')->first(); +if ($docShipment) { + try { + $docShipment->load('items'); + if ($docShipment->items->isEmpty()) { + $pdfService->invoice($docShipment); + echo " ❌ ERROR: Should have thrown exception for DOC shipment!\n\n"; + } else { + echo " ℹ️ DOC shipment has items, invoice would work\n\n"; + } + } catch (\InvalidArgumentException $e) { + echo " ✅ Correctly skipped invoice for DOC: " . $e->getMessage() . "\n\n"; + } +} else { + echo " ℹ️ No DOC shipment found for testing (skipped)\n\n"; +} + +echo "═══════════════════════════════════════════\n"; +echo "✅ Test completed!\n"; +echo "═══════════════════════════════════════════\n\n"; +echo "📁 Check the generated PDFs at:\n"; +echo " {$testDir}\n\n";