ifnex/04_Laravel/app/Services/PdfService.php
Kazem Alghasi 93bcfb9ce7 refactor(pdf): switch AWB template to landscape orientation
Update the AWB document generation to use landscape mode and adjust
the layout to utilize the additional horizontal space.

Key changes:
- Change `PdfService` orientation from portrait to landscape for AWB.
- Refactor `awb.blade.php` to use a side-by-side layout for terms
  and signatures.
- Adjust grid column widths from 50% to 25% to accommodate the
  new orientation.
- Update `@page` margin definitions for better print alignment.
2026-10-03 20:33:15 +03:30

368 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
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 Illuminate\Support\Facades\Log;
use Picqer\Barcode\BarcodeGeneratorPNG;
class PdfService
{
/**
* تولید AWB PDF (برای همه نوع محموله‌ها)
*/
public function awb(Shipment $shipment): string
{
$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),
'service_label' => $this->getServiceLabel($shipment),
'type_label' => $this->getTypeLabel($shipment),
];
$html = view('pdfs.awb', $data)->render();
return $this->generatePdf($html, 'A4', 'landscape');
}
/**
* تولید 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,
'barcode_base64' => $this->generateBarcode($shipment->awb_no),
'origin_iso' => $shipment->fromCountry?->iso_code ?? 'IR',
'dest_iso' => $shipment->toCountry?->iso_code ?? '',
];
$html = view('pdfs.label', $data)->render();
// A5 افقی (Landscape) — استاندارد لیبل پستی: 210×148 mm
return $this->generatePdf($html, 'A5', 'landscape');
}
/**
* تولید PDF فاکتور فروش خدمات (واردات) — مطابق Sheet ENG Invoice
*/
public function importInvoice(Shipment $shipment, array $options = []): string
{
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
// آدرس گیرنده
$receiver = [
'name' => $shipment->receiver_name,
'company' => $shipment->receiver_company,
'phone' => $shipment->receiver_phone,
'email' => $shipment->receiver_email,
'address' => $shipment->receiver_address,
'city' => $shipment->receiver_city,
'country' => $shipment->toCountry?->name ?? '',
];
// شماره فاکتور (از شماره AWB مشتق می‌شود)
$invoice_no = $options['invoice_no'] ?? 'PEX' . date('ymd') . substr($shipment->awb_no, -4);
// تاریخ فاکتور
$invoice_date = $options['invoice_date'] ?? now()->format('l, F j, Y');
// دفتر گمرکی
$customs_office = $options['customs_office'] ?? 'Imam Khomeini Airport';
// نرخ ارز
$exchange_rate = $options['exchange_rate'] ?? 500000; // پیش‌فرض
// اقلام خدماتی (۱۰ ردیف ثابت مطابق Sheet ENG Invoice)
$service_items = $this->buildServiceItems($shipment, $options);
// جمع‌بندی
$freight_items = array_slice($service_items, 0, 5); // آیتم‌های حمل
$clearance_items = array_slice($service_items, 5); // آیتم‌های ترخیص
$freight_subtotal_currency = array_sum(array_column($freight_items, 'total_currency'));
$freight_subtotal_rial = array_sum(array_column($freight_items, 'total_rial'));
$clearance_subtotal_currency = array_sum(array_column($clearance_items, 'total_currency'));
$clearance_subtotal_rial = array_sum(array_column($clearance_items, 'total_rial'));
$total_currency = $freight_subtotal_currency + $clearance_subtotal_currency;
$total_rial = $freight_subtotal_rial + $clearance_subtotal_rial;
// شرایط و ضوابط
$terms_and_conditions = $options['terms_and_conditions'] ??
'This proforma invoice is issued based on the shipment weight declared by the customer. Final weight and dimensions will be determined after re-weighing at the destination customs.';
$data = [
'shipment' => $shipment,
'receiver' => $receiver,
'invoice_no' => $invoice_no,
'invoice_date' => $invoice_date,
'customs_office' => $customs_office,
'exchange_rate' => $exchange_rate,
'service_items' => $service_items,
'freight_subtotal_currency' => $freight_subtotal_currency,
'freight_subtotal_rial' => $freight_subtotal_rial,
'clearance_subtotal_currency' => $clearance_subtotal_currency,
'clearance_subtotal_rial' => $clearance_subtotal_rial,
'total_currency' => $total_currency,
'total_rial' => $total_rial,
'terms_and_conditions' => $terms_and_conditions,
];
$html = view('pdfs.import_invoice', $data)->render();
return $this->generatePdf($html, 'A4', 'portrait');
}
/**
* ساخت اقلام خدماتی فاکتور واردات
*/
private function buildServiceItems(Shipment $shipment, array $options): array
{
// فیلدهای پیش‌فرض از گزینه‌ها یا مقادیر پیش‌فرض
$fields = [
'international_freight' => $options['international_freight'] ?? ($shipment->shipping_price ?? 0),
'pick_up_fee' => $options['pick_up_fee'] ?? ($shipment->domestic_pickup ?? 0),
'brand_fee' => $options['brand_fee'] ?? 0,
'report_fee' => $options['report_fee'] ?? 0,
'other_charges' => $options['other_charges'] ?? ($shipment->extra_service ?? 0),
'customs_clearance' => $options['customs_clearance'] ?? 0,
'domestic_transport' => $options['domestic_transport'] ?? ($shipment->domestic_delivery ?? 0),
'warehousing_fee' => $options['warehousing_fee'] ?? ($shipment->warehousing_cost ?? 0),
'order_registration_fee' => $options['order_registration_fee'] ?? 0,
'other_clearance_charges' => $options['other_clearance_charges'] ?? 0,
];
$exchange_rate = $options['exchange_rate'] ?? 500000;
$weight = $shipment->chargeable_weight ?? $shipment->weight;
return [
// آیتم‌های حمل (Freight)
[
'description' => 'International Freight',
'unit' => 'kg',
'quantity' => $weight,
'unit_price' => $fields['international_freight'] > 0 ? $fields['international_freight'] / $weight : 0,
'total_currency' => $fields['international_freight'],
'total_rial' => $fields['international_freight'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Pick Up Fee',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['pick_up_fee'],
'total_rial' => $fields['pick_up_fee'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Brand Fee',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['brand_fee'],
'total_rial' => $fields['brand_fee'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Report Fee',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['report_fee'],
'total_rial' => $fields['report_fee'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Other Charges',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['other_charges'],
'total_rial' => $fields['other_charges'] * $exchange_rate,
'notes' => '',
],
// آیتم‌های ترخیص (Clearance)
[
'description' => 'Customs Clearance',
'unit' => 'service',
'quantity' => $weight,
'unit_price' => $fields['customs_clearance'] > 0 ? $fields['customs_clearance'] / $weight : 0,
'total_currency' => $fields['customs_clearance'],
'total_rial' => $fields['customs_clearance'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Domestic Transport',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['domestic_transport'],
'total_rial' => $fields['domestic_transport'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Warehousing Fee',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['warehousing_fee'],
'total_rial' => $fields['warehousing_fee'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Order Registration Fee',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['order_registration_fee'],
'total_rial' => $fields['order_registration_fee'] * $exchange_rate,
'notes' => '',
],
[
'description' => 'Other Clearance Charges',
'unit' => '',
'quantity' => 0,
'unit_price' => 0,
'total_currency' => $fields['other_clearance_charges'],
'total_rial' => $fields['other_clearance_charges'] * $exchange_rate,
'notes' => '',
],
];
}
/**
* تولید بارکد از 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); // مهم: برای 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);
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'},
'zip' => $shipment->{$prefix . '_zip'},
'id_number' => $shipment->{$prefix . '_id_number'},
]);
}
}