Files
xin-procurement/app/Services/PurchaseEditService.php
T
2026-08-12 17:10:05 +08:00

220 lines
9.1 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use Illuminate\Support\Facades\DB;
/**
* C4 采购单数据修改(明细矩阵:门店单元格 / 商品行)
*
* 采购单无独立明细表,修改直接回写订货明细(store_order_item),并重算:
* 1. 受影响订单汇总(recalculateOrderTotals 同口径:商品金额 + 附加金额)
* 2. 采购单头汇总:estimate_amount = Σ订货金额;actual_amount = Σ(称重>0 ? 称重×成本 : 数量×成本)
*/
class PurchaseEditService
{
/**
* 门店单元格修改:数量(必填)/ 称重(可空),金额按 数量×单价 重算
*/
public function updateCell(StoreOrderItemModel $item, int $quantity, ?float $weight): StoreOrderItemModel
{
return DB::transaction(function () use ($item, $quantity, $weight) {
$item->quantity = $quantity;
if ($weight !== null) {
$item->weight = bcadd((string) $weight, '0', 3);
}
$item->amount = bcmul((string) $quantity, (string) $item->price, 2);
$item->save();
$order = StoreOrderModel::find($item->order_id);
if ($order !== null) {
$this->recalculateOrderTotals($order);
}
if ($item->purchase_id > 0) {
$purchase = PurchaseOrderModel::find($item->purchase_id);
if ($purchase !== null) {
$this->recalculatePurchaseTotals($purchase);
}
}
return $item->refresh();
});
}
/**
* 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细;
* 实际称重按数量比例分摊(尾差修正守恒);
* syncProduct = true 时同步更新商品档案(product 表)
*
* @param array{product_name?: string, supplier_id?: int, product_spec?: string, unit?: string, cost_price?: float} $attrs 行属性(仅同步传入键)
* @param float|null $weight 行实际称重合计,NULL 不修改
*/
public function updateRow(PurchaseOrderModel $purchase, int $productId, array $attrs, ?float $weight, bool $syncProduct = false): int
{
return DB::transaction(function () use ($purchase, $productId, $attrs, $weight, $syncProduct) {
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('product_id', $productId)
->lockForUpdate()
->get();
if ($items->isEmpty()) {
throw new RepositoryException('该采购单下无此商品的订货明细');
}
$sync = [];
if (isset($attrs['product_name'])) {
$sync['product_name'] = $attrs['product_name'];
}
if (isset($attrs['supplier_id'])) {
$sync['supplier_id'] = (int) $attrs['supplier_id'];
}
if (isset($attrs['product_spec'])) {
$sync['product_spec'] = $attrs['product_spec'];
}
if (isset($attrs['unit'])) {
$sync['unit'] = $attrs['unit'];
}
if (isset($attrs['cost_price'])) {
$sync['cost_price'] = bcadd((string) $attrs['cost_price'], '0', 2);
}
if ($sync !== []) {
foreach ($items as $item) {
$item->fill($sync)->save();
}
// 同步至商品档案(软删除商品不同步,直接报错回滚)
if ($syncProduct) {
$product = ProductModel::withTrashed()->find($productId);
if ($product === null || $product->trashed()) {
throw new RepositoryException('商品档案不存在或已删除,无法同步至商品');
}
$productAttrs = [];
if (isset($sync['product_name'])) {
$productAttrs['name'] = $sync['product_name'];
}
if (isset($sync['supplier_id'])) {
$productAttrs['supplier_id'] = $sync['supplier_id'];
}
if (isset($sync['product_spec'])) {
$productAttrs['spec'] = $sync['product_spec'];
}
if (isset($sync['unit'])) {
$productAttrs['unit'] = $sync['unit'];
}
if (isset($sync['cost_price'])) {
$productAttrs['cost_price'] = $sync['cost_price'];
}
$product->fill($productAttrs)->save();
}
}
if ($weight !== null) {
$totalQty = $items->reduce(
static fn (string $carry, $item): string => bcadd($carry, (string) $item->quantity, 2),
'0'
);
$target = bcadd((string) $weight, '0', 3);
$allocated = '0';
$lastIndex = $items->count() - 1;
foreach ($items->values() as $index => $item) {
if ($index === $lastIndex) {
// 尾差修正:最后一行 = 总称重 − 已分摊,保证守恒
$rowWeight = bcsub($target, $allocated, 3);
} elseif ((float) $totalQty > 0) {
$ratio = bcdiv((string) $item->quantity, $totalQty, 6);
$rowWeight = bcmul($target, $ratio, 3);
$allocated = bcadd($allocated, $rowWeight, 3);
} else {
$rowWeight = '0';
}
$item->weight = $rowWeight;
$item->save();
}
}
$orderIds = $items->pluck('order_id')->unique()->all();
StoreOrderModel::whereIn('id', $orderIds)->get()
->each(fn (StoreOrderModel $order) => $this->recalculateOrderTotals($order));
$this->recalculatePurchaseTotals($purchase);
return $items->count();
});
}
/**
* 重算订单汇总(与 StoreOrderController::recalculateOrderTotals 同口径)
*/
public function recalculateOrderTotals(StoreOrderModel $order): void
{
$totals = StoreOrderItemModel::query()
->where('order_id', $order->id)
->selectRaw('COALESCE(SUM(quantity), 0) as total_quantity')
->selectRaw('COALESCE(SUM(weight), 0) as total_weight')
->selectRaw('COALESCE(SUM(amount), 0) as product_amount')
->first();
$productAmount = bcadd((string) $totals->product_amount, '0', 2);
$order->total_quantity = (int) $totals->total_quantity;
$order->total_weight = bcadd((string) $totals->total_weight, '0', 3);
$order->product_amount = $productAmount;
$order->total_amount = bcadd($productAmount, (string) $order->added_amount, 2);
$order->save();
}
/**
* 重算采购单头汇总:estimate_amount = Σ订货金额(销售口径);
* actual_amount = Σ(称重>0 ? 称重×成本 : 数量×成本)(采购成本口径)
*/
public function recalculatePurchaseTotals(PurchaseOrderModel $purchase): void
{
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->get(['quantity', 'weight', 'amount', 'cost_price', 'product_spec']);
$totalQuantity = '0';
$totalWeight = '0';
$estimate = '0';
$actual = '0';
foreach ($items as $item) {
$unitCost = self::unitCost((string) ($item->cost_price ?? '0'), (string) $item->product_spec);
$totalQuantity = bcadd($totalQuantity, (string) $item->quantity, 2);
$totalWeight = bcadd($totalWeight, (string) $item->weight, 3);
$estimate = bcadd($estimate, (string) $item->amount, 2);
$actual = bcadd($actual, self::costAmount((string) $item->quantity, (string) $item->weight, $unitCost), 2);
}
$purchase->total_quantity = $totalQuantity;
$purchase->total_weight = $totalWeight;
$purchase->estimate_amount = $estimate;
$purchase->actual_amount = $actual;
$purchase->save();
}
/**
* 采购成本金额:称重>0 按 称重×单价,否则按 数量×单价
*/
public static function costAmount(string $quantity, string $weight, string $unitCost): string
{
return (float) $weight > 0
? bcmul($weight, $unitCost, 2)
: bcmul($quantity, $unitCost, 2);
}
/**
* 单价 = 成本 / 包规数值(spec 解析不出正数时按 1 处理,即单价=成本)
*/
public static function unitCost(string $costPrice, string $spec): string
{
if (preg_match('/\d+(?:\.\d+)?/', $spec, $matches) === 1 && (float) $matches[0] > 0) {
return bcdiv($costPrice, $matches[0], 2);
}
return bcadd($costPrice, '0', 2);
}
}