Files
xin-procurement/app/Exports/PurchaseOrderExport.php
T
2026-08-13 10:53:11 +08:00

218 lines
7.5 KiB
PHP

<?php
namespace App\Exports;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\SupplierModel;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 采购单导出
*/
class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, WithStyles
{
/** @var array<int, string> 商品ID => 顶级分类名 */
private array $categoryNames = [];
/** @var array<int, string> 供应商ID => 名称 */
private array $supplierNames = [];
/** @var array<int, string> 门店ID => 名称(导出列) */
private array $storeNames = [];
private ?Collection $items = null;
/**
* @param PurchaseOrderModel $purchase 采购单
* @param string $type all 全品类 / category 仅蔬果分类
*/
public function __construct(
private readonly PurchaseOrderModel $purchase,
private readonly string $type = 'all',
) {
}
/**
* 导出行(商品聚合行,按 分类sort → 商品sort 排序;蔬果分类时按顶级分类名过滤)
*/
public function collection(): Collection
{
if ($this->items !== null) {
return $this->items;
}
$orderItems = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $this->purchase->id)
->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->get()
->makeVisible('cost_price');
$this->storeNames = StoreModel::withTrashed()
->whereIn('id', $orderItems->pluck('store_id')->unique())
->pluck('name', 'id')
->toArray();
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', $orderItems->pluck('product_id')->unique())
->get()
->keyBy('id');
$rows = [];
foreach ($orderItems->groupBy('product_id') as $productId => $group) {
$first = $group->first();
$product = $products->get((int) $productId);
$unitCost = $first->cost_price ?? '0';
$quantity = '0';
$weight = '0';
$amount = '0';
$storeQuantities = array_fill_keys(array_keys($this->storeNames), '0');
foreach ($group as $item) {
$quantity = bcadd($quantity, (string) $item->quantity, 2);
$weight = bcadd($weight, (string) $item->weight, 3);
// 金额 = 数量 × 每包成本价(单价/包规不参与金额计算)
$amount = bcadd($amount, bcmul((string) $item->quantity, (string) $item->cost_price, 2), 2);
$storeQuantities[(int) $item->store_id] = bcadd(
$storeQuantities[(int) $item->store_id] ?? '0',
(string) $item->quantity,
2
);
}
$rows[] = [
'product_id' => (int) $productId,
'supplier_id' => (int) $first->supplier_id,
'product_name' => $first->product_name,
'product_spec' => $first->product_spec,
'unit' => $first->unit,
'cost_price' => (float) ($first->cost_price ?? 0),
'unit_cost' => (float) $unitCost,
'quantity' => (float) $quantity,
'weight' => (float) $weight,
'amount' => (float) $amount,
'store_quantities' => array_map('floatval', $storeQuantities),
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
];
}
usort($rows, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
$items = collect($rows);
$this->loadLookups($items);
if ($this->type === 'category') {
$items = $items->filter(function (array $row): bool {
$rootName = $this->categoryNames[$row['product_id']] ?? '';
return str_contains($rootName, '蔬菜') || str_contains($rootName, '水果');
})->values();
}
$sort = 1;
return $this->items = $items->map(static function (array $row) use (&$sort): array {
$row['sort'] = $sort++;
return $row;
})->values();
}
public function headings(): array
{
$this->collection();
return array_merge(
['序号', '分类', '品名', '供应商', '包规', '单位', '成本', '单价', '数量', '实际称重', '金额'],
array_values($this->storeNames),
);
}
public function map($row): array
{
return array_merge([
$row['sort'],
$this->categoryNames[$row['product_id']] ?? '',
$row['product_name'],
$this->supplierNames[$row['supplier_id']] ?? '',
$row['product_spec'],
$row['unit'],
$row['cost_price'],
$row['unit_cost'],
$row['quantity'],
$row['weight'],
$row['amount'],
], array_values($row['store_quantities']));
}
/**
* 表头加粗 + 冻结首行
*/
public function styles(Worksheet $sheet): array
{
$sheet->freezePane('A2');
return [
1 => ['font' => ['bold' => true]],
];
}
/**
* PDF 模板视图数据
*
* @return array{purchase: PurchaseOrderModel, items: Collection, storeNames: array<int, string>, categoryNames: array<int, string>, supplierNames: array<int, string>}
*/
public function viewData(): array
{
return [
'purchase' => $this->purchase,
'items' => $this->collection(),
'storeNames' => $this->storeNames,
'categoryNames' => $this->categoryNames,
'supplierNames' => $this->supplierNames,
];
}
/**
* 预加载供应商名与商品顶级分类名(商品/供应商均含软删除,保证历史单据可导出)
*/
private function loadLookups(Collection $items): void
{
$this->supplierNames = SupplierModel::withTrashed()
->whereIn('id', $items->pluck('supplier_id')->unique())
->pluck('name', 'id')
->toArray();
$productCategoryIds = ProductModel::withTrashed()
->whereIn('id', $items->pluck('product_id')->unique())
->pluck('category_id', 'id');
$categories = ProductCategoryModel::all()->keyBy('id');
$this->categoryNames = [];
foreach ($productCategoryIds as $productId => $categoryId) {
$rootName = '';
$cursor = (int) $categoryId;
$guard = 0;
while ($cursor > 0 && $guard++ < 20) {
$category = $categories->get($cursor);
if ($category === null) {
break;
}
$rootName = $category->name;
$cursor = (int) $category->parent_id;
}
$this->categoryNames[(int) $productId] = $rootName;
}
}
}