修复一些错误
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -33,16 +33,14 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping,
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param PurchaseOrderModel $purchase 采购单
|
* @param PurchaseOrderModel $purchase 采购单
|
||||||
* @param string $type all 全品类 / category 仅蔬果分类
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PurchaseOrderModel $purchase,
|
private readonly PurchaseOrderModel $purchase,
|
||||||
private readonly string $type = 'all',
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出行(商品聚合行,按 分类sort → 商品sort 排序;蔬果分类时按顶级分类名过滤)
|
* 导出行(商品聚合行,按 分类sort → 商品sort 排序)
|
||||||
*/
|
*/
|
||||||
public function collection(): Collection
|
public function collection(): Collection
|
||||||
{
|
{
|
||||||
@@ -114,13 +112,6 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping,
|
|||||||
$items = collect($rows);
|
$items = collect($rows);
|
||||||
$this->loadLookups($items);
|
$this->loadLookups($items);
|
||||||
|
|
||||||
if ($this->type === 'category') {
|
|
||||||
$items = $items->filter(function (array $row): bool {
|
|
||||||
$rootName = $this->categoryNames[$row['product_id']] ?? '';
|
|
||||||
return str_contains($rootName, '蔬菜') || str_contains($rootName, '水果');
|
|
||||||
})->values();
|
|
||||||
}
|
|
||||||
|
|
||||||
$sort = 1;
|
$sort = 1;
|
||||||
return $this->items = $items->map(static function (array $row) use (&$sort): array {
|
return $this->items = $items->map(static function (array $row) use (&$sort): array {
|
||||||
$row['sort'] = $sort++;
|
$row['sort'] = $sort++;
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ class NoticeController extends BaseMiniController
|
|||||||
$readBroadcastIds = NoticeModel::query()
|
$readBroadcastIds = NoticeModel::query()
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
->whereNotNull('data->broadcast_from')
|
->whereNotNull('data->broadcast_from')
|
||||||
->pluck('data->broadcast_from');
|
->pluck('data')
|
||||||
|
->map(static fn (?array $data) => $data['broadcast_from'] ?? null)
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
|
||||||
$query = NoticeModel::query()->where(function ($q) use ($user, $readBroadcastIds) {
|
$query = NoticeModel::query()->where(function ($q) use ($user, $readBroadcastIds) {
|
||||||
$q->where('user_id', $user->id)
|
$q->where('user_id', $user->id)
|
||||||
|
|||||||
@@ -101,10 +101,10 @@ class StoreOrderController extends BaseController
|
|||||||
public function status(int $id, Request $request): JsonResponse
|
public function status(int $id, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'status' => 'required|integer|in:1,3,4,9',
|
'status' => 'required|integer|in:1,9',
|
||||||
], [
|
], [
|
||||||
'status.required' => '目标状态不能为空',
|
'status.required' => '目标状态不能为空',
|
||||||
'status.in' => '目标状态值不正确',
|
'status.in' => '目标状态仅支持接单或取消订单',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$order = StoreOrderModel::find($id);
|
$order = StoreOrderModel::find($id);
|
||||||
@@ -136,13 +136,13 @@ class StoreOrderController extends BaseController
|
|||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'ids' => 'required|array|min:1',
|
'ids' => 'required|array|min:1',
|
||||||
'ids.*' => 'integer|exists:store_order,id',
|
'ids.*' => 'integer|exists:store_order,id',
|
||||||
'status' => 'required|integer|in:1,3,4,9',
|
'status' => 'required|integer|in:1,9',
|
||||||
], [
|
], [
|
||||||
'ids.required' => '请选择要流转的订单',
|
'ids.required' => '请选择要流转的订单',
|
||||||
'ids.min' => '请选择要流转的订单',
|
'ids.min' => '请选择要流转的订单',
|
||||||
'ids.*.exists' => '订单不存在',
|
'ids.*.exists' => '订单不存在',
|
||||||
'status.required' => '目标状态不能为空',
|
'status.required' => '目标状态不能为空',
|
||||||
'status.in' => '目标状态值不正确',
|
'status.in' => '目标状态仅支持接单或取消订单',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$target = (int) $data['status'];
|
$target = (int) $data['status'];
|
||||||
@@ -196,6 +196,9 @@ class StoreOrderController extends BaseController
|
|||||||
/**
|
/**
|
||||||
* 状态流转合法路径
|
* 状态流转合法路径
|
||||||
*
|
*
|
||||||
|
* 手动流转仅保留「接单」「取消订单」;后续状态由业务链路自动推进:
|
||||||
|
* 生成采购单 → 采购中;采购单完成 → 配送中;生成账单 → 已完成
|
||||||
|
*
|
||||||
* @return int[]
|
* @return int[]
|
||||||
*/
|
*/
|
||||||
private function allowedTransitions(int $status): array
|
private function allowedTransitions(int $status): array
|
||||||
@@ -205,11 +208,6 @@ class StoreOrderController extends BaseController
|
|||||||
StoreOrderModel::STATUS_SUMMARIZED,
|
StoreOrderModel::STATUS_SUMMARIZED,
|
||||||
StoreOrderModel::STATUS_CANCELLED,
|
StoreOrderModel::STATUS_CANCELLED,
|
||||||
],
|
],
|
||||||
StoreOrderModel::STATUS_DELIVERING => [
|
|
||||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
|
||||||
StoreOrderModel::STATUS_COMPLETED,
|
|
||||||
],
|
|
||||||
StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED],
|
|
||||||
default => [],
|
default => [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,19 +252,15 @@ class PurchaseOrderController extends BaseController
|
|||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
|
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
|
||||||
public function export(int $id, Request $request): Response
|
public function export(int $id): Response
|
||||||
{
|
{
|
||||||
$purchase = PurchaseOrderModel::find($id);
|
$purchase = PurchaseOrderModel::find($id);
|
||||||
if (empty($purchase)) {
|
if (empty($purchase)) {
|
||||||
throw new RepositoryException('采购单不存在');
|
throw new RepositoryException('采购单不存在');
|
||||||
}
|
}
|
||||||
$type = (string) $request->query('type', 'all');
|
|
||||||
if (! in_array($type, ['all', 'category'], true)) {
|
|
||||||
throw new RepositoryException('导出类型参数不正确(all 全品类 / category 蔬果分类)');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Excel::download(
|
return Excel::download(
|
||||||
new PurchaseOrderExport($purchase, $type),
|
new PurchaseOrderExport($purchase),
|
||||||
$purchase->purchase_no . '_采购单.xlsx',
|
$purchase->purchase_no . '_采购单.xlsx',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ namespace App\Http\Controllers\Recon;
|
|||||||
use App\Exceptions\RepositoryException;
|
use App\Exceptions\RepositoryException;
|
||||||
use App\Exports\BillExport;
|
use App\Exports\BillExport;
|
||||||
use App\Models\BillModel;
|
use App\Models\BillModel;
|
||||||
|
use App\Models\StoreModel;
|
||||||
use App\Services\BillDetailService;
|
use App\Services\BillDetailService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||||
@@ -119,7 +121,8 @@ class BillController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认收款:线下收款后手动登记付款信息,支付状态置为已支付
|
* 确认收款:线下收款后手动登记付款信息,支付状态置为已支付;
|
||||||
|
* 累加门店总采购金额(只统计商品金额)
|
||||||
*/
|
*/
|
||||||
#[PutRoute(route: '/{id}/pay', authorize: 'pay', where: ['id' => '[0-9]+'])]
|
#[PutRoute(route: '/{id}/pay', authorize: 'pay', where: ['id' => '[0-9]+'])]
|
||||||
public function pay(int $id, Request $request): JsonResponse
|
public function pay(int $id, Request $request): JsonResponse
|
||||||
@@ -132,20 +135,29 @@ class BillController extends BaseController
|
|||||||
'pay_remark.max' => '付款备注超过最大长度',
|
'pay_remark.max' => '付款备注超过最大长度',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$bill = BillModel::find($id);
|
return DB::transaction(function () use ($id, $data, $request) {
|
||||||
if (empty($bill)) {
|
$bill = BillModel::query()->lockForUpdate()->find($id);
|
||||||
throw new RepositoryException('账单不存在');
|
if (empty($bill)) {
|
||||||
}
|
throw new RepositoryException('账单不存在');
|
||||||
if ($bill->status === BillModel::STATUS_PAID) {
|
}
|
||||||
throw new RepositoryException('账单已支付,请勿重复收款');
|
if ($bill->status === BillModel::STATUS_PAID) {
|
||||||
}
|
throw new RepositoryException('账单已支付,请勿重复收款');
|
||||||
|
}
|
||||||
|
|
||||||
$bill->status = BillModel::STATUS_PAID;
|
$bill->status = BillModel::STATUS_PAID;
|
||||||
$bill->paid_at = $data['paid_at'] ?? now();
|
$bill->paid_at = $data['paid_at'] ?? now();
|
||||||
$bill->pay_remark = (string) ($data['pay_remark'] ?? '');
|
$bill->pay_remark = (string) ($data['pay_remark'] ?? '');
|
||||||
$bill->paid_operator_id = (int) $request->user()->id;
|
$bill->paid_operator_id = (int) $request->user()->id;
|
||||||
$bill->save();
|
$bill->save();
|
||||||
|
|
||||||
return $this->success([], '收款已登记');
|
// 累加门店总采购金额(只统计商品金额,不含配送费/附加金额)
|
||||||
|
$store = StoreModel::query()->lockForUpdate()->find($bill->store_id);
|
||||||
|
if ($store !== null) {
|
||||||
|
$store->total_purchase_amount = bcadd((string) $store->total_purchase_amount, (string) $bill->product_amount, 2);
|
||||||
|
$store->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([], '收款已登记');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Recon;
|
|||||||
use App\Exceptions\RepositoryException;
|
use App\Exceptions\RepositoryException;
|
||||||
use App\Models\BillModel;
|
use App\Models\BillModel;
|
||||||
use App\Models\PaymentModel;
|
use App\Models\PaymentModel;
|
||||||
|
use App\Models\StoreModel;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -112,6 +113,19 @@ class PaymentController extends BaseController
|
|||||||
'paid_operator_id' => $auditorId,
|
'paid_operator_id' => $auditorId,
|
||||||
'pay_remark' => $methodName . '(支付单号 ' . $payment->payment_no . ')',
|
'pay_remark' => $methodName . '(支付单号 ' . $payment->payment_no . ')',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 按门店累加总采购金额(只统计商品金额,不含配送费/附加金额)
|
||||||
|
foreach ($bills->groupBy('store_id')->sortKeys() as $storeId => $storeBills) {
|
||||||
|
$amount = '0';
|
||||||
|
foreach ($storeBills as $storeBill) {
|
||||||
|
$amount = bcadd($amount, (string) $storeBill->product_amount, 2);
|
||||||
|
}
|
||||||
|
$store = StoreModel::query()->lockForUpdate()->find((int) $storeId);
|
||||||
|
if ($store !== null) {
|
||||||
|
$store->total_purchase_amount = bcadd((string) $store->total_purchase_amount, $amount, 2);
|
||||||
|
$store->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
$payment->status = PaymentModel::STATUS_APPROVED;
|
$payment->status = PaymentModel::STATUS_APPROVED;
|
||||||
} else {
|
} else {
|
||||||
// 拒绝:释放账单,门店可重新发起付款
|
// 拒绝:释放账单,门店可重新发起付款
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class StoreModel extends Model
|
|||||||
'payment_cycle_days' => 'integer',
|
'payment_cycle_days' => 'integer',
|
||||||
'pending_box_num' => 'integer',
|
'pending_box_num' => 'integer',
|
||||||
'pending_tray_num' => 'integer',
|
'pending_tray_num' => 'integer',
|
||||||
|
'total_purchase_amount' => 'decimal:2',
|
||||||
'status' => 'integer',
|
'status' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use Throwable;
|
|||||||
* 1. 锁定采购单全部有效订单,按门店分组,校验提交门店完整覆盖
|
* 1. 锁定采购单全部有效订单,按门店分组,校验提交门店完整覆盖
|
||||||
* 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改)
|
* 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改)
|
||||||
* 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照)
|
* 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照)
|
||||||
* 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联
|
* 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联,订单状态置为已完成
|
||||||
*/
|
*/
|
||||||
readonly class BillGenerateService
|
readonly class BillGenerateService
|
||||||
{
|
{
|
||||||
@@ -111,10 +111,13 @@ readonly class BillGenerateService
|
|||||||
'operator_id' => $operatorId,
|
'operator_id' => $operatorId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 关联该门店在采购单中的全部订单与订单明细到账单
|
// 关联该门店在采购单中的全部订单与订单明细到账单,订单转入「已完成」
|
||||||
StoreOrderModel::query()
|
StoreOrderModel::query()
|
||||||
->whereIn('id', $storeOrders->pluck('id'))
|
->whereIn('id', $storeOrders->pluck('id'))
|
||||||
->update(['bill_id' => $bill->id]);
|
->update([
|
||||||
|
'bill_id' => $bill->id,
|
||||||
|
'status' => StoreOrderModel::STATUS_COMPLETED,
|
||||||
|
]);
|
||||||
StoreOrderItemModel::query()
|
StoreOrderItemModel::query()
|
||||||
->whereIn('order_id', $storeOrders->pluck('id'))
|
->whereIn('order_id', $storeOrders->pluck('id'))
|
||||||
->update(['bill_id' => $bill->id]);
|
->update(['bill_id' => $bill->id]);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ return new class extends Migration
|
|||||||
$table->integer('payment_cycle_days')->default(0)->comment('回款周期(天)');
|
$table->integer('payment_cycle_days')->default(0)->comment('回款周期(天)');
|
||||||
$table->integer('pending_box_num')->default(0)->comment('待回筐数量(生成账单压筐累加,回筐登记扣减)');
|
$table->integer('pending_box_num')->default(0)->comment('待回筐数量(生成账单压筐累加,回筐登记扣减)');
|
||||||
$table->integer('pending_tray_num')->default(0)->comment('待回托盘数量');
|
$table->integer('pending_tray_num')->default(0)->comment('待回托盘数量');
|
||||||
|
$table->decimal('total_purchase_amount', 12, 2)->default(0)->comment('总采购金额(只统计商品金额,账单支付后累加)');
|
||||||
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
|
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
|
||||||
$table->string('remark', 255)->nullable()->default('')->comment('备注');
|
$table->string('remark', 255)->nullable()->default('')->comment('备注');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\BillModel;
|
||||||
|
use App\Models\CustomerLevelModel;
|
||||||
|
use App\Models\PaymentModel;
|
||||||
|
use App\Models\ProductModel;
|
||||||
|
use App\Models\ProductPriceModel;
|
||||||
|
use App\Models\PurchaseOrderModel;
|
||||||
|
use App\Models\StoreModel;
|
||||||
|
use App\Models\StoreOrderModel;
|
||||||
|
use App\Models\UserModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单链路与支付:订单状态随业务链自动推进(采购单完成→配送中、生成账单→已完成);
|
||||||
|
* 账单支付后累加门店总采购金额(只统计商品金额,手动收款与支付审核双入口)
|
||||||
|
*/
|
||||||
|
class BillPaymentTest extends ProcurementTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 完整业务链造账单:下单 → 接单 → 生成采购单 → 完成采购单 → 生成账单
|
||||||
|
* (商品 5.00 × 4 = 商品金额 20.00,配送费/筐/托盘均 0)
|
||||||
|
*
|
||||||
|
* @return array{0: StoreModel, 1: StoreOrderModel, 2: BillModel}
|
||||||
|
*/
|
||||||
|
private function makeBillViaChain(): array
|
||||||
|
{
|
||||||
|
$level = CustomerLevelModel::factory()->create();
|
||||||
|
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||||
|
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||||
|
ProductPriceModel::factory()->create([
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'level_id' => $level->id,
|
||||||
|
'price' => '5.00',
|
||||||
|
]);
|
||||||
|
$user = UserModel::factory()->forStore($store->id)->create();
|
||||||
|
|
||||||
|
$this->actingAsMiniUser($user);
|
||||||
|
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
// 接单
|
||||||
|
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
// 生成采购单 → 采购中
|
||||||
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$purchase = PurchaseOrderModel::first();
|
||||||
|
// 完成采购单 → 配送中
|
||||||
|
$this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true);
|
||||||
|
// 生成账单 → 已完成
|
||||||
|
$this->postJson("/purchase/order/{$purchase->id}/bill", [
|
||||||
|
'stores' => [['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]],
|
||||||
|
])->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
return [$store, $order->fresh(), BillModel::where('store_id', $store->id)->first()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 造一张指定商品金额的未支付账单(总额=商品金额) */
|
||||||
|
private function makeBill(StoreModel $store, string $productAmount, array $attributes = []): BillModel
|
||||||
|
{
|
||||||
|
return BillModel::create(array_merge([
|
||||||
|
'bill_no' => 'ZD' . random_int(100000000000, 999999999999),
|
||||||
|
'purchase_id' => PurchaseOrderModel::factory()->create()->id,
|
||||||
|
'store_id' => $store->id,
|
||||||
|
'bill_date' => '2026-08-10',
|
||||||
|
'product_amount' => $productAmount,
|
||||||
|
'delivery_fee' => '0.00',
|
||||||
|
'box_num' => 0,
|
||||||
|
'tray_num' => 0,
|
||||||
|
'box_price' => '0.00',
|
||||||
|
'tray_price' => '0.00',
|
||||||
|
'added_amount' => '0.00',
|
||||||
|
'total_amount' => $productAmount,
|
||||||
|
'status' => BillModel::STATUS_UNPAID,
|
||||||
|
], $attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单状态随业务链自动推进:接单→采购中→配送中→(生成账单)已完成 */
|
||||||
|
public function test_order_status_progresses_via_business_chain(): void
|
||||||
|
{
|
||||||
|
$level = CustomerLevelModel::factory()->create();
|
||||||
|
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||||
|
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||||
|
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '5.00']);
|
||||||
|
$user = UserModel::factory()->forStore($store->id)->create();
|
||||||
|
|
||||||
|
$this->actingAsMiniUser($user);
|
||||||
|
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_PENDING, $order->status);
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
// 接单 → 已接单
|
||||||
|
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status);
|
||||||
|
|
||||||
|
// 生成采购单 → 采购中
|
||||||
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$purchase = PurchaseOrderModel::first();
|
||||||
|
$order->refresh();
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->status);
|
||||||
|
$this->assertSame($purchase->id, $order->purchase_id);
|
||||||
|
|
||||||
|
// 完成采购单 → 配送中
|
||||||
|
$this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_DISTRIBUTION, $order->fresh()->status);
|
||||||
|
|
||||||
|
// 生成账单 → 已完成 + 回写 bill_id
|
||||||
|
$this->postJson("/purchase/order/{$purchase->id}/bill", [
|
||||||
|
'stores' => [['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]],
|
||||||
|
])->assertJsonPath('success', true);
|
||||||
|
$order->refresh();
|
||||||
|
$bill = BillModel::where('store_id', $store->id)->first();
|
||||||
|
$this->assertNotNull($bill);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_COMPLETED, $order->status);
|
||||||
|
$this->assertSame($bill->id, $order->bill_id);
|
||||||
|
$this->assertSame('20.00', (string) $bill->product_amount, '5.00 × 4');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后台手动确认收款:累加门店总采购金额(只统计商品金额),重复收款拒绝不重复累加 */
|
||||||
|
public function test_manual_pay_accumulates_store_total_purchase_amount(): void
|
||||||
|
{
|
||||||
|
[$store, , $bill] = $this->makeBillViaChain();
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$this->putJson("/recon/bill/{$bill->id}/pay", ['pay_remark' => '线下现金'])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$store->refresh();
|
||||||
|
$this->assertSame('20.00', (string) $store->total_purchase_amount, '只累加商品金额 20.00');
|
||||||
|
|
||||||
|
// 第二笔账单收款后继续累加
|
||||||
|
$bill2 = $this->makeBill($store, '100.00');
|
||||||
|
$this->putJson("/recon/bill/{$bill2->id}/pay")->assertJsonPath('success', true);
|
||||||
|
$this->assertSame('120.00', (string) $store->fresh()->total_purchase_amount);
|
||||||
|
|
||||||
|
// 重复收款拒绝,金额不重复累加
|
||||||
|
$this->putJson("/recon/bill/{$bill->id}/pay")->assertJsonPath('success', false);
|
||||||
|
$this->assertSame('120.00', (string) $store->fresh()->total_purchase_amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 支付审核通过:关联账单置已支付并累加门店总采购金额 */
|
||||||
|
public function test_payment_audit_pass_accumulates_store_total_purchase_amount(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$user = UserModel::factory()->forStore($store->id)->create();
|
||||||
|
|
||||||
|
$payment = PaymentModel::create([
|
||||||
|
'payment_no' => 'ZF202608140001',
|
||||||
|
'store_id' => $store->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'amount' => '150.00',
|
||||||
|
'pay_method' => PaymentModel::METHOD_BANK,
|
||||||
|
'voucher_ids' => '',
|
||||||
|
'status' => PaymentModel::STATUS_PENDING,
|
||||||
|
]);
|
||||||
|
$bill1 = $this->makeBill($store, '100.00', ['payment_id' => $payment->id]);
|
||||||
|
$bill2 = $this->makeBill($store, '50.00', ['payment_id' => $payment->id]);
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$this->putJson("/recon/payment/{$payment->id}/audit", ['result' => 'pass'])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$this->assertSame(BillModel::STATUS_PAID, $bill1->fresh()->status);
|
||||||
|
$this->assertSame(BillModel::STATUS_PAID, $bill2->fresh()->status);
|
||||||
|
$this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount);
|
||||||
|
|
||||||
|
// 重复审核拒绝,不重复累加
|
||||||
|
$this->putJson("/recon/payment/{$payment->id}/audit", ['result' => 'pass'])
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
$this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核拒绝:账单释放回待支付,不累加门店总采购金额 */
|
||||||
|
public function test_payment_audit_reject_does_not_accumulate(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$user = UserModel::factory()->forStore($store->id)->create();
|
||||||
|
|
||||||
|
$payment = PaymentModel::create([
|
||||||
|
'payment_no' => 'ZF202608140002',
|
||||||
|
'store_id' => $store->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'amount' => '100.00',
|
||||||
|
'pay_method' => PaymentModel::METHOD_WECHAT,
|
||||||
|
'voucher_ids' => '',
|
||||||
|
'status' => PaymentModel::STATUS_PENDING,
|
||||||
|
]);
|
||||||
|
$bill = $this->makeBill($store, '100.00', ['payment_id' => $payment->id]);
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$this->putJson("/recon/payment/{$payment->id}/audit", [
|
||||||
|
'result' => 'reject',
|
||||||
|
'audit_remark' => '凭证不清晰',
|
||||||
|
])->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$bill->refresh();
|
||||||
|
$this->assertSame(BillModel::STATUS_UNPAID, $bill->status);
|
||||||
|
$this->assertSame(0, $bill->payment_id, '账单释放可重新付款');
|
||||||
|
$this->assertSame('0.00', (string) $store->fresh()->total_purchase_amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Exports\PurchaseOrderExport;
|
|
||||||
use App\Models\CustomerLevelModel;
|
use App\Models\CustomerLevelModel;
|
||||||
use App\Models\ProductCategoryModel;
|
use App\Models\ProductCategoryModel;
|
||||||
use App\Models\ProductModel;
|
use App\Models\ProductModel;
|
||||||
@@ -11,10 +10,9 @@ use App\Models\PurchaseOrderModel;
|
|||||||
use App\Models\StoreModel;
|
use App\Models\StoreModel;
|
||||||
use App\Models\StoreOrderModel;
|
use App\Models\StoreOrderModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 采购单导出(仅 Excel 表格):xlsx Content-Type / 蔬果分类过滤 / type 非法拒绝 /
|
* 采购单导出(仅 Excel 表格,全品类):xlsx Content-Type /
|
||||||
* 权限拦截 / 中文文件名 RFC 5987 编码
|
* 权限拦截 / 中文文件名 RFC 5987 编码
|
||||||
*/
|
*/
|
||||||
class ExportTest extends ProcurementTestCase
|
class ExportTest extends ProcurementTestCase
|
||||||
@@ -71,35 +69,6 @@ class ExportTest extends ProcurementTestCase
|
|||||||
$this->assertStringContainsString(rawurlencode('采购单'), $disposition);
|
$this->assertStringContainsString(rawurlencode('采购单'), $disposition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 蔬果分类过滤:type=category 仅导出蔬菜/水果顶级分类商品 */
|
|
||||||
public function test_export_category_filters_to_vegetables(): void
|
|
||||||
{
|
|
||||||
$purchase = $this->buildPurchaseWithItems();
|
|
||||||
$this->actingAsSysUser();
|
|
||||||
|
|
||||||
Excel::fake();
|
|
||||||
$this->get("/purchase/order/{$purchase->id}/export?type=category")->assertOk();
|
|
||||||
|
|
||||||
Excel::assertDownloaded(
|
|
||||||
$purchase->purchase_no . '_采购单.xlsx',
|
|
||||||
static function (PurchaseOrderExport $export): bool {
|
|
||||||
$items = $export->collection();
|
|
||||||
// 仅蔬菜商品一行,肉禽被过滤
|
|
||||||
return $items->count() === 1;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** type 参数非法 → 拒绝 */
|
|
||||||
public function test_export_invalid_type_rejected(): void
|
|
||||||
{
|
|
||||||
$purchase = $this->buildPurchaseWithItems();
|
|
||||||
$this->actingAsSysUser();
|
|
||||||
|
|
||||||
$this->get("/purchase/order/{$purchase->id}/export?type=doc")
|
|
||||||
->assertJsonPath('success', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 采购单不存在 → 拒绝 */
|
/** 采购单不存在 → 拒绝 */
|
||||||
public function test_export_missing_purchase_rejected(): void
|
public function test_export_missing_purchase_rejected(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\NoticeModel;
|
||||||
|
use App\Models\StoreModel;
|
||||||
|
use App\Models\UserModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序消息通知:本人通知 + 全员广播;广播已读复制本人副本(data.broadcast_from 记来源),
|
||||||
|
* 列表排除已读广播。回归:列表曾报 Undefined property: stdClass::$data->broadcast_from
|
||||||
|
* (pluck JSON 路径被 Query Builder 当作结果列名读取 stdClass 属性)
|
||||||
|
*/
|
||||||
|
class MiniNoticeTest extends ProcurementTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 造一个绑定门店的小程序用户
|
||||||
|
*
|
||||||
|
* @return array{0: StoreModel, 1: UserModel}
|
||||||
|
*/
|
||||||
|
private function makeStoreWithUser(): array
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
|
||||||
|
return [$store, UserModel::factory()->forStore($store->id)->create()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通知列表:本人通知 + 未读广播,unread_count 正确 */
|
||||||
|
public function test_notice_list_includes_personal_and_broadcast(): void
|
||||||
|
{
|
||||||
|
[, $user] = $this->makeStoreWithUser();
|
||||||
|
$this->actingAsMiniUser($user);
|
||||||
|
|
||||||
|
NoticeModel::create([
|
||||||
|
'user_id' => NoticeModel::BROADCAST_USER_ID,
|
||||||
|
'type' => NoticeModel::TYPE_SYSTEM,
|
||||||
|
'title' => '系统公告',
|
||||||
|
'content' => '全员可见',
|
||||||
|
'is_read' => NoticeModel::UNREAD,
|
||||||
|
]);
|
||||||
|
NoticeModel::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'type' => NoticeModel::TYPE_ORDER,
|
||||||
|
'title' => '订单状态更新',
|
||||||
|
'content' => '您的订单已接单',
|
||||||
|
'is_read' => NoticeModel::UNREAD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/notice')->assertOk()->json('data');
|
||||||
|
|
||||||
|
$this->assertSame(2, $data['total']);
|
||||||
|
$this->assertSame(2, $data['unread_count']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 广播标记已读:复制本人已读副本,列表排除原广播(回归:列表不再报错) */
|
||||||
|
public function test_broadcast_read_creates_personal_copy(): void
|
||||||
|
{
|
||||||
|
[, $user] = $this->makeStoreWithUser();
|
||||||
|
$this->actingAsMiniUser($user);
|
||||||
|
|
||||||
|
$broadcast = NoticeModel::create([
|
||||||
|
'user_id' => NoticeModel::BROADCAST_USER_ID,
|
||||||
|
'type' => NoticeModel::TYPE_SYSTEM,
|
||||||
|
'title' => '系统公告',
|
||||||
|
'content' => '全员可见',
|
||||||
|
'data' => ['foo' => 'bar'],
|
||||||
|
'is_read' => NoticeModel::UNREAD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
// 已读副本:本人记录、已读、broadcast_from 记来源且保留原 data
|
||||||
|
$copy = NoticeModel::where('user_id', $user->id)->first();
|
||||||
|
$this->assertNotNull($copy);
|
||||||
|
$this->assertSame(NoticeModel::READ, $copy->is_read);
|
||||||
|
$this->assertSame($broadcast->id, $copy->data['broadcast_from']);
|
||||||
|
$this->assertSame('bar', $copy->data['foo']);
|
||||||
|
|
||||||
|
// 列表:原广播被排除,仅剩已读副本,未读数归零
|
||||||
|
$data = $this->getJson('/mini/notice')->assertOk()->json('data');
|
||||||
|
$this->assertSame(1, $data['total']);
|
||||||
|
$this->assertSame(0, $data['unread_count']);
|
||||||
|
$this->assertSame($copy->id, $data['data'][0]['id']);
|
||||||
|
|
||||||
|
// 重复标记不重复复制
|
||||||
|
$this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(1, NoticeModel::where('user_id', $user->id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人通知标记已读:直接更新原记录,不产生副本 */
|
||||||
|
public function test_personal_notice_read_marks_directly(): void
|
||||||
|
{
|
||||||
|
[, $user] = $this->makeStoreWithUser();
|
||||||
|
$this->actingAsMiniUser($user);
|
||||||
|
|
||||||
|
$notice = NoticeModel::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'type' => NoticeModel::TYPE_ORDER,
|
||||||
|
'title' => '订单状态更新',
|
||||||
|
'content' => '您的订单已接单',
|
||||||
|
'is_read' => NoticeModel::UNREAD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(NoticeModel::READ, $notice->fresh()->is_read);
|
||||||
|
$this->assertSame(1, NoticeModel::count(), '个人通知直接更新,不产生副本');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 他人通知不可标记(仅本人通知与广播可读) */
|
||||||
|
public function test_other_users_notice_not_found(): void
|
||||||
|
{
|
||||||
|
[, $user] = $this->makeStoreWithUser();
|
||||||
|
$this->actingAsMiniUser($user);
|
||||||
|
|
||||||
|
[, $other] = $this->makeStoreWithUser();
|
||||||
|
$notice = NoticeModel::create([
|
||||||
|
'user_id' => $other->id,
|
||||||
|
'type' => NoticeModel::TYPE_ORDER,
|
||||||
|
'title' => '订单状态更新',
|
||||||
|
'content' => '他人通知',
|
||||||
|
'is_read' => NoticeModel::UNREAD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', false);
|
||||||
|
$this->assertSame(NoticeModel::UNREAD, $notice->fresh()->is_read);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -257,4 +257,68 @@ class StoreOrderTest extends ProcurementTestCase
|
|||||||
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
|
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
|
||||||
$this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false);
|
$this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 手动流转:仅保留接单与取消(待接单 → 已接单/已取消) */
|
||||||
|
public function test_manual_status_flow_only_accept_and_cancel(): void
|
||||||
|
{
|
||||||
|
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status);
|
||||||
|
|
||||||
|
$order2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_CANCELLED])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->fresh()->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动流转:配送中/已完成等目标状态被拒绝(由采购/账单链路自动推进) */
|
||||||
|
public function test_manual_status_flow_rejects_delivery_and_completion(): void
|
||||||
|
{
|
||||||
|
foreach ([StoreOrderModel::STATUS_DISTRIBUTION, StoreOrderModel::STATUS_COMPLETED] as $target) {
|
||||||
|
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_DELIVERING);
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$this->putJson("/order/store/{$order->id}/status", ['status' => $target])
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->fresh()->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已接单不能手动转采购中(须经生成采购单)
|
||||||
|
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_DELIVERING])
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量流转:仅接单/取消可选,配送/完成目标整批校验拒绝 */
|
||||||
|
public function test_batch_status_only_accept_and_cancel(): void
|
||||||
|
{
|
||||||
|
$pending1 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||||||
|
$pending2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
// 批量接单
|
||||||
|
$this->putJson('/order/store/batchStatus', [
|
||||||
|
'ids' => [$pending1->id, $pending2->id],
|
||||||
|
'status' => StoreOrderModel::STATUS_SUMMARIZED,
|
||||||
|
])->assertJsonPath('success', true);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->fresh()->status);
|
||||||
|
|
||||||
|
// 批量完成被拒绝(目标状态校验拦截)
|
||||||
|
$this->putJson('/order/store/batchStatus', [
|
||||||
|
'ids' => [$pending1->id],
|
||||||
|
'status' => StoreOrderModel::STATUS_COMPLETED,
|
||||||
|
])->assertJsonPath('success', false);
|
||||||
|
|
||||||
|
// 已接单订单不可批量取消(仅待接单可取消),整批中止
|
||||||
|
$this->putJson('/order/store/batchStatus', [
|
||||||
|
'ids' => [$pending1->id],
|
||||||
|
'status' => StoreOrderModel::STATUS_CANCELLED,
|
||||||
|
])->assertJsonPath('success', false);
|
||||||
|
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type {
|
|||||||
IPurchaseCell,
|
IPurchaseCell,
|
||||||
IPurchaseDetail,
|
IPurchaseDetail,
|
||||||
IPurchaseStoreSummary,
|
IPurchaseStoreSummary,
|
||||||
PurchaseExportType,
|
|
||||||
} from '@/domain/iPurchaseOrder.ts';
|
} from '@/domain/iPurchaseOrder.ts';
|
||||||
|
|
||||||
/** 订单商品参数修改 */
|
/** 订单商品参数修改 */
|
||||||
@@ -103,11 +102,11 @@ export async function generateBill(purchaseId: number, stores: BillGenerateStore
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** C2/C3 导出采购单(Excel 表格):type=all 全品类 / category 仅蔬果分类 */
|
/** 导出采购单(Excel 表格,全品类) */
|
||||||
export async function exportPurchase(id: number, type: PurchaseExportType = 'all') {
|
export async function exportPurchase(id: number) {
|
||||||
return downloadBlob(
|
return downloadBlob(
|
||||||
`/purchase/order/${id}/export`,
|
`/purchase/order/${id}/export`,
|
||||||
{ type },
|
{},
|
||||||
`采购单_${id}.xlsx`,
|
`采购单_${id}.xlsx`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,4 @@ export const PURCHASE_STATUS_MAP: Record<number, { text: string; color: string }
|
|||||||
3: { text: '已完成', color: 'success' },
|
3: { text: '已完成', color: 'success' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseExportType = 'all' | 'category';
|
|
||||||
export type ExportFormat = 'xlsx' | 'pdf';
|
export type ExportFormat = 'xlsx' | 'pdf';
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default interface IStore {
|
|||||||
pending_box_num?: number;
|
pending_box_num?: number;
|
||||||
/** 待回托盘数量 */
|
/** 待回托盘数量 */
|
||||||
pending_tray_num?: number;
|
pending_tray_num?: number;
|
||||||
|
/** 总采购金额(只统计商品金额,账单支付后累加) */
|
||||||
|
total_purchase_amount?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ const StorePage: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '总采购金额',
|
||||||
|
dataIndex: 'total_purchase_amount',
|
||||||
|
hideInForm: true,
|
||||||
|
hideInSearch: true,
|
||||||
|
align: 'center',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Text strong className={'text-[red]'}>¥{record.total_purchase_amount ?? '0.00'}</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
|
|||||||
@@ -41,20 +41,16 @@ const { Title, Text } = Typography;
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
* 状态流转合法路径:手动流转仅保留「接单」「取消订单」
|
||||||
* (已接单→采购中 通过「生成采购单」完成,不在流转按钮内)
|
* (已接单→采购中 由「生成采购单」推进;采购单完成→配送中、生成账单→已完成 由业务链路自动推进)
|
||||||
*/
|
*/
|
||||||
const NEXT_STATUS: Record<number, { status: number; label: string; }> = {
|
const NEXT_STATUS: Record<number, { status: number; label: string; }> = {
|
||||||
0: { status: 1, label: '接单' },
|
0: { status: 1, label: '接单' },
|
||||||
2: { status: 3, label: '开始配送' },
|
|
||||||
3: { status: 4, label: '完成订单' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 批量流转可选目标状态(弹窗单选,后端全量校验,任一不允许则整批中止) */
|
/** 批量流转可选目标状态(弹窗单选,后端全量校验,任一不允许则整批中止) */
|
||||||
const BATCH_TARGET_OPTIONS = [
|
const BATCH_TARGET_OPTIONS = [
|
||||||
{ value: 1, label: '接单(待接单 → 已接单)' },
|
{ value: 1, label: '接单(待接单 → 已接单)' },
|
||||||
{ value: 3, label: '开始配送(采购中 → 配送中)' },
|
|
||||||
{ value: 4, label: '完成订单(采购中/配送中 → 已完成)' },
|
|
||||||
{ value: 9, label: '取消订单(待接单 → 已取消)' },
|
{ value: 9, label: '取消订单(待接单 → 已取消)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -411,7 +407,7 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Title level={3}>门店订单</Title>
|
<Title level={3}>门店订单</Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
小程序下单为待接单;接单后通过「生成采购单」合并为采购单并转采购中,采购中可批量流转为配送中或已完成。
|
小程序下单为待接单;接单后通过「生成采购单」合并为采购单并转采购中,采购单完成后订单自动转配送中,生成账单后订单自动完成。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<XinTable<IStoreOrder> {...tableProps} />
|
<XinTable<IStoreOrder> {...tableProps} />
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Dropdown,
|
|
||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
Image,
|
Image,
|
||||||
@@ -833,18 +832,14 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Space style={{ marginBottom: 20 }}>
|
<Space style={{ marginBottom: 20 }}>
|
||||||
<AuthButton key="export" auth="purchase.order.export">
|
<AuthButton key="export" auth="purchase.order.export">
|
||||||
<Dropdown
|
<Button
|
||||||
menu={{
|
type="primary"
|
||||||
items: [
|
ghost
|
||||||
{ key: 'all', label: '导出全品类', onClick: () => detail && exportPurchase(detail.purchase.id!, 'all') },
|
icon={<DownloadOutlined />}
|
||||||
{ key: 'category', label: '仅导出蔬果分类', onClick: () => detail && exportPurchase(detail.purchase.id!, 'category') },
|
onClick={() => detail && exportPurchase(detail.purchase.id!)}
|
||||||
],
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button type="primary" ghost icon={<DownloadOutlined />}>
|
导出
|
||||||
导出
|
</Button>
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</AuthButton>
|
</AuthButton>
|
||||||
</Space>
|
</Space>
|
||||||
<Table<IPurchaseDetailRow>
|
<Table<IPurchaseDetailRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user