账单导出

This commit is contained in:
2026-10-30 10:51:09 +08:00
parent 42a3fa00d4
commit 7366d88baf
178 changed files with 1166 additions and 898 deletions
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Exports;
use App\Models\BillModel;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
/**
* 门店账单批量导出:同一门店的账单合并在一张工作表,不同门店拆分为多个工作表
*/
class BillBatchExport implements WithMultipleSheets
{
/**
* @param Collection<int, BillModel> $bills 导出账单(with store)
*/
public function __construct(private readonly Collection $bills)
{
}
/**
* @return array<int, BillExport>
*/
public function sheets(): array
{
$usedNames = [];
$sheets = [];
foreach ($this->bills->groupBy('store_id') as $storeBills) {
$store = $storeBills->first()->store;
$storeId = (int) $storeBills->first()->store_id;
$sheetTitle = SheetName::make($store->name ?? ('门店#' . $storeId), $storeId, $usedNames);
$sheets[] = new BillExport($storeBills->values(), 0, $sheetTitle);
}
return $sheets;
}
}
+65 -26
View File
@@ -11,16 +11,17 @@ use App\Services\WeightEstimator;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 门店账单单独导出:账单内明细按商品合并(可按一级分类过滤),明细行展示商品一级分类,
* 表尾汇总商品金额/配送费/附加金额/售后金额/总金额
* 门店账单导出:单账单可直接下载;多账单在同一工作表依次展示(批量导出按门店分组),
* 账单内明细按商品合并(可按一级分类过滤),表尾汇总商品金额/配送费/附加金额/售后金额/总金额
*/
class BillExport implements FromCollection, WithStyles
class BillExport implements FromCollection, WithStyles, WithTitle
{
/** 数量列填充色(浅黄) */
private const string QUANTITY_FILL = 'FFFFF2CC';
@@ -45,21 +46,38 @@ class BillExport implements FromCollection, WithStyles
/** 列头所在行索引 */
private int $headerRow = 1;
/** @var array<int, int> 各账单列头所在行索引 */
private array $headerRows = [];
/** @var array<int, array{header: int, last: int}> 各账单表格区域 */
private array $tableRanges = [];
/** 数据末行索引 */
private int $lastRow = 1;
/** @var Collection<int, BillModel> */
private Collection $bills;
/**
* @param BillModel $bill 导出账单(with store)
* @param BillModel|Collection<int, BillModel> $bills 导出账单(with store);多账单须属于同一门店
* @param int $categoryId 一级分类ID(0=全部)
* @param string $sheetTitle 工作表名称
*/
public function __construct(
private readonly BillModel $bill,
BillModel|Collection $bills,
private readonly int $categoryId = 0,
private readonly string $sheetTitle = '门店账单',
) {
$this->bills = $bills instanceof BillModel ? collect([$bills]) : $bills->values();
}
public function title(): string
{
return $this->sheetTitle;
}
/**
* 导出行:标题/范围/列头/明细/合计全部手工构建(行位置不固定,不用 WithHeadings)
* 导出行:每张账单保留独立的标题/范围/列头/明细/合计,多账单之间空一行
*/
public function collection(): Collection
{
@@ -67,7 +85,27 @@ class BillExport implements FromCollection, WithStyles
return $this->rows;
}
$items = app(BillDetailService::class)->mergedItems($this->bill);
$rows = [];
$rowIndex = 0;
foreach ($this->bills as $index => $bill) {
if ($index > 0) {
$rows[] = [''];
$rowIndex++;
}
$this->appendBillRows($bill, $rows, $rowIndex);
}
return $this->rows = collect($rows);
}
/**
* 追加一张账单的完整表格
*
* @param array<int, array<int, mixed>> $rows
*/
private function appendBillRows(BillModel $bill, array &$rows, int &$rowIndex): void
{
$items = app(BillDetailService::class)->mergedItems($bill);
// 商品 => 一级分类解析(明细行「一级分类」列与分类过滤共用同一份结果)
[$categories, $rootIds] = $this->resolveRootCategories($items);
@@ -77,10 +115,6 @@ class BillExport implements FromCollection, WithStyles
[$items, $categoryName] = $this->filterByRootCategory($items, $categories, $rootIds);
}
$bill = $this->bill;
$rows = [];
$rowIndex = 0;
// 标题行
$rows[] = ['门店账单 ' . $bill->bill_no];
$this->specialRows[++$rowIndex] = 'title';
@@ -102,7 +136,11 @@ class BillExport implements FromCollection, WithStyles
// 列头(包规=包规+单位合并;单价=每件零售价+单价单位;一级分类=商品根分类名)
$rows[] = ['序号', '品名', '一级分类', '包规', '单价', '数量', '重量(斤)', '金额'];
$this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex;
$currentHeaderRow = $rowIndex;
$this->headerRows[] = $currentHeaderRow;
if (count($this->headerRows) === 1) {
$this->headerRow = $rowIndex;
}
// 明细行(账单内同商品已合并,单价为每件价折算的零售价)
$totalQuantity = 0;
@@ -159,8 +197,7 @@ class BillExport implements FromCollection, WithStyles
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $grandTotal];
$this->lastRow = $rowIndex;
return $this->rows = collect($rows);
$this->tableRanges[] = ['header' => $currentHeaderRow, 'last' => $rowIndex];
}
/**
@@ -194,20 +231,22 @@ class BillExport implements FromCollection, WithStyles
$sheet->getStyle('A' . $row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT);
}
// 表格区域(列头 → 末行)添加所有边框
$sheet->getStyle('A' . $this->headerRow . ':H' . $this->lastRow)
->getBorders()
->getAllBorders()
->setBorderStyle(Border::BORDER_THIN);
// 金额列货币格式:¥ + 两位小数(单价列为文本,不设格式)
$sheet->getStyle('H' . ($this->headerRow + 1) . ':H' . $this->lastRow)
->getNumberFormat()
->setFormatCode(self::CURRENCY_FORMAT);
// 各账单表格区域(列头 → 末行)添加所有边框,金额列设置货币格式
foreach ($this->tableRanges as $range) {
$sheet->getStyle('A' . $range['header'] . ':H' . $range['last'])
->getBorders()
->getAllBorders()
->setBorderStyle(Border::BORDER_THIN);
$sheet->getStyle('H' . ($range['header'] + 1) . ':H' . $range['last'])
->getNumberFormat()
->setFormatCode(self::CURRENCY_FORMAT);
}
// 列头固定填色:数量列(F)浅黄、金额列(H)浅红
$this->fillCell($sheet, 'F' . $this->headerRow, self::QUANTITY_FILL);
$this->fillCell($sheet, 'H' . $this->headerRow, self::AMOUNT_FILL);
foreach ($this->headerRows as $headerRow) {
$this->fillCell($sheet, 'F' . $headerRow, self::QUANTITY_FILL);
$this->fillCell($sheet, 'H' . $headerRow, self::AMOUNT_FILL);
}
// 数据区条件填色:非 0 填色,0 无背景(金额含负值)
foreach ($this->fillCells as $cell) {
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Exports\BillBatchExport;
use App\Exports\BillExport;
use App\Models\BillModel;
use App\Services\BillDetailService;
@@ -86,6 +87,37 @@ class BillController extends BaseMiniController
return $this->success($data);
}
/**
* 批量导出账单:仅可导出本店账单,选中账单合并在同一工作表
*/
#[GetRoute('/bill/batch-export', authorize: true)]
public function batchExport(Request $request): Response
{
$data = $request->validate([
'ids' => 'required|array|min:1|max:100',
'ids.*' => 'required|integer|distinct',
], [
'ids.required' => '请选择要导出的账单',
'ids.max' => '单次最多导出 100 张账单',
'ids.*.distinct' => '账单不能重复选择',
]);
$store = $this->currentStore($request);
$bills = BillModel::query()
->with('store:id,name')
->where('store_id', $store->id)
->whereIn('id', $data['ids'])
->orderBy('bill_date')
->orderBy('id')
->get();
if ($bills->count() !== count($data['ids'])) {
throw new RepositoryException('包含不存在或不属于当前门店的账单');
}
return Excel::download(new BillBatchExport($bills), '门店账单_批量导出.xlsx');
}
/**
* 单独导出账单:仅可导出本店账单(强制 store_id 过滤);
* ?category_id= 一级分类过滤(0/缺省=全部)
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Recon;
use App\Exceptions\RepositoryException;
use App\Exports\BillBatchExport;
use App\Exports\BillExport;
use App\Models\BillModel;
use App\Models\StoreModel;
@@ -60,6 +61,27 @@ class BillController extends BaseController
return $this->success($data);
}
/**
* 批量导出账单:同一门店的账单合并在一张工作表,不同门店拆分为多个工作表
*/
#[GetRoute(route: '/batch-export', authorize: 'export')]
public function batchExport(Request $request): Response
{
$data = $request->validate([
'ids' => 'required|array|min:1|max:100',
'ids.*' => 'required|integer|distinct|exists:bill,id',
]);
$bills = BillModel::query()
->with('store:id,name')
->whereIn('id', $data['ids'])
->orderBy('bill_date')
->orderBy('id')
->get();
return Excel::download(new BillBatchExport($bills), '门店账单_批量导出.xlsx');
}
/**
* 单独导出账单:账单内明细按商品合并(可按一级分类过滤),
* 表尾汇总商品金额/配送费/附加金额/售后金额/总金额