采购单优化
This commit is contained in:
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Exports\PurchaseOrderExport;
|
||||
use App\Exports\PurchaseStoreExport;
|
||||
use App\Exports\PurchaseSupplierExport;
|
||||
use App\Models\CustomerLevelModel;
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
/**
|
||||
* 采购单导出(仅 Excel 表格,全品类):xlsx Content-Type /
|
||||
@@ -85,4 +90,259 @@ class ExportTest extends ProcurementTestCase
|
||||
$response = $this->get("/purchase/order/{$purchase->id}/export");
|
||||
$this->assertFalse($response->json('success'), '缺少权限点应被拦截');
|
||||
}
|
||||
|
||||
/**
|
||||
* 造含双门店/双供应商的采购单:
|
||||
* 门店A 订 蔬菜2件(供应商甲)+ 肉1件(供应商乙);门店B 订 蔬菜3件(供应商甲)
|
||||
*
|
||||
* @return array{0: PurchaseOrderModel, 1: ProductModel, 2: ProductModel, 3: StoreModel, 4: StoreModel, 5: SupplierModel, 6: SupplierModel}
|
||||
*/
|
||||
private function buildPurchaseWithSuppliers(): array
|
||||
{
|
||||
$supplierA = SupplierModel::factory()->create();
|
||||
$supplierB = SupplierModel::factory()->create();
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$veg = ProductModel::factory()->create([
|
||||
'status' => ProductModel::STATUS_ON,
|
||||
'cost_price' => '5.00',
|
||||
'spec' => '10斤/箱',
|
||||
'supplier_id' => $supplierA->id,
|
||||
]);
|
||||
$meat = ProductModel::factory()->create([
|
||||
'status' => ProductModel::STATUS_ON,
|
||||
'cost_price' => '20.00',
|
||||
'spec' => '20斤/箱',
|
||||
'supplier_id' => $supplierB->id,
|
||||
]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeA->id)->create());
|
||||
$this->postJson('/mini/order', ['items' => [
|
||||
['product_id' => $veg->id, 'quantity' => 2],
|
||||
['product_id' => $meat->id, 'quantity' => 1],
|
||||
]])->assertJsonPath('success', true);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
|
||||
$this->postJson('/mini/order', ['items' => [
|
||||
['product_id' => $veg->id, 'quantity' => 3],
|
||||
]])->assertJsonPath('success', true);
|
||||
|
||||
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
return [PurchaseOrderModel::first(), $veg, $meat, $storeA, $storeB, $supplierA, $supplierB];
|
||||
}
|
||||
|
||||
/** 商品明细导出:含系统全部商品行(无订货数量 0)+ 行列合计 + 参考零售价列 */
|
||||
public function test_export_lists_all_catalog_products_with_totals_row(): void
|
||||
{
|
||||
[$purchase, $veg, $meat, $storeA, $storeB] = $this->buildPurchaseWithSuppliers();
|
||||
// 无订货的上架商品也应出现在导出中
|
||||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '7.00']);
|
||||
|
||||
Excel::fake();
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/export")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
$purchase->purchase_no . '_采购单.xlsx',
|
||||
static function (PurchaseOrderExport $export) use ($veg, $meat, $extra, $storeA, $storeB): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 标题1 + 范围1 + 空行1 + 列头1 + 商品3 + 合计1 = 8 行
|
||||
if ($rows->count() !== 8) {
|
||||
return false;
|
||||
}
|
||||
// 列头含参考零售价与门店列
|
||||
$header = $rows[3];
|
||||
if ($header[7] !== '参考零售价' || $header[11] !== $storeA->name || $header[12] !== $storeB->name) {
|
||||
return false;
|
||||
}
|
||||
// 蔬菜行:数量 2+3=5、金额 25、参考零售价 5÷10=0.50、门店列 2/3
|
||||
$vegRow = $rows->firstWhere(2, $veg->name);
|
||||
if ($vegRow === null
|
||||
|| (float) $vegRow[8] !== 5.0 || (float) $vegRow[10] !== 25.0
|
||||
|| (float) $vegRow[7] !== 0.5
|
||||
|| (float) $vegRow[11] !== 2.0 || (float) $vegRow[12] !== 3.0) {
|
||||
return false;
|
||||
}
|
||||
// 无订货商品行:数量 0、参考零售价留空、门店列 0
|
||||
$extraRow = $rows->firstWhere(2, $extra->name);
|
||||
if ($extraRow === null
|
||||
|| (float) $extraRow[8] !== 0.0 || $extraRow[7] !== ''
|
||||
|| (float) $extraRow[11] !== 0.0) {
|
||||
return false;
|
||||
}
|
||||
// 合计行:总数量 6、总金额 45、门店列合计 3/3
|
||||
$total = $rows->last();
|
||||
if ($total[2] !== '合计') {
|
||||
return false;
|
||||
}
|
||||
return (float) $total[8] === 6.0 && (float) $total[10] === 45.0
|
||||
&& (float) $total[11] === 3.0 && (float) $total[12] === 3.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 商品明细导出:按供应商筛选(范围行标注供应商,仅含其商品行) */
|
||||
public function test_export_supplier_filter(): void
|
||||
{
|
||||
[$purchase, $veg, $meat, , , $supplierA] = $this->buildPurchaseWithSuppliers();
|
||||
|
||||
Excel::fake();
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/export?supplier_id={$supplierA->id}")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
$purchase->purchase_no . '_采购单_' . $supplierA->name . '.xlsx',
|
||||
static function (PurchaseOrderExport $export) use ($veg, $meat, $supplierA): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 范围行标注供应商
|
||||
if (! str_contains((string) $rows[1][0], $supplierA->name)) {
|
||||
return false;
|
||||
}
|
||||
// 数据行(去头尾)只含供应商甲的蔬菜,不含供应商乙的肉
|
||||
$names = $rows->slice(4, -1)->map(static fn ($row) => $row[2])->all();
|
||||
return in_array($veg->name, $names, true) && ! in_array($meat->name, $names, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 门店购买详情导出:多门店合并一个 XLSX(工作表名=门店名),含合计行 */
|
||||
public function test_export_stores_multi_sheet(): void
|
||||
{
|
||||
[$purchase, , , $storeA, $storeB] = $this->buildPurchaseWithSuppliers();
|
||||
|
||||
Excel::fake();
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/exportStores")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
$purchase->purchase_no . '_门店购买详情.xlsx',
|
||||
static function (PurchaseStoreExport $export) use ($storeA, $storeB): bool {
|
||||
$sheets = $export->sheets();
|
||||
if (count($sheets) !== 2) {
|
||||
return false;
|
||||
}
|
||||
$titles = array_map(static fn ($sheet) => $sheet->title(), $sheets);
|
||||
if ($titles !== [$storeA->name, $storeB->name]) {
|
||||
return false;
|
||||
}
|
||||
// 门店A 工作表:标题/空行/列头/明细2/合计 = 6 行
|
||||
$rows = $sheets[0]->collection()->values();
|
||||
if ($rows->count() !== 6) {
|
||||
return false;
|
||||
}
|
||||
if ($rows[2] !== ['序号', '品名', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']) {
|
||||
return false;
|
||||
}
|
||||
// 合计:数量 2+1=3,金额 10+20=30
|
||||
$total = $rows->last();
|
||||
return $total[1] === '合计' && (int) $total[5] === 3 && (float) $total[7] === 30.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 门店购买详情导出:单门店导出 + 无明细门店拒绝 */
|
||||
public function test_export_stores_single_store(): void
|
||||
{
|
||||
[$purchase, , , , $storeB] = $this->buildPurchaseWithSuppliers();
|
||||
|
||||
Excel::fake();
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/exportStores?store_id={$storeB->id}")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
$purchase->purchase_no . '_门店购买详情_' . $storeB->name . '.xlsx',
|
||||
static function (PurchaseStoreExport $export) use ($storeB): bool {
|
||||
$sheets = $export->sheets();
|
||||
return count($sheets) === 1 && $sheets[0]->title() === $storeB->name;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 门店购买详情导出:门店无明细 → 拒绝(真实执行 sheets() 才触发空校验,不走 fake) */
|
||||
public function test_export_stores_without_items_rejected(): void
|
||||
{
|
||||
[$purchase] = $this->buildPurchaseWithSuppliers();
|
||||
$otherStore = StoreModel::factory()->create();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/exportStores?store_id={$otherStore->id}")
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '该门店在此采购单中无采购商品');
|
||||
}
|
||||
|
||||
/** 供应商采购明细导出:多供应商合并一个 XLSX(工作表名=供应商名),按供应商聚合 */
|
||||
public function test_export_suppliers_multi_sheet(): void
|
||||
{
|
||||
[$purchase, $veg, $meat, , , $supplierA, $supplierB] = $this->buildPurchaseWithSuppliers();
|
||||
|
||||
Excel::fake();
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/exportSuppliers")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
$purchase->purchase_no . '_供应商采购明细.xlsx',
|
||||
static function (PurchaseSupplierExport $export) use ($supplierA, $supplierB, $veg, $meat): bool {
|
||||
$sheets = $export->sheets();
|
||||
if (count($sheets) !== 2) {
|
||||
return false;
|
||||
}
|
||||
$titles = array_map(static fn ($sheet) => $sheet->title(), $sheets);
|
||||
if ($titles !== [$supplierA->name, $supplierB->name]) {
|
||||
return false;
|
||||
}
|
||||
// 供应商甲:蔬菜 2+3=5 件、金额 5×5=25
|
||||
$rowsA = $sheets[0]->collection()->values();
|
||||
if ($rowsA[2] !== ['序号', '品名', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']) {
|
||||
return false;
|
||||
}
|
||||
$vegRow = $rowsA->firstWhere(1, $veg->name);
|
||||
if ($vegRow === null || (int) $vegRow[5] !== 5 || (float) $vegRow[7] !== 25.0) {
|
||||
return false;
|
||||
}
|
||||
$totalA = $rowsA->last();
|
||||
if ($totalA[1] !== '合计' || (int) $totalA[5] !== 5 || (float) $totalA[7] !== 25.0) {
|
||||
return false;
|
||||
}
|
||||
// 供应商乙:肉 1 件、金额 20
|
||||
$rowsB = $sheets[1]->collection()->values();
|
||||
$meatRow = $rowsB->firstWhere(1, $meat->name);
|
||||
return $meatRow !== null && (int) $meatRow[5] === 1 && (float) $meatRow[7] === 20.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 供应商采购明细导出:单供应商导出 + 无明细供应商拒绝 */
|
||||
public function test_export_suppliers_single(): void
|
||||
{
|
||||
[$purchase, , , , , , $supplierB] = $this->buildPurchaseWithSuppliers();
|
||||
|
||||
Excel::fake();
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/exportSuppliers?supplier_id={$supplierB->id}")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
$purchase->purchase_no . '_供应商采购明细_' . $supplierB->name . '.xlsx',
|
||||
static function (PurchaseSupplierExport $export) use ($supplierB): bool {
|
||||
$sheets = $export->sheets();
|
||||
return count($sheets) === 1 && $sheets[0]->title() === $supplierB->name;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 供应商采购明细导出:供应商无明细 → 拒绝(真实执行 sheets() 才触发空校验,不走 fake) */
|
||||
public function test_export_suppliers_without_items_rejected(): void
|
||||
{
|
||||
[$purchase] = $this->buildPurchaseWithSuppliers();
|
||||
$otherSupplier = SupplierModel::factory()->create();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/purchase/order/{$purchase->id}/exportSuppliers?supplier_id={$otherSupplier->id}")
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '该供应商在此采购单中无采购明细');
|
||||
}
|
||||
}
|
||||
|
||||
+180
-103
@@ -12,8 +12,9 @@ use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑/同步、行级成本,
|
||||
* 修改同步订货明细并重算订单/采购单汇总(金额口径:数量(包) × 每包价格,单价/包规不参与金额计算)
|
||||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑、行级修改同步商品档案、
|
||||
* 门店购买详情单品增删改,修改后重算订单/采购单汇总
|
||||
* (金额口径:订单=数量×单价;采购单预估成本=Σ数量×每包成本价)
|
||||
*/
|
||||
class PurchaseEditTest extends ProcurementTestCase
|
||||
{
|
||||
@@ -69,6 +70,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->assertSame('10斤/箱', $row['product_spec']);
|
||||
$this->assertSame('斤', $row['unit']);
|
||||
$this->assertSame('10.00', (string) $row['cost_price']);
|
||||
$this->assertSame('50.00', (string) $row['amount'], '订货金额 = Σ明细 amount(5 × 10.00)');
|
||||
$this->assertSame(5, $row['quantity'], '2+3');
|
||||
|
||||
$cells = $row['cells'];
|
||||
@@ -76,16 +78,15 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->assertSame(3, $cells[$stores[1]->id]);
|
||||
}
|
||||
|
||||
/** 门店单元格数量修改 → 明细金额重算(数量×单价),订单与采购单汇总同步 */
|
||||
/** 门店单元格数量/单价修改 → 明细金额重算(数量×单价),订单与采购单汇总同步 */
|
||||
public function test_update_cell_syncs_order_item_and_totals(): void
|
||||
{
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5])
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonPath('data.amount', 50);
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
$item = $item->fresh();
|
||||
$this->assertSame(5, $item->quantity);
|
||||
@@ -93,45 +94,59 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
|
||||
$order = StoreOrderModel::find($item->order_id);
|
||||
$this->assertSame(5, $order->total_quantity);
|
||||
$this->assertSame('50.00', (string) $order->product_amount);
|
||||
$this->assertSame('50.00', (string) $order->total_amount);
|
||||
|
||||
$purchase = $purchase->fresh();
|
||||
$this->assertSame('8.00', (string) $purchase->total_quantity, '5+3');
|
||||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '50+30');
|
||||
$this->assertSame('80.00', (string) $purchase->actual_amount, '8 包 × 每包成本 10.00');
|
||||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00');
|
||||
}
|
||||
|
||||
/** 行级成本修改 → 同步该商品全部订货明细,采购单实际金额按 数量×每包成本 重算 */
|
||||
/** 行级成本修改 → 同步该商品全部订货明细与商品档案,采购单预估成本重算 */
|
||||
public function test_update_row_cost_syncs_all_order_items(): void
|
||||
{
|
||||
[$purchase, $product] = $this->buildPurchase();
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30])
|
||||
->assertJsonPath('success', true)
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||||
'product_name' => $product->name,
|
||||
'supplier_id' => SupplierModel::factory()->create()->id,
|
||||
'product_spec' => $product->spec,
|
||||
'unit' => $product->unit,
|
||||
'cost_price' => 30,
|
||||
])->assertJsonPath('success', true)
|
||||
->assertJsonPath('data.count', 2);
|
||||
|
||||
$this->assertSame(2, StoreOrderItemModel::where('cost_price', '30.00')->count());
|
||||
$this->assertSame('30.00', (string) $product->fresh()->cost_price, '商品档案成本价应同步更新');
|
||||
|
||||
$purchase = $purchase->fresh();
|
||||
$this->assertSame('150.00', (string) $purchase->actual_amount, '5 包 × 每包成本 30.00');
|
||||
$this->assertSame('50.00', (string) $purchase->estimate_amount, '订货金额不受成本修改影响');
|
||||
$this->assertSame('150.00', (string) $purchase->estimate_amount, '5 包 × 每包成本 30.00');
|
||||
}
|
||||
|
||||
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细(重量为手动录入,不受行修改影响) */
|
||||
/** 行级属性修改不影响称重(重量为手动录入,仅作参考) */
|
||||
public function test_update_row_does_not_touch_manual_weight(): void
|
||||
{
|
||||
[$purchase, $product] = $this->buildPurchase();
|
||||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30])
|
||||
// 先录入一笔手动称重
|
||||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 2, 'price' => 10, 'weight' => 4.5])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame(0, StoreOrderItemModel::where('weight', '<>', 0)->count(), '重量保持手动录入值,不自动计算');
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||||
'product_name' => $product->name,
|
||||
'supplier_id' => $supplier->id,
|
||||
'product_spec' => $product->spec,
|
||||
'unit' => $product->unit,
|
||||
'cost_price' => 30,
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame('4.500', (string) $item->fresh()->weight, '重量保持手动录入值,不受行修改影响');
|
||||
}
|
||||
|
||||
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细 */
|
||||
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细与商品档案 */
|
||||
public function test_update_row_syncs_product_attributes(): void
|
||||
{
|
||||
[$purchase, $product] = $this->buildPurchase();
|
||||
@@ -157,29 +172,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->assertSame('25.00', (string) $item->cost_price);
|
||||
}
|
||||
|
||||
// 未传 sync_product:商品档案保持原值
|
||||
$product = $product->fresh();
|
||||
$this->assertNotSame('优选土豆', $product->name);
|
||||
|
||||
$this->assertSame('125.00', (string) $purchase->fresh()->actual_amount, '5 包 × 每包成本 25.00');
|
||||
}
|
||||
|
||||
/** sync_product=true:订货明细与商品档案同步更新;软删除商品拒绝 */
|
||||
public function test_update_row_syncs_to_product_catalog(): void
|
||||
{
|
||||
[$purchase, $product] = $this->buildPurchase();
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||||
'product_name' => '优选土豆',
|
||||
'supplier_id' => $supplier->id,
|
||||
'product_spec' => '5斤/袋',
|
||||
'unit' => '袋',
|
||||
'cost_price' => 25,
|
||||
'sync_product' => true,
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
// 商品档案同步保存(商品列表可见)
|
||||
$product = $product->fresh();
|
||||
$this->assertSame('优选土豆', $product->name);
|
||||
$this->assertSame($supplier->id, $product->supplier_id);
|
||||
@@ -187,16 +180,33 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->assertSame('袋', $product->unit);
|
||||
$this->assertSame('25.00', (string) $product->cost_price);
|
||||
|
||||
// 软删除商品 → 拒绝并回滚(订货明细不同步)
|
||||
$product->delete();
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||||
'product_name' => '再次改名',
|
||||
'sync_product' => true,
|
||||
])->assertJsonPath('success', false);
|
||||
$this->assertSame('优选土豆', StoreOrderItemModel::first()->product_name, '回滚后明细保持原值');
|
||||
$this->assertSame('125.00', (string) $purchase->fresh()->estimate_amount, '5 包 × 每包成本 25.00');
|
||||
}
|
||||
|
||||
/** 行级修改:属性与称重至少一项 */
|
||||
/** 行级修改:商品已软删除时明细照常更新、跳过档案同步 */
|
||||
public function test_update_row_skips_catalog_sync_when_product_deleted(): void
|
||||
{
|
||||
[$purchase, $product] = $this->buildPurchase();
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$product->delete(); // 软删除
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||||
'product_name' => '已删商品新名',
|
||||
'supplier_id' => $supplier->id,
|
||||
'product_spec' => '5斤/袋',
|
||||
'unit' => '袋',
|
||||
'cost_price' => 25,
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonPath('msg', '已同步 2 条订货明细;商品已删除,档案未同步');
|
||||
|
||||
$this->assertSame('已删商品新名', StoreOrderItemModel::first()->product_name, '订货明细照常更新');
|
||||
$this->assertNotSame('已删商品新名', $product->fresh()->name, '商品档案不同步');
|
||||
}
|
||||
|
||||
/** 行级修改:必填字段校验(空 payload 拒绝) */
|
||||
public function test_update_row_requires_at_least_one_field(): void
|
||||
{
|
||||
[$purchase, $product] = $this->buildPurchase();
|
||||
@@ -213,11 +223,11 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => -1])
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => -1, 'price' => 10])
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 单元格下钻:返回该门店该商品的全部订货明细(订单号/状态/快照/可编辑标记),其他门店无明细 */
|
||||
/** 单元格下钻:返回该门店该商品的全部订货明细(订单号/状态/快照),其他门店无明细 */
|
||||
public function test_cell_detail_returns_items_with_order_info(): void
|
||||
{
|
||||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||||
@@ -237,7 +247,6 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->assertSame(2, $item['quantity']);
|
||||
$this->assertSame('20.00', (string) $item['amount']);
|
||||
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $item['order_status'], '生成采购单后源订单为采购中');
|
||||
$this->assertTrue($item['editable']);
|
||||
|
||||
// 其他门店 → 另一条明细
|
||||
$this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[1]->id}")
|
||||
@@ -262,9 +271,8 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'weight' => 4.5])
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonPath('data.amount', 50);
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10, 'weight' => 4.5])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
$item = $item->fresh();
|
||||
$this->assertSame('4.500', (string) $item->weight);
|
||||
@@ -272,60 +280,120 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
|
||||
$order = StoreOrderModel::find($item->order_id);
|
||||
$this->assertSame('4.500', (string) $order->total_weight);
|
||||
$this->assertSame('50.00', (string) $order->product_amount);
|
||||
$this->assertSame('50.00', (string) $order->total_amount);
|
||||
|
||||
$purchase = $purchase->fresh();
|
||||
$this->assertSame('4.500', (string) $purchase->total_weight);
|
||||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '50+30 订货金额');
|
||||
$this->assertSame('80.00', (string) $purchase->actual_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)');
|
||||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)');
|
||||
}
|
||||
|
||||
/** 单元格一键同步:按商品ID同步档案 + 等级上浮价重算,订货单与采购单汇总级联 */
|
||||
public function test_cell_sync_refreshes_snapshot_and_cascades(): void
|
||||
/** 门店购买详情-新增单品:挂靠最新一笔订单,汇总级联重算;重复添加拒绝 */
|
||||
public function test_store_item_add_creates_item_and_recalculates(): void
|
||||
{
|
||||
// 等级上浮 30%:下单时成本 10.00 → 售价 13.00
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create([
|
||||
'status' => ProductModel::STATUS_ON,
|
||||
'cost_price' => '10.00',
|
||||
'spec' => '1斤/袋',
|
||||
'unit' => '斤',
|
||||
]);
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 8]);
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
|
||||
->assertJsonPath('success', true);
|
||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||||
'product_id' => $extra->id,
|
||||
'quantity' => 4,
|
||||
'weight' => 1.5,
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
|
||||
$this->assertNotNull($item);
|
||||
$this->assertSame($stores[0]->id, $item->store_id);
|
||||
$this->assertSame($purchase->id, $item->purchase_id);
|
||||
$this->assertSame('8.00', (string) $item->price, '等级上浮 0% → 单价=成本价');
|
||||
$this->assertSame('32.00', (string) $item->amount, '8.00 × 4');
|
||||
$this->assertSame('1.500', (string) $item->weight);
|
||||
|
||||
$order = StoreOrderModel::find($item->order_id);
|
||||
$this->assertSame(6, $order->total_quantity, '2+4');
|
||||
$this->assertSame('52.00', (string) $order->total_amount, '20+32');
|
||||
|
||||
$purchase = $purchase->fresh();
|
||||
$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('1.500', (string) $purchase->total_weight);
|
||||
|
||||
// 重复添加同一商品 → 拒绝
|
||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||||
'product_id' => $extra->id,
|
||||
'quantity' => 1,
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '该商品已在此门店采购明细中,请直接修改数量');
|
||||
}
|
||||
|
||||
/** 门店购买详情-修改单品:同商品多笔订单明细合并到最早一条,涉及订单逐一重算 */
|
||||
public function test_store_item_update_merges_multi_order_rows(): void
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
|
||||
|
||||
// 同一门店两笔订单各订 2 件(同商品)→ 同一采购单
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
$this->actingAsMiniUser($user);
|
||||
foreach ([2, 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();
|
||||
|
||||
$this->assertSame(2, StoreOrderItemModel::count(), '两笔订单各一条明细');
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||||
'quantity' => 10,
|
||||
'price' => 12,
|
||||
'weight' => 5,
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame(1, StoreOrderItemModel::count(), '多笔明细合并为一条');
|
||||
$item = StoreOrderItemModel::first();
|
||||
$this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%');
|
||||
$this->assertSame('26.00', (string) $purchase->estimate_amount);
|
||||
$this->assertSame(10, $item->quantity);
|
||||
$this->assertSame('12.00', (string) $item->price);
|
||||
$this->assertSame('120.00', (string) $item->amount);
|
||||
$this->assertSame('5.000', (string) $item->weight);
|
||||
|
||||
// 成本上调后同步:单价按最新成本重算,金额与两级汇总级联
|
||||
$product->update(['cost_price' => '20.00']);
|
||||
|
||||
$this->putJson("/purchase/order/cell/{$item->id}/sync")
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonPath('data.amount', 52);
|
||||
|
||||
$item->refresh();
|
||||
$this->assertSame('20.00', (string) $item->cost_price);
|
||||
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
|
||||
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
|
||||
|
||||
$order = StoreOrderModel::first();
|
||||
$this->assertSame('52.00', (string) $order->product_amount);
|
||||
$this->assertSame('52.00', (string) $order->total_amount);
|
||||
// 保留行所在订单重算;被合并订单清零
|
||||
$survivedOrder = StoreOrderModel::find($item->order_id);
|
||||
$this->assertSame(10, $survivedOrder->total_quantity);
|
||||
$this->assertSame('120.00', (string) $survivedOrder->total_amount);
|
||||
$mergedOrder = StoreOrderModel::where('id', '<>', $item->order_id)->first();
|
||||
$this->assertSame(0, $mergedOrder->total_quantity);
|
||||
$this->assertSame('0.00', (string) $mergedOrder->total_amount);
|
||||
|
||||
$purchase = $purchase->fresh();
|
||||
$this->assertSame('52.00', (string) $purchase->estimate_amount);
|
||||
$this->assertSame('40.00', (string) $purchase->actual_amount, '2 包 × 每包成本 20.00');
|
||||
$this->assertSame('10.00', (string) $purchase->total_quantity);
|
||||
$this->assertSame('100.00', (string) $purchase->estimate_amount, '10 包 × 成本 10.00');
|
||||
$this->assertSame('5.000', (string) $purchase->total_weight);
|
||||
}
|
||||
|
||||
/** 门店购买详情-移除单品:明细删除,订单与采购单汇总级联重算 */
|
||||
public function test_store_item_remove_deletes_and_recalculates(): void
|
||||
{
|
||||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}")
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame(0, StoreOrderItemModel::where('store_id', $stores[0]->id)->count());
|
||||
$this->assertSame(1, StoreOrderItemModel::count(), '门店B 明细不受影响');
|
||||
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->assertSame(0, $order->total_quantity);
|
||||
$this->assertSame('0.00', (string) $order->total_amount);
|
||||
|
||||
$purchase = $purchase->fresh();
|
||||
$this->assertSame('3.00', (string) $purchase->total_quantity, '仅剩门店B 3 件');
|
||||
$this->assertSame('30.00', (string) $purchase->estimate_amount);
|
||||
}
|
||||
|
||||
/** 门店购买详情:按商品聚合该门店采购汇总(数量/包规/单位/单价/预计金额),仅含该门店明细 */
|
||||
@@ -408,28 +476,37 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
->assertJsonPath('msg', '采购单不存在');
|
||||
}
|
||||
|
||||
/** 采购单已完成:单元格编辑/同步、行修改均拒绝,明细不变 */
|
||||
/** 采购单已完成:单元格编辑、行修改、门店单品增删改均拒绝,明细不变 */
|
||||
public function test_edits_rejected_when_purchase_completed(): void
|
||||
{
|
||||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5])
|
||||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10])
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
|
||||
$this->putJson("/purchase/order/cell/{$item->id}/sync")
|
||||
->assertJsonPath('success', false);
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30])
|
||||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||||
'product_name' => '改名',
|
||||
'supplier_id' => $supplier->id,
|
||||
'product_spec' => '5斤/袋',
|
||||
'unit' => '袋',
|
||||
'cost_price' => 30,
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
|
||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||||
'product_id' => $product->id,
|
||||
'quantity' => 1,
|
||||
])->assertJsonPath('success', false);
|
||||
$this->putJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}", [
|
||||
'quantity' => 5,
|
||||
])->assertJsonPath('success', false);
|
||||
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}")
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
$this->assertSame(2, $item->fresh()->quantity, '被拒绝后明细不变');
|
||||
$this->assertSame('10.00', (string) $item->fresh()->cost_price);
|
||||
|
||||
// 下钻明细同步标记不可编辑
|
||||
$this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[0]->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.editable', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,16 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Models\CustomerLevelModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 门店订单明细(商品快照):
|
||||
* 下单快照完整商品档案、成本价防泄漏、后台明细修改重算总价、一键同步商品档案、按商品名称搜索订单
|
||||
* 下单快照完整商品档案、成本价防泄漏、采购单内门店单品增删改重算汇总、按商品名称搜索订单
|
||||
*/
|
||||
class StoreOrderItemTest extends ProcurementTestCase
|
||||
{
|
||||
@@ -50,6 +52,17 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
return $order;
|
||||
}
|
||||
|
||||
/** 下单 → 接单 → 生成采购单,返回采购单 */
|
||||
private function generatePurchase(): PurchaseOrderModel
|
||||
{
|
||||
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||
$this->actingAsSysUser();
|
||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
return PurchaseOrderModel::first();
|
||||
}
|
||||
|
||||
/** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */
|
||||
public function test_place_order_snapshots_full_product_info(): void
|
||||
{
|
||||
@@ -114,33 +127,23 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
->assertJsonPath('data.items.0.supplier.name', $supplier->name);
|
||||
}
|
||||
|
||||
/** 修改明细:重算单品金额与订单总量/总额(总额 = 商品金额,附加金额已迁入账单) */
|
||||
public function test_update_item_recalculates_order_totals(): void
|
||||
/** 采购单内修改门店单品:重算单品金额与订单/采购单汇总 */
|
||||
public function test_update_store_item_recalculates_totals(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$item = $order->items->first();
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/item/{$item->id}", [
|
||||
'supplier_id' => $supplier->id,
|
||||
'product_name' => '改名后的商品',
|
||||
'product_spec' => '新规格',
|
||||
'unit' => '箱',
|
||||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||||
'price' => '6.00',
|
||||
'cost_price' => '3.50',
|
||||
'quantity' => 5,
|
||||
'weight' => '2.5',
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$item->refresh();
|
||||
$this->assertSame($supplier->id, $item->supplier_id);
|
||||
$this->assertSame('改名后的商品', $item->product_name);
|
||||
$this->assertSame('新规格', $item->product_spec);
|
||||
$this->assertSame('箱', $item->unit);
|
||||
$this->assertSame('6.00', (string) $item->price);
|
||||
$this->assertSame('3.50', (string) $item->cost_price);
|
||||
$this->assertSame(5, $item->quantity);
|
||||
$this->assertSame('2.500', (string) $item->weight);
|
||||
$this->assertSame('30.00', (string) $item->amount, '6.00 × 5');
|
||||
@@ -148,139 +151,106 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
$order->refresh();
|
||||
$this->assertSame(5, $order->total_quantity, '订货总量 = 明细合计');
|
||||
$this->assertSame('2.500', (string) $order->total_weight, '总重量 = 明细合计');
|
||||
$this->assertSame('30.00', (string) $order->product_amount, '商品总金额 = 明细金额合计');
|
||||
$this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 商品金额');
|
||||
$this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 明细金额合计');
|
||||
|
||||
$purchase->refresh();
|
||||
$this->assertSame('5.00', (string) $purchase->total_quantity);
|
||||
$this->assertSame('2.500', (string) $purchase->total_weight);
|
||||
$this->assertSame('20.00', (string) $purchase->estimate_amount, '5 包 × 成本 4.00');
|
||||
}
|
||||
|
||||
/** 已完成/已取消订单不允许修改明细 */
|
||||
public function test_update_item_rejected_when_completed_or_cancelled(): void
|
||||
/** 采购单已完成:门店单品增删改均拒绝,明细不变 */
|
||||
public function test_store_item_edits_rejected_when_purchase_completed(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $user, 1);
|
||||
$item = $order->items->first();
|
||||
$payload = [
|
||||
'supplier_id' => 0,
|
||||
'product_name' => '不应生效',
|
||||
'product_spec' => '',
|
||||
'unit' => '斤',
|
||||
'price' => '1.00',
|
||||
'cost_price' => '1.00',
|
||||
'quantity' => 1,
|
||||
'weight' => 0,
|
||||
];
|
||||
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
$store = StoreModel::find($order->store_id);
|
||||
|
||||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||||
$this->actingAsSysUser();
|
||||
foreach ([StoreOrderModel::STATUS_COMPLETED, StoreOrderModel::STATUS_CANCELLED] as $status) {
|
||||
$order->update(['status' => $status]);
|
||||
$this->putJson("/order/store/item/{$item->id}", $payload)
|
||||
->assertOk()->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
$this->assertNotSame('不应生效', $item->fresh()->product_name, '被拒绝后明细不变');
|
||||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||||
'quantity' => 9,
|
||||
])->assertOk()->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
|
||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
|
||||
'product_id' => $product->id,
|
||||
'quantity' => 1,
|
||||
])->assertJsonPath('success', false);
|
||||
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}")
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
$item = $order->items->first();
|
||||
$this->assertSame(1, $item->fresh()->quantity, '被拒绝后明细不变');
|
||||
$this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变');
|
||||
}
|
||||
|
||||
/** 修改明细参数校验:负单价被拦截 */
|
||||
public function test_update_item_validates_negative_price(): void
|
||||
/** 修改门店单品参数校验:负单价/负数量被拦截 */
|
||||
public function test_update_store_item_validates_negative_values(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$item = $order->items->first();
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/item/{$item->id}", [
|
||||
'supplier_id' => 0,
|
||||
'product_name' => $item->product_name,
|
||||
'unit' => '斤',
|
||||
'price' => -1,
|
||||
'cost_price' => 0,
|
||||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||||
'quantity' => 1,
|
||||
'weight' => 0,
|
||||
'price' => -1,
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '单价不能小于 0');
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||||
'quantity' => -1,
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '采购数量不能小于 0');
|
||||
}
|
||||
|
||||
/** 一键同步:按商品ID同步最新档案(固定价等级保留原单价) */
|
||||
public function test_sync_item_updates_snapshot_from_product(): void
|
||||
/** 新增单品:单价按门店等级上浮比例换算(成本 20 上浮 30% = 26.00) */
|
||||
public function test_add_store_item_uses_level_percent_price(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.50', '4.00');
|
||||
$order = $this->placeOrder($product, $user, 3, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$item = $order->items->first();
|
||||
$this->assertSame('16.50', (string) $item->amount);
|
||||
|
||||
// 下单后商品档案变更:改名/改规格/改单位/换供应商/调成本价
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
$product->update([
|
||||
'name' => '同步后的品名',
|
||||
'spec' => '同步后的规格',
|
||||
'unit' => '箱',
|
||||
'supplier_id' => $supplier->id,
|
||||
'cost_price' => '9.99',
|
||||
]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/item/{$item->id}/sync")
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$item->refresh();
|
||||
$this->assertSame('同步后的品名', $item->product_name);
|
||||
$this->assertSame('同步后的规格', $item->product_spec);
|
||||
$this->assertSame('箱', $item->unit);
|
||||
$this->assertSame($supplier->id, $item->supplier_id);
|
||||
$this->assertSame('9.99', (string) $item->cost_price);
|
||||
$this->assertSame('5.50', (string) $item->price, '固定价等级单价不随档案变化');
|
||||
$this->assertSame('16.50', (string) $item->amount, '单价未变,金额不变');
|
||||
}
|
||||
|
||||
/** 一键同步:百分比计价等级按最新成本价重算单价与订单总价 */
|
||||
public function test_sync_item_recalculates_percent_price_with_latest_cost(): void
|
||||
{
|
||||
// 等级上浮 30%:下单时成本 10.00 → 售价 13.00
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create([
|
||||
'status' => ProductModel::STATUS_ON,
|
||||
'cost_price' => '10.00',
|
||||
]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '10.00']);
|
||||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '20.00']);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$item = $order->items->first();
|
||||
$this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%');
|
||||
$this->assertSame('26.00', (string) $order->total_amount);
|
||||
|
||||
// 成本价上调后同步:单价应按最新成本重算
|
||||
$product->update(['cost_price' => '20.00']);
|
||||
$this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/item/{$item->id}/sync")
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
|
||||
'product_id' => $extra->id,
|
||||
'quantity' => 2,
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$item->refresh();
|
||||
$this->assertSame('20.00', (string) $item->cost_price);
|
||||
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
|
||||
$this->assertNotNull($item);
|
||||
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
|
||||
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame('52.00', (string) $order->product_amount);
|
||||
$this->assertSame('52.00', (string) $order->total_amount);
|
||||
$this->assertSame('20.00', (string) $item->cost_price, '快照成本价');
|
||||
$this->assertSame($extra->name, $item->product_name, '快照品名取自商品档案');
|
||||
}
|
||||
|
||||
/** 一键同步:商品已删除时拒绝同步 */
|
||||
public function test_sync_item_rejected_when_product_deleted(): void
|
||||
/** 新增单品:商品已删除时拒绝 */
|
||||
public function test_add_store_item_rejected_when_product_deleted(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$item = $order->items->first();
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
$product->delete(); // 软删除
|
||||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '9.00']);
|
||||
$extra->delete(); // 软删除
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/item/{$item->id}/sync")
|
||||
->assertOk()
|
||||
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
|
||||
'product_id' => $extra->id,
|
||||
'quantity' => 1,
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '商品不存在或已被删除,无法同步');
|
||||
->assertJsonPath('msg', '商品不存在或已被删除,无法添加');
|
||||
}
|
||||
|
||||
/** 订单列表按包含的商品名称搜索 */
|
||||
|
||||
Reference in New Issue
Block a user