优化采购单
This commit is contained in:
@@ -11,6 +11,9 @@ use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
|
||||
/**
|
||||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑/同步、行级成本,
|
||||
@@ -52,6 +55,23 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
return [PurchaseOrderModel::first(), $product, [$storeA, $storeB]];
|
||||
}
|
||||
|
||||
/** 播种站点配置:周转框单价 2.00、周转托盘单价 10.00 */
|
||||
private function seedContainerConfig(): void
|
||||
{
|
||||
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']);
|
||||
foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) {
|
||||
SysSiteConfigItemsModel::create([
|
||||
'group_id' => $group->id,
|
||||
'key' => $key,
|
||||
'title' => $title,
|
||||
'type' => 'InputNumber',
|
||||
'values' => $value,
|
||||
'sort' => 0,
|
||||
]);
|
||||
}
|
||||
Cache::forget('site_config');
|
||||
}
|
||||
|
||||
/** 详情返回「商品行 × 门店列」矩阵:cells 按门店聚合数量 */
|
||||
public function test_detail_returns_store_matrix(): void
|
||||
{
|
||||
@@ -440,4 +460,148 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.editable', false);
|
||||
}
|
||||
|
||||
/** 详情返回周转框/托盘合并记录:按门店聚合全部订单(含底层订单明细与单价) */
|
||||
public function test_detail_returns_container_summary(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
|
||||
StoreOrderModel::where('store_id', $stores[0]->id)->update(['box_num' => 2, 'tray_num' => 1]);
|
||||
StoreOrderModel::where('store_id', $stores[1]->id)->update(['box_num' => 3, 'tray_num' => 0]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$containers = $this->getJson("/purchase/order/{$purchase->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->json('data.containers');
|
||||
$this->assertCount(2, $containers);
|
||||
$this->assertSame($stores[0]->id, $containers[0]['store_id'], '与门店列同序');
|
||||
|
||||
$row = $containers[0];
|
||||
$this->assertSame($stores[0]->name, $row['store_name']);
|
||||
$this->assertSame(2, $row['box_num']);
|
||||
$this->assertSame(1, $row['tray_num']);
|
||||
$this->assertSame('2.00', $row['box_price']);
|
||||
$this->assertSame('10.00', $row['tray_price']);
|
||||
$this->assertSame('14.00', $row['added_amount'], '2×2.00 + 1×10.00');
|
||||
$this->assertSame(1, $row['order_count']);
|
||||
$this->assertCount(1, $row['orders']);
|
||||
|
||||
$order = $row['orders'][0];
|
||||
$this->assertStringStartsWith('SO', $order['order_no']);
|
||||
$this->assertSame(2, $order['box_num']);
|
||||
$this->assertSame(1, $order['tray_num']);
|
||||
|
||||
$this->assertSame(3, $containers[1]['box_num']);
|
||||
$this->assertSame('6.00', $containers[1]['added_amount']);
|
||||
}
|
||||
|
||||
/** 合并记录修改:逐笔更新订单数量并重算附加金额与订单总金额,其他门店不受影响 */
|
||||
public function test_update_container_syncs_order_amounts(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->assertSame('20.00', (string) $order->product_amount, '下单时应写入商品金额');
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [['order_id' => $order->id, 'box_num' => 3, 'tray_num' => 1]],
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame(3, $order->box_num);
|
||||
$this->assertSame(1, $order->tray_num);
|
||||
$this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00');
|
||||
$this->assertSame('20.00', (string) $order->product_amount);
|
||||
$this->assertSame('36.00', (string) $order->total_amount, '20.00 + 16.00');
|
||||
|
||||
$other = StoreOrderModel::where('store_id', $stores[1]->id)->first();
|
||||
$this->assertSame(0, $other->box_num, '其他门店订单不受影响');
|
||||
$this->assertSame('30.00', (string) $other->total_amount);
|
||||
}
|
||||
|
||||
/** 采购单已完成:合并记录修改拒绝,数量与金额不变 */
|
||||
public function test_update_container_rejected_when_purchase_completed(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [['order_id' => $order->id, 'box_num' => 5, 'tray_num' => 5]],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '采购单已完成,不允许修改周转框/托盘');
|
||||
|
||||
$this->assertSame(0, $order->fresh()->box_num, '被拒绝后数量不变');
|
||||
}
|
||||
|
||||
/** 提交必须覆盖该门店全部订单;跨门店/跨采购单订单拒绝 */
|
||||
public function test_update_container_rejects_incomplete_or_foreign_orders(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 10.00]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
foreach ([1, 2] 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);
|
||||
$purchase = PurchaseOrderModel::first();
|
||||
$orders = StoreOrderModel::where('purchase_id', $purchase->id)->orderBy('id')->get();
|
||||
$this->assertCount(2, $orders, '同一门店两笔订单');
|
||||
|
||||
// 只提交一笔 → 缺少另一笔,整批拒绝
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$store->id}", [
|
||||
'orders' => [['order_id' => $orders[0]->id, 'box_num' => 1, 'tray_num' => 0]],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '提交不完整,缺少订单:' . $orders[1]->id);
|
||||
|
||||
// 混入其他门店订单(同一采购单另一门店)→ 拒绝
|
||||
$otherStore = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($otherStore->id)->create());
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
$foreign = StoreOrderModel::where('store_id', $otherStore->id)->first();
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$store->id}", [
|
||||
'orders' => [
|
||||
['order_id' => $orders[0]->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
['order_id' => $orders[1]->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
['order_id' => $foreign->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '包含不属于该采购单该门店的订单');
|
||||
|
||||
$this->assertSame(0, $orders[0]->fresh()->box_num, '被拒绝后数量不变');
|
||||
}
|
||||
|
||||
/** 数量为负/重复订单时校验失败 */
|
||||
public function test_update_container_validates_negative_and_duplicate_orders(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [['order_id' => $order->id, 'box_num' => -1, 'tray_num' => 0]],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '周转框数量不能小于 0');
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [
|
||||
['order_id' => $order->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
['order_id' => $order->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
],
|
||||
])->assertJsonPath('success', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* 造当日已接单订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品),下单后统一接单
|
||||
* 门店等级价 5.00(另设低等级价 4.00 不影响本店下单价)
|
||||
* 门店等级价 5.00(另设低等级价 4.00 不影响本店下单价),每包成本 6.00
|
||||
*/
|
||||
private function seedAcceptedOrders(): ProductModel
|
||||
{
|
||||
@@ -27,7 +27,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
$levelLow = CustomerLevelModel::factory()->create();
|
||||
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '6.00']);
|
||||
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]);
|
||||
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $levelLow->id, 'price' => 4.00]);
|
||||
|
||||
@@ -43,7 +43,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
return $product;
|
||||
}
|
||||
|
||||
/** 头部汇总取订货明细合计:total_quantity = Σ数量,estimate_amount = Σ订货金额 */
|
||||
/** 头部汇总取订货明细合计:total_quantity = Σ数量,estimate_amount = Σ数量×成本价(预估成本) */
|
||||
public function test_generate_aggregates_order_items_into_header(): void
|
||||
{
|
||||
$this->seedAcceptedOrders();
|
||||
@@ -61,7 +61,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
$this->assertSame(PurchaseOrderModel::STATUS_PENDING, $purchase->status);
|
||||
|
||||
$this->assertSame('10.00', (string) $purchase->total_quantity, '3+3+4');
|
||||
$this->assertSame('50.00', (string) $purchase->estimate_amount, '10 件 × 等级价 5.00');
|
||||
$this->assertSame('60.00', (string) $purchase->estimate_amount, '10 件 × 每包成本 6.00');
|
||||
$this->assertSame('0.00', (string) $purchase->actual_amount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@ use App\Models\ProductPriceModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
|
||||
/**
|
||||
* 小程序下单:等级价快照、服务端重算总价、取消限制、门店数据隔离
|
||||
@@ -133,23 +130,6 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
|
||||
}
|
||||
|
||||
/** 种子化周转框/托盘单价配置(框 2.00、托盘 10.00) */
|
||||
private function seedContainerConfig(): void
|
||||
{
|
||||
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']);
|
||||
foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) {
|
||||
SysSiteConfigItemsModel::create([
|
||||
'group_id' => $group->id,
|
||||
'key' => $key,
|
||||
'title' => $title,
|
||||
'type' => 'InputNumber',
|
||||
'values' => $value,
|
||||
'sort' => 0,
|
||||
]);
|
||||
}
|
||||
Cache::forget('site_config');
|
||||
}
|
||||
|
||||
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */
|
||||
private function makeOrderWithStatus(int $status): StoreOrderModel
|
||||
{
|
||||
@@ -163,94 +143,6 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
return $order;
|
||||
}
|
||||
|
||||
/** 修改周转框/托盘:自动重算附加金额与订单总金额 */
|
||||
public function test_container_update_recalculates_amounts(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$this->assertSame('10.00', (string) $order->product_amount, '下单时应写入商品金额');
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 3, 'tray_num' => 1])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame(3, $order->box_num);
|
||||
$this->assertSame(1, $order->tray_num);
|
||||
$this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00');
|
||||
$this->assertSame('10.00', (string) $order->product_amount);
|
||||
$this->assertSame('26.00', (string) $order->total_amount, '10.00 + 16.00');
|
||||
}
|
||||
|
||||
/** 已接单、采购中、配送中均可修改 */
|
||||
public function test_container_update_allowed_in_editable_statuses(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$this->actingAsSysUser();
|
||||
|
||||
foreach ([
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
] as $status) {
|
||||
$order->update(['status' => $status]);
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 1, 'tray_num' => 0])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
$this->assertSame('2.00', (string) $order->fresh()->added_amount);
|
||||
}
|
||||
}
|
||||
|
||||
/** 待接单、已完成、已取消不允许修改 */
|
||||
public function test_container_update_rejected_in_other_statuses(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||||
$this->actingAsSysUser();
|
||||
|
||||
foreach ([
|
||||
StoreOrderModel::STATUS_PENDING,
|
||||
StoreOrderModel::STATUS_COMPLETED,
|
||||
StoreOrderModel::STATUS_CANCELLED,
|
||||
] as $status) {
|
||||
$order->update(['status' => $status]);
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 5, 'tray_num' => 5])
|
||||
->assertOk()->assertJsonPath('success', false);
|
||||
}
|
||||
$order->refresh();
|
||||
$this->assertSame(0, $order->box_num, '被拒绝后数量不变');
|
||||
$this->assertSame('10.00', (string) $order->total_amount, '被拒绝后总金额不变');
|
||||
}
|
||||
|
||||
/** 历史订单未写商品金额时按「总额 - 附加」反推回写 */
|
||||
public function test_container_update_heals_legacy_product_amount(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$order->update(['product_amount' => 0]); // 模拟历史数据
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 2, 'tray_num' => 0])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame('10.00', (string) $order->product_amount, '反推回写商品金额');
|
||||
$this->assertSame('4.00', (string) $order->added_amount, '2×2.00');
|
||||
$this->assertSame('14.00', (string) $order->total_amount);
|
||||
}
|
||||
|
||||
/** 数量为负时校验失败 */
|
||||
public function test_container_update_validates_negative_numbers(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => -1, 'tray_num' => 0])
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '周转框数量不能小于 0');
|
||||
}
|
||||
|
||||
/** 软删除:仅已取消订单可由后台删除,删除后后台/小程序端均不可见 */
|
||||
public function test_soft_delete_only_cancelled_orders(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user