账单生成
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user