采购单优化
This commit is contained in:
@@ -8,7 +8,6 @@ use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Services\PurchaseEditService;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
@@ -75,7 +74,7 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping,
|
||||
foreach ($orderItems->groupBy('product_id') as $productId => $group) {
|
||||
$first = $group->first();
|
||||
$product = $products->get((int) $productId);
|
||||
$unitCost = PurchaseEditService::unitCost((string) ($first->cost_price ?? '0'), (string) $first->product_spec);
|
||||
$unitCost = $first->cost_price ?? '0';
|
||||
|
||||
$quantity = '0';
|
||||
$weight = '0';
|
||||
@@ -84,11 +83,8 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping,
|
||||
foreach ($group as $item) {
|
||||
$quantity = bcadd($quantity, (string) $item->quantity, 2);
|
||||
$weight = bcadd($weight, (string) $item->weight, 3);
|
||||
$amount = bcadd($amount, PurchaseEditService::costAmount(
|
||||
(string) $item->quantity,
|
||||
(string) $item->weight,
|
||||
$unitCost
|
||||
), 2);
|
||||
// 金额 = 数量 × 每包成本价(单价/包规不参与金额计算)
|
||||
$amount = bcadd($amount, bcmul((string) $item->quantity, (string) $item->cost_price, 2), 2);
|
||||
$storeQuantities[(int) $item->store_id] = bcadd(
|
||||
$storeQuantities[(int) $item->store_id] ?? '0',
|
||||
(string) $item->quantity,
|
||||
|
||||
@@ -18,13 +18,13 @@ use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
|
||||
/**
|
||||
* 小程序门店订单(下单 / 历史 / 详情 / 取消 / 周期汇总)
|
||||
* 小程序门店订单
|
||||
*/
|
||||
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
|
||||
class OrderController extends BaseMiniController
|
||||
{
|
||||
/**
|
||||
* 下单:逐行取当前门店等级价快照,服务端重算 amount 与 total(不接受前端金额)
|
||||
* 下单
|
||||
*/
|
||||
#[PostRoute('/order', authorize: true)]
|
||||
public function store(MiniOrderRequest $request): JsonResponse
|
||||
|
||||
@@ -4,11 +4,9 @@ namespace App\Http\Controllers\Order;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Models\NoticeModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\ProductPriceModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Services\ItemImageResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -17,7 +15,6 @@ use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 门店订单管理(订单只读 + 状态管理 + 明细修改/同步;创建/取消在小程序端)
|
||||
@@ -25,24 +22,6 @@ use Modules\SystemTool\Models\SysFileModel;
|
||||
#[RequestAttribute('/order/store', 'order.store')]
|
||||
class StoreOrderController extends BaseController
|
||||
{
|
||||
/** 状态中文名(通知文案用) */
|
||||
private const array STATUS_NAMES = [
|
||||
StoreOrderModel::STATUS_PENDING => '待接单',
|
||||
StoreOrderModel::STATUS_SUMMARIZED => '已接单',
|
||||
StoreOrderModel::STATUS_DELIVERING => '采购中',
|
||||
StoreOrderModel::STATUS_DISTRIBUTION => '配送中',
|
||||
StoreOrderModel::STATUS_COMPLETED => '已完成',
|
||||
StoreOrderModel::STATUS_CANCELLED => '已取消',
|
||||
];
|
||||
|
||||
/** 明细可编辑状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
|
||||
private const array ITEM_EDITABLE_STATUS = [
|
||||
StoreOrderModel::STATUS_PENDING,
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
];
|
||||
|
||||
protected array $searchField = [
|
||||
'store_id' => '=',
|
||||
'status' => '=',
|
||||
@@ -84,7 +63,7 @@ class StoreOrderController extends BaseController
|
||||
|
||||
// 明细封面图:从快照 image_ids 批量解析(不再关联商品档案表)
|
||||
foreach ($data['data'] as &$order) {
|
||||
$this->resolveItemImages($order['items']);
|
||||
app(ItemImageResolver::class)->resolve($order['items']);
|
||||
}
|
||||
unset($order);
|
||||
|
||||
@@ -99,48 +78,7 @@ class StoreOrderController extends BaseController
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已接单预览:聚合所有已接单订单明细(按商品分组),
|
||||
* 供生成采购单前确认(C1 前置);单位取明细快照
|
||||
*/
|
||||
#[GetRoute('/summary', 'query')]
|
||||
public function summary(): JsonResponse
|
||||
{
|
||||
$rows = StoreOrderItemModel::query()
|
||||
->select('product_id')
|
||||
->selectRaw('MAX(product_name) as product_name')
|
||||
->selectRaw('MAX(product_spec) as product_spec')
|
||||
->selectRaw('MAX(unit) as unit')
|
||||
->selectRaw('SUM(quantity) as total_quantity')
|
||||
->selectRaw('COUNT(DISTINCT store_id) as store_count')
|
||||
->whereHas('order', function ($query) {
|
||||
$query->where('status', StoreOrderModel::STATUS_SUMMARIZED);
|
||||
})
|
||||
->groupBy('product_id')
|
||||
->orderBy('product_id')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
// 快照无单位的历史明细兜底:从商品档案(含已下架/软删除)补齐
|
||||
$emptyUnitProductIds = array_column(array_filter(
|
||||
$rows,
|
||||
static fn (array $row) => ($row['unit'] ?? '') === ''
|
||||
), 'product_id');
|
||||
if ($emptyUnitProductIds !== []) {
|
||||
$units = ProductModel::withTrashed()
|
||||
->whereIn('id', $emptyUnitProductIds)
|
||||
->pluck('unit', 'id');
|
||||
foreach ($rows as &$row) {
|
||||
if (($row['unit'] ?? '') === '') {
|
||||
$row['unit'] = $units[$row['product_id']] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success($rows);
|
||||
}
|
||||
|
||||
/** 订单详情:订单头 + 明细(含商品快照、首图;后台侧成本价可见、附供应商名) */
|
||||
/** 订单详情 */
|
||||
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
|
||||
public function detail(int $id): JsonResponse
|
||||
{
|
||||
@@ -158,7 +96,7 @@ class StoreOrderController extends BaseController
|
||||
$data = $order->toArray();
|
||||
|
||||
// 明细首图 + 附加金额(与列表接口一致)
|
||||
$this->resolveItemImages($data['items']);
|
||||
app(ItemImageResolver::class)->resolve($data['items']);
|
||||
$boxAmount = site_config('services.box_amount');
|
||||
$trayAmount = site_config('services.tray_amount');
|
||||
$data['box_price'] = number_format($boxAmount, 2);
|
||||
@@ -170,160 +108,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 明细首图解析:从快照 image_ids 批量解析文件 URL,未找到时置空字符串(列表/详情共用)
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $items 订单明细数组(引用修改)
|
||||
*/
|
||||
private function resolveItemImages(array &$items): void
|
||||
{
|
||||
$fileIds = [];
|
||||
foreach ($items as $line) {
|
||||
foreach ((array) ($line['image_ids'] ?? []) as $fileId) {
|
||||
if ($fileId !== '' && $fileId !== null) {
|
||||
$fileIds[] = (int) $fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileUrls = [];
|
||||
if ($fileIds !== []) {
|
||||
foreach (SysFileModel::query()->whereIn('id', array_unique($fileIds))->get() as $file) {
|
||||
$fileUrls[$file->id] = $file->preview_url;
|
||||
}
|
||||
}
|
||||
foreach ($items as &$product) {
|
||||
$product['image'] = '';
|
||||
foreach ((array) ($product['image_ids'] ?? []) as $fileId) {
|
||||
if ($fileId !== '' && $fileId !== null && isset($fileUrls[(int) $fileId])) {
|
||||
$product['image'] = $fileUrls[(int) $fileId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($product['image_ids']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改订单明细(商品快照 + 订货量/重量),事务内重算单品金额与订单总价。
|
||||
* 可改字段:供应商、品名、规格、单位、单价、成本价、订货量、重量
|
||||
*/
|
||||
#[PutRoute(route: '/item/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function updateItem(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'supplier_id' => 'required|integer|min:0',
|
||||
'product_name' => 'required|string|max:100',
|
||||
'product_spec' => 'nullable|string|max:100',
|
||||
'unit' => 'required|string|max:20',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'cost_price' => 'required|numeric|min:0',
|
||||
'quantity' => 'required|integer|min:1',
|
||||
'weight' => 'required|numeric|min:0',
|
||||
'remark' => 'nullable|string'
|
||||
], [
|
||||
'supplier_id.required' => '供应商不能为空',
|
||||
'supplier_id.integer' => '供应商ID必须为整数',
|
||||
'supplier_id.min' => '供应商ID不正确',
|
||||
'product_name.required' => '品名不能为空',
|
||||
'product_name.max' => '品名最长 100 个字符',
|
||||
'product_spec.max' => '规格最长 100 个字符',
|
||||
'unit.required' => '计价单位不能为空',
|
||||
'unit.max' => '计价单位最长 20 个字符',
|
||||
'price.required' => '单价不能为空',
|
||||
'price.numeric' => '单价必须为数字',
|
||||
'price.min' => '单价不能小于 0',
|
||||
'cost_price.required' => '成本价不能为空',
|
||||
'cost_price.numeric' => '成本价必须为数字',
|
||||
'cost_price.min' => '成本价不能小于 0',
|
||||
'quantity.required' => '订货量不能为空',
|
||||
'quantity.integer' => '订货量必须为整数',
|
||||
'quantity.min' => '订货量必须大于 0',
|
||||
'weight.required' => '重量不能为空',
|
||||
'weight.numeric' => '重量必须为数字',
|
||||
'weight.min' => '重量不能小于 0',
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($id, $data) {
|
||||
$item = StoreOrderItemModel::query()->lockForUpdate()->find($id);
|
||||
if (empty($item)) {
|
||||
throw new RepositoryException('订单明细不存在');
|
||||
}
|
||||
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
|
||||
if (empty($order)) {
|
||||
throw new RepositoryException('订单不存在');
|
||||
}
|
||||
$this->assertItemEditable($order);
|
||||
|
||||
$item->fill($data);
|
||||
$item->product_spec = (string) ($data['product_spec'] ?? '');
|
||||
$item->amount = bcmul(
|
||||
bcadd((string) $data['price'], '0', 2),
|
||||
(string) $data['quantity'],
|
||||
2
|
||||
);
|
||||
$item->save();
|
||||
|
||||
$this->recalculateOrderTotals($order);
|
||||
|
||||
return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键同步明细商品快照:按商品ID同步最新商品档案的
|
||||
* 供应商、品名、规格、单位、成本价;单价按门店当前等级价重算
|
||||
* (未设置等级价时保留原单价),并重算订单总价
|
||||
*/
|
||||
#[PutRoute(route: '/item/{id}/sync', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function syncItem(int $id): JsonResponse
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
$item = StoreOrderItemModel::query()->lockForUpdate()->find($id);
|
||||
if (empty($item)) {
|
||||
throw new RepositoryException('订单明细不存在');
|
||||
}
|
||||
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
|
||||
if (empty($order)) {
|
||||
throw new RepositoryException('订单不存在');
|
||||
}
|
||||
$this->assertItemEditable($order);
|
||||
|
||||
// 软删除商品无法同步(下架商品仍可同步最新档案)
|
||||
$product = ProductModel::find($item->product_id);
|
||||
if (empty($product)) {
|
||||
throw new RepositoryException('商品不存在或已被删除,无法同步');
|
||||
}
|
||||
|
||||
$item->supplier_id = (int) $product->supplier_id;
|
||||
$item->product_name = $product->name;
|
||||
$item->product_spec = (string) $product->spec;
|
||||
$item->unit = (string) $product->unit;
|
||||
$item->cost_price = $product->cost_price;
|
||||
|
||||
// 单价:按订货门店当前客户等级价重算(百分比计价按最新成本价换算)
|
||||
$levelId = (int) ($order->store->level_id ?? 0);
|
||||
$priceRow = ProductPriceModel::query()
|
||||
->forProductLevel((int) $item->product_id, $levelId)
|
||||
->first(['price', 'price_type', 'percent']);
|
||||
if ($priceRow !== null) {
|
||||
$item->price = ProductPriceModel::calcActualPrice(
|
||||
(int) $priceRow->price_type,
|
||||
$priceRow->price,
|
||||
$priceRow->percent,
|
||||
$product->cost_price,
|
||||
);
|
||||
}
|
||||
|
||||
$item->amount = bcmul((string) $item->price, (string) $item->quantity, 2);
|
||||
$item->save();
|
||||
|
||||
$this->recalculateOrderTotals($order);
|
||||
|
||||
return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态流转:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||
* 状态流转
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function status(int $id, Request $request): JsonResponse
|
||||
@@ -343,7 +128,7 @@ class StoreOrderController extends BaseController
|
||||
$target = (int) $data['status'];
|
||||
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
|
||||
throw new RepositoryException(
|
||||
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作'
|
||||
'订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -356,8 +141,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量状态流转:先全量校验流转路径,任一订单不允许则整批中止;
|
||||
* 全部合法时在事务内流转并逐单通知门店
|
||||
* 批量状态流转
|
||||
*/
|
||||
#[PutRoute(route: '/batchStatus', authorize: 'update')]
|
||||
public function batchStatus(Request $request): JsonResponse
|
||||
@@ -381,12 +165,12 @@ class StoreOrderController extends BaseController
|
||||
$blocked = [];
|
||||
foreach ($orders as $order) {
|
||||
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
|
||||
$blocked[] = $order->order_no . '(' . (self::STATUS_NAMES[$order->status] ?? $order->status) . ')';
|
||||
$blocked[] = $order->order_no . '(' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . ')';
|
||||
}
|
||||
}
|
||||
if (! empty($blocked)) {
|
||||
throw new RepositoryException(
|
||||
'以下订单不允许流转为「' . (self::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked)
|
||||
'以下订单不允许流转为「' . (StoreOrderModel::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,7 +186,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除订单(软删除):仅已取消订单允许删除;删除后后台列表/详情、小程序端均不可见
|
||||
* 删除订单(软删除)
|
||||
*/
|
||||
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
|
||||
public function delete(int $id): JsonResponse
|
||||
@@ -413,7 +197,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
if ($order->status !== StoreOrderModel::STATUS_CANCELLED) {
|
||||
throw new RepositoryException(
|
||||
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,仅已取消订单可删除'
|
||||
'订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,仅已取消订单可删除'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -423,31 +207,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态流转合法路径(与迁移状态定义一致):
|
||||
* 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||
* (已接单→采购中 只能经「生成采购单」完成,不在本流转内)
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function allowedTransitions(int $status): array
|
||||
{
|
||||
return match ($status) {
|
||||
StoreOrderModel::STATUS_PENDING => [
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_CANCELLED,
|
||||
],
|
||||
StoreOrderModel::STATUS_DELIVERING => [
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
StoreOrderModel::STATUS_COMPLETED,
|
||||
],
|
||||
StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改周转框/周转托盘数量(仅已接单、采购中、配送中可改),
|
||||
* 自动重算附加金额与订单总金额
|
||||
* 修改周转框/周转托盘数量
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/container', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function container(int $id, Request $request): JsonResponse
|
||||
@@ -476,7 +236,7 @@ class StoreOrderController extends BaseController
|
||||
];
|
||||
if (! in_array($order->status, $editable, true)) {
|
||||
throw new RepositoryException(
|
||||
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量'
|
||||
'订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -501,36 +261,24 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 明细编辑状态校验:已完成/已取消订单锁定
|
||||
* 状态流转合法路径
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function assertItemEditable(StoreOrderModel $order): void
|
||||
private function allowedTransitions(int $status): array
|
||||
{
|
||||
if (! in_array($order->status, self::ITEM_EDITABLE_STATUS, true)) {
|
||||
throw new RepositoryException(
|
||||
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改商品明细'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重算订单汇总:明细金额合计 → 商品总金额;订货量/重量合计 → 订货总量/总重量;
|
||||
* 订单总金额 = 商品总金额 + 附加金额(恒成立)
|
||||
*/
|
||||
private 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();
|
||||
return match ($status) {
|
||||
StoreOrderModel::STATUS_PENDING => [
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_CANCELLED,
|
||||
],
|
||||
StoreOrderModel::STATUS_DELIVERING => [
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
StoreOrderModel::STATUS_COMPLETED,
|
||||
],
|
||||
StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,7 +291,7 @@ class StoreOrderController extends BaseController
|
||||
->where('status', UserModel::STATUS_NORMAL)
|
||||
->pluck('id');
|
||||
|
||||
$statusName = self::STATUS_NAMES[$order->status] ?? (string) $order->status;
|
||||
$statusName = StoreOrderModel::STATUS_NAMES[$order->status] ?? (string) $order->status;
|
||||
foreach ($userIds as $userId) {
|
||||
NoticeModel::create([
|
||||
'user_id' => $userId,
|
||||
|
||||
@@ -9,17 +9,17 @@ use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Services\ExportService;
|
||||
use App\Services\PurchaseEditService;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Services\ItemImageResolver;
|
||||
use App\Services\PurchaseGenerateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -50,8 +50,7 @@ class PurchaseOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 采购单详情:头 + 明细矩阵(商品行 × 门店列)
|
||||
* 行 = 商品(按分类 sort → 商品 sort 排序),列 = 商品信息 + 每个门店一格(数量/金额)
|
||||
* 采购单详情
|
||||
*/
|
||||
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
|
||||
public function detail(int $id): JsonResponse
|
||||
@@ -163,8 +162,7 @@ class PurchaseOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids),
|
||||
* 生成后源订单转为「采购中」并回写 purchase_id(订单与订货明细同步归集)
|
||||
* 生成采购单
|
||||
*/
|
||||
#[PostRoute('/generate', 'generate')]
|
||||
public function generate(Request $request): JsonResponse
|
||||
@@ -192,29 +190,77 @@ class PurchaseOrderController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
/** C2/C3 导出采购单:?type=all|category & format=xlsx|pdf */
|
||||
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
|
||||
public function export(int $id, Request $request): Response
|
||||
/**
|
||||
* 单元格下钻:采购单中某门店某商品的全部订货明细
|
||||
*/
|
||||
#[GetRoute(route: '/{id}/cell', authorize: 'query', where: ['id' => '[0-9]+'])]
|
||||
public function cell(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$productId = (int) $request->query('product_id', 0);
|
||||
$storeId = (int) $request->query('store_id', 0);
|
||||
if ($productId <= 0 || $storeId <= 0) {
|
||||
throw new RepositoryException('缺少商品或门店参数');
|
||||
}
|
||||
$purchase = PurchaseOrderModel::find($id);
|
||||
if (empty($purchase)) {
|
||||
throw new RepositoryException('采购单不存在');
|
||||
}
|
||||
$type = (string) $request->query('type', 'all');
|
||||
if (! in_array($type, ['all', 'category'], true)) {
|
||||
throw new RepositoryException('导出类型参数不正确(all 全品类 / category 蔬果分类)');
|
||||
}
|
||||
|
||||
return app(ExportService::class)->download(
|
||||
'purchase',
|
||||
$purchase,
|
||||
(string) $request->query('format', ExportService::FORMAT_XLSX),
|
||||
type: $type,
|
||||
);
|
||||
$items = StoreOrderItemModel::query()
|
||||
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
|
||||
->where('store_order_item.purchase_id', $purchase->id)
|
||||
->where('store_order_item.product_id', $productId)
|
||||
->where('store_order_item.store_id', $storeId)
|
||||
->whereNull('store_order.deleted_at')
|
||||
->with('supplier:id,name')
|
||||
->select(
|
||||
'store_order_item.*',
|
||||
'store_order.order_no',
|
||||
'store_order.order_date',
|
||||
'store_order.status as order_status',
|
||||
)
|
||||
->orderBy('store_order.id')
|
||||
->get()
|
||||
->makeVisible('cost_price');
|
||||
|
||||
$rows = [];
|
||||
foreach ($items as $item) {
|
||||
$rows[] = [
|
||||
'id' => $item->id,
|
||||
'order_id' => $item->order_id,
|
||||
'order_no' => $item->order_no,
|
||||
'order_date' => (string) $item->order_date,
|
||||
'order_status' => (int) $item->order_status,
|
||||
'product_name' => $item->product_name,
|
||||
'product_spec' => $item->product_spec,
|
||||
'unit' => $item->unit,
|
||||
'supplier_id' => (int) $item->supplier_id,
|
||||
'supplier' => $item->supplier ? ['id' => $item->supplier->id, 'name' => $item->supplier->name] : null,
|
||||
'price' => (string) $item->price,
|
||||
'cost_price' => (string) $item->cost_price,
|
||||
'quantity' => (int) $item->quantity,
|
||||
'weight' => (string) $item->weight,
|
||||
'amount' => (string) $item->amount,
|
||||
'remark' => (string) $item->remark,
|
||||
'image_ids' => $item->image_ids,
|
||||
'editable' => $purchase->status === PurchaseOrderModel::STATUS_PENDING
|
||||
&& in_array((int) $item->order_status, StoreOrderItemModel::ITEM_EDITABLE_STATUS, true),
|
||||
];
|
||||
}
|
||||
app(ItemImageResolver::class)->resolve($rows);
|
||||
|
||||
$store = StoreModel::withTrashed()->find($storeId);
|
||||
$product = ProductModel::withTrashed()->find($productId);
|
||||
|
||||
return $this->success([
|
||||
'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null,
|
||||
'product' => $product ? ['id' => $product->id, 'name' => $product->name] : null,
|
||||
'items' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细;
|
||||
* 商品行修改:品名/供应商/包规/单位/成本
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])]
|
||||
@@ -224,25 +270,77 @@ class PurchaseOrderController extends BaseController
|
||||
if (empty($purchase)) {
|
||||
throw new RepositoryException('采购单不存在');
|
||||
}
|
||||
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
|
||||
throw new RepositoryException('采购单已完成,不允许修改明细');
|
||||
}
|
||||
|
||||
$validated = $request->validated();
|
||||
|
||||
$attrs = [];
|
||||
foreach (['product_name', 'supplier_id', 'product_spec', 'unit', 'cost_price'] as $field) {
|
||||
if (isset($validated[$field])) {
|
||||
$attrs[$field] = $validated[$field];
|
||||
$items = StoreOrderItemModel::query()
|
||||
->where('purchase_id', $purchase->id)
|
||||
->where('product_id', $productId)
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
if ($items->isEmpty()) {
|
||||
throw new RepositoryException('该采购单下无此商品的订货明细');
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
$item->fill($validated)->save();
|
||||
}
|
||||
|
||||
// 重算采购单成本
|
||||
$query = StoreOrderItemModel::query()->where('purchase_id', $purchase->id);
|
||||
$purchase->total_quantity = $query->sum('quantity');
|
||||
$purchase->estimate_amount = $query->sum(DB::raw('quantity * cost_price'));
|
||||
$purchase->save();
|
||||
|
||||
return $this->success(['count' => $items->count()], '已同步 ' . $items->count() . ' 条订货明细');
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店订单明细修改(订货量、重量、单价),自动重算价格
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PutRoute(route: '/cell/{itemId}', authorize: 'update', where: ['itemId' => '[0-9]+'])]
|
||||
public function cellUpdate(int $itemId, PurchaseCellUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
return DB::transaction(function () use ($itemId, $validated) {
|
||||
$item = StoreOrderItemModel::query()->lockForUpdate()->find($itemId);
|
||||
if (empty($item) || (int) $item->purchase_id === 0) {
|
||||
throw new RepositoryException('采购单明细不存在');
|
||||
}
|
||||
$purchase = PurchaseOrderModel::find((int) $item->purchase_id);
|
||||
if (empty($purchase)) {
|
||||
throw new RepositoryException('采购单不存在');
|
||||
}
|
||||
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
|
||||
throw new RepositoryException('采购单已完成,不允许修改明细');
|
||||
}
|
||||
}
|
||||
if ($attrs === []) {
|
||||
throw new RepositoryException('品名/供应商/包规/单位/成本至少填写一项');
|
||||
}
|
||||
|
||||
$count = app(PurchaseEditService::class)->updateRow(
|
||||
$purchase,
|
||||
$productId,
|
||||
$attrs,
|
||||
(bool) ($validated['sync_product'] ?? false),
|
||||
);
|
||||
$item->quantity = $validated['quantity'];
|
||||
$item->price = $validated['price'];
|
||||
if (array_key_exists('weight', $validated)) {
|
||||
$item->weight = bcadd((string) $validated['weight'], '0', 3);
|
||||
}
|
||||
$item->amount = bcmul($validated['quantity'], $validated['price'], 3);
|
||||
$item->save();
|
||||
|
||||
return $this->success(['count' => $count], '已同步 ' . $count . ' 条订货明细');
|
||||
// 重算订单金额
|
||||
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
|
||||
$order->product_amount = $order->items()->sum('amount');
|
||||
$order->total_weight = $order->items()->sum('weight');
|
||||
$order->total_amount = bcadd($order->product_amount, $order->added_amount, 2);
|
||||
$order->save();
|
||||
|
||||
// 重算采购单重量
|
||||
$purchase->total_weight = StoreOrderItemModel::query()
|
||||
->where('purchase_id', $purchase->id)
|
||||
->sum('weight');
|
||||
$purchase->save();
|
||||
|
||||
return $this->success();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class PurchaseCellUpdateRequest extends BaseFormRequest
|
||||
{
|
||||
return [
|
||||
'quantity' => 'required|integer|min:0',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'weight' => 'nullable|numeric|min:0',
|
||||
];
|
||||
}
|
||||
@@ -25,6 +26,9 @@ class PurchaseCellUpdateRequest extends BaseFormRequest
|
||||
'quantity.required' => '数量不能为空',
|
||||
'quantity.integer' => '数量必须为整数',
|
||||
'quantity.min' => '数量不能小于 0',
|
||||
'price.required' => '单价不能为空',
|
||||
'price.numeric' => '单价必须为数字',
|
||||
'price.min' => '单价不能小于 0',
|
||||
'weight.numeric' => '实际称重必须为数字',
|
||||
'weight.min' => '实际称重不能小于 0',
|
||||
];
|
||||
|
||||
@@ -5,8 +5,7 @@ namespace App\Http\Requests\Purchase;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
/**
|
||||
* 采购明细商品行修改 验证(C4;品名/供应商/包规/单位/成本/称重至少一项,
|
||||
* 提交后一键同步该商品在本采购单下的全部订货明细)
|
||||
* 采购明细商品行修改
|
||||
*/
|
||||
class PurchaseRowUpdateRequest extends BaseFormRequest
|
||||
{
|
||||
@@ -15,12 +14,11 @@ class PurchaseRowUpdateRequest extends BaseFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'product_name' => 'nullable|string|max:100',
|
||||
'supplier_id' => 'nullable|integer|exists:supplier,id',
|
||||
'product_spec' => 'nullable|string|max:100',
|
||||
'unit' => 'nullable|string|max:20',
|
||||
'cost_price' => 'nullable|numeric|min:0',
|
||||
'sync_product' => 'nullable|boolean',
|
||||
'product_name' => 'required|string|max:100',
|
||||
'supplier_id' => 'required|integer|exists:supplier,id',
|
||||
'product_spec' => 'required|string|max:100',
|
||||
'unit' => 'required|string|max:20',
|
||||
'cost_price' => 'required|numeric|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -28,11 +26,20 @@ class PurchaseRowUpdateRequest extends BaseFormRequest
|
||||
{
|
||||
return [
|
||||
'product_name.max' => '品名不能超过 100 字',
|
||||
'product_name.required' => '品名不能为空',
|
||||
'product_name.string' => '品名必须为字符串',
|
||||
'supplier_id.supplier_id' => '供应商不能为空',
|
||||
'supplier_id.integer' => '供应商格式错误',
|
||||
'supplier_id.exists' => '供应商不存在',
|
||||
'product_spec.required' => '包规不能为空',
|
||||
'product_spec.string' => '包规必须为字符串',
|
||||
'product_spec.max' => '包规不能超过 100 字',
|
||||
'unit.required' => '单位不能为空',
|
||||
'unit.string' => '单位必须为字符串',
|
||||
'unit.max' => '单位不能超过 20 字',
|
||||
'cost_price.numeric' => '采购成本必须为数字',
|
||||
'cost_price.min' => '采购成本不能小于 0',
|
||||
'cost_price.required' => '成本不能为空',
|
||||
'cost_price.numeric' => '成本必须为数字',
|
||||
'cost_price.min' => '成本不能小于 0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,14 @@ class StoreOrderItemModel extends Model
|
||||
*/
|
||||
protected $hidden = ['cost_price'];
|
||||
|
||||
/** 明细可编辑状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
|
||||
public const array ITEM_EDITABLE_STATUS = [
|
||||
StoreOrderModel::STATUS_PENDING,
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
];
|
||||
|
||||
/**
|
||||
* 商品图片ID(逗号分隔字符串 ↔ 数组)
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,16 @@ class StoreOrderModel extends Model
|
||||
/** 状态:已取消 */
|
||||
public const int STATUS_CANCELLED = 9;
|
||||
|
||||
/** 状态中文名(后台文案/订单通知用) */
|
||||
public const array STATUS_NAMES = [
|
||||
self::STATUS_PENDING => '待接单',
|
||||
self::STATUS_SUMMARIZED => '已接单',
|
||||
self::STATUS_DELIVERING => '采购中',
|
||||
self::STATUS_DISTRIBUTION => '配送中',
|
||||
self::STATUS_COMPLETED => '已完成',
|
||||
self::STATUS_CANCELLED => '已取消',
|
||||
];
|
||||
|
||||
protected $table = 'store_order';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 商品快照首图解析:从明细 image_ids 批量解析文件 URL(门店订单列表/详情、采购单元格下钻共用)
|
||||
*/
|
||||
class ItemImageResolver
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items 明细数组(引用修改:写入 image,移除 image_ids)
|
||||
*/
|
||||
public function resolve(array &$items): void
|
||||
{
|
||||
$fileIds = [];
|
||||
foreach ($items as $line) {
|
||||
foreach ((array) ($line['image_ids'] ?? []) as $fileId) {
|
||||
if ($fileId !== '' && $fileId !== null) {
|
||||
$fileIds[] = (int) $fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileUrls = [];
|
||||
if ($fileIds !== []) {
|
||||
foreach (SysFileModel::query()->whereIn('id', array_unique($fileIds))->get() as $file) {
|
||||
$fileUrls[$file->id] = $file->preview_url;
|
||||
}
|
||||
}
|
||||
foreach ($items as &$product) {
|
||||
$product['image'] = '';
|
||||
foreach ((array) ($product['image_ids'] ?? []) as $fileId) {
|
||||
if ($fileId !== '' && $fileId !== null && isset($fileUrls[(int) $fileId])) {
|
||||
$product['image'] = $fileUrls[(int) $fileId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($product['image_ids']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
<?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;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 采购单数据修改
|
||||
*/
|
||||
class PurchaseEditService
|
||||
{
|
||||
|
||||
/**
|
||||
* 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细;
|
||||
* 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 bool $syncProduct
|
||||
* @return int
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function updateRow(PurchaseOrderModel $purchase, int $productId, array $attrs, bool $syncProduct = false): int
|
||||
{
|
||||
return DB::transaction(function () use ($purchase, $productId, $attrs, $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();
|
||||
}
|
||||
}
|
||||
|
||||
$this->recalculatePurchaseTotals($purchase);
|
||||
|
||||
return $items->count();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重算采购金额汇总
|
||||
*/
|
||||
public function recalculatePurchaseTotals(PurchaseOrderModel $purchase): void
|
||||
{
|
||||
$items = StoreOrderItemModel::query()
|
||||
->where('purchase_id', $purchase->id)
|
||||
->get(['quantity', 'cost_price']);
|
||||
|
||||
$totalQuantity = 0;
|
||||
$estimate = '0';
|
||||
foreach ($items as $item) {
|
||||
$totalQuantity += $item->quantity;
|
||||
$item_estimate = bcmul($item->cost_price, $item->quantity, 2);
|
||||
$estimate = bcadd($estimate, $item_estimate, 2);
|
||||
}
|
||||
|
||||
$purchase->total_quantity = $totalQuantity;
|
||||
$purchase->estimate_amount = $estimate;
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,9 @@ readonly class PurchaseGenerateService
|
||||
static fn (string $carry, $item): string => bcadd($carry, (string) $item->quantity, 2),
|
||||
'0'
|
||||
);
|
||||
// 预估金额 = Σ订货金额(明细金额 price×quantity),与采购单修改重算口径一致
|
||||
$estimateAmount = $items->reduce(
|
||||
static fn (string $carry, $item): string => bcadd($carry, bcmul($item->quantity, (string) $item->cost_price, 2), 2),
|
||||
static fn (string $carry, $item): string => bcadd($carry, (string) $item->amount, 2),
|
||||
'0'
|
||||
);
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ use Illuminate\Support\Facades\DB;
|
||||
* 流程(事务内,可重复 build:先清后建):
|
||||
* 1. 按周期 + 品类(含子孙分类)+ 供应商拉取「已完成」门店订单的订货明细
|
||||
* 2. 每条订货明细 → 一条对账明细:
|
||||
* publish_amount = 订货金额(order_item.amount)
|
||||
* actual_amount = 采购成本(称重>0 ? 称重×单价 : 数量×单价,单价 = 成本/包规)
|
||||
* publish_amount = 订货金额(order_item.amount = 每包等级价×数量)
|
||||
* actual_amount = 采购成本(数量 × 每包成本价;单价/包规不参与金额计算)
|
||||
* diff = publish − actual,冗余 product_name / store_id
|
||||
* 3. 汇总写回头的 publish/actual/diff_amount,status → 对账中
|
||||
*/
|
||||
@@ -64,15 +64,7 @@ class ReconciliationBuildService
|
||||
$now = now();
|
||||
foreach ($orderItems as $orderItem) {
|
||||
$publish = (string) $orderItem->amount;
|
||||
$unitCost = PurchaseEditService::unitCost(
|
||||
(string) ($orderItem->cost_price ?? '0'),
|
||||
(string) $orderItem->product_spec
|
||||
);
|
||||
$actual = PurchaseEditService::costAmount(
|
||||
(string) $orderItem->quantity,
|
||||
(string) $orderItem->weight,
|
||||
$unitCost
|
||||
);
|
||||
$actual = bcmul((string) $orderItem->quantity, (string) ($orderItem->cost_price ?? '0'), 2);
|
||||
$publishTotal = bcadd($publishTotal, $publish, 2);
|
||||
$actualTotal = bcadd($actualTotal, $actual, 2);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user