默认重量
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ use App\Models\StoreOrderItemModel;
|
|||||||
use App\Models\StoreOrderModel;
|
use App\Models\StoreOrderModel;
|
||||||
use App\Services\BillNumberService;
|
use App\Services\BillNumberService;
|
||||||
use App\Services\ItemImageResolver;
|
use App\Services\ItemImageResolver;
|
||||||
|
use App\Services\WeightEstimator;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -72,6 +73,7 @@ class OrderController extends BaseMiniController
|
|||||||
|
|
||||||
$totalQuantity = '0';
|
$totalQuantity = '0';
|
||||||
$totalAmount = '0';
|
$totalAmount = '0';
|
||||||
|
$totalWeight = '0';
|
||||||
$now = now();
|
$now = now();
|
||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($items as $row) {
|
foreach ($items as $row) {
|
||||||
@@ -85,8 +87,11 @@ class OrderController extends BaseMiniController
|
|||||||
$price = CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent);
|
$price = CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent);
|
||||||
$quantity = (string) $row['quantity'];
|
$quantity = (string) $row['quantity'];
|
||||||
$amount = bcmul($price, $quantity, 2);
|
$amount = bcmul($price, $quantity, 2);
|
||||||
|
// 参考重量 = 订货量 × 规格折算(仅作参考,实际称重以采购录入为准)
|
||||||
|
$weight = WeightEstimator::estimate((string) $product->spec, (string) $product->unit, $quantity);
|
||||||
$totalQuantity = bcadd($totalQuantity, $quantity, 2);
|
$totalQuantity = bcadd($totalQuantity, $quantity, 2);
|
||||||
$totalAmount = bcadd($totalAmount, $amount, 2);
|
$totalAmount = bcadd($totalAmount, $amount, 2);
|
||||||
|
$totalWeight = bcadd($totalWeight, $weight, 3);
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'store_id' => $store->id,
|
'store_id' => $store->id,
|
||||||
@@ -102,7 +107,7 @@ class OrderController extends BaseMiniController
|
|||||||
'content' => (string) $product->content,
|
'content' => (string) $product->content,
|
||||||
'shelf_life' => (int) $product->shelf_life,
|
'shelf_life' => (int) $product->shelf_life,
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
'weight' => 0,
|
'weight' => $weight,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
'cost_price' => (string) $product->cost_price,
|
'cost_price' => (string) $product->cost_price,
|
||||||
'remark' => '',
|
'remark' => '',
|
||||||
@@ -116,7 +121,7 @@ class OrderController extends BaseMiniController
|
|||||||
'store_id' => $store->id,
|
'store_id' => $store->id,
|
||||||
'order_date' => $now->toDateString(),
|
'order_date' => $now->toDateString(),
|
||||||
'total_quantity' => $totalQuantity,
|
'total_quantity' => $totalQuantity,
|
||||||
'total_weight' => 0,
|
'total_weight' => $totalWeight,
|
||||||
'total_amount' => $totalAmount,
|
'total_amount' => $totalAmount,
|
||||||
'status' => StoreOrderModel::STATUS_PENDING,
|
'status' => StoreOrderModel::STATUS_PENDING,
|
||||||
'remark' => $remark,
|
'remark' => $remark,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class ProductController extends BaseController
|
|||||||
$params,
|
$params,
|
||||||
ProductModel::query()->with(['category:id,name', 'supplier:id,name'])
|
ProductModel::query()->with(['category:id,name', 'supplier:id,name'])
|
||||||
)
|
)
|
||||||
->orderBy('sort', 'desc')
|
->orderBy('sort')
|
||||||
->orderBy('id', 'desc')
|
->orderBy('id', 'desc')
|
||||||
->paginate($pageSize);
|
->paginate($pageSize);
|
||||||
$data->getCollection()->makeVisible('cost_price');
|
$data->getCollection()->makeVisible('cost_price');
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use App\Services\BillGenerateService;
|
|||||||
use App\Services\ItemImageResolver;
|
use App\Services\ItemImageResolver;
|
||||||
use App\Services\PurchaseGenerateService;
|
use App\Services\PurchaseGenerateService;
|
||||||
use App\Services\PurchaseItemService;
|
use App\Services\PurchaseItemService;
|
||||||
|
use App\Services\WeightEstimator;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -481,7 +482,10 @@ class PurchaseOrderController extends BaseController
|
|||||||
'content' => (string) $product->content,
|
'content' => (string) $product->content,
|
||||||
'shelf_life' => (int) $product->shelf_life,
|
'shelf_life' => (int) $product->shelf_life,
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
'weight' => bcadd((string) ($validated['weight'] ?? 0), '0', 3),
|
// 未传称重时按订货量 × 规格预填参考重量
|
||||||
|
'weight' => isset($validated['weight'])
|
||||||
|
? bcadd((string) $validated['weight'], '0', 3)
|
||||||
|
: WeightEstimator::estimate((string) $product->spec, (string) $product->unit, (string) $quantity),
|
||||||
'amount' => bcmul($price, (string) $quantity, 2),
|
'amount' => bcmul($price, (string) $quantity, 2),
|
||||||
'cost_price' => (string) $product->cost_price,
|
'cost_price' => (string) $product->cost_price,
|
||||||
'remark' => '',
|
'remark' => '',
|
||||||
|
|||||||
@@ -63,13 +63,18 @@ readonly class PurchaseGenerateService
|
|||||||
static fn (string $carry, $item): string => bcadd($carry, bcmul($item->quantity, $item->cost_price, 2), 2),
|
static fn (string $carry, $item): string => bcadd($carry, bcmul($item->quantity, $item->cost_price, 2), 2),
|
||||||
'0'
|
'0'
|
||||||
);
|
);
|
||||||
|
// 参考总重量 = Σ 明细参考重量(下单时按订货量 × 规格预填)
|
||||||
|
$totalWeight = $items->reduce(
|
||||||
|
static fn (string $carry, $item): string => bcadd($carry, (string) $item->weight, 3),
|
||||||
|
'0'
|
||||||
|
);
|
||||||
|
|
||||||
$purchase = PurchaseOrderModel::create([
|
$purchase = PurchaseOrderModel::create([
|
||||||
'purchase_no' => $this->billNumberService->make('PO'),
|
'purchase_no' => $this->billNumberService->make('PO'),
|
||||||
'purchase_date' => $date,
|
'purchase_date' => $date,
|
||||||
'status' => PurchaseOrderModel::STATUS_PENDING,
|
'status' => PurchaseOrderModel::STATUS_PENDING,
|
||||||
'total_quantity' => $totalQuantity,
|
'total_quantity' => $totalQuantity,
|
||||||
'total_weight' => 0,
|
'total_weight' => $totalWeight,
|
||||||
'estimate_amount' => $estimateAmount,
|
'estimate_amount' => $estimateAmount,
|
||||||
'actual_amount' => 0,
|
'actual_amount' => 0,
|
||||||
'operator_id' => $operatorId,
|
'operator_id' => $operatorId,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考重量估算:订货量 × 单品规格折算(斤),仅作参考,实际称重以人工录入为准
|
||||||
|
*/
|
||||||
|
class WeightEstimator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 估算参考重量(斤,3 位小数)
|
||||||
|
*
|
||||||
|
* 计价单位本身是重量单位时订货量即重量;否则从规格(如「10斤/箱」「500g/袋」)
|
||||||
|
* 解析每件重量再乘订货量;规格无法解析时计 0
|
||||||
|
*/
|
||||||
|
public static function estimate(string $spec, string $unit, string $quantity): string
|
||||||
|
{
|
||||||
|
return bcmul($quantity === '' ? '0' : $quantity, self::perUnitJin($spec, $unit), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个计价单位折算重量(斤)
|
||||||
|
*/
|
||||||
|
private static function perUnitJin(string $spec, string $unit): string
|
||||||
|
{
|
||||||
|
// 按重量计价的单位:订货量本身就是重量
|
||||||
|
$unitWeight = self::toJin('1', $unit);
|
||||||
|
if ($unitWeight !== null) {
|
||||||
|
return $unitWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从规格解析每件重量(如「10斤/箱」「500g/袋」「1.5kg/箱」)
|
||||||
|
if (preg_match('/(\d+(?:\.\d+)?)\s*(公斤|千克|kg|克|斤|g)/iu', $spec, $matches) === 1) {
|
||||||
|
$parsed = self::toJin($matches[1], $matches[2]);
|
||||||
|
if ($parsed !== null) {
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规格中的裸数字按斤计(如「10/箱」);无法解析计 0
|
||||||
|
if (preg_match('/\d+(?:\.\d+)?/', $spec, $matches) === 1) {
|
||||||
|
return $matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数值按单位折算为斤;非重量单位返回 null(1斤=500g,1公斤/千克/kg=2斤)
|
||||||
|
*/
|
||||||
|
private static function toJin(string $value, string $unit): ?string
|
||||||
|
{
|
||||||
|
return match (mb_strtolower(trim($unit))) {
|
||||||
|
'斤' => $value,
|
||||||
|
'公斤', '千克', 'kg' => bcmul($value, '2', 3),
|
||||||
|
'克', 'g' => bcdiv($value, '500', 3),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -282,7 +282,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
|||||||
$this->assertSame('50.00', (string) $order->total_amount);
|
$this->assertSame('50.00', (string) $order->total_amount);
|
||||||
|
|
||||||
$purchase = $purchase->fresh();
|
$purchase = $purchase->fresh();
|
||||||
$this->assertSame('4.500', (string) $purchase->total_weight);
|
$this->assertSame('7.500', (string) $purchase->total_weight, '4.500 手动称重 + B 店参考重量 3.000');
|
||||||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)');
|
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
|||||||
$purchase = $purchase->fresh();
|
$purchase = $purchase->fresh();
|
||||||
$this->assertSame('9.00', (string) $purchase->total_quantity, '2+3+4');
|
$this->assertSame('9.00', (string) $purchase->total_quantity, '2+3+4');
|
||||||
$this->assertSame('82.00', (string) $purchase->estimate_amount, '50+32(4 包 × 成本 8.00)');
|
$this->assertSame('82.00', (string) $purchase->estimate_amount, '50+32(4 包 × 成本 8.00)');
|
||||||
$this->assertSame('1.500', (string) $purchase->total_weight);
|
$this->assertSame('6.500', (string) $purchase->total_weight, '原有参考重量 2+3 斤 + 新增 1.500');
|
||||||
|
|
||||||
// 重复添加同一商品 → 拒绝
|
// 重复添加同一商品 → 拒绝
|
||||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||||||
@@ -402,6 +402,28 @@ class PurchaseEditTest extends ProcurementTestCase
|
|||||||
->assertJsonPath('msg', '该商品已在此门店采购明细中,请直接修改数量');
|
->assertJsonPath('msg', '该商品已在此门店采购明细中,请直接修改数量');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情-新增单品未传称重:按订货量 × 规格预填参考重量并级联汇总 */
|
||||||
|
public function test_store_item_add_defaults_reference_weight(): void
|
||||||
|
{
|
||||||
|
[$purchase, , $stores] = $this->buildPurchase();
|
||||||
|
$extra = ProductModel::factory()->create([
|
||||||
|
'status' => ProductModel::STATUS_ON,
|
||||||
|
'cost_price' => 8,
|
||||||
|
'spec' => '25斤/袋',
|
||||||
|
'unit' => '袋',
|
||||||
|
]);
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||||||
|
'product_id' => $extra->id,
|
||||||
|
'quantity' => 2,
|
||||||
|
])->assertOk()->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
|
||||||
|
$this->assertSame('50.000', (string) $item->weight, '2 袋 × 25斤/袋');
|
||||||
|
$this->assertSame('55.000', (string) $purchase->fresh()->total_weight, '原有参考重量 5.000 + 新增 50.000');
|
||||||
|
}
|
||||||
|
|
||||||
/** 门店购买详情-修改单品:同商品多笔订单明细合并到最早一条,涉及订单逐一重算 */
|
/** 门店购买详情-修改单品:同商品多笔订单明细合并到最早一条,涉及订单逐一重算 */
|
||||||
public function test_store_item_update_merges_multi_order_rows(): void
|
public function test_store_item_update_merges_multi_order_rows(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -139,4 +139,30 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
|||||||
|
|
||||||
$this->assertSame(1, PurchaseOrderModel::count(), '第二次生成应被拒绝,不产生新采购单');
|
$this->assertSame(1, PurchaseOrderModel::count(), '第二次生成应被拒绝,不产生新采购单');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 采购单总重量 = 订货明细参考重量合计(下单时已按订货量 × 规格预填) */
|
||||||
|
public function test_generate_sums_reference_weight(): void
|
||||||
|
{
|
||||||
|
$level = CustomerLevelModel::factory()->create();
|
||||||
|
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||||
|
$product = ProductModel::factory()->create([
|
||||||
|
'status' => ProductModel::STATUS_ON,
|
||||||
|
'cost_price' => '6.00',
|
||||||
|
'spec' => '10斤/箱',
|
||||||
|
'unit' => '箱',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
foreach ([3, 4] as $qty) {
|
||||||
|
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
}
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$this->assertSame('70.000', (string) PurchaseOrderModel::first()->total_weight, '(3+4) 箱 × 10斤/箱');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,40 @@ class StoreOrderTest extends ProcurementTestCase
|
|||||||
$this->assertSame('10.00', (string) $order->total_amount, '应按等级价 5.00×2 计算,忽略前端金额');
|
$this->assertSame('10.00', (string) $order->total_amount, '应按等级价 5.00×2 计算,忽略前端金额');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 下单预填参考重量 = 订货量 × 规格折算(按重量计价的商品订货量即重量),订单总重量 = 明细合计 */
|
||||||
|
public function test_place_order_prefills_reference_weight(): void
|
||||||
|
{
|
||||||
|
$level = CustomerLevelModel::factory()->create();
|
||||||
|
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||||
|
$byBox = ProductModel::factory()->create([
|
||||||
|
'status' => ProductModel::STATUS_ON,
|
||||||
|
'cost_price' => '5.00',
|
||||||
|
'spec' => '10斤/箱',
|
||||||
|
'unit' => '箱',
|
||||||
|
]);
|
||||||
|
$byJin = ProductModel::factory()->create([
|
||||||
|
'status' => ProductModel::STATUS_ON,
|
||||||
|
'cost_price' => '5.00',
|
||||||
|
'spec' => '散装',
|
||||||
|
'unit' => '斤',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
$this->postJson('/mini/order', [
|
||||||
|
'items' => [
|
||||||
|
['product_id' => $byBox->id, 'quantity' => 3],
|
||||||
|
['product_id' => $byJin->id, 'quantity' => 5],
|
||||||
|
],
|
||||||
|
])->assertOk()->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||||||
|
$boxItem = $order->items->firstWhere('product_id', $byBox->id);
|
||||||
|
$jinItem = $order->items->firstWhere('product_id', $byJin->id);
|
||||||
|
$this->assertSame('30.000', (string) $boxItem->weight, '3 箱 × 10斤/箱');
|
||||||
|
$this->assertSame('5.000', (string) $jinItem->weight, '按斤计价:订货量即重量');
|
||||||
|
$this->assertSame('35.000', (string) $order->total_weight, '订单总重量 = 明细参考重量合计');
|
||||||
|
}
|
||||||
|
|
||||||
/** 门店绑定的客户等级被删除时拒绝下单 */
|
/** 门店绑定的客户等级被删除时拒绝下单 */
|
||||||
public function test_store_level_missing_rejected(): void
|
public function test_store_level_missing_rejected(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Services\WeightEstimator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考重量估算:重量单位直算、规格解析折算、无法解析兜底(单位:斤)
|
||||||
|
*/
|
||||||
|
class WeightEstimatorTest extends TestCase
|
||||||
|
{
|
||||||
|
/** 计价单位本身是重量单位:订货量即重量(忽略规格) */
|
||||||
|
public function test_weight_unit_quantity_is_weight(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('5.000', WeightEstimator::estimate('10斤/箱', '斤', '5'));
|
||||||
|
$this->assertSame('6.000', WeightEstimator::estimate('', '公斤', '3'));
|
||||||
|
$this->assertSame('6.000', WeightEstimator::estimate('', 'KG', '3'));
|
||||||
|
$this->assertSame('1.000', WeightEstimator::estimate('', '克', '500'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规格解析每件重量 × 订货量 */
|
||||||
|
public function test_spec_per_unit_weight(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('30.000', WeightEstimator::estimate('10斤/箱', '箱', '3'));
|
||||||
|
$this->assertSame('75.000', WeightEstimator::estimate('25斤/袋', '袋', '3'));
|
||||||
|
$this->assertSame('3.000', WeightEstimator::estimate('500g/袋', '袋', '3'));
|
||||||
|
$this->assertSame('3.000', WeightEstimator::estimate('500克/袋', '袋', '3'));
|
||||||
|
$this->assertSame('9.000', WeightEstimator::estimate('1.5kg/箱', '箱', '3'));
|
||||||
|
$this->assertSame('12.000', WeightEstimator::estimate('2公斤/筐', '筐', '3'));
|
||||||
|
$this->assertSame('15.000', WeightEstimator::estimate('10斤/箱', '箱', '1.5'), '订货量可为小数');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规格无法解析:裸数字按斤计,无数字计 0 */
|
||||||
|
public function test_unparsable_spec_fallback(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('20.000', WeightEstimator::estimate('10/箱', '箱', '2'), '裸数字按斤计');
|
||||||
|
$this->assertSame('0.000', WeightEstimator::estimate('散装', '箱', '3'));
|
||||||
|
$this->assertSame('0.000', WeightEstimator::estimate('', '箱', '3'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user