账单导出

This commit is contained in:
liu
2026-09-13 23:42:35 +08:00
parent 5784875939
commit 80cbd452e5
12 changed files with 112 additions and 228 deletions
+44 -58
View File
@@ -12,8 +12,8 @@ use App\Models\StoreOrderItemModel;
use Maatwebsite\Excel\Facades\Excel;
/**
* 门店账单合并导出:跨账单按商品合并明细(加权平均单价)/ 一级分类过滤 /
* 配送费与附加金额全额汇总 / ids 校验 / 权限拦截 / 分类下无明细拒绝
* 门店账单单独导出:账单内按商品合并明细(加权平均单价)/ 一级分类过滤 /
* 费用行取账单自身字段 / 权限拦截 / 账单不存在 / 分类下无明细拒绝 / 小程序端本店口径
*/
class BillExportTest extends ProcurementTestCase
{
@@ -60,43 +60,44 @@ class BillExportTest extends ProcurementTestCase
]);
}
/** 跨账单合并:同商品数量累加、单价加权平均,配送费/附加/总金额全额汇总 */
public function test_export_merges_items_across_bills(): void
/** 单独导出:账单内同商品合并(数量累加、单价加权平均),费用行取账单自身字段 */
public function test_export_merges_items_within_bill(): void
{
$this->freezeTime();
$root = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
$store = StoreModel::factory()->create();
$productA = ProductModel::factory()->create(['category_id' => $root->id]);
$productB = ProductModel::factory()->create(['category_id' => $root->id]);
$bill1 = $this->makeBill($store);
$bill2 = $this->makeBill($store, ['after_sale' => '-8.00']);
// 同一商品跨账单:10×2.00 + 6×3.00 → 数量 16、金额 38.00、加权单价 2.37
$this->makeItem($bill1, $productA, 10, '2.00');
$this->makeItem($bill2, $productA, 6, '3.00');
$this->makeItem($bill2, $productB, 4, '5.00');
$bill = $this->makeBill($store, ['after_sale' => '-8.00']);
// 同一商品两条明细:10×2.00 + 6×3.00 → 数量 16、金额 38.00、加权单价 2.37
$this->makeItem($bill, $productA, 10, '2.00');
$this->makeItem($bill, $productA, 6, '3.00');
$this->makeItem($bill, $productB, 4, '5.00');
$this->actingAsSysUser();
Excel::fake();
$this->get("/recon/bill/export?ids={$bill1->id},{$bill2->id}")->assertOk();
$this->get("/recon/bill/{$bill->id}/export")->assertOk();
Excel::assertDownloaded(
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
static function (BillExport $export) use ($productA, $productB): bool {
'门店账单_' . $bill->bill_no . '.xlsx',
static function (BillExport $export) use ($bill, $productA, $productB): bool {
$rows = $export->collection()->values();
// 标题1 + 范围2 + 空行1 + 列头1 + 明细2 + 合计5 = 12 行
if ($rows->count() !== 12) {
// 标题1 + 范围1 + 空行1 + 列头1 + 明细2 + 合计5 = 11 行
if ($rows->count() !== 11) {
return false;
}
// 标题行含账单号
if (! str_contains((string) $rows[0][0], $bill->bill_no)) {
return false;
}
$details = $rows->slice(5, 2)->values();
$details = $rows->slice(4, 2)->values();
$rowA = $details->firstWhere(1, $productA->name);
$rowB = $details->firstWhere(1, $productB->name);
if ($rowA === null || $rowB === null) {
return false;
}
// 跨账单同商品合并:数量 16、加权单价 2.37、金额 38.00
// 账单内同商品合并:数量 16、加权单价 2.37、金额 38.00
if ((float) $rowA[4] !== 2.37 || (int) $rowA[5] !== 16 || (float) $rowA[7] !== 38.0) {
return false;
}
@@ -104,31 +105,29 @@ class BillExportTest extends ProcurementTestCase
return false;
}
$summary = $rows->slice(7)->values();
// 合计:数量 20、商品金额 58.00;配送费 20.00;附加 60.00(筐 4 / 托盘 2);售后 -8.00;总金额 130.00
$summary = $rows->slice(6)->values();
// 合计:数量 20、商品金额 58.00;配送费 10.00;附加 30.00(筐 2 / 托盘 1);售后 -8.00;总金额 90.00
if ((int) $summary[0][5] !== 20 || (float) $summary[0][7] !== 58.0) {
return false;
}
if ($summary[1][0] !== '配送费合计' || (float) $summary[1][7] !== 20.0) {
if ($summary[1][0] !== '配送费' || (float) $summary[1][7] !== 10.0) {
return false;
}
if (! str_contains((string) $summary[2][0], '筐 4 个 / 周转托盘 2 个') || (float) $summary[2][7] !== 60.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][7] !== -8.0) {
if (! str_contains((string) $summary[3][0], '售后金额') || (float) $summary[3][7] !== -8.0) {
return false;
}
return (float) $summary[4][7] === 130.0;
return (float) $summary[4][7] === 90.0;
}
);
}
/** 一级分类过滤:子分类商品归入根分类,其他分类商品忽略;费用行仍全额汇总 */
/** 一级分类过滤:子分类商品归入根分类,其他分类商品忽略;费用行仍全额计入 */
public function test_export_filters_by_root_category(): void
{
$this->freezeTime();
$vegRoot = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
$vegChild = ProductCategoryModel::create(['name' => '叶菜类', 'parent_id' => $vegRoot->id, 'sort' => 0, 'status' => 1]);
$meatRoot = ProductCategoryModel::create(['name' => '肉禽蛋', 'parent_id' => 0, 'sort' => 1, 'status' => 1]);
@@ -143,23 +142,23 @@ class BillExportTest extends ProcurementTestCase
$this->actingAsSysUser();
Excel::fake();
$this->get("/recon/bill/export?ids={$bill->id}&category_id={$vegRoot->id}")->assertOk();
$this->get("/recon/bill/{$bill->id}/export?category_id={$vegRoot->id}")->assertOk();
Excel::assertDownloaded(
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
'门店账单_' . $bill->bill_no . '.xlsx',
static function (BillExport $export) use ($veg): bool {
$rows = $export->collection()->values();
// 范围行标注所选分类
if (! str_contains((string) $rows[2][0], '分类:蔬菜')) {
if (! str_contains((string) $rows[1][0], '分类:蔬菜')) {
return false;
}
// 明细仅剩蔬菜(子分类归入根分类),肉禽被忽略
$detail = $rows->slice(5, 1)->values()->first();
$detail = $rows->slice(4, 1)->values()->first();
if ($detail === null || $detail[1] !== $veg->name) {
return false;
}
$summary = $rows->slice(6)->values();
$summary = $rows->slice(5)->values();
// 商品金额合计 20.00(仅蔬菜);配送费 10.00 / 附加 30.00 / 售后 0.00 全额;总金额 60.00
if ((float) $summary[0][7] !== 20.0) {
return false;
@@ -186,7 +185,7 @@ class BillExportTest extends ProcurementTestCase
$this->makeItem($bill, $meat, 5, '20.00');
$this->actingAsSysUser();
$this->get("/recon/bill/export?ids={$bill->id}&category_id={$vegRoot->id}")
$this->get("/recon/bill/{$bill->id}/export?category_id={$vegRoot->id}")
->assertJsonPath('success', false);
}
@@ -202,30 +201,22 @@ class BillExportTest extends ProcurementTestCase
// 仅持有查询权限的用户
$this->actingAsSysUser(['recon.bill.query']);
$response = $this->get("/recon/bill/export?ids={$bill->id}");
$response = $this->get("/recon/bill/{$bill->id}/export");
$this->assertFalse($response->json('success'), '缺少权限点应被拦截');
}
/** ids 缺失 / 超 100 张 / 账单不存在 → 拒绝 */
public function test_export_invalid_ids_rejected(): void
/** 账单不存在 → 拒绝 */
public function test_export_bill_not_found_rejected(): void
{
$this->actingAsSysUser();
$this->get('/recon/bill/export')
->assertJsonPath('success', false);
$this->get('/recon/bill/export?ids=' . implode(',', range(1, 101)))
->assertJsonPath('success', false);
$this->get('/recon/bill/export?ids=99999')
$this->get('/recon/bill/99999/export')
->assertJsonPath('success', false);
}
/** 小程序端导出:强制本店过滤,混入他店账单整批拒绝 */
/** 小程序端导出:强制本店过滤,他店账单拒绝 */
public function test_mini_export_scoped_to_own_store(): void
{
$this->freezeTime();
$root = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
$storeA = StoreModel::factory()->create();
$storeB = StoreModel::factory()->create();
@@ -234,28 +225,23 @@ class BillExportTest extends ProcurementTestCase
$billA = $this->makeBill($storeA);
$billB = $this->makeBill($storeB);
$this->makeItem($billA, $product, 10, '2.00');
$this->makeItem($billB, $product, 5, '20.00');
$this->actingAsMiniStore($storeA);
// 本店账单正常导出
Excel::fake();
$this->get("/mini/bill/export?ids={$billA->id}")->assertOk();
$this->get("/mini/bill/{$billA->id}/export")->assertOk();
Excel::assertDownloaded(
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
'门店账单_' . $billA->bill_no . '.xlsx',
static function (BillExport $export): bool {
$rows = $export->collection()->values();
// 明细仅本店账单的 1 行(他店账单未混入)
return $rows->count() === 11;
// 标题1 + 范围1 + 空行1 + 列头1 + 明细1 + 合计5 = 10 行
return $rows->count() === 10;
}
);
// 混入他店账单 → 张数不匹配整批拒绝
$this->get("/mini/bill/export?ids={$billA->id},{$billB->id}")
->assertJsonPath('success', false);
// 仅他店账单 → 本店口径查无账单
$this->get("/mini/bill/export?ids={$billB->id}")
// 他店账单 → 本店口径查无账单
$this->get("/mini/bill/{$billB->id}/export")
->assertJsonPath('success', false);
}
}