246 lines
9.2 KiB
PHP
246 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\ProductModel;
|
|
use App\Models\PurchaseOrderModel;
|
|
use App\Models\StoreModel;
|
|
use App\Models\StoreOrderItemModel;
|
|
use App\Models\StoreOrderModel;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* 采购单明细共享服务:订单/采购单汇总重算 + 门店/供应商维度的商品行聚合
|
|
*
|
|
* 汇总口径(勿偏离):
|
|
* - 订单:total_quantity=Σ数量、total_weight=Σ称重、total_amount=Σ金额
|
|
* - 采购单:total_quantity=Σ数量、estimate_amount=Σ(数量×成本价)(预估成本)、total_weight=Σ称重
|
|
*/
|
|
class PurchaseItemService
|
|
{
|
|
/**
|
|
* 重算门店订单汇总(明细增删改后调用)
|
|
*/
|
|
public function recalcOrder(int $orderId): void
|
|
{
|
|
$order = StoreOrderModel::query()->find($orderId);
|
|
if ($order === null) {
|
|
return;
|
|
}
|
|
$order->total_quantity = (int) $order->items()->sum('quantity');
|
|
$order->total_weight = $order->items()->sum('weight');
|
|
$order->total_amount = $order->items()->sum('amount');
|
|
$order->save();
|
|
}
|
|
|
|
/**
|
|
* 重算采购单汇总(明细增删改后调用)
|
|
*/
|
|
public function recalcPurchase(int $purchaseId): void
|
|
{
|
|
$purchase = PurchaseOrderModel::query()->find($purchaseId);
|
|
if ($purchase === null) {
|
|
return;
|
|
}
|
|
$purchase->total_quantity = (int) StoreOrderItemModel::query()
|
|
->where('purchase_id', $purchaseId)->sum('quantity');
|
|
$purchase->estimate_amount = StoreOrderItemModel::query()
|
|
->where('purchase_id', $purchaseId)->sum(DB::raw('quantity * cost_price'));
|
|
$purchase->total_weight = StoreOrderItemModel::query()
|
|
->where('purchase_id', $purchaseId)->sum('weight');
|
|
$purchase->save();
|
|
}
|
|
|
|
/**
|
|
* 采购单内指定门店的采购汇总行(按商品聚合,单价=加权平均 Σ金额÷Σ数量,
|
|
* 排序:分类 sort → 商品 sort,与明细矩阵同序)
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function storeSummaryRows(int $purchaseId, int $storeId): array
|
|
{
|
|
$items = $this->purchaseItems($purchaseId)
|
|
->where('store_id', $storeId)
|
|
->sortBy('id');
|
|
|
|
$rows = [];
|
|
foreach ($items->groupBy('product_id') as $productId => $group) {
|
|
$first = $group->first();
|
|
$quantity = 0;
|
|
$amount = '0';
|
|
$weight = '0';
|
|
foreach ($group as $item) {
|
|
$weight = bcadd($weight, (string) $item->weight, 3);
|
|
$quantity += (int) $item->quantity;
|
|
$amount = bcadd($amount, (string) $item->amount, 2);
|
|
}
|
|
|
|
$rows[] = [
|
|
'product_id' => (int) $productId,
|
|
'product_name' => $first->product_name,
|
|
'product_spec' => $first->product_spec,
|
|
'unit' => $first->unit,
|
|
'market' => (string) data_get($first, 'market', ''),
|
|
// 加权平均单价(保证 单价×数量=预计金额)
|
|
'price' => $quantity > 0
|
|
? bcdiv($amount, (string) $quantity, 2)
|
|
: (string) $first->price,
|
|
'quantity' => $quantity,
|
|
'amount' => $amount,
|
|
'weight' => $weight,
|
|
'category_sort' => (int) data_get($first, 'category_sort', 9999),
|
|
'product_sort' => (int) data_get($first, 'product_sort', 9999),
|
|
];
|
|
}
|
|
|
|
return $this->sortRows($rows);
|
|
}
|
|
|
|
/**
|
|
* 采购单内指定供应商的商品聚合行(成本口径:金额=Σ数量×成本价,
|
|
* 排序:分类 sort → 商品 sort)
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function supplierRows(int $purchaseId, int $supplierId): array
|
|
{
|
|
$items = $this->purchaseItems($purchaseId)
|
|
->where('supplier_id', $supplierId)
|
|
->sortBy('id');
|
|
|
|
$rows = [];
|
|
foreach ($items->groupBy('product_id') as $productId => $group) {
|
|
$first = $group->first();
|
|
$quantity = 0;
|
|
$weight = '0';
|
|
$amount = '0';
|
|
foreach ($group as $item) {
|
|
$quantity += (int) $item->quantity;
|
|
$weight = bcadd($weight, (string) $item->weight, 3);
|
|
$amount = bcadd($amount, bcmul((string) $item->quantity, (string) $item->cost_price, 2), 2);
|
|
}
|
|
|
|
$rows[] = [
|
|
'product_id' => (int) $productId,
|
|
'product_name' => $first->product_name,
|
|
'product_spec' => $first->product_spec,
|
|
'unit' => $first->unit,
|
|
'market' => (string) data_get($first, 'market', ''),
|
|
'cost_price' => (string) $first->cost_price,
|
|
'quantity' => $quantity,
|
|
'weight' => $weight,
|
|
'amount' => $amount,
|
|
'category_sort' => (int) data_get($first, 'category_sort', 9999),
|
|
'product_sort' => (int) data_get($first, 'product_sort', 9999),
|
|
];
|
|
}
|
|
|
|
return $this->sortRows($rows);
|
|
}
|
|
|
|
/**
|
|
* 供应商采购明细矩阵(导出用):该供应商在采购单内的商品行按市场分组,
|
|
* 行附「汇总数量 + 各门店数量」;门店列=本采购单内有该供应商明细的门店(按ID排序)
|
|
*
|
|
* @return array{
|
|
* stores: array<int, string>,
|
|
* markets: array<string, array<int, array<string, mixed>>>
|
|
* }
|
|
*/
|
|
public function supplierMarketMatrix(int $purchaseId, int $supplierId): array
|
|
{
|
|
$items = $this->purchaseItems($purchaseId)
|
|
->where('supplier_id', $supplierId)
|
|
->sortBy('id');
|
|
|
|
$storeIds = $items->pluck('store_id')
|
|
->map(static fn ($id): int => (int) $id)
|
|
->unique()->sort()->values()->all();
|
|
$stores = StoreModel::withTrashed()
|
|
->whereIn('id', $storeIds)
|
|
->orderBy('id')
|
|
->pluck('name', 'id')
|
|
->toArray();
|
|
|
|
$rows = [];
|
|
foreach ($items->groupBy('product_id') as $productId => $group) {
|
|
$first = $group->first();
|
|
$quantity = 0;
|
|
$storeQuantities = array_fill_keys(array_map('intval', array_keys($stores)), 0);
|
|
foreach ($group as $item) {
|
|
$quantity += (int) $item->quantity;
|
|
$storeQuantities[(int) $item->store_id] += (int) $item->quantity;
|
|
}
|
|
|
|
$rows[] = [
|
|
'product_id' => (int) $productId,
|
|
'product_name' => $first->product_name,
|
|
'market' => (string) data_get($first, 'market', ''),
|
|
'quantity' => $quantity,
|
|
'store_quantities' => $storeQuantities,
|
|
'category_sort' => (int) data_get($first, 'category_sort', 9999),
|
|
'product_sort' => (int) data_get($first, 'product_sort', 9999),
|
|
];
|
|
}
|
|
|
|
// 排序后按市场分组(组间/组内均保持 分类sort → 商品sort 顺序)
|
|
$markets = [];
|
|
foreach ($this->sortRows($rows) as $row) {
|
|
$markets[$row['market']][] = $row;
|
|
}
|
|
|
|
return ['stores' => $stores, 'markets' => $markets];
|
|
}
|
|
|
|
/**
|
|
* 采购单订货明细(关联订单过滤软删、成本价可见、附分类/商品排序键)
|
|
*
|
|
* @return Collection<int, StoreOrderItemModel>
|
|
*/
|
|
private function purchaseItems(int $purchaseId): Collection
|
|
{
|
|
$items = StoreOrderItemModel::query()
|
|
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
|
|
->where('store_order_item.purchase_id', $purchaseId)
|
|
->whereNull('store_order.deleted_at')
|
|
->select('store_order_item.*')
|
|
->get()
|
|
->makeVisible('cost_price');
|
|
|
|
// 排序键:商品分类 sort → 商品 sort;市场取商品档案(商品含软删除,保证历史单据可导出)
|
|
$products = ProductModel::withTrashed()
|
|
->with('category:id,sort')
|
|
->whereIn('id', $items->pluck('product_id')->unique())
|
|
->get(['id', 'category_id', 'sort', 'market'])
|
|
->keyBy('id');
|
|
|
|
foreach ($items as $item) {
|
|
$product = $products->get((int) $item->product_id);
|
|
$item->setAttribute('category_sort', (int) ($product?->category?->sort ?? 9999));
|
|
$item->setAttribute('product_sort', (int) ($product?->sort ?? 9999));
|
|
$item->setAttribute('market', (string) ($product?->market ?? ''));
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* 排序并剥离排序键
|
|
*
|
|
* @param array<int, array<string, mixed>> $rows
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function sortRows(array $rows): array
|
|
{
|
|
usort($rows, static fn (array $a, array $b): int =>
|
|
[$a['category_sort'], $a['product_sort'], $a['product_id']]
|
|
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
|
|
|
|
return array_map(static function (array $row): array {
|
|
unset($row['category_sort'], $row['product_sort']);
|
|
return $row;
|
|
}, $rows);
|
|
}
|
|
}
|