采购单优化
This commit is contained in:
@@ -20,6 +20,7 @@ use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 采购单管理(C1 汇总生成 / C2-C3 导出 / C4 修改)
|
||||
@@ -213,33 +214,8 @@ class PurchaseOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* C4 门店单元格修改:数量/称重回写订货明细,金额重算并同步订单/采购单汇总
|
||||
*/
|
||||
#[PutRoute(route: '/cell/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function updateCell(int $id, PurchaseCellUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$item = StoreOrderItemModel::find($id);
|
||||
if (empty($item)) {
|
||||
throw new RepositoryException('订货明细不存在');
|
||||
}
|
||||
$validated = $request->validated();
|
||||
|
||||
$item = app(PurchaseEditService::class)->updateCell(
|
||||
$item,
|
||||
(int) $validated['quantity'],
|
||||
isset($validated['weight']) ? (float) $validated['weight'] : null,
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'quantity' => (int) $item->quantity,
|
||||
'weight' => (float) $item->weight,
|
||||
'amount' => (float) $item->amount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* C4 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细;
|
||||
* 实际称重按数量比例分摊(尾差修正守恒)
|
||||
* 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细;
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])]
|
||||
public function updateRow(int $id, int $productId, PurchaseRowUpdateRequest $request): JsonResponse
|
||||
@@ -256,16 +232,14 @@ class PurchaseOrderController extends BaseController
|
||||
$attrs[$field] = $validated[$field];
|
||||
}
|
||||
}
|
||||
$weight = isset($validated['weight']) ? (float) $validated['weight'] : null;
|
||||
if ($attrs === [] && $weight === null) {
|
||||
throw new RepositoryException('品名/供应商/包规/单位/成本/实际称重至少填写一项');
|
||||
if ($attrs === []) {
|
||||
throw new RepositoryException('品名/供应商/包规/单位/成本至少填写一项');
|
||||
}
|
||||
|
||||
$count = app(PurchaseEditService::class)->updateRow(
|
||||
$purchase,
|
||||
$productId,
|
||||
$attrs,
|
||||
$weight,
|
||||
(bool) ($validated['sync_product'] ?? false),
|
||||
);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ class PurchaseRowUpdateRequest extends BaseFormRequest
|
||||
'product_spec' => 'nullable|string|max:100',
|
||||
'unit' => 'nullable|string|max:20',
|
||||
'cost_price' => 'nullable|numeric|min:0',
|
||||
'weight' => 'nullable|numeric|min:0',
|
||||
'sync_product' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
@@ -34,8 +33,6 @@ class PurchaseRowUpdateRequest extends BaseFormRequest
|
||||
'unit.max' => '单位不能超过 20 字',
|
||||
'cost_price.numeric' => '采购成本必须为数字',
|
||||
'cost_price.min' => '采购成本不能小于 0',
|
||||
'weight.numeric' => '实际称重必须为数字',
|
||||
'weight.min' => '实际称重不能小于 0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ class PurchaseOrderModel extends Model
|
||||
use HasFactory;
|
||||
|
||||
/** 状态:进行中(已生成,待完成) */
|
||||
public const STATUS_PENDING = 0;
|
||||
public const int STATUS_PENDING = 0;
|
||||
/** 状态:已完成 */
|
||||
public const STATUS_COMPLETED = 3;
|
||||
public const int STATUS_COMPLETED = 3;
|
||||
|
||||
protected $table = 'purchase_order';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
@@ -8,55 +8,28 @@ use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 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 PurchaseOrderModel $purchase
|
||||
* @param int $productId
|
||||
* @param array{product_name?: string, supplier_id?: int, product_spec?: string, unit?: string, cost_price?: float} $attrs 行属性(仅同步传入键)
|
||||
* @param float|null $weight 行实际称重合计,NULL 不修改
|
||||
* @param bool $syncProduct
|
||||
* @return int
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function updateRow(PurchaseOrderModel $purchase, int $productId, array $attrs, ?float $weight, bool $syncProduct = false): int
|
||||
public function updateRow(PurchaseOrderModel $purchase, int $productId, array $attrs, bool $syncProduct = false): int
|
||||
{
|
||||
return DB::transaction(function () use ($purchase, $productId, $attrs, $weight, $syncProduct) {
|
||||
return DB::transaction(function () use ($purchase, $productId, $attrs, $syncProduct) {
|
||||
$items = StoreOrderItemModel::query()
|
||||
->where('purchase_id', $purchase->id)
|
||||
->where('product_id', $productId)
|
||||
@@ -87,7 +60,7 @@ class PurchaseEditService
|
||||
$item->fill($sync)->save();
|
||||
}
|
||||
|
||||
// 同步至商品档案(软删除商品不同步,直接报错回滚)
|
||||
// 同步至商品档案
|
||||
if ($syncProduct) {
|
||||
$product = ProductModel::withTrashed()->find($productId);
|
||||
if ($product === null || $product->trashed()) {
|
||||
@@ -113,33 +86,6 @@ class PurchaseEditService
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -147,51 +93,24 @@ class PurchaseEditService
|
||||
}
|
||||
|
||||
/**
|
||||
* 重算订单汇总(与 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']);
|
||||
->get(['quantity', 'cost_price']);
|
||||
|
||||
$totalQuantity = '0';
|
||||
$totalWeight = '0';
|
||||
$totalQuantity = 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);
|
||||
$totalQuantity += $item->quantity;
|
||||
$item_estimate = bcmul($item->cost_price, $item->quantity, 2);
|
||||
$estimate = bcadd($estimate, $item_estimate, 2);
|
||||
}
|
||||
|
||||
$purchase->total_quantity = $totalQuantity;
|
||||
$purchase->total_weight = $totalWeight;
|
||||
$purchase->estimate_amount = $estimate;
|
||||
$purchase->actual_amount = $actual;
|
||||
$purchase->save();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user