采购单优化
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -259,6 +259,84 @@ class PurchaseOrderController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店购买详情:采购单内指定门店的采购汇总(按商品聚合)
|
||||||
|
* 单价为加权平均口径(Σ金额÷Σ数量),保证 单价×数量=预计金额
|
||||||
|
*/
|
||||||
|
#[GetRoute(route: '/{id}/store', authorize: 'query', where: ['id' => '[0-9]+'])]
|
||||||
|
public function storeSummary(int $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$storeId = (int) $request->query('store_id', 0);
|
||||||
|
if ($storeId <= 0) {
|
||||||
|
throw new RepositoryException('缺少门店参数');
|
||||||
|
}
|
||||||
|
$purchase = PurchaseOrderModel::find($id);
|
||||||
|
if (empty($purchase)) {
|
||||||
|
throw new RepositoryException('采购单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$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.store_id', $storeId)
|
||||||
|
->whereNull('store_order.deleted_at')
|
||||||
|
->select('store_order_item.*')
|
||||||
|
->orderBy('store_order_item.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 排序键:分类 sort → 商品 sort(与明细矩阵同序)
|
||||||
|
$products = ProductModel::withTrashed()
|
||||||
|
->with('category:id,sort')
|
||||||
|
->whereIn('id', $items->pluck('product_id')->unique())
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($items->groupBy('product_id') as $productId => $group) {
|
||||||
|
$product = $products->get((int) $productId);
|
||||||
|
$first = $group->first();
|
||||||
|
// 采购总数量(包数)
|
||||||
|
$quantity = 0;
|
||||||
|
// 预计金额 = Σ 明细 amount
|
||||||
|
$amount = '0';
|
||||||
|
// 总重量
|
||||||
|
$weight = '0';
|
||||||
|
foreach ($group as $item) {
|
||||||
|
$weight = bcadd($weight, (string) $item->weight, 3);
|
||||||
|
$quantity += (int) $item->quantity;
|
||||||
|
$amount = bcadd($amount, (string) $item->amount, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'product_id' => (int) $productId,
|
||||||
|
'product_name' => $first->product_name,
|
||||||
|
'product_spec' => $first->product_spec, // 包规
|
||||||
|
'unit' => $first->unit, // 单位
|
||||||
|
'price' => $quantity > 0 // 加权平均单价
|
||||||
|
? bcdiv($amount, (string) $quantity, 2)
|
||||||
|
: (string) $first->price,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'amount' => $amount,
|
||||||
|
'weight' => $weight,
|
||||||
|
'category_sort' => (int) ($product->category->sort ?? 9999),
|
||||||
|
'product_sort' => (int) ($product->sort ?? 9999),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
usort($rows, static fn (array $a, array $b): int =>
|
||||||
|
[$a['category_sort'], $a['product_sort'], $a['product_id']]
|
||||||
|
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
|
||||||
|
|
||||||
|
$store = StoreModel::withTrashed()->find($storeId);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null,
|
||||||
|
'items' => array_map(static function (array $row): array {
|
||||||
|
unset($row['category_sort'], $row['product_sort']);
|
||||||
|
return $row;
|
||||||
|
}, $rows),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商品行修改:品名/供应商/包规/单位/成本
|
* 商品行修改:品名/供应商/包规/单位/成本
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
|
|||||||
@@ -335,6 +335,87 @@ class PurchaseEditTest extends ProcurementTestCase
|
|||||||
$this->assertSame('40.00', (string) $purchase->actual_amount, '2 包 × 每包成本 20.00');
|
$this->assertSame('40.00', (string) $purchase->actual_amount, '2 包 × 每包成本 20.00');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情:按商品聚合该门店采购汇总(数量/包规/单位/单价/预计金额),仅含该门店明细 */
|
||||||
|
public function test_store_summary_returns_aggregated_items(): void
|
||||||
|
{
|
||||||
|
[$purchase, $product, $stores] = $this->buildPurchase();
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$response = $this->getJson("/purchase/order/{$purchase->id}/store?store_id={$stores[0]->id}")
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$data = $response->json('data');
|
||||||
|
$this->assertSame($stores[0]->name, $data['store']['name']);
|
||||||
|
$this->assertCount(1, $data['items']);
|
||||||
|
|
||||||
|
$row = $data['items'][0];
|
||||||
|
$this->assertSame($product->id, $row['product_id']);
|
||||||
|
$this->assertSame('10斤/箱', $row['product_spec']);
|
||||||
|
$this->assertSame('斤', $row['unit']);
|
||||||
|
$this->assertSame('10.00', (string) $row['price']);
|
||||||
|
$this->assertSame(2, $row['quantity']);
|
||||||
|
$this->assertSame('20.00', (string) $row['amount']);
|
||||||
|
|
||||||
|
// 另一门店只见自身明细
|
||||||
|
$this->getJson("/purchase/order/{$purchase->id}/store?store_id={$stores[1]->id}")
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items.0.quantity', 3)
|
||||||
|
->assertJsonPath('data.items.0.amount', '30.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情:同一商品多笔订单不同单价 → 数量合计、金额求和、单价为加权平均 */
|
||||||
|
public function test_store_summary_aggregates_multiple_orders_with_weighted_price(): void
|
||||||
|
{
|
||||||
|
$level = CustomerLevelModel::factory()->create();
|
||||||
|
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||||
|
$product = ProductModel::factory()->create([
|
||||||
|
'status' => ProductModel::STATUS_ON,
|
||||||
|
'cost_price' => 20,
|
||||||
|
'spec' => '10斤/箱',
|
||||||
|
'unit' => '斤',
|
||||||
|
]);
|
||||||
|
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 10.00]);
|
||||||
|
|
||||||
|
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||||
|
foreach ([2, 3] 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();
|
||||||
|
// 第二笔明细单价改为 20.00,构造同商品双单价:2×10 + 3×20
|
||||||
|
$secondItem = StoreOrderItemModel::orderBy('id')->skip(1)->first();
|
||||||
|
$this->putJson("/purchase/order/cell/{$secondItem->id}", ['quantity' => 3, 'price' => 20])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$this->getJson("/purchase/order/{$purchase->id}/store?store_id={$store->id}")
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items.0.quantity', 5)
|
||||||
|
->assertJsonPath('data.items.0.price', '16.00', '(20+60) ÷ 5 加权平均')
|
||||||
|
->assertJsonPath('data.items.0.amount', '80.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情参数校验:缺少门店参数 / 采购单不存在均拒绝 */
|
||||||
|
public function test_store_summary_rejects_missing_or_invalid_params(): void
|
||||||
|
{
|
||||||
|
[$purchase] = $this->buildPurchase();
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$this->getJson("/purchase/order/{$purchase->id}/store")
|
||||||
|
->assertJsonPath('success', false)
|
||||||
|
->assertJsonPath('msg', '缺少门店参数');
|
||||||
|
|
||||||
|
$this->getJson('/purchase/order/99999/store?store_id=1')
|
||||||
|
->assertJsonPath('success', false)
|
||||||
|
->assertJsonPath('msg', '采购单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
/** 采购单已完成:单元格编辑/同步、行修改均拒绝,明细不变 */
|
/** 采购单已完成:单元格编辑/同步、行修改均拒绝,明细不变 */
|
||||||
public function test_edits_rejected_when_purchase_completed(): void
|
public function test_edits_rejected_when_purchase_completed(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import createAxios from '@/utils/request';
|
|||||||
import type {
|
import type {
|
||||||
IPurchaseCell,
|
IPurchaseCell,
|
||||||
IPurchaseDetail,
|
IPurchaseDetail,
|
||||||
|
IPurchaseStoreSummary,
|
||||||
} from '@/domain/iPurchaseOrder.ts';
|
} from '@/domain/iPurchaseOrder.ts';
|
||||||
|
|
||||||
/** 订单商品参数修改 */
|
/** 订单商品参数修改 */
|
||||||
@@ -64,3 +65,12 @@ export async function updatePurchaseCellItem(itemId: number, data: PurchaseCellU
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情:采购单内某门店的采购汇总(按商品聚合) */
|
||||||
|
export async function getPurchaseStoreSummary(purchaseId: number, storeId: number) {
|
||||||
|
return createAxios<IPurchaseStoreSummary>({
|
||||||
|
url: `/purchase/order/${purchaseId}/store`,
|
||||||
|
method: 'get',
|
||||||
|
params: { store_id: storeId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,6 +84,28 @@ export interface IPurchaseCell {
|
|||||||
items: IPurchaseCellItem[];
|
items: IPurchaseCellItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情行(门店采购汇总,按商品聚合) */
|
||||||
|
export interface IPurchaseStoreItem {
|
||||||
|
product_id: number;
|
||||||
|
product_name: string;
|
||||||
|
/** 规格/包规 */
|
||||||
|
product_spec: string;
|
||||||
|
unit: string;
|
||||||
|
/** 单价(每包;多笔单价时为加权平均,保证 单价×数量=预计金额) */
|
||||||
|
price: string;
|
||||||
|
/** 数量(包数) */
|
||||||
|
quantity: number;
|
||||||
|
/** 预计金额 = Σ 明细 amount */
|
||||||
|
amount: string;
|
||||||
|
weight: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店购买详情(采购单内某门店的采购汇总) */
|
||||||
|
export interface IPurchaseStoreSummary {
|
||||||
|
store: { id: number; name: string } | null;
|
||||||
|
items: IPurchaseStoreItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export const PURCHASE_STATUS_MAP: Record<number, { text: string; color: string }> = {
|
export const PURCHASE_STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||||
0: { text: '进行中', color: 'processing' },
|
0: { text: '进行中', color: 'processing' },
|
||||||
3: { text: '已完成', color: 'success' },
|
3: { text: '已完成', color: 'success' },
|
||||||
|
|||||||
+170
-18
@@ -15,6 +15,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Table,
|
Table,
|
||||||
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
@@ -32,12 +33,16 @@ import type {
|
|||||||
IPurchaseCellItem,
|
IPurchaseCellItem,
|
||||||
IPurchaseDetail,
|
IPurchaseDetail,
|
||||||
IPurchaseDetailRow,
|
IPurchaseDetailRow,
|
||||||
|
IPurchaseStoreItem,
|
||||||
|
IPurchaseStoreSummary,
|
||||||
} from '@/domain/iPurchaseOrder.ts';
|
} from '@/domain/iPurchaseOrder.ts';
|
||||||
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
|
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
|
||||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||||
import {
|
import {
|
||||||
getPurchaseCell,
|
getPurchaseCell,
|
||||||
getPurchaseDetail, type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
getPurchaseDetail,
|
||||||
|
getPurchaseStoreSummary,
|
||||||
|
type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
||||||
updatePurchaseCellItem,
|
updatePurchaseCellItem,
|
||||||
updatePurchaseRow,
|
updatePurchaseRow,
|
||||||
} from '@/api/purchase/order.ts';
|
} from '@/api/purchase/order.ts';
|
||||||
@@ -48,10 +53,10 @@ import AuthButton from '@/components/AuthButton';
|
|||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
/** 参考单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
|
/** 每单位参考价 = 整单价(成本/售价) ÷ 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
|
||||||
const calcUnitCost = (cost: number, spec: string): number => {
|
const calcUnitRefPrice = (total: number, spec: string): number => {
|
||||||
const pack = parseFloat(spec);
|
const pack = parseFloat(spec);
|
||||||
return Number.isFinite(pack) && pack > 0 ? cost / pack : cost;
|
return Number.isFinite(pack) && pack > 0 ? total / pack : total;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,6 +90,12 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
const [cellItemSaving, setCellItemSaving] = useState(false);
|
const [cellItemSaving, setCellItemSaving] = useState(false);
|
||||||
const [cellItemForm] = Form.useForm<PurchaseCellUpdateParams>();
|
const [cellItemForm] = Form.useForm<PurchaseCellUpdateParams>();
|
||||||
|
|
||||||
|
// 门店购买详情(按商品聚合的门店采购汇总)
|
||||||
|
const [detailTab, setDetailTab] = useState('items');
|
||||||
|
const [storeId, setStoreId] = useState<number>(0);
|
||||||
|
const [storeSummary, setStoreSummary] = useState<IPurchaseStoreSummary | null>(null);
|
||||||
|
const [storeLoading, setStoreLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -96,6 +107,20 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [cellOpen, cellQuery]);
|
}, [cellOpen, cellQuery]);
|
||||||
|
|
||||||
|
// 详情加载后默认选中第一个门店(当前选中门店仍在采购单内则保留)
|
||||||
|
useEffect(() => {
|
||||||
|
if (detail && detail.stores.length > 0) {
|
||||||
|
setStoreId((prev) => (detail.stores.some((s) => s.id === prev) ? prev : detail.stores[0].id));
|
||||||
|
}
|
||||||
|
}, [detail]);
|
||||||
|
|
||||||
|
// 切到「门店购买详情」页签或切换门店时加载汇总
|
||||||
|
useEffect(() => {
|
||||||
|
if (detailOpen && detailTab === 'stores' && detail && storeId > 0) {
|
||||||
|
void loadStoreSummary();
|
||||||
|
}
|
||||||
|
}, [detailOpen, detailTab, storeId, detail?.purchase.id]);
|
||||||
|
|
||||||
const loadDetail = async (id: number) => {
|
const loadDetail = async (id: number) => {
|
||||||
setDetailLoading(true);
|
setDetailLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -107,10 +132,26 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openDetail = async (id: number) => {
|
const openDetail = async (id: number) => {
|
||||||
|
setDetailTab('items');
|
||||||
|
setStoreSummary(null);
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
await loadDetail(id);
|
await loadDetail(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 加载门店购买详情(门店采购汇总) */
|
||||||
|
const loadStoreSummary = async () => {
|
||||||
|
if (!detail || storeId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStoreLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getPurchaseStoreSummary(detail.purchase.id!, storeId);
|
||||||
|
setStoreSummary(res.data.data ?? null);
|
||||||
|
} finally {
|
||||||
|
setStoreLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
|
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
|
||||||
const openCell = (row: IPurchaseDetailRow, store: { id: number; name: string }) => {
|
const openCell = (row: IPurchaseDetailRow, store: { id: number; name: string }) => {
|
||||||
setCellQuery({ productId: row.product_id, storeId: store.id });
|
setCellQuery({ productId: row.product_id, storeId: store.id });
|
||||||
@@ -226,11 +267,11 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
render: (_, row) => row.supplier?.name ?? '-',
|
render: (_, row) => row.supplier?.name ?? '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '参考单价',
|
title: '参考成本单价',
|
||||||
key: 'unit_cost',
|
key: 'unit_cost',
|
||||||
width: 100,
|
width: 100,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, row) => `¥${calcUnitCost(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
|
render: (_, row) => `¥${calcUnitRefPrice(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{ title: '包规', dataIndex: 'product_spec', align: 'center', width: 90, render: (v) => v || '-' },
|
{ title: '包规', dataIndex: 'product_spec', align: 'center', width: 90, render: (v) => v || '-' },
|
||||||
{ title: '单位', dataIndex: 'unit', width: 70, align: 'center', render: (v) => v || '-' },
|
{ title: '单位', dataIndex: 'unit', width: 70, align: 'center', render: (v) => v || '-' },
|
||||||
@@ -317,6 +358,66 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 */
|
||||||
|
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
|
||||||
|
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
|
||||||
|
{
|
||||||
|
title: '参考零售价',
|
||||||
|
key: 'retail_price',
|
||||||
|
width: 130,
|
||||||
|
align: 'center',
|
||||||
|
render: (_, row) => `¥${calcUnitRefPrice(Number(row.price), row.product_spec).toFixed(2)}`,
|
||||||
|
},
|
||||||
|
{ title: '包规', dataIndex: 'product_spec', width: 90, align: 'center', render: (v) => v || '-' },
|
||||||
|
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
|
||||||
|
{
|
||||||
|
title: '单价',
|
||||||
|
dataIndex: 'price',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数量',
|
||||||
|
dataIndex: 'quantity',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
render: (v) => <Text strong>{v}</Text>,
|
||||||
|
},
|
||||||
|
{ title: '重量', dataIndex: 'weight', width: 90, align: 'center', render: (v) => `${v || '-'}斤` },
|
||||||
|
{
|
||||||
|
title: '预计金额',
|
||||||
|
dataIndex: 'amount',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 门店购买详情合计行:总数量 + 总预计金额 */
|
||||||
|
const renderStoreTotal = () => {
|
||||||
|
const items = storeSummary?.items ?? [];
|
||||||
|
const totalQuantity = items.reduce((sum, row) => sum + Number(row.quantity), 0);
|
||||||
|
const totalAmount = items.reduce((sum, row) => sum + Number(row.amount), 0);
|
||||||
|
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
|
||||||
|
return (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell index={0} colSpan={5} align="center">
|
||||||
|
<Text strong>合计</Text>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell index={5} align="center">
|
||||||
|
<Text strong>{totalQuantity}</Text>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell index={6} align="center">
|
||||||
|
<Text strong>{totalWeight.toFixed(3)}斤</Text>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell index={7} align="center">
|
||||||
|
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const columns: XinTableColumn<IPurchaseOrder>[] = [
|
const columns: XinTableColumn<IPurchaseOrder>[] = [
|
||||||
{
|
{
|
||||||
title: '采购单号',
|
title: '采购单号',
|
||||||
@@ -444,18 +545,69 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
<Descriptions.Item label="总重量">{detail.purchase.total_weight}</Descriptions.Item>
|
<Descriptions.Item label="总重量">{detail.purchase.total_weight}</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
|
||||||
<Title level={5} className="mt-5! mb-3!">
|
<Tabs
|
||||||
商品明细
|
activeKey={detailTab}
|
||||||
</Title>
|
onChange={setDetailTab}
|
||||||
<Table<IPurchaseDetailRow>
|
className="mt-3!"
|
||||||
rowKey="product_id"
|
items={[
|
||||||
size="small"
|
{
|
||||||
bordered
|
key: 'items',
|
||||||
columns={buildItemColumns()}
|
label: '商品明细',
|
||||||
dataSource={detail.items}
|
children: (
|
||||||
pagination={false}
|
<Table<IPurchaseDetailRow>
|
||||||
scroll={{ x: 'max-content' }}
|
rowKey="product_id"
|
||||||
summary={renderSummary}
|
size="small"
|
||||||
|
bordered
|
||||||
|
columns={buildItemColumns()}
|
||||||
|
dataSource={detail.items}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
summary={renderSummary}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stores',
|
||||||
|
label: '门店购买详情',
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<Text>选择门店:</Text>
|
||||||
|
<Select
|
||||||
|
value={storeId || undefined}
|
||||||
|
onChange={(value) => setStoreId(value)}
|
||||||
|
placeholder="选择门店"
|
||||||
|
className="w-60!"
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={detail.stores.map((s) => ({ value: s.id, label: s.name }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spin spinning={storeLoading}>
|
||||||
|
{storeSummary && storeSummary.items.length > 0 ? (
|
||||||
|
<Table<IPurchaseStoreItem>
|
||||||
|
rowKey="product_id"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
columns={storeColumns}
|
||||||
|
dataSource={storeSummary.items}
|
||||||
|
pagination={false}
|
||||||
|
summary={renderStoreTotal}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
!storeLoading && (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description="该门店在此采购单中无采购商品"
|
||||||
|
className="py-8!"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user