From 543a94609cfd9c57faaba4f3607aae83e82a72a3 Mon Sep 17 00:00:00 2001 From: HHY Date: Fri, 25 Sep 2026 06:47:20 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20P1=E6=89=B9=E6=AC=A14=20CRM+=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=9F=9F(=E5=AE=A2=E6=88=B7=E8=B7=9F=E8=BF=9B/?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E5=91=98=E4=B8=9A=E7=BB=A9/=E4=BD=99?= =?UTF-8?q?=E6=96=99/=E9=A2=86=E6=96=99/=E9=87=87=E8=B4=AD=E5=8D=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 2 + doc/API.md | 20 ++++ frontend/src/views/CustomersView.vue | 131 ++++++++++++++++++++++++- frontend/src/views/CutSolutionView.vue | 28 ++++++ frontend/src/views/WarehouseView.vue | 79 ++++++++++++++- src/huamenchuang/api/customers.py | 43 ++++++++ src/huamenchuang/api/purchases.py | 110 +++++++++++++++++++++ src/huamenchuang/api/router.py | 4 + src/huamenchuang/api/salesmen.py | 82 ++++++++++++++++ src/huamenchuang/api/warehouse.py | 91 ++++++++++++++++- src/huamenchuang/models/__init__.py | 6 ++ src/huamenchuang/models/business.py | 47 +++++++++ 12 files changed, 636 insertions(+), 7 deletions(-) create mode 100644 src/huamenchuang/api/purchases.py create mode 100644 src/huamenchuang/api/salesmen.py diff --git a/CHANGELOG b/CHANGELOG index df6eea8..85854c2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [2026-09-25 22:40] FEAT: v2.32 P1 批次4 CRM+仓库域 5 项。①客户跟进: CustomerFollow 模型+/customers/{id}/follows CRUD+客户页跟进对话框(timeline 历史+新增/下次日期); ②业务员体系: Salesman 模型+/api/salesmen CRUD+statistics 业绩(名下客户/合同额/预估提成)+客户页业务员 Tab+客户表单业务员选择; ③余料管理: surplus-in(读合同方案按最小长度入库 note=surplus)+surplus 列表+下料方案页余料入库按钮+仓库页余料筛选; ④领料: pickout(按领料清单扣库存+流水, 缺料报缺口)+下料方案页领料出库按钮; ⑤采购单: PurchaseOrder 模型(CG+日期+流水)+POST 自动读缺料清单/手动明细+receive 入库联动(库存+StockMove)+仓库页采购单 Tab(状态/入库); 实测: 跟进/业绩/余料 2 根入库/领料 5 根出库/采购 CG001 入库联动全通; 254 套全量校验 0 失败; vue-tsc 0 错+构建通过 (Files: src/huamenchuang/models/business.py, src/huamenchuang/api/customers.py, src/huamenchuang/api/salesmen.py, src/huamenchuang/api/purchases.py, src/huamenchuang/api/warehouse.py, src/huamenchuang/api/router.py, frontend/src/views/CustomersView.vue, frontend/src/views/CutSolutionView.vue, frontend/src/views/WarehouseView.vue, doc/API.md) + - [2026-09-25 21:50] FEAT: v2.31 P1 批次3 生产域 4 项。①车间管理: Staff/ProcessAssignment 模型+/api/workshop(staff CRUD/assign 派工/assignments 派工历史/scan 扫码跟踪/board 跟单中心看板), 新页 /workshop 三 Tab(看板按环节列/环节扫码 BarcodeDetector 摄像头+扫码枪输入框/员工名册+派工), 路由菜单; ②双通道优化: optimize_1d_fine(BFD 最佳适应递减+有界余料均衡, 修复 while 死循环卡死事件循环)+optimize1d method=fast|fine, 优化页通道选择; ③订单利润: GET /reports/order-profit(订单金额 vs BOM 材料成本估算, 价格本成本均价口径)+利润汇总卡+明细表, 报表页嵌入; ④条码打印: 打印模板加 barcode 区块(JsBarcode Code128 订单号)+模板管理可加条码块; vue-tsc 0 错+构建通过; API 实测双通道/看板/利润/员工 (Files: src/huamenchuang/models/extend.py, src/huamenchuang/api/workshop.py, src/huamenchuang/api/optimize.py, src/huamenchuang/services/optimize_service.py, src/huamenchuang/api/reports.py, src/huamenchuang/api/router.py, frontend/src/views/WorkshopView.vue, frontend/src/views/OptimizeView.vue, frontend/src/views/ReportView.vue, frontend/src/views/PrinterView.vue, frontend/src/router/index.ts, frontend/src/views/LayoutView.vue) - [2026-09-25 21:00] FEAT: v2.30 P1 批次2 报价/产品域 3 项。①型材系列管理: /api/product-catelogs 树 CRUD(系列下有产品禁删)+产品挂系列(catelog_id)+按系列筛选, 产品页左树右表布局+编辑表单系列选择+补回丢失的产品编辑对话框; ②副梃计价收尾: 产品类别加 subbar(按尺寸/米计价, 随逐格编辑 assigns.subBar 算料); ③成本报价: 报价保存/更新按产品名匹配价格本成本写行 _cost(quote_by_cost 语义)+响应加 cost_total/profit, 报价页加成本/毛利列+分享链接(quote_link 走 shares type=quote); 实测: 成本匹配 6×38=228 毛利 84; vue-tsc 0 错+构建通过 (Files: src/huamenchuang/api/business.py, frontend/src/views/ProductsView.vue, frontend/src/views/QuotationsView.vue, doc/API.md) diff --git a/doc/API.md b/doc/API.md index 104bbfc..9b9d5df 100644 --- a/doc/API.md +++ b/doc/API.md @@ -501,3 +501,23 @@ uv run uvicorn huamenchuang.main:app --reload # 启动,http://localhost:8000 | 新增/改名/删系列 | POST/PUT/DELETE | `/api/product-catelogs[/{id}]` | `{name, fid?}` | 系列下有产品时禁止删除 | | 产品筛选 | GET | `/api/products` | 增加 `catelog_id` | 产品挂系列(catelog_id);material 新增 `subbar`(副梃) | | **成本口径** | — | (自动) | — | 对齐 quote_by_cost:报价保存/更新时按产品名匹配价格本成本写入行 `_cost`;报价响应新增 `cost_total`(成本合计)与 `profit`(毛利);报价链接分享(quote_link)走 `/api/shares` type=quote 公开页 | + +### 12.26 P1 生产/CRM/仓库域(v2.31~v2.32) + +| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 | +|---|---|---|---|---| +| 车间员工 CRUD | GET/POST/PUT/DELETE | `/api/workshop/staff[/{id}]` | `{name, phone?, role(环节), status?}` | 车间管理页名册 | +| **派工** | POST | `/api/workshop/assign` | `{order_id, process_name, staff_id, note?}` | 进度节点指派员工,记录 staffHistory | +| 派工历史 | GET | `/api/workshop/assignments` | `page` | | +| 扫码跟踪 | GET | `/api/workshop/scan/{order_no}` | 路径(条码号=订单号) | 对齐 scan_tracking;返回订单+7 节点进度 | +| 跟单看板 | GET | `/api/workshop/board` | — | 在产订单按当前环节分列 | +| 双通道优化 | POST | `/api/optimize1d` | 增加 `method: fast\|fine` | fast=FFD 快速;fine=BFD+余料均衡(详细优化,对齐 optimize1d-ai) | +| **订单利润** | GET | `/api/reports/order-profit` | `days?` | 对齐 order-profit:金额 vs BOM 材料成本估算(价格本成本均价),行级毛利/毛利率 | +| **客户跟进** | GET/POST/DELETE | `/api/customers/{id}/follows[/{fid}]` | `{content, next_date?}` | 对齐 customer_records;客户页"跟进"对话框 | +| 业务员体系 | GET/POST/PUT/DELETE | `/api/salesmen[/{id}]` | `{name, phone?, commission_rate?}` | 名册 CRUD;客户表 salesmen 字段选择业务员 | +| 业务员业绩 | GET | `/api/salesmen/statistics` | — | 名下客户数/合同数/合同额/预估提成(salesmen_statistics) | +| **余料入库** | POST | `/api/warehouse/surplus-in` | `{contract_id, min_length?}` | 读合同下料方案,≥最小长度余料逐根入库(note=surplus) | +| 余料列表 | GET | `/api/warehouse/surplus` | `page` | | +| **领料出库** | POST | `/api/warehouse/pickout` | `{contract_id, note?}` | 对齐 pickout:按领料清单扣库存+流水;库存不足报缺口 | +| **采购单** | POST | `/api/purchases` | `{contract_id?(自动读缺料), items?, note?}` | 缺料清单自动生成(CG+日期+流水) | +| 采购列表/入库 | GET/POST | `/api/purchases[?status]` · `/{id}/receive` | — | 入库联动:明细写入库存+StockMove 流水 | diff --git a/frontend/src/views/CustomersView.vue b/frontend/src/views/CustomersView.vue index e6a073f..6bcab87 100644 --- a/frontend/src/views/CustomersView.vue +++ b/frontend/src/views/CustomersView.vue @@ -1,5 +1,8 @@ @@ -58,7 +129,7 @@ const keyword = ref('') const loading = ref(false) const editVisible = ref(false) const formRef = ref() -const form = reactive({ id: 0, name: '', phone: '', address: '', note: '' }) +const form = reactive({ id: 0, name: '', phone: '', address: '', note: '', salesmen: '' }) const rules = { name: [{ required: true, message: '请输入客户姓名', trigger: 'blur' }], phone: [{ pattern: /^[0-9+\-\s]{5,20}$/, message: '电话格式不正确', trigger: 'blur' }], @@ -74,7 +145,7 @@ async function load() { } function openEdit(row?: any) { - Object.assign(form, { id: row?.id ?? 0, name: row?.name ?? '', phone: row?.phone ?? '', address: row?.address ?? '', note: row?.note ?? '' }) + Object.assign(form, { id: row?.id ?? 0, name: row?.name ?? '', phone: row?.phone ?? '', address: row?.address ?? '', note: row?.note ?? '', salesmen: row?.salesmen ?? '' }) editVisible.value = true } @@ -109,9 +180,61 @@ async function doImport(opt: any) { function downloadTemplate() { apiDownload('/api/customers/excel-template', 'customers_template.xlsx') } function exportExcel() { apiDownload('/api/customers/excel-out', 'customers.xlsx') } -onMounted(load) +// ---- 业务员体系 ---- +const tab = ref('list') +const salesmen = ref([]) +const smStats = ref([]) +const smForm = reactive({ name: '', phone: '', commission_rate: 0 }) + +async function loadSalesmen() { + salesmen.value = (await api('get', '/api/salesmen')) ?? [] + smStats.value = (await api('get', '/api/salesmen/statistics')) ?? [] +} + +async function addSalesman() { + if (!smForm.name.trim()) return ElMessage.warning('请填写姓名') + await api('post', '/api/salesmen', smForm) + ElMessage.success('已添加') + smForm.name = ''; smForm.phone = ''; smForm.commission_rate = 0 + loadSalesmen() +} + +async function removeSalesman(row: any) { + await ElMessageBox.confirm(`确定删除业务员「${row.name}」?`, '提示', { type: 'warning' }) + await api('delete', `/api/salesmen/${row.id}`) + loadSalesmen() +} + +// ---- 跟进记录 ---- +const followVisible = ref(false) +const followCustomer = ref(null) +const follows = ref([]) +const followForm = reactive({ content: '', next_date: null }) + +function openFollow(row: any) { + followCustomer.value = row + followForm.content = ''; followForm.next_date = null + loadFollows() + followVisible.value = true +} + +async function loadFollows() { + follows.value = (await api('get', `/api/customers/${followCustomer.value.id}/follows`)) ?? [] +} + +async function addFollow() { + if (!followForm.content.trim()) return ElMessage.warning('请填写跟进内容') + await api('post', `/api/customers/${followCustomer.value.id}/follows`, followForm) + followForm.content = ''; followForm.next_date = null + loadFollows() +} + +onMounted(() => { load(); loadSalesmen() }) diff --git a/frontend/src/views/CutSolutionView.vue b/frontend/src/views/CutSolutionView.vue index d17bfd8..06f08ea 100644 --- a/frontend/src/views/CutSolutionView.vue +++ b/frontend/src/views/CutSolutionView.vue @@ -15,6 +15,18 @@ 生成下料方案 打印方案 + + + + + + + + + +