售后金额
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
/**
|
||||
* 门店账单合并导出:勾选账单跨账单按商品合并明细(可按一级分类过滤),
|
||||
* 表尾汇总商品金额/配送费/附加金额/总金额
|
||||
* 表尾汇总商品金额/配送费/附加金额/售后金额/总金额
|
||||
*/
|
||||
class BillExport implements FromCollection, WithStyles
|
||||
{
|
||||
@@ -113,18 +113,20 @@ class BillExport implements FromCollection, WithStyles
|
||||
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
|
||||
// 配送费/附加金额/总金额(账单级费用全额汇总,不受分类过滤影响)
|
||||
// 配送费/附加金额/售后金额/总金额(账单级费用全额汇总,不受分类过滤影响)
|
||||
$deliveryTotal = '0';
|
||||
$addedTotal = '0';
|
||||
$afterSaleTotal = '0';
|
||||
$boxNum = 0;
|
||||
$trayNum = 0;
|
||||
foreach ($this->bills as $bill) {
|
||||
$deliveryTotal = bcadd($deliveryTotal, (string) $bill->delivery_fee, 2);
|
||||
$addedTotal = bcadd($addedTotal, (string) $bill->added_amount, 2);
|
||||
$afterSaleTotal = bcadd($afterSaleTotal, (string) $bill->after_sale, 2);
|
||||
$boxNum += (int) $bill->box_num;
|
||||
$trayNum += (int) $bill->tray_num;
|
||||
}
|
||||
$grandTotal = bcadd(bcadd($totalAmount, $deliveryTotal, 2), $addedTotal, 2);
|
||||
$grandTotal = bcadd(bcadd(bcadd($totalAmount, $deliveryTotal, 2), $addedTotal, 2), $afterSaleTotal, 2);
|
||||
|
||||
$rows[] = ['配送费合计', '', '', '', '', '', '', (float) $deliveryTotal];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
@@ -134,7 +136,11 @@ class BillExport implements FromCollection, WithStyles
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
|
||||
$rows[] = ['总金额(商品金额+配送费+附加金额)', '', '', '', '', '', '', (float) $grandTotal];
|
||||
$rows[] = ['售后金额合计(可正负)', '', '', '', '', '', '', (float) $afterSaleTotal];
|
||||
$this->specialRows[++$rowIndex] = 'summary';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
|
||||
$rows[] = ['总金额(商品金额+配送费+附加金额+售后金额)', '', '', '', '', '', '', (float) $grandTotal];
|
||||
$this->specialRows[++$rowIndex] = 'grand';
|
||||
$this->mergeRows[] = $rowIndex;
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ class BillController extends BaseMiniController
|
||||
'box_price' => $bill->box_price,
|
||||
'tray_price' => $bill->tray_price,
|
||||
'added_amount' => $bill->added_amount,
|
||||
'after_sale' => $bill->after_sale,
|
||||
'total_amount' => $bill->total_amount,
|
||||
'status' => $bill->status,
|
||||
'status_name' => BillModel::STATUS_NAMES[$bill->status] ?? '',
|
||||
|
||||
@@ -145,6 +145,7 @@ class CartController extends BaseMiniController
|
||||
'name' => $product->name ?? '',
|
||||
'spec' => $product->spec ?? '',
|
||||
'unit' => $product->unit ?? '',
|
||||
'price_unit' => $product->price_unit ?? '',
|
||||
'image' => $firstFile?->file_url ?? '',
|
||||
'price' => $price,
|
||||
'quantity' => $quantity,
|
||||
|
||||
@@ -97,6 +97,7 @@ class OrderController extends BaseMiniController
|
||||
'product_spec' => $product->spec,
|
||||
'unit' => (string) $product->unit,
|
||||
'price' => $price,
|
||||
'price_unit' => $product->price_unit,
|
||||
'image_ids' => implode(',', (array) $product->image_ids),
|
||||
'content' => (string) $product->content,
|
||||
'shelf_life' => (int) $product->shelf_life,
|
||||
|
||||
@@ -152,7 +152,7 @@ class PaymentController extends BaseMiniController
|
||||
|
||||
$bills = $payment->bills()
|
||||
->orderBy('id')
|
||||
->get(['id', 'bill_no', 'bill_date', 'product_amount', 'delivery_fee', 'added_amount', 'total_amount', 'status'])
|
||||
->get(['id', 'bill_no', 'bill_date', 'product_amount', 'delivery_fee', 'added_amount', 'after_sale', 'total_amount', 'status'])
|
||||
->toArray();
|
||||
|
||||
$data = $payment->toArray();
|
||||
|
||||
@@ -56,7 +56,7 @@ class PaymentController extends BaseController
|
||||
|
||||
$bills = $payment->bills()
|
||||
->orderBy('id')
|
||||
->get(['id', 'bill_no', 'bill_date', 'product_amount', 'delivery_fee', 'added_amount', 'total_amount', 'status'])
|
||||
->get(['id', 'bill_no', 'bill_date', 'product_amount', 'delivery_fee', 'added_amount', 'after_sale', 'total_amount', 'status'])
|
||||
->toArray();
|
||||
|
||||
$data = $payment->toArray();
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace App\Http\Requests\Purchase;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
/**
|
||||
* 采购单生成账单 验证(按门店提交配送费/周转筐/托盘数量与售后/备注,金额由系统汇总不可修改;
|
||||
* 筐/托盘数量正数=压筐附加金额,负数=回筐抵扣金额)
|
||||
* 采购单生成账单 验证(按门店提交配送费/周转筐/托盘数量与售后金额/备注,金额由系统汇总不可修改;
|
||||
* 筐/托盘数量正数=压筐附加金额,负数=回筐抵扣金额;售后金额可正负,计入总金额)
|
||||
*/
|
||||
class PurchaseBillGenerateRequest extends BaseFormRequest
|
||||
{
|
||||
@@ -20,7 +20,7 @@ class PurchaseBillGenerateRequest extends BaseFormRequest
|
||||
'stores.*.delivery_fee' => 'required|numeric|min:0',
|
||||
'stores.*.box_num' => 'required|integer',
|
||||
'stores.*.tray_num' => 'required|integer',
|
||||
'stores.*.after_sale' => 'nullable|string|max:255',
|
||||
'stores.*.after_sale' => 'nullable|numeric',
|
||||
'stores.*.remark' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class PurchaseBillGenerateRequest extends BaseFormRequest
|
||||
'stores.*.box_num.integer' => '周转筐数量必须为整数(正数=压筐,负数=回筐)',
|
||||
'stores.*.tray_num.required' => '周转托盘数量不能为空',
|
||||
'stores.*.tray_num.integer' => '周转托盘数量必须为整数(正数=压筐,负数=回筐)',
|
||||
'stores.*.after_sale.max' => '售后说明最长 255 个字符',
|
||||
'stores.*.after_sale.numeric' => '售后金额格式错误(可正负)',
|
||||
'stores.*.remark.max' => '备注最长 255 个字符',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ class BillModel extends Model
|
||||
'tray_price' => 'decimal:2',
|
||||
'added_amount' => 'decimal:2',
|
||||
'total_amount' => 'decimal:2',
|
||||
'after_sale' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'payment_id' => 'integer',
|
||||
'paid_at' => 'datetime:Y-m-d H:i:s',
|
||||
|
||||
@@ -30,6 +30,7 @@ class StoreOrderItemModel extends Model
|
||||
'product_spec',
|
||||
'unit',
|
||||
'price',
|
||||
'price_unit',
|
||||
'image_ids',
|
||||
'content',
|
||||
'shelf_life',
|
||||
|
||||
@@ -20,8 +20,9 @@ use Throwable;
|
||||
* 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改)
|
||||
* 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照;
|
||||
* 数量正数=压筐附加金额,负数=回筐抵扣金额)
|
||||
* 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联,订单状态置为已完成
|
||||
* 5. 压回筐记录:筐/托盘数量非 0 时写入完整快照(数量/单价/金额均可为负)
|
||||
* 4. 售后金额 = 按门店填写的调整金额(可正负:正数=加收,负数=售后减免)
|
||||
* 5. 总金额 = 商品金额 + 配送费 + 附加金额 + 售后金额;回写门店订单 bill_id 完成关联,订单状态置为已完成
|
||||
* 6. 压回筐记录:筐/托盘数量非 0 时写入完整快照(数量/单价/金额均可为负)
|
||||
*/
|
||||
readonly class BillGenerateService
|
||||
{
|
||||
@@ -31,7 +32,7 @@ readonly class BillGenerateService
|
||||
|
||||
/**
|
||||
* @param PurchaseOrderModel $purchase 已完成采购单
|
||||
* @param array<int, array{store_id: int, delivery_fee: string, box_num: int, tray_num: int, after_sale?: string, remark?: string}> $stores 按门店提交的配送费/周转筐/托盘数量与售后/备注
|
||||
* @param array<int, array{store_id: int, delivery_fee: string, box_num: int, tray_num: int, after_sale?: string|int|float, remark?: string}> $stores 按门店提交的配送费/周转筐/托盘数量与售后金额/备注
|
||||
* @param int $operatorId 生成人(后台系统用户ID)
|
||||
* @return BillModel[] 生成的账单列表
|
||||
* @throws Throwable
|
||||
@@ -95,7 +96,8 @@ readonly class BillGenerateService
|
||||
bcmul((string) (int) $row['tray_num'], $trayPrice, 2),
|
||||
2
|
||||
);
|
||||
$totalAmount = bcadd(bcadd($productAmount, $deliveryFee, 2), $addedAmount, 2);
|
||||
$afterSale = bcadd((string) ($row['after_sale'] ?? '0'), '0', 2);
|
||||
$totalAmount = bcadd(bcadd(bcadd($productAmount, $deliveryFee, 2), $addedAmount, 2), $afterSale, 2);
|
||||
|
||||
$bill = BillModel::create([
|
||||
'bill_no' => $this->billNumberService->make('ZD'),
|
||||
@@ -111,7 +113,7 @@ readonly class BillGenerateService
|
||||
'added_amount' => $addedAmount,
|
||||
'total_amount' => $totalAmount,
|
||||
'operator_id' => $operatorId,
|
||||
'after_sale' => (string) ($row['after_sale'] ?? ''),
|
||||
'after_sale' => $afterSale,
|
||||
'remark' => (string) ($row['remark'] ?? ''),
|
||||
]);
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ return new class extends Migration
|
||||
$table->string('product_spec', 100)->default('')->comment('规格/包规');
|
||||
$table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)');
|
||||
$table->decimal('price', 10, 2)->default(0)->comment('单价');
|
||||
$table->string('price_unit', 20)->default('')->comment('单价单位');
|
||||
$table->string('image_ids', 255)->default('')->comment('商品图片');
|
||||
$table->text('content')->comment('商品图文详情');
|
||||
$table->integer('shelf_life')->default(0)->comment('保质期');
|
||||
|
||||
@@ -28,7 +28,7 @@ return new class extends Migration
|
||||
$table->decimal('box_price', 10, 2)->default(0)->comment('周转筐单价(生成时快照)');
|
||||
$table->decimal('tray_price', 10, 2)->default(0)->comment('周转托盘单价(生成时快照)');
|
||||
$table->decimal('added_amount', 10, 2)->default(0)->comment('附加金额(周转筐/托盘金额)');
|
||||
$table->decimal('total_amount', 10, 2)->default(0)->comment('账单总金额 = 商品金额 + 配送费 + 附加金额');
|
||||
$table->decimal('total_amount', 10, 2)->default(0)->comment('账单总金额 = 商品金额 + 配送费 + 附加金额 + 售后金额');
|
||||
$table->integer('status')->default(0)->comment('支付状态(0未支付 1已支付)');
|
||||
$table->integer('payment_id')->default(0)->comment('关联支付记录ID(0=未发起支付)');
|
||||
$table->timestamp('paid_at')->nullable()->comment('付款时间(线下收款手动登记)');
|
||||
@@ -36,7 +36,7 @@ return new class extends Migration
|
||||
$table->integer('paid_operator_id')->default(0)->comment('收款操作人(后台系统用户ID)');
|
||||
$table->integer('operator_id')->default(0)->comment('生成人(后台系统用户ID)');
|
||||
$table->string('remark', 255)->nullable()->default('')->comment('备注(生成账单时按门店填写)');
|
||||
$table->string('after_sale', 255)->nullable()->default('')->comment('售后说明(生成账单时按门店填写)');
|
||||
$table->decimal('after_sale', 10, 2)->default(0)->comment('售后金额(生成账单时按门店填写,可正负,计入总金额)');
|
||||
$table->timestamps();
|
||||
$table->unique(['purchase_id', 'store_id'], 'bill_purchase_store_unique');
|
||||
$table->index(['store_id', 'bill_date'], 'bill_store_date_index');
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicons.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XinAdmin</title>
|
||||
<script type="module" crossorigin src="/assets/index-CA-P_7Vi.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-eBrnVZAn.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-BgaNhQyE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-CRBytmvs.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/chunk-KS7C4IRE-Zm15rq6F.js">
|
||||
@@ -92,7 +92,7 @@
|
||||
<link rel="modulepreload" crossorigin href="/assets/useMobile-Bcq0nkW4.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/dict-CDRllPHM.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/relativeTime-jamE_cdZ.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-9KKK8arf.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CkDjMKy9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -19,7 +19,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
{
|
||||
private static int $billSeq = 0;
|
||||
|
||||
/** 造一张账单(默认配送费 10、筐 2×5 + 托盘 1×20 = 附加 30、总额 140,未支付) */
|
||||
/** 造一张账单(默认配送费 10、筐 2×5 + 托盘 1×20 = 附加 30、售后 0、总额 140,未支付) */
|
||||
private function makeBill(StoreModel $store, array $attributes = []): BillModel
|
||||
{
|
||||
$seq = ++self::$billSeq;
|
||||
@@ -36,6 +36,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
'box_price' => '5.00',
|
||||
'tray_price' => '20.00',
|
||||
'added_amount' => '30.00',
|
||||
'after_sale' => '0.00',
|
||||
'total_amount' => '140.00',
|
||||
'status' => BillModel::STATUS_UNPAID,
|
||||
], $attributes));
|
||||
@@ -70,7 +71,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
$productB = ProductModel::factory()->create(['category_id' => $root->id]);
|
||||
|
||||
$bill1 = $this->makeBill($store);
|
||||
$bill2 = $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');
|
||||
@@ -84,8 +85,8 @@ class BillExportTest extends ProcurementTestCase
|
||||
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
|
||||
static function (BillExport $export) use ($productA, $productB): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 标题1 + 范围2 + 空行1 + 列头1 + 明细2 + 合计4 = 11 行
|
||||
if ($rows->count() !== 11) {
|
||||
// 标题1 + 范围2 + 空行1 + 列头1 + 明细2 + 合计5 = 12 行
|
||||
if ($rows->count() !== 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
}
|
||||
|
||||
$summary = $rows->slice(7)->values();
|
||||
// 合计:数量 20、商品金额 58.00;配送费 20.00;附加 60.00(筐 4 / 托盘 2);总金额 138.00
|
||||
// 合计:数量 20、商品金额 58.00;配送费 20.00;附加 60.00(筐 4 / 托盘 2);售后 -8.00;总金额 130.00
|
||||
if ((int) $summary[0][5] !== 20 || (float) $summary[0][7] !== 58.0) {
|
||||
return false;
|
||||
}
|
||||
@@ -114,8 +115,11 @@ class BillExportTest extends ProcurementTestCase
|
||||
if (! str_contains((string) $summary[2][0], '筐 4 个 / 周转托盘 2 个') || (float) $summary[2][7] !== 60.0) {
|
||||
return false;
|
||||
}
|
||||
if (! str_contains((string) $summary[3][0], '售后金额合计') || (float) $summary[3][7] !== -8.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (float) $summary[3][7] === 138.0;
|
||||
return (float) $summary[4][7] === 130.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -156,15 +160,15 @@ class BillExportTest extends ProcurementTestCase
|
||||
}
|
||||
|
||||
$summary = $rows->slice(6)->values();
|
||||
// 商品金额合计 20.00(仅蔬菜);配送费 10.00 / 附加 30.00 全额;总金额 60.00
|
||||
// 商品金额合计 20.00(仅蔬菜);配送费 10.00 / 附加 30.00 / 售后 0.00 全额;总金额 60.00
|
||||
if ((float) $summary[0][7] !== 20.0) {
|
||||
return false;
|
||||
}
|
||||
if ((float) $summary[1][7] !== 10.0 || (float) $summary[2][7] !== 30.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[3][7] === 60.0;
|
||||
return (float) $summary[4][7] === 60.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -242,7 +246,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
static function (BillExport $export): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 明细仅本店账单的 1 行(他店账单未混入)
|
||||
return $rows->count() === 10;
|
||||
return $rows->count() === 11;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/** 生成账单:按门店保存售后说明与备注(可选,不参与金额计算) */
|
||||
/** 生成账单:按门店保存售后金额(可正负,计入总金额)与备注(仅存档不参与计算) */
|
||||
public function test_generate_bill_saves_after_sale_and_remark(): void
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
@@ -97,17 +97,17 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
'delivery_fee' => 0,
|
||||
'box_num' => 0,
|
||||
'tray_num' => 0,
|
||||
'after_sale' => '白菜烂叶 2 斤已协商',
|
||||
'remark' => '下次配送顺带回收筐',
|
||||
'after_sale' => -5.5,
|
||||
'remark' => '白菜烂叶 2 斤已协商',
|
||||
]],
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$bill = BillModel::where('store_id', $store->id)->first();
|
||||
$this->assertSame('白菜烂叶 2 斤已协商', $bill->after_sale);
|
||||
$this->assertSame('下次配送顺带回收筐', $bill->remark);
|
||||
$this->assertSame('20.00', (string) $bill->total_amount, '售后/备注不影响金额');
|
||||
$this->assertSame('-5.50', (string) $bill->after_sale);
|
||||
$this->assertSame('白菜烂叶 2 斤已协商', $bill->remark);
|
||||
$this->assertSame('14.50', (string) $bill->total_amount, '总金额 = 商品 20.00 + 售后金额 -5.50');
|
||||
|
||||
// 不传售后/备注时默认空字符串
|
||||
// 不传售后金额时按 0 计入,备注默认空字符串
|
||||
$store2 = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$this->actingAsMiniStore($store2);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
@@ -125,8 +125,65 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$bill2 = BillModel::where('store_id', $store2->id)->first();
|
||||
$this->assertSame('', (string) $bill2->after_sale);
|
||||
$this->assertSame('0.00', (string) $bill2->after_sale);
|
||||
$this->assertSame('', (string) $bill2->remark);
|
||||
$this->assertSame('5.00', (string) $bill2->total_amount, '未传售后金额不影响总金额');
|
||||
}
|
||||
|
||||
/** 生成账单:售后金额为正数时加收计入总金额 */
|
||||
public function test_generate_bill_positive_after_sale_added_to_total(): void
|
||||
{
|
||||
[$store, , $bill] = $this->makeBillViaChain();
|
||||
$this->assertSame('20.00', (string) $bill->total_amount, '基线:无售后金额时总额=商品金额');
|
||||
|
||||
// 重新走一条链:售后金额 +8.00,配送费 2.00 → 总额 20 + 2 + 8 = 30.00
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store2 = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
|
||||
$this->actingAsMiniStore($store2);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
|
||||
->assertJsonPath('success', true);
|
||||
$order2 = StoreOrderModel::where('store_id', $store2->id)->first();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||
->assertJsonPath('success', true);
|
||||
$purchase2 = PurchaseOrderModel::latest('id')->first();
|
||||
$this->putJson("/purchase/order/{$purchase2->id}/finish")->assertJsonPath('success', true);
|
||||
$this->postJson("/purchase/order/{$purchase2->id}/bill", [
|
||||
'stores' => [[
|
||||
'store_id' => $store2->id,
|
||||
'delivery_fee' => 2,
|
||||
'box_num' => 0,
|
||||
'tray_num' => 0,
|
||||
'after_sale' => 8,
|
||||
]],
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$bill2 = BillModel::where('store_id', $store2->id)->first();
|
||||
$this->assertSame('8.00', (string) $bill2->after_sale);
|
||||
$this->assertSame('30.00', (string) $bill2->total_amount, '总金额 = 商品 20.00 + 配送费 2.00 + 售后金额 8.00');
|
||||
}
|
||||
|
||||
/** 生成账单:售后金额非数值 → 拒绝 */
|
||||
public function test_generate_bill_rejects_non_numeric_after_sale(): void
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$this->actingAsSysUser();
|
||||
$purchase = PurchaseOrderModel::factory()->create(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||||
|
||||
$this->postJson("/purchase/order/{$purchase->id}/bill", [
|
||||
'stores' => [[
|
||||
'store_id' => $store->id,
|
||||
'delivery_fee' => 0,
|
||||
'box_num' => 0,
|
||||
'tray_num' => 0,
|
||||
'after_sale' => '白菜烂叶 2 斤已协商',
|
||||
]],
|
||||
])->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 订单状态随业务链自动推进:接单→采购中→配送中→(生成账单)已完成 */
|
||||
|
||||
@@ -156,7 +156,7 @@ class MiniBillTest extends ProcurementTestCase
|
||||
'uploader_id' => 1,
|
||||
]);
|
||||
|
||||
$bill = $this->makeBill($store);
|
||||
$bill = $this->makeBill($store, ['after_sale' => '-3.00']);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||
$order = StoreOrderModel::factory()->create([
|
||||
'store_id' => $store->id,
|
||||
@@ -182,6 +182,7 @@ class MiniBillTest extends ProcurementTestCase
|
||||
$data = $this->getJson("/mini/bill/{$bill->id}")->assertOk()->json('data');
|
||||
|
||||
$this->assertSame($bill->bill_no, $data['bill']['bill_no']);
|
||||
$this->assertSame('-3.00', $data['bill']['after_sale'], '售后金额输出给门店(计入总金额构成)');
|
||||
$this->assertSame('待支付', $data['bill']['pay_state_name']);
|
||||
$this->assertSame('2026-08-10', $data['bill']['settlement_date'], '回款周期 0 天,出账当天应结算');
|
||||
$this->assertSame($bill->purchase->purchase_no, $data['bill']['purchase']['purchase_no']);
|
||||
|
||||
@@ -25,14 +25,14 @@ export interface PurchaseCellUpdateParams {
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
/** 生成账单:单个门店的配送费/周转筐/托盘数量与售后/备注 */
|
||||
/** 生成账单:单个门店的配送费/周转筐/托盘数量与售后金额/备注 */
|
||||
export interface BillGenerateStoreParams {
|
||||
store_id: number;
|
||||
delivery_fee: number;
|
||||
box_num: number;
|
||||
tray_num: number;
|
||||
/** 售后说明(文本,仅记录) */
|
||||
after_sale?: string;
|
||||
/** 售后金额(可正负,计入总金额;正数=加收,负数=售后减免) */
|
||||
after_sale?: number;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export interface IBill {
|
||||
tray_price: string;
|
||||
/** 附加金额 = 周转筐×筐单价 + 托盘×托盘单价 */
|
||||
added_amount: string;
|
||||
/** 账单总金额 = 商品金额 + 配送费 + 附加金额 */
|
||||
/** 账单总金额 = 商品金额 + 配送费 + 附加金额 + 售后金额 */
|
||||
total_amount: string;
|
||||
/** 支付状态:0未支付 1已支付 */
|
||||
status?: number;
|
||||
@@ -122,7 +122,7 @@ export interface IBill {
|
||||
paid_operator_id?: number;
|
||||
order_count?: number;
|
||||
remark?: string;
|
||||
/** 售后说明(生成账单时按门店填写) */
|
||||
/** 售后金额(生成账单时按门店填写,可正负,计入总金额) */
|
||||
after_sale?: string;
|
||||
created_at?: string;
|
||||
/** 门店账单列表/详情接口附带 */
|
||||
|
||||
@@ -187,17 +187,18 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const [billSaving, setBillSaving] = useState(false);
|
||||
const [billForm] = Form.useForm<{ stores: BillGenerateStoreParams[] }>();
|
||||
|
||||
// 弹窗内实时预览:附加金额 = 筐×筐单价 + 托盘×托盘单价;总额 = 商品 + 配送费 + 附加
|
||||
// 弹窗内实时预览:附加金额 = 筐×筐单价 + 托盘×托盘单价;总额 = 商品 + 配送费 + 附加 + 售后金额(可正负)
|
||||
const watchBillStores = Form.useWatch('stores', billForm) ?? [];
|
||||
const billPreview = (billPrepare?.stores ?? []).map((row, index) => {
|
||||
const input = watchBillStores[index] ?? {};
|
||||
const deliveryFee = Number(input.delivery_fee ?? 0);
|
||||
const afterSale = Number(input.after_sale ?? 0);
|
||||
const added =
|
||||
Number(input.box_num ?? 0) * Number(row.box_price) +
|
||||
Number(input.tray_num ?? 0) * Number(row.tray_price);
|
||||
return {
|
||||
added,
|
||||
total: Number(row.product_amount) + deliveryFee + added,
|
||||
total: Number(row.product_amount) + deliveryFee + added + afterSale,
|
||||
};
|
||||
});
|
||||
const billAllGenerated =
|
||||
@@ -441,7 +442,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
delivery_fee: row.bill ? Number(row.bill.delivery_fee) : 0,
|
||||
box_num: row.bill ? row.bill.box_num : 0,
|
||||
tray_num: row.bill ? row.bill.tray_num : 0,
|
||||
after_sale: row.bill?.after_sale ?? '',
|
||||
after_sale: row.bill ? Number(row.bill.after_sale) : 0,
|
||||
remark: row.bill?.remark ?? '',
|
||||
})),
|
||||
});
|
||||
@@ -1428,7 +1429,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 生成账单:按门店填写配送费/周转筐/托盘数量(商品金额只读,由订单汇总) */}
|
||||
{/* 生成账单:按门店填写配送费/周转筐/托盘数量与售后金额(商品金额只读,由订单汇总) */}
|
||||
<Modal
|
||||
title={billPrepare ? `生成账单 · ${billPrepare.purchase.purchase_no}` : '生成账单'}
|
||||
open={billOpen}
|
||||
@@ -1447,7 +1448,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
{billPrepare && (
|
||||
<>
|
||||
<div className="py-2 text-gray-500">
|
||||
每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费、周转筐/托盘数量(正数=压筐附加金额,负数=回筐抵扣金额)与售后/备注(可选,随账单存档)。
|
||||
每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费、周转筐/托盘数量(正数=压筐附加金额,负数=回筐抵扣金额)、售后金额(可正负,计入总金额:正数=加收,负数=售后减免)与备注(可选,随账单存档)。
|
||||
{billAllGenerated ? '该采购单已全部生成账单,仅可查看。' : '生成后采购单中的全部订单将关联到对应门店账单。'}
|
||||
</div>
|
||||
<Form form={billForm} layout="vertical" onFinish={handleBillSave}>
|
||||
@@ -1460,7 +1461,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
<div className="w-32 shrink-0 text-center">配送费(元)</div>
|
||||
<div className="w-32 shrink-0 text-center">周转筐(¥{billPrepare.stores[0]?.box_price ?? '0.00'}/个)</div>
|
||||
<div className="w-32 shrink-0 text-center">周转托盘(¥{billPrepare.stores[0]?.tray_price ?? '0.00'}/个)</div>
|
||||
<div className="w-36 shrink-0 text-center">售后</div>
|
||||
<div className="w-36 shrink-0 text-center">售后金额(元)</div>
|
||||
<div className="w-36 shrink-0 text-center">备注</div>
|
||||
<div className="w-28 shrink-0 text-center">附加金额</div>
|
||||
<div className="flex-1 text-center">账单总金额</div>
|
||||
@@ -1524,10 +1525,11 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
className="m-0! w-36 shrink-0 px-1!"
|
||||
name={[field.name, 'after_sale']}
|
||||
>
|
||||
<Input
|
||||
maxLength={255}
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
precision={2}
|
||||
disabled={billed}
|
||||
placeholder="售后说明(可选)"
|
||||
placeholder="可正负,计入总额"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -266,6 +266,18 @@ const BillPage: React.FC = () => {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '售后金额',
|
||||
dataIndex: 'after_sale',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Text type={Number(record.after_sale ?? 0) < 0 ? 'success' : undefined}>
|
||||
¥{record.after_sale ?? '0.00'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '账单总金额',
|
||||
dataIndex: 'total_amount',
|
||||
@@ -361,7 +373,7 @@ const BillPage: React.FC = () => {
|
||||
<div className="mb-5">
|
||||
<Title level={3}>门店账单</Title>
|
||||
<Text type="secondary">
|
||||
采购单完成后在「采购单」页生成,每个门店单独一张;账单总金额 = 商品金额 + 配送费 + 附加金额(周转筐/托盘);
|
||||
采购单完成后在「采购单」页生成,每个门店单独一张;账单总金额 = 商品金额 + 配送费 + 附加金额(周转筐/托盘)+ 售后金额(可正负);
|
||||
勾选多张账单可合并导出为一个 Excel(明细跨账单按商品合并,可按一级分类过滤)。
|
||||
</Text>
|
||||
</div>
|
||||
@@ -388,6 +400,12 @@ const BillPage: React.FC = () => {
|
||||
(筐 {detail.bill.box_num}×¥{detail.bill.box_price},托盘 {detail.bill.tray_num}×¥{detail.bill.tray_price})
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="售后金额">
|
||||
<Text type={Number(detail.bill.after_sale ?? 0) < 0 ? 'success' : undefined}>
|
||||
¥{detail.bill.after_sale ?? '0.00'}
|
||||
</Text>
|
||||
<Text type="secondary" className="ml-2!">(可正负,计入总金额)</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账单总金额">
|
||||
<Text strong type="danger">¥{detail.bill.total_amount}</Text>
|
||||
</Descriptions.Item>
|
||||
@@ -403,9 +421,6 @@ const BillPage: React.FC = () => {
|
||||
{detail.bill.pay_remark ? (
|
||||
<Descriptions.Item label="付款备注" span={3}>{detail.bill.pay_remark}</Descriptions.Item>
|
||||
) : null}
|
||||
{detail.bill.after_sale ? (
|
||||
<Descriptions.Item label="售后" span={3}>{detail.bill.after_sale}</Descriptions.Item>
|
||||
) : null}
|
||||
{detail.bill.remark ? (
|
||||
<Descriptions.Item label="备注" span={3}>{detail.bill.remark}</Descriptions.Item>
|
||||
) : null}
|
||||
@@ -453,7 +468,7 @@ const BillPage: React.FC = () => {
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
应收金额 <Text strong type="danger">¥{payTarget?.total_amount ?? '0.00'}</Text>
|
||||
(商品 ¥{payTarget?.product_amount ?? '0.00'} + 配送费 ¥{payTarget?.delivery_fee ?? '0.00'} + 附加 ¥{payTarget?.added_amount ?? '0.00'});
|
||||
(商品 ¥{payTarget?.product_amount ?? '0.00'} + 配送费 ¥{payTarget?.delivery_fee ?? '0.00'} + 附加 ¥{payTarget?.added_amount ?? '0.00'} + 售后 ¥{payTarget?.after_sale ?? '0.00'});
|
||||
线下收款完成后登记付款信息,账单支付状态将置为「已支付」。
|
||||
</div>
|
||||
<Form form={payForm} layout="vertical" onFinish={handlePaySave}>
|
||||
|
||||
Reference in New Issue
Block a user