账单导出
This commit is contained in:
File diff suppressed because one or more lines are too long
+65
-47
@@ -17,7 +17,7 @@ use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
/**
|
||||
* 门店账单单独导出:账单内明细按商品合并(可按一级分类过滤),
|
||||
* 门店账单单独导出:账单内明细按商品合并(可按一级分类过滤),明细行展示商品一级分类,
|
||||
* 表尾汇总商品金额/配送费/附加金额/售后金额/总金额
|
||||
*/
|
||||
class BillExport implements FromCollection, WithStyles
|
||||
@@ -36,10 +36,10 @@ class BillExport implements FromCollection, WithStyles
|
||||
/** @var array<int, string> 特殊行索引(1 起)=> 类型(title/scope/header/summary/grand) */
|
||||
private array $specialRows = [];
|
||||
|
||||
/** @var array<int, int> 标签需左合并 A:F 的合计行索引 */
|
||||
/** @var array<int, int> 标签需左合并 A:G 的合计行索引 */
|
||||
private array $mergeRows = [];
|
||||
|
||||
/** @var array<int, array{row: int, column: string, value: float}> 条件填色单元格(E=数量浅黄,G=金额浅红,非 0 填色) */
|
||||
/** @var array<int, array{row: int, column: string, value: float}> 条件填色单元格(F=数量浅黄,H=金额浅红,非 0 填色) */
|
||||
private array $fillCells = [];
|
||||
|
||||
/** 列头所在行索引 */
|
||||
@@ -69,9 +69,12 @@ class BillExport implements FromCollection, WithStyles
|
||||
|
||||
$items = app(BillDetailService::class)->mergedItems($this->bill);
|
||||
|
||||
// 商品 => 一级分类解析(明细行「一级分类」列与分类过滤共用同一份结果)
|
||||
[$categories, $rootIds] = $this->resolveRootCategories($items);
|
||||
|
||||
$categoryName = '全部';
|
||||
if ($this->categoryId > 0) {
|
||||
[$items, $categoryName] = $this->filterByRootCategory($items);
|
||||
[$items, $categoryName] = $this->filterByRootCategory($items, $categories, $rootIds);
|
||||
}
|
||||
|
||||
$bill = $this->bill;
|
||||
@@ -96,8 +99,8 @@ class BillExport implements FromCollection, WithStyles
|
||||
$rows[] = [''];
|
||||
$rowIndex++;
|
||||
|
||||
// 列头(包规=包规+单位合并;单价=每件零售价+单价单位)
|
||||
$rows[] = ['序号', '品名', '包规', '单价', '数量', '重量(斤)', '金额'];
|
||||
// 列头(包规=包规+单位合并;单价=每件零售价+单价单位;一级分类=商品根分类名)
|
||||
$rows[] = ['序号', '品名', '一级分类', '包规', '单价', '数量', '重量(斤)', '金额'];
|
||||
$this->specialRows[++$rowIndex] = 'header';
|
||||
$this->headerRow = $rowIndex;
|
||||
|
||||
@@ -109,6 +112,7 @@ class BillExport implements FromCollection, WithStyles
|
||||
$rows[] = [
|
||||
$sort + 1,
|
||||
$item['product_name'],
|
||||
$categories->get($rootIds[$item['product_id']] ?? 0)?->name ?? '',
|
||||
$item['product_spec'] . $item['unit'],
|
||||
self::retailPriceLabel($item),
|
||||
(int) $item['quantity'],
|
||||
@@ -116,18 +120,18 @@ class BillExport implements FromCollection, WithStyles
|
||||
(float) $item['amount'],
|
||||
];
|
||||
$rowIndex++;
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'E', 'value' => (float) $item['quantity']];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'G', 'value' => (float) $item['amount']];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'F', 'value' => (float) $item['quantity']];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $item['amount']];
|
||||
$totalQuantity += (int) $item['quantity'];
|
||||
$totalWeight = bcadd($totalWeight, $item['weight'], 3);
|
||||
$totalAmount = bcadd($totalAmount, $item['amount'], 2);
|
||||
}
|
||||
|
||||
// 商品合计行
|
||||
$rows[] = ['', '合计', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
||||
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'E', 'value' => (float) $totalQuantity];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'G', 'value' => (float) $totalAmount];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'F', 'value' => (float) $totalQuantity];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $totalAmount];
|
||||
|
||||
// 配送费/附加金额/售后金额/总金额(账单级费用全额计入,不受分类过滤影响)
|
||||
$deliveryTotal = (string) $bill->delivery_fee;
|
||||
@@ -135,32 +139,32 @@ class BillExport implements FromCollection, WithStyles
|
||||
$afterSaleTotal = (string) $bill->after_sale;
|
||||
$grandTotal = bcadd(bcadd(bcadd($totalAmount, $deliveryTotal, 2), $addedTotal, 2), $afterSaleTotal, 2);
|
||||
|
||||
$rows[] = ['配送费', '', '', '', '', '', (float) $deliveryTotal];
|
||||
$rows[] = ['配送费', '', '', '', '', '', '', (float) $deliveryTotal];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'G', 'value' => (float) $deliveryTotal];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $deliveryTotal];
|
||||
|
||||
$rows[] = ['附加金额(周转筐 ' . (int) $bill->box_num . ' 个 / 周转托盘 ' . (int) $bill->tray_num . ' 个)', '', '', '', '', '', (float) $addedTotal];
|
||||
$rows[] = ['附加金额(周转筐 ' . (int) $bill->box_num . ' 个 / 周转托盘 ' . (int) $bill->tray_num . ' 个)', '', '', '', '', '', '', (float) $addedTotal];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'G', 'value' => (float) $addedTotal];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $addedTotal];
|
||||
|
||||
$rows[] = ['售后金额(可正负)', '', '', '', '', '', (float) $afterSaleTotal];
|
||||
$rows[] = ['售后金额(可正负)', '', '', '', '', '', '', (float) $afterSaleTotal];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'G', 'value' => (float) $afterSaleTotal];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $afterSaleTotal];
|
||||
|
||||
$rows[] = ['总金额(商品金额+配送费+附加金额+售后金额)', '', '', '', '', '', (float) $grandTotal];
|
||||
$rows[] = ['总金额(商品金额+配送费+附加金额+售后金额)', '', '', '', '', '', '', (float) $grandTotal];
|
||||
$this->specialRows[++$rowIndex] = 'grand';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'G', 'value' => (float) $grandTotal];
|
||||
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $grandTotal];
|
||||
$this->lastRow = $rowIndex;
|
||||
|
||||
return $this->rows = collect($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标题/列头/合计行加粗,合计行标签左合并 A:F;全表居中 + 全边框;金额列货币格式;
|
||||
* 标题/列头/合计行加粗,合计行标签左合并 A:G;全表居中 + 全边框;金额列货币格式;
|
||||
* 数量(浅黄)/金额(浅红)非 0 条件填色(0 不填、列头固定填色),冻结列头
|
||||
*/
|
||||
public function styles(Worksheet $sheet): array
|
||||
@@ -170,18 +174,19 @@ class BillExport implements FromCollection, WithStyles
|
||||
$sheet->freezePane('A' . ($this->headerRow + 1));
|
||||
$sheet->getColumnDimension('A')->setWidth(6);
|
||||
$sheet->getColumnDimension('B')->setWidth(24);
|
||||
$sheet->getColumnDimension('C')->setWidth(14);
|
||||
$sheet->getColumnDimension('D')->setWidth(12);
|
||||
$sheet->getColumnDimension('E')->setWidth(10);
|
||||
$sheet->getColumnDimension('F')->setWidth(12);
|
||||
$sheet->getColumnDimension('C')->setWidth(12);
|
||||
$sheet->getColumnDimension('D')->setWidth(14);
|
||||
$sheet->getColumnDimension('E')->setWidth(12);
|
||||
$sheet->getColumnDimension('F')->setWidth(10);
|
||||
$sheet->getColumnDimension('G')->setWidth(12);
|
||||
$sheet->getColumnDimension('H')->setWidth(12);
|
||||
|
||||
foreach ($this->mergeRows as $row) {
|
||||
$sheet->mergeCells('A' . $row . ':F' . $row);
|
||||
$sheet->mergeCells('A' . $row . ':G' . $row);
|
||||
}
|
||||
|
||||
// 全部单元格水平/垂直居中(合计行标签保持左对齐)
|
||||
$sheet->getStyle('A1:G' . $this->lastRow)
|
||||
$sheet->getStyle('A1:H' . $this->lastRow)
|
||||
->getAlignment()
|
||||
->setHorizontal(Alignment::HORIZONTAL_CENTER)
|
||||
->setVertical(Alignment::VERTICAL_CENTER);
|
||||
@@ -190,19 +195,19 @@ class BillExport implements FromCollection, WithStyles
|
||||
}
|
||||
|
||||
// 表格区域(列头 → 末行)添加所有边框
|
||||
$sheet->getStyle('A' . $this->headerRow . ':G' . $this->lastRow)
|
||||
$sheet->getStyle('A' . $this->headerRow . ':H' . $this->lastRow)
|
||||
->getBorders()
|
||||
->getAllBorders()
|
||||
->setBorderStyle(Border::BORDER_THIN);
|
||||
|
||||
// 金额列货币格式:¥ + 两位小数(单价列为文本,不设格式)
|
||||
$sheet->getStyle('G' . ($this->headerRow + 1) . ':G' . $this->lastRow)
|
||||
$sheet->getStyle('H' . ($this->headerRow + 1) . ':H' . $this->lastRow)
|
||||
->getNumberFormat()
|
||||
->setFormatCode(self::CURRENCY_FORMAT);
|
||||
|
||||
// 列头固定填色:数量列(E)浅黄、金额列(G)浅红
|
||||
$this->fillCell($sheet, 'E' . $this->headerRow, self::QUANTITY_FILL);
|
||||
$this->fillCell($sheet, 'G' . $this->headerRow, self::AMOUNT_FILL);
|
||||
// 列头固定填色:数量列(F)浅黄、金额列(H)浅红
|
||||
$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) {
|
||||
@@ -210,7 +215,7 @@ class BillExport implements FromCollection, WithStyles
|
||||
$this->fillCell(
|
||||
$sheet,
|
||||
$cell['column'] . $cell['row'],
|
||||
$cell['column'] === 'E' ? self::QUANTITY_FILL : self::AMOUNT_FILL
|
||||
$cell['column'] === 'F' ? self::QUANTITY_FILL : self::AMOUNT_FILL
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -260,16 +265,39 @@ class BillExport implements FromCollection, WithStyles
|
||||
}
|
||||
|
||||
/**
|
||||
* 按一级分类过滤明细:沿 parent_id 上溯取根分类(商品含软删除,保证历史账单可导出),
|
||||
* 仅保留根分类为所选分类的行
|
||||
* 按一级分类过滤明细:仅保留根分类为所选分类的行
|
||||
*
|
||||
* @param array<int, array> $items 合并后的商品明细
|
||||
* @param Collection<int, ProductCategoryModel> $categories 全部分类(id => 分类)
|
||||
* @param array<int, int> $rootIds 商品ID => 一级分类ID
|
||||
* @return array{0: array<int, array>, 1: string} [过滤后明细, 一级分类名]
|
||||
*/
|
||||
private function filterByRootCategory(array $items): array
|
||||
private function filterByRootCategory(array $items, Collection $categories, array $rootIds): array
|
||||
{
|
||||
$categoryName = $categories->get($this->categoryId)?->name ?? ('分类#' . $this->categoryId);
|
||||
|
||||
$categoryId = $this->categoryId;
|
||||
$filtered = array_values(array_filter(
|
||||
$items,
|
||||
static fn (array $row): bool => ($rootIds[$row['product_id']] ?? 0) === $categoryId
|
||||
));
|
||||
|
||||
if ($filtered === []) {
|
||||
throw new RepositoryException('该账单在「' . $categoryName . '」分类下无商品明细');
|
||||
}
|
||||
|
||||
return [$filtered, $categoryName];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析明细商品的一级分类:沿 parent_id 上溯取根分类(商品含软删除,保证历史账单可导出)
|
||||
*
|
||||
* @param array<int, array> $items 合并后的商品明细
|
||||
* @return array{0: Collection<int, ProductCategoryModel>, 1: array<int, int>} [全部分类(id=>分类), 商品ID=>一级分类ID]
|
||||
*/
|
||||
private function resolveRootCategories(array $items): array
|
||||
{
|
||||
$categories = ProductCategoryModel::all()->keyBy('id');
|
||||
$categoryName = $categories->get($this->categoryId)->name ?? ('分类#' . $this->categoryId);
|
||||
|
||||
$productCategoryIds = ProductModel::withTrashed()
|
||||
->whereIn('id', array_column($items, 'product_id'))
|
||||
@@ -292,16 +320,6 @@ class BillExport implements FromCollection, WithStyles
|
||||
$rootIds[(int) $productId] = $rootId;
|
||||
}
|
||||
|
||||
$categoryId = $this->categoryId;
|
||||
$filtered = array_values(array_filter(
|
||||
$items,
|
||||
static fn (array $row): bool => ($rootIds[$row['product_id']] ?? 0) === $categoryId
|
||||
));
|
||||
|
||||
if ($filtered === []) {
|
||||
throw new RepositoryException('该账单在「' . $categoryName . '」分类下无商品明细');
|
||||
}
|
||||
|
||||
return [$filtered, $categoryName];
|
||||
return [$categories, $rootIds];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,38 +98,42 @@ class BillExportTest extends ProcurementTestCase
|
||||
if ($rowA === null || $rowB === null) {
|
||||
return false;
|
||||
}
|
||||
// 一级分类列=商品根分类名
|
||||
if ($rowA[2] !== '蔬菜' || $rowB[2] !== '蔬菜') {
|
||||
return false;
|
||||
}
|
||||
// 包规列=包规+单位合并;单价列=每件价÷每件斤数+单价单位(A:2.37÷10=0.23元/斤)
|
||||
if ($rowA[2] !== '10斤/箱' || $rowA[3] !== '0.23元/斤') {
|
||||
if ($rowA[3] !== '10斤/箱' || $rowA[4] !== '0.23元/斤') {
|
||||
return false;
|
||||
}
|
||||
// 散装无法折算 → 单价按每件原价显示(B:5.00元/斤)
|
||||
if ($rowB[2] !== '散装筐' || $rowB[3] !== '5.00元/斤') {
|
||||
if ($rowB[3] !== '散装筐' || $rowB[4] !== '5.00元/斤') {
|
||||
return false;
|
||||
}
|
||||
// 账单内同商品合并:数量 16、金额 38.00
|
||||
if ((int) $rowA[4] !== 16 || (float) $rowA[6] !== 38.0) {
|
||||
if ((int) $rowA[5] !== 16 || (float) $rowA[7] !== 38.0) {
|
||||
return false;
|
||||
}
|
||||
if ((int) $rowB[4] !== 4 || (float) $rowB[6] !== 20.0) {
|
||||
if ((int) $rowB[5] !== 4 || (float) $rowB[7] !== 20.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$summary = $rows->slice(6)->values();
|
||||
// 合计:数量 20、商品金额 58.00;配送费 10.00;附加 30.00(筐 2 / 托盘 1);售后 -8.00;总金额 90.00
|
||||
if ((int) $summary[0][4] !== 20 || (float) $summary[0][6] !== 58.0) {
|
||||
if ((int) $summary[0][5] !== 20 || (float) $summary[0][7] !== 58.0) {
|
||||
return false;
|
||||
}
|
||||
if ($summary[1][0] !== '配送费' || (float) $summary[1][6] !== 10.0) {
|
||||
if ($summary[1][0] !== '配送费' || (float) $summary[1][7] !== 10.0) {
|
||||
return false;
|
||||
}
|
||||
if (! str_contains((string) $summary[2][0], '筐 2 个 / 周转托盘 1 个') || (float) $summary[2][6] !== 30.0) {
|
||||
if (! str_contains((string) $summary[2][0], '筐 2 个 / 周转托盘 1 个') || (float) $summary[2][7] !== 30.0) {
|
||||
return false;
|
||||
}
|
||||
if (! str_contains((string) $summary[3][0], '售后金额') || (float) $summary[3][6] !== -8.0) {
|
||||
if (! str_contains((string) $summary[3][0], '售后金额') || (float) $summary[3][7] !== -8.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (float) $summary[4][6] === 90.0;
|
||||
return (float) $summary[4][7] === 90.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -161,22 +165,22 @@ class BillExportTest extends ProcurementTestCase
|
||||
if (! str_contains((string) $rows[1][0], '分类:蔬菜')) {
|
||||
return false;
|
||||
}
|
||||
// 明细仅剩蔬菜(子分类归入根分类),肉禽被忽略
|
||||
// 明细仅剩蔬菜(子分类归入根分类),肉禽被忽略;一级分类列显示根分类名
|
||||
$detail = $rows->slice(4, 1)->values()->first();
|
||||
if ($detail === null || $detail[1] !== $veg->name) {
|
||||
if ($detail === null || $detail[1] !== $veg->name || $detail[2] !== '蔬菜') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$summary = $rows->slice(5)->values();
|
||||
// 商品金额合计 20.00(仅蔬菜);配送费 10.00 / 附加 30.00 / 售后 0.00 全额;总金额 60.00
|
||||
if ((float) $summary[0][6] !== 20.0) {
|
||||
if ((float) $summary[0][7] !== 20.0) {
|
||||
return false;
|
||||
}
|
||||
if ((float) $summary[1][6] !== 10.0 || (float) $summary[2][6] !== 30.0 || (float) $summary[3][6] !== 0.0) {
|
||||
if ((float) $summary[1][7] !== 10.0 || (float) $summary[2][7] !== 30.0 || (float) $summary[3][7] !== 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (float) $summary[4][6] === 60.0;
|
||||
return (float) $summary[4][7] === 60.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user