账单生成

This commit is contained in:
liu
2026-08-14 00:15:28 +08:00
parent 61a70319a0
commit e35c951a59
19 changed files with 728 additions and 631 deletions
@@ -4,17 +4,18 @@ namespace App\Http\Controllers\Purchase;
use App\Exceptions\RepositoryException;
use App\Exports\PurchaseOrderExport;
use App\Http\Requests\Purchase\PurchaseBillGenerateRequest;
use App\Http\Requests\Purchase\PurchaseCellUpdateRequest;
use App\Http\Requests\Purchase\PurchaseContainerUpdateRequest;
use App\Http\Requests\Purchase\PurchaseRowUpdateRequest;
use App\Models\BillModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Services\BillGenerateService;
use App\Services\ItemImageResolver;
use App\Services\PurchaseGenerateService;
use App\Services\StoreOrderContainerService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -135,44 +136,21 @@ class PurchaseOrderController extends BaseController
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
// 周转框/托盘合并记录:按门店聚合全部订单(附底层订单明细,供采购单完成前修改
$storeOrders = StoreOrderModel::query()
// 门店账单:采购单完成后按门店生成(含软删除门店,保证历史单据可见
$bills = BillModel::query()
->where('purchase_id', $purchase->id)
->whereNull('deleted_at')
->with('store:id,name')
->orderBy('id')
->get(['id', 'order_no', 'store_id', 'box_num', 'tray_num']);
$boxPrice = (float) site_config('services.box_amount', 0);
$trayPrice = (float) site_config('services.tray_amount', 0);
$storeSort = $stores->pluck('id')->flip();
$containers = [];
foreach ($storeOrders->groupBy('store_id') as $storeId => $orders) {
$boxNum = (int) $orders->sum('box_num');
$trayNum = (int) $orders->sum('tray_num');
$containers[] = [
'store_id' => (int) $storeId,
'store_name' => $stores->firstWhere('id', (int) $storeId)['name'] ?? '门店#' . $storeId,
'box_num' => $boxNum,
'tray_num' => $trayNum,
'box_price' => number_format($boxPrice, 2),
'tray_price' => number_format($trayPrice, 2),
'added_amount' => number_format($boxNum * $boxPrice + $trayNum * $trayPrice, 2),
'order_count' => $orders->count(),
'orders' => $orders->map(static fn (StoreOrderModel $order) => [
'order_id' => $order->id,
'order_no' => $order->order_no,
'box_num' => (int) $order->box_num,
'tray_num' => (int) $order->tray_num,
])->values()->toArray(),
'store_sort' => (int) ($storeSort[$storeId] ?? 9999),
];
}
usort($containers, static fn (array $a, array $b): int =>
[$a['store_sort'], $a['store_id']] <=> [$b['store_sort'], $b['store_id']]);
$containers = array_map(static function (array $row): array {
unset($row['store_sort']);
return $row;
}, $containers);
->get()
->map(static function (BillModel $bill): array {
$row = $bill->toArray();
$row['store_name'] = $bill->store->name ?? ('门店#' . $bill->store_id);
$row['order_count'] = $bill->orders()->count();
unset($row['store']);
return $row;
})
->values()
->toArray();
return $this->success([
'purchase' => $purchase->toArray(),
@@ -181,7 +159,7 @@ class PurchaseOrderController extends BaseController
unset($row['category_sort'], $row['product_sort']);
return $row;
}, $rows),
'containers' => $containers,
'bills' => $bills,
]);
}
@@ -232,7 +210,6 @@ class PurchaseOrderController extends BaseController
StoreOrderModel::where('purchase_id', $purchase->id)->update([
'status' => StoreOrderModel::STATUS_DISTRIBUTION
]);
// 生成并发送账单
return $this->success();
});
@@ -438,71 +415,81 @@ class PurchaseOrderController extends BaseController
}
/**
* 周转框/托盘合并记录修改:按门店覆盖全部订单逐笔更新(仅采购单进行中可改)
* @throws Throwable
* 账单生成预览
*/
#[PutRoute(route: '/{id}/container/{storeId}', authorize: 'update', where: ['id' => '[0-9]+', 'storeId' => '[0-9]+'])]
public function updateContainer(int $id, int $storeId, PurchaseContainerUpdateRequest $request): JsonResponse
#[GetRoute(route: '/{id}/bill/prepare', authorize: 'query', where: ['id' => '[0-9]+'])]
public function billPrepare(int $id): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
throw new RepositoryException('采购单已完成,不允许修改周转框/托盘');
$orders = StoreOrderModel::query()
->where('purchase_id', $purchase->id)
->whereNull('deleted_at')
->orderBy('id')
->get(['id', 'store_id', 'total_amount']);
$stores = StoreModel::withTrashed()
->whereIn('id', $orders->pluck('store_id')->unique())
->get(['id', 'name'])
->keyBy('id');
$bills = BillModel::query()
->where('purchase_id', $purchase->id)
->get()
->keyBy('store_id');
$boxPrice = number_format((float) site_config('services.box_amount', 0), 2);
$trayPrice = number_format((float) site_config('services.tray_amount', 0), 2);
$rows = [];
foreach ($orders->groupBy('store_id') as $storeId => $storeOrders) {
$productAmount = $storeOrders->reduce(
static fn (string $carry, StoreOrderModel $order): string => bcadd($carry, (string) $order->total_amount, 2),
'0'
);
$bill = $bills->get((int) $storeId);
$rows[] = [
'store_id' => (int) $storeId,
'store_name' => $stores->get((int) $storeId)->name ?? ('门店#' . $storeId),
'order_count' => $storeOrders->count(),
'product_amount' => $productAmount,
'box_price' => $boxPrice,
'tray_price' => $trayPrice,
'bill' => $bill?->toArray(),
];
}
$submitted = $request->validated()['orders'];
return $this->success([
'purchase' => $purchase->only(['id', 'purchase_no', 'status']),
'stores' => $rows,
]);
}
return DB::transaction(function () use ($purchase, $storeId, $submitted) {
$orders = StoreOrderModel::query()
->where('purchase_id', $purchase->id)
->where('store_id', $storeId)
->whereNull('deleted_at')
->lockForUpdate()
->get();
/**
* 生成账单
* @throws Throwable
*/
#[PostRoute(route: '/{id}/bill', authorize: 'bill', where: ['id' => '[0-9]+'])]
public function generateBill(int $id, PurchaseBillGenerateRequest $request): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_COMPLETED) {
throw new RepositoryException('采购单未完成,不允许生成账单');
}
if ($orders->isEmpty()) {
throw new RepositoryException('该采购单下无此门店的订单');
}
$bills = app(BillGenerateService::class)->generate(
$purchase,
$request->validated()['stores'],
(int) $request->user()->id,
);
// 合并记录修改必须覆盖该门店全部订单,避免只改部分造成汇总偏差
$actualIds = $orders->pluck('id')->map(static fn ($orderId) => (int) $orderId)->all();
$submittedIds = array_map(static fn (array $row) => (int) $row['order_id'], $submitted);
$missing = array_diff($actualIds, $submittedIds);
if ($missing !== []) {
throw new RepositoryException('提交不完整,缺少订单:' . implode('、', $missing));
}
if (array_diff($submittedIds, $actualIds) !== []) {
throw new RepositoryException('包含不属于该采购单该门店的订单');
}
foreach ($orders as $order) {
if (! in_array($order->status, [
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_DELIVERING,
StoreOrderModel::STATUS_DISTRIBUTION,
], true)) {
throw new RepositoryException(
'订单 ' . $order->order_no . ' 当前状态为「'
. (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status)
. '」,不允许修改周转框/托盘数量'
);
}
}
$ordersById = $orders->keyBy('id');
$containerService = app(StoreOrderContainerService::class);
foreach ($submitted as $row) {
$containerService->update(
$ordersById->get($row['order_id']),
(int) $row['box_num'],
(int) $row['tray_num'],
);
}
return $this->success();
});
return $this->success(['count' => count($bills)], '已生成 ' . count($bills) . ' 张门店账单');
}
/**
@@ -575,9 +562,8 @@ class PurchaseOrderController extends BaseController
// 重算订单金额
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
$order->product_amount = $order->items()->sum('amount');
$order->total_weight = $order->items()->sum('weight');
$order->total_amount = bcadd($order->product_amount, $order->added_amount, 2);
$order->total_amount = $order->items()->sum('amount');
$order->save();
// 重算采购单重量