账单导出

This commit is contained in:
2026-10-30 11:17:15 +08:00
parent 7366d88baf
commit f91939f1f0
137 changed files with 1227 additions and 74 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
/**
* 门店账单批量导出:同一门店的账单合并在一张工作表,不同门店拆分为多个工作表
* 门店账单批量导出:同一门店的账单数据合并为一张表,不同门店拆分为多个工作表
*/
class BillBatchExport implements WithMultipleSheets
{
+86 -49
View File
@@ -18,8 +18,8 @@ use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 门店账单导出:单账单可直接下载;多账单在同一工作表依次展示(批量导出按门店分组),
* 账单内明细按商品合并(可按一级分类过滤),表尾汇总商品金额/配送费/附加金额/售后金额/总金额
* 门店账单导出:单账单可直接下载;多账单按商品合并为一张汇总表(批量导出按门店分组),
* 明细可按一级分类过滤,表尾汇总商品金额/配送费/附加金额/售后金额/总金额
*/
class BillExport implements FromCollection, WithStyles, WithTitle
{
@@ -46,12 +46,6 @@ class BillExport implements FromCollection, WithStyles, WithTitle
/** 列头所在行索引 */
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;
@@ -77,7 +71,7 @@ class BillExport implements FromCollection, WithStyles, WithTitle
}
/**
* 导出行:每张账单保留独立的标题/范围/列头/明细/合计,多账单之间空一行
* 导出行:所选门店账单合并为一张表格,同商品及账单级费用分别汇总
*/
public function collection(): Collection
{
@@ -87,25 +81,20 @@ class BillExport implements FromCollection, WithStyles, WithTitle
$rows = [];
$rowIndex = 0;
foreach ($this->bills as $index => $bill) {
if ($index > 0) {
$rows[] = [''];
$rowIndex++;
}
$this->appendBillRows($bill, $rows, $rowIndex);
}
$this->appendBillRows($this->bills, $rows, $rowIndex);
return $this->rows = collect($rows);
}
/**
* 追加一张账单的完整表格
* 追加所选账单的合并表格
*
* @param Collection<int, BillModel> $bills
* @param array<int, array<int, mixed>> $rows
*/
private function appendBillRows(BillModel $bill, array &$rows, int &$rowIndex): void
private function appendBillRows(Collection $bills, array &$rows, int &$rowIndex): void
{
$items = app(BillDetailService::class)->mergedItems($bill);
$items = $this->mergedItems($bills);
// 商品 => 一级分类解析(明细行「一级分类」列与分类过滤共用同一份结果)
[$categories, $rootIds] = $this->resolveRootCategories($items);
@@ -115,15 +104,29 @@ class BillExport implements FromCollection, WithStyles, WithTitle
[$items, $categoryName] = $this->filterByRootCategory($items, $categories, $rootIds);
}
$bill = $bills->first();
$isMerged = $bills->count() > 1;
// 标题行
$rows[] = ['门店账单 ' . $bill->bill_no];
$rows[] = [$isMerged ? '门店账单 合并导出(' . $bills->count() . '张)' : '门店账单 ' . $bill->bill_no];
$this->specialRows[++$rowIndex] = 'title';
// 范围行:门店 / 账单日期 / 分类 / 导出时间
// 范围行:门店 / 账单日期(合并时取起止日期)/ 分类 / 导出时间
$billDate = $bill->bill_date?->toDateString();
if ($isMerged) {
$dates = $bills->map(static fn (BillModel $item): ?string => $item->bill_date?->toDateString())
->filter()
->sort()
->values();
$billDate = $dates->first() === $dates->last()
? $dates->first()
: $dates->first() . ' 至 ' . $dates->last();
}
$storeName = $bill->store->name ?? ('门店#' . $bill->store_id);
$rows[] = [
'门店:' . $storeName
. ' 账单日期:' . $bill->bill_date?->toDateString()
. ' 账单日期:' . $billDate
. ($isMerged ? ' 账单数量:' . $bills->count() : '')
. ' 分类:' . $categoryName
. ' 导出时间:' . now('Asia/Shanghai')->format('Y-m-d H:i:s'),
];
@@ -136,13 +139,9 @@ class BillExport implements FromCollection, WithStyles, WithTitle
// 列头(包规=包规+单位合并;单价=每件零售价+单价单位;一级分类=商品根分类名)
$rows[] = ['序号', '品名', '一级分类', '包规', '单价', '数量', '重量(斤)', '金额'];
$this->specialRows[++$rowIndex] = 'header';
$currentHeaderRow = $rowIndex;
$this->headerRows[] = $currentHeaderRow;
if (count($this->headerRows) === 1) {
$this->headerRow = $rowIndex;
}
$this->headerRow = $rowIndex;
// 明细行(账单内同商品已合并,单价为每件价折算的零售价)
// 明细行(所选账单的同商品已合并,单价为合并后的每件价折算零售价)
$totalQuantity = 0;
$totalWeight = '0';
$totalAmount = '0';
@@ -171,10 +170,19 @@ class BillExport implements FromCollection, WithStyles, WithTitle
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'F', 'value' => (float) $totalQuantity];
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $totalAmount];
// 配送费/附加金额/售后金额/总金额(账单级费用全额计入,不受分类过滤影响)
$deliveryTotal = (string) $bill->delivery_fee;
$addedTotal = (string) $bill->added_amount;
$afterSaleTotal = (string) $bill->after_sale;
// 配送费/附加金额/售后金额按所选账单分别累加,不受分类过滤影响
$deliveryTotal = '0';
$addedTotal = '0';
$afterSaleTotal = '0';
$boxNum = 0;
$trayNum = 0;
foreach ($bills as $selectedBill) {
$deliveryTotal = bcadd($deliveryTotal, (string) $selectedBill->delivery_fee, 2);
$addedTotal = bcadd($addedTotal, (string) $selectedBill->added_amount, 2);
$afterSaleTotal = bcadd($afterSaleTotal, (string) $selectedBill->after_sale, 2);
$boxNum += (int) $selectedBill->box_num;
$trayNum += (int) $selectedBill->tray_num;
}
$grandTotal = bcadd(bcadd(bcadd($totalAmount, $deliveryTotal, 2), $addedTotal, 2), $afterSaleTotal, 2);
$rows[] = ['配送费', '', '', '', '', '', '', (float) $deliveryTotal];
@@ -182,7 +190,7 @@ class BillExport implements FromCollection, WithStyles, WithTitle
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $deliveryTotal];
$rows[] = ['附加金额(周转筐 ' . (int) $bill->box_num . ' 个 / 周转托盘 ' . (int) $bill->tray_num . ' 个)', '', '', '', '', '', '', (float) $addedTotal];
$rows[] = ['附加金额(周转筐 ' . $boxNum . ' 个 / 周转托盘 ' . $trayNum . ' 个)', '', '', '', '', '', '', (float) $addedTotal];
$this->specialRows[++$rowIndex] = 'summary';
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $addedTotal];
@@ -197,7 +205,38 @@ class BillExport implements FromCollection, WithStyles, WithTitle
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $grandTotal];
$this->lastRow = $rowIndex;
$this->tableRanges[] = ['header' => $currentHeaderRow, 'last' => $rowIndex];
}
/**
* 合并所选账单商品:数量、重量、金额累加,单价按总金额 ÷ 总数量重新加权
*
* @param Collection<int, BillModel> $bills
* @return array<int, array{product_id: int, product_name: string, product_spec: string, unit: string, price: string, quantity: int, weight: string, amount: string, spec: string, price_unit: string, image: string}>
*/
private function mergedItems(Collection $bills): array
{
$merged = [];
$service = app(BillDetailService::class);
foreach ($bills as $bill) {
foreach ($service->mergedItems($bill) as $item) {
$productId = (int) $item['product_id'];
if (! isset($merged[$productId])) {
$merged[$productId] = $item;
continue;
}
$merged[$productId]['quantity'] += (int) $item['quantity'];
$merged[$productId]['weight'] = bcadd($merged[$productId]['weight'], $item['weight'], 3);
$merged[$productId]['amount'] = bcadd($merged[$productId]['amount'], $item['amount'], 2);
$merged[$productId]['price'] = bcdiv(
$merged[$productId]['amount'],
(string) $merged[$productId]['quantity'],
2
);
}
}
return array_values($merged);
}
/**
@@ -231,22 +270,20 @@ class BillExport implements FromCollection, WithStyles, WithTitle
$sheet->getStyle('A' . $row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT);
}
// 各账单表格区域(列头 → 末行)添加所有边框,金额列设置货币格式
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);
}
// 表格区域(列头 → 末行)添加所有边框
$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);
// 列头固定填色:数量列(F)浅黄、金额列(H)浅红
foreach ($this->headerRows as $headerRow) {
$this->fillCell($sheet, 'F' . $headerRow, self::QUANTITY_FILL);
$this->fillCell($sheet, 'H' . $headerRow, self::AMOUNT_FILL);
}
$this->fillCell($sheet, 'F' . $this->headerRow, self::QUANTITY_FILL);
$this->fillCell($sheet, 'H' . $this->headerRow, self::AMOUNT_FILL);
// 数据区条件填色:非 0 填色,0 无背景(金额含负值)
foreach ($this->fillCells as $cell) {
@@ -322,7 +359,7 @@ class BillExport implements FromCollection, WithStyles, WithTitle
));
if ($filtered === []) {
throw new RepositoryException('该账单在「' . $categoryName . '」分类下无商品明细');
throw new RepositoryException('所选账单在「' . $categoryName . '」分类下无商品明细');
}
return [$filtered, $categoryName];