账单生成

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
+123
View File
@@ -0,0 +1,123 @@
<?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;
});
}
}
+2 -1
View File
@@ -27,12 +27,13 @@ class BillNumberService
'RC' => ['reconciliation', 'recon_no'],
'ST' => ['statement', 'statement_no'],
'JS' => ['settlement', 'settlement_no'],
'ZD' => ['bill', 'bill_no'],
];
/**
* 生成业务单号
*
* @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / ST 对账单 / JS 结算
* @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / ST 对账单 / JS 结算 / ZD 账单
* @return string 如 PO202607230001
*/
public function make(string $prefix): string
@@ -1,34 +0,0 @@
<?php
namespace App\Services;
use App\Models\StoreOrderModel;
/**
* 周转框/周转托盘数量修改(订单附加金额重算)
*/
readonly class StoreOrderContainerService
{
/**
* 更新订单周转框/托盘数量并重算附加金额与订单总金额。
* 历史订单未写商品金额:按「总额 - 附加」反推并回写,保证 总额 = 商品 + 附加 恒成立。
*/
public function update(StoreOrderModel $order, int $boxNum, int $trayNum): void
{
$boxPrice = (float) site_config('services.box_amount', 0);
$trayPrice = (float) site_config('services.tray_amount', 0);
$addedAmount = round($boxNum * $boxPrice + $trayNum * $trayPrice, 2);
$productAmount = (float) $order->product_amount;
if ($productAmount <= 0 && (float) $order->total_amount > 0) {
$productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2);
}
$order->box_num = $boxNum;
$order->tray_num = $trayNum;
$order->product_amount = $productAmount;
$order->added_amount = $addedAmount;
$order->total_amount = round($productAmount + $addedAmount, 2);
$order->save();
}
}