回筐导出
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\ContainerReturnModel;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
/**
|
||||
* 压回筐记录汇总导出:行=日期(区间内逐日),列=门店,
|
||||
* 单元格=当日该门店抵扣(附加)金额合计(同日多条记录合并),无记录填 0;
|
||||
* 末列为行合计,末行为列合计(右下角为总计)
|
||||
*
|
||||
* WithStrictNullComparison:PhpSpreadsheet fromArray 默认松散比较(0 == null 为 true),
|
||||
* 数值 0 的单元格会被跳过写成空白,必须启用严格比较才能把 0 填进表格
|
||||
*/
|
||||
class ContainerReturnExport implements FromCollection, WithStyles, WithStrictNullComparison
|
||||
{
|
||||
private ?Collection $rows = null;
|
||||
|
||||
/** @var array<int, string> 特殊行索引(1 起)=> 类型(title/scope/header/total) */
|
||||
private array $specialRows = [];
|
||||
|
||||
/** 列头所在行索引 */
|
||||
private int $headerRow = 1;
|
||||
|
||||
/**
|
||||
* @param Collection<int, ContainerReturnModel> $records 区间内压回筐记录
|
||||
* @param array<int, string> $storeNames 门店ID => 名称(导出列,升序)
|
||||
* @param string $startDate 开始日期 Y-m-d
|
||||
* @param string $endDate 结束日期 Y-m-d
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Collection $records,
|
||||
private readonly array $storeNames,
|
||||
private readonly string $startDate,
|
||||
private readonly string $endDate,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出行:标题/范围/列头/日期矩阵/合计全部手工构建(行位置不固定,不用 WithHeadings)
|
||||
*/
|
||||
public function collection(): Collection
|
||||
{
|
||||
if ($this->rows !== null) {
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
// 同日期同门店多条记录合并:金额代数和(正=压筐附加,负=回筐抵扣)
|
||||
$amounts = [];
|
||||
foreach ($this->records as $record) {
|
||||
$date = $record->created_at->format('Y-m-d');
|
||||
$amounts[$date][(int) $record->store_id] = bcadd(
|
||||
$amounts[$date][(int) $record->store_id] ?? '0',
|
||||
(string) $record->amount,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$rowIndex = 0;
|
||||
|
||||
// 标题行
|
||||
$rows[] = ['压回筐记录汇总(' . $this->startDate . ' ~ ' . $this->endDate . ')'];
|
||||
$this->specialRows[++$rowIndex] = 'title';
|
||||
|
||||
// 范围行:门店 / 导出时间
|
||||
$rows[] = ['门店:' . implode('、', array_values($this->storeNames)) . ' 导出时间:' . now()->format('Y-m-d H:i:s')];
|
||||
$this->specialRows[++$rowIndex] = 'scope';
|
||||
|
||||
// 空行
|
||||
$rows[] = [''];
|
||||
$rowIndex++;
|
||||
|
||||
// 列头:日期 + 门店列 + 行合计
|
||||
$rows[] = array_merge(['日期'], array_values($this->storeNames), ['合计']);
|
||||
$this->specialRows[++$rowIndex] = 'header';
|
||||
$this->headerRow = $rowIndex;
|
||||
|
||||
// 日期矩阵:区间内逐日一行,当天门店无记录填 0
|
||||
$storeIds = array_map(intval(...), array_keys($this->storeNames));
|
||||
$columnTotals = array_fill_keys($storeIds, '0');
|
||||
$grandTotal = '0';
|
||||
foreach (CarbonPeriod::create($this->startDate, $this->endDate) as $date) {
|
||||
$dateKey = $date->format('Y-m-d');
|
||||
$row = [$dateKey];
|
||||
$rowTotal = '0';
|
||||
foreach ($storeIds as $storeId) {
|
||||
$amount = $amounts[$dateKey][$storeId] ?? '0.00';
|
||||
$row[] = (float) $amount;
|
||||
$rowTotal = bcadd($rowTotal, $amount, 2);
|
||||
$columnTotals[$storeId] = bcadd($columnTotals[$storeId], $amount, 2);
|
||||
}
|
||||
$grandTotal = bcadd($grandTotal, $rowTotal, 2);
|
||||
$row[] = (float) $rowTotal;
|
||||
$rows[] = $row;
|
||||
$rowIndex++;
|
||||
}
|
||||
|
||||
// 合计行:各门店列合计 + 总计
|
||||
$totalRow = ['合计'];
|
||||
foreach ($storeIds as $storeId) {
|
||||
$totalRow[] = (float) $columnTotals[$storeId];
|
||||
}
|
||||
$totalRow[] = (float) $grandTotal;
|
||||
$rows[] = $totalRow;
|
||||
$this->specialRows[++$rowIndex] = 'total';
|
||||
|
||||
return $this->rows = collect($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标题/列头/合计行加粗,冻结列头行
|
||||
*/
|
||||
public function styles(Worksheet $sheet): array
|
||||
{
|
||||
$this->collection();
|
||||
|
||||
$sheet->freezePane('A' . ($this->headerRow + 1));
|
||||
$sheet->getColumnDimension('A')->setWidth(12);
|
||||
$columnCount = count($this->storeNames) + 2;
|
||||
for ($column = 2; $column <= $columnCount; $column++) {
|
||||
$sheet->getColumnDimensionByColumn($column)->setWidth(12);
|
||||
}
|
||||
|
||||
$styles = [];
|
||||
foreach ($this->specialRows as $row => $type) {
|
||||
$style = match ($type) {
|
||||
'title' => ['font' => ['bold' => true, 'size' => 14]],
|
||||
'header', 'total' => ['font' => ['bold' => true]],
|
||||
default => [],
|
||||
};
|
||||
if ($style !== []) {
|
||||
$styles[$row] = $style;
|
||||
}
|
||||
}
|
||||
|
||||
return $styles;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
namespace App\Http\Controllers\Recon;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Exports\ContainerReturnExport;
|
||||
use App\Models\ContainerReturnModel;
|
||||
use App\Models\StoreModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 压回筐记录(周转筐/托盘跟账单走,生成账单时自动写入完整快照,只读)
|
||||
@@ -39,4 +45,61 @@ class ContainerReturnController extends BaseController
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总导出:按日期区间 + 门店导出抵扣(附加)金额矩阵,
|
||||
* 行=日期(同日记录合并),列=门店,含行合计/列合计,当天门店无记录填 0
|
||||
*/
|
||||
#[GetRoute(route: '/export', authorize: 'export')]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $request->validate([
|
||||
'start_date' => 'required|date_format:Y-m-d',
|
||||
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||
'store_ids' => 'nullable|string',
|
||||
], [
|
||||
'start_date.required' => '请选择开始日期',
|
||||
'start_date.date_format' => '开始日期格式为 Y-m-d',
|
||||
'end_date.required' => '请选择结束日期',
|
||||
'end_date.date_format' => '结束日期格式为 Y-m-d',
|
||||
'end_date.after_or_equal' => '结束日期不能早于开始日期',
|
||||
]);
|
||||
|
||||
$startDate = $data['start_date'];
|
||||
$endDate = $data['end_date'];
|
||||
if (Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)) > 366) {
|
||||
throw new RepositoryException('日期区间不能超过 366 天');
|
||||
}
|
||||
|
||||
$storeIds = array_values(array_filter(
|
||||
array_map(intval(...), explode(',', (string) ($data['store_ids'] ?? '')))
|
||||
));
|
||||
|
||||
$records = ContainerReturnModel::query()
|
||||
->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59'])
|
||||
->when($storeIds !== [], static fn ($query) => $query->whereIn('store_id', $storeIds))
|
||||
->get(['id', 'store_id', 'amount', 'created_at']);
|
||||
|
||||
// 已选门店:列取所选门店(无记录填 0);未选门店:列取区间内有记录的门店
|
||||
$columnStoreIds = $storeIds !== []
|
||||
? $storeIds
|
||||
: $records->pluck('store_id')->unique()->map(static fn ($id): int => (int) $id)->all();
|
||||
if ($columnStoreIds === []) {
|
||||
throw new RepositoryException('所选日期区间内无压回筐记录');
|
||||
}
|
||||
sort($columnStoreIds);
|
||||
|
||||
$names = StoreModel::withTrashed()
|
||||
->whereIn('id', $columnStoreIds)
|
||||
->pluck('name', 'id');
|
||||
$storeNames = [];
|
||||
foreach ($columnStoreIds as $storeId) {
|
||||
$storeNames[$storeId] = $names->get($storeId, '门店#' . $storeId);
|
||||
}
|
||||
|
||||
return Excel::download(
|
||||
new ContainerReturnExport($records, $storeNames, $startDate, $endDate),
|
||||
'回筐记录_' . $startDate . '_' . $endDate . '.xlsx'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user