124 lines
5.3 KiB
PHP
124 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Exceptions\RepositoryException;
|
|
use App\Models\BillModel;
|
|
use App\Models\PurchaseOrderModel;
|
|
use App\Models\StoreModel;
|
|
use App\Models\StoreOrderModel;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Throwable;
|
|
|
|
/**
|
|
* 门店账单生成(采购单完成后,按门店各生成一张账单)
|
|
*
|
|
* 流程(事务内):
|
|
* 1. 锁定采购单全部有效订单,按门店分组,校验提交门店完整覆盖
|
|
* 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改)
|
|
* 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照)
|
|
* 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联
|
|
*/
|
|
readonly class BillGenerateService
|
|
{
|
|
public function __construct(private BillNumberService $billNumberService)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @param PurchaseOrderModel $purchase 已完成采购单
|
|
* @param array<int, array{store_id: int, delivery_fee: string, box_num: int, tray_num: int}> $stores 按门店提交的配送费/周转筐/托盘数量
|
|
* @param int $operatorId 生成人(后台系统用户ID)
|
|
* @return BillModel[] 生成的账单列表
|
|
* @throws Throwable
|
|
*/
|
|
public function generate(PurchaseOrderModel $purchase, array $stores, int $operatorId): array
|
|
{
|
|
return DB::transaction(function () use ($purchase, $stores, $operatorId) {
|
|
// 1. 行锁采购单全部有效订单(并发防护),按门店分组
|
|
$orders = StoreOrderModel::query()
|
|
->where('purchase_id', $purchase->id)
|
|
->whereNull('deleted_at')
|
|
->lockForUpdate()
|
|
->get();
|
|
if ($orders->isEmpty()) {
|
|
throw new RepositoryException('采购单下无门店订单,无法生成账单');
|
|
}
|
|
$ordersByStore = $orders->groupBy('store_id');
|
|
|
|
// 提交的门店必须完整覆盖采购单门店,避免漏门店造成订单未入账
|
|
$submitted = [];
|
|
foreach ($stores as $row) {
|
|
$submitted[(int) $row['store_id']] = $row;
|
|
}
|
|
$missing = array_diff(array_map('intval', $ordersByStore->keys()->all()), array_keys($submitted));
|
|
if ($missing !== []) {
|
|
throw new RepositoryException('提交不完整,缺少门店账单信息:' . implode('、', $missing));
|
|
}
|
|
if (array_diff(array_keys($submitted), array_map('intval', $ordersByStore->keys()->all())) !== []) {
|
|
throw new RepositoryException('包含不属于该采购单的门店');
|
|
}
|
|
|
|
// 2. 防重复生成:任一门店已出账则整批拒绝
|
|
$billedStoreIds = BillModel::query()
|
|
->where('purchase_id', $purchase->id)
|
|
->pluck('store_id')
|
|
->map(static fn ($id) => (int) $id)
|
|
->all();
|
|
if ($billedStoreIds !== []) {
|
|
$names = StoreModel::withTrashed()
|
|
->whereIn('id', $billedStoreIds)
|
|
->pluck('name')
|
|
->implode('、');
|
|
throw new RepositoryException('以下门店已生成账单,不允许重复生成:' . $names);
|
|
}
|
|
|
|
// 3. 逐门店生成账单并关联订单
|
|
$boxPrice = (string) site_config('services.box_amount', 0);
|
|
$trayPrice = (string) site_config('services.tray_amount', 0);
|
|
$billDate = now()->toDateString();
|
|
$bills = [];
|
|
foreach ($ordersByStore as $storeId => $storeOrders) {
|
|
$row = $submitted[(int) $storeId];
|
|
|
|
$productAmount = $storeOrders->reduce(
|
|
static fn (string $carry, StoreOrderModel $order): string => bcadd($carry, (string) $order->total_amount, 2),
|
|
'0'
|
|
);
|
|
$deliveryFee = bcadd((string) $row['delivery_fee'], '0', 2);
|
|
$addedAmount = bcadd(
|
|
bcmul((string) (int) $row['box_num'], $boxPrice, 2),
|
|
bcmul((string) (int) $row['tray_num'], $trayPrice, 2),
|
|
2
|
|
);
|
|
$totalAmount = bcadd(bcadd($productAmount, $deliveryFee, 2), $addedAmount, 2);
|
|
|
|
$bill = BillModel::create([
|
|
'bill_no' => $this->billNumberService->make('ZD'),
|
|
'purchase_id' => $purchase->id,
|
|
'store_id' => (int) $storeId,
|
|
'bill_date' => $billDate,
|
|
'product_amount' => $productAmount,
|
|
'delivery_fee' => $deliveryFee,
|
|
'box_num' => (int) $row['box_num'],
|
|
'tray_num' => (int) $row['tray_num'],
|
|
'box_price' => bcadd($boxPrice, '0', 2),
|
|
'tray_price' => bcadd($trayPrice, '0', 2),
|
|
'added_amount' => $addedAmount,
|
|
'total_amount' => $totalAmount,
|
|
'operator_id' => $operatorId,
|
|
]);
|
|
|
|
// 关联该门店在采购单中的全部订单到账单
|
|
StoreOrderModel::query()
|
|
->whereIn('id', $storeOrders->pluck('id'))
|
|
->update(['bill_id' => $bill->id]);
|
|
|
|
$bills[] = $bill;
|
|
}
|
|
|
|
return $bills;
|
|
});
|
|
}
|
|
}
|