From 099e92b1ff50a40f69dc3e11d22d306406e56499 Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Thu, 13 Aug 2026 10:53:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=87=87=E8=B4=AD=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Exports/PurchaseOrderExport.php | 5 +- .../Order/StoreOrderController.php | 1 + .../Purchase/PurchaseOrderController.php | 54 +++- app/Models/PurchaseOrderModel.php | 1 + app/Models/StoreOrderModel.php | 8 + web/domain/iPurchaseOrder.ts | 2 - web/domain/iStoreOrder.ts | 16 +- web/pages/order/store.tsx | 21 +- web/pages/purchase/order.tsx | 246 ++++++++++-------- 9 files changed, 221 insertions(+), 133 deletions(-) diff --git a/app/Exports/PurchaseOrderExport.php b/app/Exports/PurchaseOrderExport.php index 67ec5c7..89a593d 100644 --- a/app/Exports/PurchaseOrderExport.php +++ b/app/Exports/PurchaseOrderExport.php @@ -16,8 +16,7 @@ use Maatwebsite\Excel\Concerns\WithStyles; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; /** - * 采购单导出(C2 全品类按分类 sort 排序 / C3 仅蔬果分类) - * 数据源为订货明细按商品聚合(无独立采购明细表),每门店一列显示数量 + * 采购单导出 */ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, WithStyles { @@ -135,7 +134,7 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, return array_merge( ['序号', '分类', '品名', '供应商', '包规', '单位', '成本', '单价', '数量', '实际称重', '金额'], - array_map(static fn (string $name): string => $name . '(数量)', array_values($this->storeNames)), + array_values($this->storeNames), ); } diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index 5295e6f..f1a880f 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -38,6 +38,7 @@ class StoreOrderController extends BaseController $query = StoreOrderModel::query()->with([ 'store:id,name,address,contact,phone', 'items:id,order_id,product_id,product_name,product_spec,unit,price,quantity,amount,image_ids', + 'purchase:id,purchase_no,purchase_date,status' ]); // 按包含的商品名称搜索:任一明细品名包含关键字即命中 diff --git a/app/Http/Controllers/Purchase/PurchaseOrderController.php b/app/Http/Controllers/Purchase/PurchaseOrderController.php index ab5de07..3d95a66 100644 --- a/app/Http/Controllers/Purchase/PurchaseOrderController.php +++ b/app/Http/Controllers/Purchase/PurchaseOrderController.php @@ -26,8 +26,7 @@ use Symfony\Component\HttpFoundation\Response; use Throwable; /** - * 采购单管理(C1 汇总生成 / C2-C3 导出 / C4 修改) - * 采购单无独立明细表,明细直接溯源门店订货明细(store_order_item.purchase_id) + * 采购单管理 */ #[RequestAttribute('/purchase/order', 'purchase.order')] class PurchaseOrderController extends BaseController @@ -144,26 +143,61 @@ class PurchaseOrderController extends BaseController ]); } - /** C4 修改采购单头信息(采购日期、状态、备注) */ + /** 修改采购单信息(采购日期、实际金额、备注) */ #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function update(int $id, Request $request): JsonResponse { $data = $request->validate([ - 'purchase_date' => 'nullable|date_format:Y-m-d', - 'status' => 'nullable|integer|in:' . PurchaseOrderModel::STATUS_PENDING . ',' . PurchaseOrderModel::STATUS_COMPLETED, + 'actual_amount' => 'required|numeric|min:0', 'remark' => 'nullable|string|max:255', ], [ - 'purchase_date.date_format' => '采购日期格式为 Y-m-d', - 'status.in' => '采购单状态不正确', + 'actual_amount.required' => '实际金额不能为空', + 'actual_amount.numeric' => '实际金额格式错误', + 'actual_amount.min' => '实际金额不能小于0', + 'remark.max' => '备注超过最大长度', ]); $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } + if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { + throw new RepositoryException('采购单已完成,不允许修改信息'); + } $purchase->update(array_filter($data, static fn ($v) => $v !== null)); return $this->success(); } + /** + * 完成采购单 + * @throws + */ + #[PutRoute(route: '/{id}/finish', authorize: 'update', where: ['id' => '[0-9]+'])] + public function finish(int $id): JsonResponse + { + $purchase = PurchaseOrderModel::find($id); + if (empty($purchase)) { + throw new RepositoryException('采购单不存在'); + } + if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { + throw new RepositoryException('采购单已完成,不允许修改信息'); + } + + return DB::transaction(function () use ($purchase) { + // 修改采购单状态 + $purchase->status = PurchaseOrderModel::STATUS_COMPLETED; + $purchase->save(); + // 修改所有订单状态为配送中 + StoreOrderModel::where('purchase_id', $purchase->id)->update([ + 'status' => StoreOrderModel::STATUS_DISTRIBUTION + ]); + // 生成并发送账单 + + return $this->success(); + }); + + + } + /** * 生成采购单 */ @@ -194,7 +228,9 @@ class PurchaseOrderController extends BaseController } /** - * C2/C3 导出采购单(Excel 表格):?type=all 全品类 / category 仅蔬果分类 + * 导出采购单 Excel 表格 + * + * @throws */ #[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])] public function export(int $id, Request $request): Response @@ -267,8 +303,6 @@ class PurchaseOrderController extends BaseController 'amount' => (string) $item->amount, 'remark' => (string) $item->remark, 'image_ids' => $item->image_ids, - 'editable' => $purchase->status === PurchaseOrderModel::STATUS_PENDING - && in_array((int) $item->order_status, StoreOrderItemModel::ITEM_EDITABLE_STATUS, true), ]; } app(ItemImageResolver::class)->resolve($rows); diff --git a/app/Models/PurchaseOrderModel.php b/app/Models/PurchaseOrderModel.php index 5a9f289..a29487b 100644 --- a/app/Models/PurchaseOrderModel.php +++ b/app/Models/PurchaseOrderModel.php @@ -43,6 +43,7 @@ class PurchaseOrderModel extends Model 'estimate_amount' => 'decimal:2', 'actual_amount' => 'decimal:2', 'operator_id' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', ]; /** diff --git a/app/Models/StoreOrderModel.php b/app/Models/StoreOrderModel.php index d38ca51..592d040 100644 --- a/app/Models/StoreOrderModel.php +++ b/app/Models/StoreOrderModel.php @@ -88,4 +88,12 @@ class StoreOrderModel extends Model { return $this->hasMany(StoreOrderItemModel::class, 'order_id', 'id'); } + + /** + * 采购单 + */ + public function purchase(): BelongsTo + { + return $this->belongsTo(PurchaseOrderModel::class, 'purchase_id', 'id'); + } } diff --git a/web/domain/iPurchaseOrder.ts b/web/domain/iPurchaseOrder.ts index b9e4690..fb86298 100644 --- a/web/domain/iPurchaseOrder.ts +++ b/web/domain/iPurchaseOrder.ts @@ -73,8 +73,6 @@ export interface IPurchaseCellItem { remark: string; /** 首图 */ image?: string; - /** 是否可编辑/同步(采购单进行中且订货单未锁定) */ - editable: boolean; } /** 单元格下钻数据(门店 + 商品 + 全部订货明细) */ diff --git a/web/domain/iStoreOrder.ts b/web/domain/iStoreOrder.ts index 3e98d6e..9e91911 100644 --- a/web/domain/iStoreOrder.ts +++ b/web/domain/iStoreOrder.ts @@ -73,6 +73,12 @@ export default interface IStoreOrder { remark?: string; items?: IStoreOrderItem[]; created_at?: string; + purchase?: { + id: number; + purchase_no: string; + purchase_date: string; + status: number; + } } export const STORE_ORDER_STATUS_MAP: Record = { @@ -83,13 +89,3 @@ export const STORE_ORDER_STATUS_MAP: Record { } }, { - title: '采购单ID', + title: '采购单', dataIndex: 'purchase_id', valueType: 'digit', hideInForm: true, + render: (_, record) => record.purchase ? ( + +
+ 采购单号: + {record.purchase.purchase_no} +
+
+ 采购单日期: + {record.purchase.purchase_date} +
+
+ 采购单状态: + + {PURCHASE_STATUS_MAP[record.purchase.status ?? 0]?.text} + +
+
+ ) : '-' }, { title: '账单ID', diff --git a/web/pages/purchase/order.tsx b/web/pages/purchase/order.tsx index cb52553..f6e5083 100644 --- a/web/pages/purchase/order.tsx +++ b/web/pages/purchase/order.tsx @@ -20,7 +20,7 @@ import { Tag, Typography, } from 'antd'; -import { CheckOutlined, DownloadOutlined, EditOutlined } from '@ant-design/icons'; +import {DownloadOutlined, EditOutlined, UnorderedListOutlined} from '@ant-design/icons'; import type { TableProps } from 'antd'; import XinTable from '@/components/XinTable'; import type { @@ -242,15 +242,11 @@ const PurchaseOrderPage: React.FC = () => { } }; - const handleComplete = async () => { - if (!detail) { - return; - } + const handleComplete = async (id: number) => { setCompleting(true); try { - await Update(`/purchase/order/${detail.purchase.id}`, { status: 3 }); + await Update(`/purchase/order/${id}/finish`, { status: 3 }); message.success('采购单已标记完成'); - await loadDetail(detail.purchase.id!); await tableRef.current?.reload(); } finally { setCompleting(false); @@ -310,16 +306,20 @@ const PurchaseOrderPage: React.FC = () => { fixed: 'right', align: 'center', render: (_, row) => ( - - - + <> + { detail?.purchase.status === 0 ? ( + + + + ) : '-' } + ), }, ]; @@ -426,14 +426,16 @@ const PurchaseOrderPage: React.FC = () => { dataIndex: 'purchase_no', valueType: 'text', hideInForm: true, + align: 'center', + width: 260, render: (_, record) => {record.purchase_no}, }, { title: '采购日期', dataIndex: 'purchase_date', - valueType: 'dateRange', - hideInForm: true, + valueType: 'date', align: 'center', + hideInForm: true, render: (_, record) => record.purchase_date, }, { @@ -441,15 +443,20 @@ const PurchaseOrderPage: React.FC = () => { dataIndex: 'estimate_amount', hideInForm: true, hideInSearch: true, - align: 'right', + align: 'center', render: (_, record) => `¥${record.estimate_amount}`, }, { title: '实际成本', dataIndex: 'actual_amount', - hideInForm: true, hideInSearch: true, - align: 'right', + valueType: "digit", + fieldProps: { + min: 0, + precision: 3, + placeholder: "请输入单价" + }, + align: 'center', render: (_, record) => Number(record.actual_amount) > 0 ? ( ¥{record.actual_amount} @@ -457,6 +464,28 @@ const PurchaseOrderPage: React.FC = () => { 未录入 ), }, + { + title: '总件数', + dataIndex: 'total_quantity', + hideInForm: true, + hideInSearch: true, + align: 'center', + }, + { + title: '备注', + dataIndex: 'remark', + valueType: "textarea", + hideInTable: true, + hideInSearch: true + }, + { + title: '总重量', + dataIndex: 'total_weight', + hideInForm: true, + hideInSearch: true, + align: 'center', + render: (_, record) => `${record.total_weight}斤`, + }, { title: '状态', dataIndex: 'status', @@ -484,24 +513,18 @@ const PurchaseOrderPage: React.FC = () => { }, ]; - const operateRender: XinTableProps['operateRender'] = (record) => [ - , - - exportPurchase(record.id!, 'all') }, - { key: 'category', label: '仅导出蔬果分类', onClick: () => exportPurchase(record.id!, 'category') }, - ], - }} - > - - - , + const operateRender: XinTableProps['operateRender'] = (record, dom) => [ + record.status === 0 ? dom.edit : null, + + + + ) : null ]; const tableProps: XinTableProps = { @@ -511,6 +534,9 @@ const PurchaseOrderPage: React.FC = () => { accessName: 'purchase.order', tableRef, operateRender, + operateProps: { + width: 300 + }, formProps: false, }; @@ -531,19 +557,10 @@ const PurchaseOrderPage: React.FC = () => { onClose={() => setDetailOpen(false)} size={1200} loading={detailLoading} - extra={detail?.purchase.status === 0 && ( - - - - - - )} > {detail && ( <> - + <Title level={5} className="mb-3!"> 采购单信息 @@ -556,9 +573,12 @@ const PurchaseOrderPage: React.FC = () => { {detail.purchase.operator?.nickname ?? '-'} - ¥{detail.purchase.estimate_amount} - ¥{detail.purchase.actual_amount} + ¥{detail.purchase.estimate_amount} + ¥{detail.purchase.actual_amount} + {detail.purchase.total_quantity} {detail.purchase.total_weight} + {detail.purchase.created_at} + {detail.purchase.remark} { onChange={setDetailTab} className="mt-3!" items={[ - { - key: 'items', - label: '商品明细', - children: ( - + { key: 'items', label: '商品明细' }, + { key: 'stores', label: '门店购买详情' }, + ]} + /> + + { detailTab === 'stores' ? ( + <> +
+ 选择门店: + setStoreId(value)} - placeholder="选择门店" - className="w-60!" - showSearch - optionFilterProp="label" - options={detail.stores.map((s) => ({ value: s.id, label: s.name }))} - /> -
- - {storeSummary && storeSummary.items.length > 0 ? ( - - rowKey="product_id" - size="small" - bordered - columns={storeColumns} - dataSource={storeSummary.items} - pagination={false} - summary={renderStoreTotal} - /> - ) : ( - !storeLoading && ( - - ) - )} - - - ), - }, - ]} - /> + ) : ( + !storeLoading && ( + + ) + )} + + + ) : ( + <> + + + detail && exportPurchase(detail.purchase.id!, 'all') }, + { key: 'category', label: '仅导出蔬果分类', onClick: () => detail && exportPurchase(detail.purchase.id!, 'category') }, + ], + }} + > + + + + + + rowKey="product_id" + size="small" + bordered + columns={buildItemColumns()} + dataSource={detail.items} + pagination={false} + scroll={{ x: 'max-content' }} + summary={renderSummary} + /> + + ) } )} @@ -766,7 +798,7 @@ const PurchaseOrderPage: React.FC = () => { {item.weight ?? '-'} 斤
- {item.editable ? ( + {detail?.purchase.status === 0 ? (