feat: P0-3 打印模板体系(区块化模板CRUD+打印中心重构)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# CHANGELOG
|
||||
|
||||
- [2026-09-25 12:20] FEAT: v2.22 P0-3 打印模板体系。①OrderFormat 模型(blocks 区块 JSON: title/info/image/table/sign)+种子 2 套默认模板; ②/api/order-formats CRUD(区块类型校验+is_default 唯一默认); ③打印中心重构为模板驱动: A4 预览按 blocks 渲染(BOM 块自动拉取 /orders/{id}/bom), 模板管理对话框(区块增删/上下移/title 文本/info 字段多选/table kind 选择); 修复 move(i) v-for 索引 string|number 类型; vue-tsc 0 错+构建通过 (Files: src/huamenchuang/models/extend.py, src/huamenchuang/api/order_format.py, src/huamenchuang/api/router.py, src/huamenchuang/scripts/seed.py, frontend/src/views/PrinterView.vue, doc/API.md)
|
||||
|
||||
- [2026-09-25 11:40] FEAT: v2.21 P0-2 订单 BOM 计算单+Excel 导出。①quote_service.bom() 从 drawing_data 重放算料输出型材下料清单(calcbomOrder 语义); ②services/excel_service.py 通用 xlsx 工具(标题/带样式表格/列宽)+order_workbook 订单导出(信息+型材清单+玻璃五金汇总+签审行); ③GET /orders/{id}/bom + GET /orders/{id}/export(attachment 下载); ④前端订单详情抽屉加"BOM 单"弹窗(汇总+下料表+合计米数)+导出 Excel(apiDownload blob 带 token); openpyxl 入依赖; vue-tsc 0 错+构建通过 (Files: pyproject.toml, src/huamenchuang/services/excel_service.py, src/huamenchuang/services/quote_service.py, src/huamenchuang/api/orders.py, frontend/src/api/index.ts, frontend/src/views/OrdersView.vue, doc/API.md)
|
||||
|
||||
- [2026-09-25 11:00] FEAT: v2.20 P0-1 价格脚本引擎。①services/formula.py 安全公式引擎(递归下降解析, 无 eval; 四则/括号/min/max/round/abs/ceil/floor/sqrt; ALLOWED_VARS 变量白名单+validate_formula 语法校验); ②PriceScript 模型(系列名/target/frame~total 六作用项/formula/enabled)+种子 5 条示例; ③/api/scripts CRUD+preview 试算(product-test 语义); ④offer-price 集成 use_scripts/scripts 参数, 行级 scripted 标记+script_total 直取; ⑤产品管理页拆双 Tab(价格本/脚本 CRUD+试算), 报价弹窗"脚本计价"标记; 前端 vue-tsc 0 错+构建通过 (Files: src/huamenchuang/services/formula.py, src/huamenchuang/models/business.py, src/huamenchuang/api/script.py, src/huamenchuang/api/quote.py, src/huamenchuang/services/quote_service.py, src/huamenchuang/scripts/seed.py, frontend/src/views/ProductsView.vue, frontend/src/components/QuoteDialog.vue, doc/API.md)
|
||||
|
||||
+15
@@ -390,3 +390,18 @@ uv run uvicorn huamenchuang.main:app --reload # 启动,http://localhost:8000
|
||||
|---|---|---|---|---|
|
||||
| **订单 BOM 单** | GET | `/api/orders/{id}/bom` | 路径 | 对齐目标站 calcbomOrder;返回 `{bars:[{kind,note,length,qty}], glass_area_m2, sash_count, width_mm, height_mm}`(从 drawing_data 重放算料,不含价格) |
|
||||
| **订单导出 Excel** | GET | `/api/orders/{id}/export` | 路径 | 对齐 orders/export;xlsx(订单信息+BOM 下料清单+玻璃/五金汇总),`Content-Disposition: attachment` |
|
||||
|
||||
### 12.17 打印模板 `/api/order-formats`(v2.20 新增,P0-打印模板体系)
|
||||
|
||||
对齐目标站 `order_format` 语义:模板=区块列表 `blocks[]`,前端按区块渲染 A4。
|
||||
|
||||
区块类型:`title`(text=标题文字)、`info`(fields=订单字段列表:order_no/customer_name/phone/address/count/area/money/created_at/note)、`image`(图纸缩略图)、`table`(kind=process 生产进度 / bom 下料清单)、`sign`(签字行)。
|
||||
|
||||
| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| 模板列表 | GET | `/api/order-formats` | — | 按 sort 排序 |
|
||||
| 新增模板 | POST | `/api/order-formats` | `{name, order_type, blocks[], is_default?}` | 区块类型校验;is_default=1 时清除其他默认 |
|
||||
| 编辑模板 | PUT | `/api/order-formats/{id}` | 同新增 | |
|
||||
| 删除模板 | DELETE | `/api/order-formats/{id}` | 路径 | |
|
||||
|
||||
种子模板 2 套:标准生产订单(默认)、生产订单+BOM 下料单。
|
||||
|
||||
@@ -8,65 +8,234 @@
|
||||
<el-select v-model="orderId" filterable placeholder="选择订单" style="width:280px" @change="onPick">
|
||||
<el-option v-for="o in orders" :key="o.id" :label="`${o.order_no} ${o.customer_name || ''}`" :value="o.id" />
|
||||
</el-select>
|
||||
<el-select v-model="formatId" style="width:200px" placeholder="打印模板" @change="onFormatChange">
|
||||
<el-option v-for="f in formats" :key="f.id" :label="`${f.name}${f.is_default ? ' (默认)' : ''}`" :value="f.id" />
|
||||
</el-select>
|
||||
<el-button :disabled="!current" @click="fmtVisible = true">模板管理</el-button>
|
||||
<el-button type="primary" :disabled="!current" @click="print">打印</el-button>
|
||||
</div>
|
||||
|
||||
<!-- A4 预览:按模板区块渲染 -->
|
||||
<div v-if="current" class="a4" id="print-area">
|
||||
<h2 class="doc-title">生产订单</h2>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="订单号">{{ current.order_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ current.customer_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ current.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址">{{ current.address || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ current.count }}</el-descriptions-item>
|
||||
<el-descriptions-item label="金额">¥{{ current.money }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="pic-box">
|
||||
<img v-if="current.pic_url" :src="current.pic_url" alt="图纸" />
|
||||
<div v-else class="no-pic">暂无图纸缩略图</div>
|
||||
</div>
|
||||
<el-divider>生产进度</el-divider>
|
||||
<table class="simple">
|
||||
<tr v-for="p in current.processes" :key="p.name">
|
||||
<td style="width:90px">{{ p.name }}</td>
|
||||
<td>{{ p.status === 2 ? `已完成 ${p.finish_time || ''}` : p.status === 1 ? '进行中' : '未开始' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="sign">客户签字:____________ 日期:____________</p>
|
||||
<template v-for="(b, i) in activeBlocks" :key="i">
|
||||
<h2 v-if="b.type === 'title'" class="doc-title">{{ b.text }}</h2>
|
||||
<el-descriptions v-else-if="b.type === 'info'" :column="3" border size="small">
|
||||
<el-descriptions-item v-for="f in b.fields" :key="f" :label="FIELD_DEFS[String(f)]?.label ?? f">
|
||||
{{ renderField(String(f)) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-else-if="b.type === 'image'" class="pic-box">
|
||||
<img v-if="current.pic_url" :src="current.pic_url" alt="图纸" />
|
||||
<div v-else class="no-pic">暂无图纸缩略图</div>
|
||||
</div>
|
||||
<template v-else-if="b.type === 'table' && b.kind === 'process'">
|
||||
<el-divider>生产进度</el-divider>
|
||||
<table class="simple">
|
||||
<tr v-for="p in current.processes" :key="p.name">
|
||||
<td style="width:90px">{{ p.name }}</td>
|
||||
<td>{{ p.status === 2 ? `已完成 ${p.finish_time || ''}` : p.status === 1 ? '进行中' : '未开始' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<template v-else-if="b.type === 'table' && b.kind === 'bom'">
|
||||
<el-divider>型材下料清单</el-divider>
|
||||
<table class="simple">
|
||||
<tr class="thead"><td>用途</td><td>单件长度(mm)</td><td>数量(件)</td><td>总长(m)</td></tr>
|
||||
<tr v-for="(row, ri) in bom?.bars ?? []" :key="ri">
|
||||
<td>{{ row.note || row.kind }}</td><td>{{ row.length }}</td><td>{{ row.qty }}</td>
|
||||
<td>{{ (row.length * row.qty / 1000).toFixed(2) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!bom?.bars?.length"><td colspan="4" class="no-pic">无下料数据</td></tr>
|
||||
</table>
|
||||
</template>
|
||||
<p v-else-if="b.type === 'sign'" class="sign">客户签字:____________ 日期:____________</p>
|
||||
</template>
|
||||
</div>
|
||||
<el-empty v-else description="选择订单后预览打印" />
|
||||
|
||||
<!-- 模板管理 -->
|
||||
<el-dialog v-model="fmtVisible" title="打印模板管理" width="680px">
|
||||
<div class="fmt-list">
|
||||
<el-table :data="formats" size="small" border>
|
||||
<el-table-column prop="name" label="模板" min-width="150" />
|
||||
<el-table-column label="默认" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_default" size="small" type="success">默认</el-tag>
|
||||
<el-button v-else size="small" text @click="setDefault(row)">设默认</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editFormat(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="removeFormat(row)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-button size="small" type="primary" plain style="margin-top:8px" @click="newFormat">新增模板</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">{{ fmtForm.id ? `编辑:${fmtForm.name}` : '新增模板' }}</el-divider>
|
||||
<el-form label-width="70px" size="small">
|
||||
<el-form-item label="模板名"><el-input v-model="fmtForm.name" style="width:240px" /></el-form-item>
|
||||
<el-form-item label="区块设计">
|
||||
<div class="blocks-editor">
|
||||
<div v-for="(b, i) in fmtForm.blocks" :key="i" class="block-row">
|
||||
<el-tag size="small" effect="dark" class="block-tag">{{ BLOCK_LABEL[b.type] }}</el-tag>
|
||||
<el-input v-if="b.type === 'title'" v-model="b.text" size="small" style="width:180px" placeholder="标题文字" />
|
||||
<el-select v-if="b.type === 'info'" v-model="b.fields" multiple size="small" style="min-width:280px"
|
||||
placeholder="选择展示字段">
|
||||
<el-option v-for="(def, k) in FIELD_DEFS" :key="k" :label="def.label" :value="k" />
|
||||
</el-select>
|
||||
<el-select v-if="b.type === 'table'" v-model="b.kind" size="small" style="width:130px">
|
||||
<el-option value="process" label="生产进度" /><el-option value="bom" label="BOM 下料清单" />
|
||||
</el-select>
|
||||
<span v-if="b.type === 'image'" class="block-note">订单图纸缩略图</span>
|
||||
<span v-else-if="b.type === 'sign'" class="block-note">签字/日期行</span>
|
||||
<div style="flex:1" />
|
||||
<el-button size="small" text :disabled="i === 0" @click="move(i, -1)">↑</el-button>
|
||||
<el-button size="small" text :disabled="i === fmtForm.blocks.length - 1" @click="move(i, 1)">↓</el-button>
|
||||
<el-button size="small" text type="danger" @click="fmtForm.blocks.splice(i, 1)">删</el-button>
|
||||
</div>
|
||||
<el-dropdown @command="(t: string) => addBlock(t)">
|
||||
<el-button size="small" type="primary" plain>+ 添加区块</el-button>
|
||||
<template #dropdown><el-dropdown-menu>
|
||||
<el-dropdown-item v-for="(lbl, t) in BLOCK_LABEL" :key="t" :command="t">{{ lbl }}</el-dropdown-item>
|
||||
</el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="fmtVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="saveFormat">保存模板</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const FIELD_DEFS: Record<string, { label: string; render: (o: any) => string }> = {
|
||||
order_no: { label: '订单号', render: o => o.order_no },
|
||||
customer_name: { label: '客户', render: o => o.customer_name || '-' },
|
||||
phone: { label: '电话', render: o => o.phone || '-' },
|
||||
address: { label: '地址', render: o => o.address || '-' },
|
||||
count: { label: '数量', render: o => String(o.count ?? 0) },
|
||||
area: { label: '面积', render: o => `${o.area ?? 0} ㎡` },
|
||||
money: { label: '金额', render: o => `¥${o.money ?? 0}` },
|
||||
created_at: { label: '下单时间', render: o => String(o.created_at ?? '').slice(0, 10) },
|
||||
note: { label: '备注', render: o => o.note || '-' },
|
||||
}
|
||||
const BLOCK_LABEL: Record<string, string> = { title: '标题', info: '订单信息', image: '图纸', table: '表格', sign: '签字' }
|
||||
|
||||
const orders = ref<any[]>([])
|
||||
const orderId = ref<number | undefined>()
|
||||
const orderType = ref(0)
|
||||
const current = ref<any>(null)
|
||||
const bom = ref<any>(null)
|
||||
const formats = ref<any[]>([])
|
||||
const formatId = ref<number | undefined>()
|
||||
const fmtVisible = ref(false)
|
||||
const fmtForm = reactive<any>({ id: 0, name: '', blocks: [], is_default: 0 })
|
||||
|
||||
const activeBlocks = computed<any[]>(() => {
|
||||
const f = formats.value.find(x => x.id === formatId.value)
|
||||
return f?.blocks ?? []
|
||||
})
|
||||
|
||||
function renderField(f: string) {
|
||||
const def = FIELD_DEFS[f]
|
||||
return def ? def.render(current.value) : ''
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const d = orderType.value === 1
|
||||
? await api<any>('get', `/api/sunny_house_orders?page=1&page_size=100`)
|
||||
: await api<any>('post', '/api/orders/search', { page: 1, page_size: 100 })
|
||||
orders.value = orderType.value === 1 ? d.data : d.data
|
||||
const d = await api<any>('post', '/api/orders/search', { page: 1, page_size: 100, status: orderType.value })
|
||||
orders.value = d.data
|
||||
current.value = null
|
||||
}
|
||||
|
||||
async function loadFormats(keep = false) {
|
||||
formats.value = (await api<any[]>('get', '/api/order-formats')) ?? []
|
||||
if (!keep) {
|
||||
const def = formats.value.find(f => f.is_default) ?? formats.value[0]
|
||||
formatId.value = def?.id
|
||||
}
|
||||
}
|
||||
|
||||
async function onPick(oid: number | undefined) {
|
||||
if (!oid) { current.value = null; return }
|
||||
current.value = await api<any>('get', `/api/orders/${oid}`)
|
||||
await refreshBom()
|
||||
}
|
||||
|
||||
async function refreshBom() {
|
||||
bom.value = null
|
||||
if (!current.value || !activeBlocks.value.some(b => b.type === 'table' && b.kind === 'bom')) return
|
||||
try { bom.value = await api<any>('get', `/api/orders/${current.value.id}/bom`) } catch { /* 无图纸时静默 */ }
|
||||
}
|
||||
|
||||
function onFormatChange() { refreshBom() }
|
||||
|
||||
function print() { window.print() }
|
||||
|
||||
onMounted(load)
|
||||
// ---- 模板管理 ----
|
||||
function newFormat() {
|
||||
Object.assign(fmtForm, { id: 0, name: '', blocks: [{ type: 'title', text: '生产订单' }], is_default: 0 })
|
||||
}
|
||||
|
||||
function editFormat(row: any) {
|
||||
Object.assign(fmtForm, { id: row.id, name: row.name, blocks: JSON.parse(JSON.stringify(row.blocks)), is_default: row.is_default })
|
||||
}
|
||||
|
||||
function addBlock(t: string) {
|
||||
const seed: Record<string, any> = {
|
||||
title: { type: 'title', text: '标题' },
|
||||
info: { type: 'info', fields: ['order_no', 'customer_name', 'count', 'money'] },
|
||||
image: { type: 'image' },
|
||||
table: { type: 'table', kind: 'process' },
|
||||
sign: { type: 'sign' },
|
||||
}
|
||||
fmtForm.blocks.push(JSON.parse(JSON.stringify(seed[t])))
|
||||
}
|
||||
|
||||
function move(idx: number | string, dir: number) {
|
||||
const i = Number(idx)
|
||||
const arr = fmtForm.blocks
|
||||
const [x] = arr.splice(i, 1)
|
||||
arr.splice(i + dir, 0, x)
|
||||
}
|
||||
|
||||
async function saveFormat() {
|
||||
if (!fmtForm.name.trim()) return ElMessage.warning('请填写模板名')
|
||||
if (fmtForm.id) await api('put', `/api/order-formats/${fmtForm.id}`, fmtForm)
|
||||
else await api('post', '/api/order-formats', fmtForm)
|
||||
ElMessage.success('模板已保存')
|
||||
await loadFormats(true)
|
||||
editFormat(formats.value.find(f => f.id === fmtForm.id)!)
|
||||
}
|
||||
|
||||
async function setDefault(row: any) {
|
||||
await api('put', `/api/order-formats/${row.id}`, { ...row, is_default: 1 })
|
||||
ElMessage.success('已设为默认')
|
||||
loadFormats(true)
|
||||
}
|
||||
|
||||
async function removeFormat(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除模板「${row.name}」?`, '提示', { type: 'warning' })
|
||||
await api('delete', `/api/order-formats/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
await loadFormats(true)
|
||||
if (formatId.value === row.id) formatId.value = formats.value[0]?.id
|
||||
}
|
||||
|
||||
onMounted(() => { load(); loadFormats() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.a4 { width: 210mm; min-height: 240mm; margin: 0 auto; background: #fff; padding: 14mm; box-shadow: 0 2px 12px rgba(0,0,0,.12); }
|
||||
.doc-title { text-align: center; letter-spacing: 8px; }
|
||||
.pic-box { height: 80mm; display: flex; align-items: center; justify-content: center; background: #f7f8f9; margin-top: 12px; }
|
||||
@@ -74,7 +243,12 @@ onMounted(load)
|
||||
.no-pic { color: #909399; }
|
||||
.simple { width: 100%; border-collapse: collapse; }
|
||||
.simple td { border: 1px solid #dcdfe6; padding: 5px 8px; font-size: 13px; }
|
||||
.simple .thead td { background: #f5f7fa; font-weight: 600; }
|
||||
.sign { margin-top: 30px; text-align: right; color: #606266; }
|
||||
.blocks-editor { width: 100%; }
|
||||
.block-row { display: flex; gap: 8px; align-items: center; padding: 4px 0; }
|
||||
.block-tag { width: 64px; justify-content: center; }
|
||||
.block-note { color: #909399; font-size: 12px; }
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.a4 { box-shadow: none; width: 100%; }
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""打印模板路由:订单打印模板 CRUD(对齐目标站 order_format 语义)。"""
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.models.extend import OrderFormat
|
||||
|
||||
router = APIRouter(prefix="/order-formats", tags=["打印模板"])
|
||||
|
||||
BLOCK_TYPES = ("title", "info", "image", "table", "sign")
|
||||
TABLE_KINDS = ("process", "bom")
|
||||
|
||||
|
||||
class FormatBody(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=64)
|
||||
order_type: int = Field(default=0, ge=0, le=1)
|
||||
blocks: list[dict] = Field(default_factory=list, description="模板区块列表")
|
||||
is_default: int = Field(default=0, ge=0, le=1)
|
||||
|
||||
|
||||
def _validate_blocks(blocks: list[dict]) -> str | None:
|
||||
for b in blocks:
|
||||
if b.get("type") not in BLOCK_TYPES:
|
||||
return f"不支持的区块类型: {b.get('type')}"
|
||||
if b.get("type") == "table" and b.get("kind") not in TABLE_KINDS:
|
||||
return "table 区块需要 kind: process/bom"
|
||||
return None
|
||||
|
||||
|
||||
def _to_out(f: OrderFormat) -> dict:
|
||||
return {"id": f.id, "name": f.name, "order_type": f.order_type,
|
||||
"blocks": json.loads(f.blocks or "[]"), "is_default": f.is_default, "sort": f.sort}
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_formats(user: CurrentUser, db: DbSession):
|
||||
stmt = (select(OrderFormat).where(OrderFormat.company_id == user.company_id)
|
||||
.order_by(OrderFormat.sort, OrderFormat.id))
|
||||
rows = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=[_to_out(r) for r in rows])
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_format(body: FormatBody, user: CurrentUser, db: DbSession):
|
||||
err = _validate_blocks(body.blocks)
|
||||
if err:
|
||||
return fail(err)
|
||||
fmt = OrderFormat(company_id=user.company_id,
|
||||
name=body.name, order_type=body.order_type,
|
||||
blocks=json.dumps(body.blocks, ensure_ascii=False),
|
||||
is_default=body.is_default)
|
||||
if body.is_default: # 唯一默认
|
||||
for other in (await db.execute(select(OrderFormat).where(
|
||||
OrderFormat.company_id == user.company_id))).scalars():
|
||||
other.is_default = 0
|
||||
db.add(fmt)
|
||||
await db.flush()
|
||||
return ok(data=_to_out(fmt), message="模板已创建")
|
||||
|
||||
|
||||
@router.put("/{format_id}")
|
||||
async def update_format(format_id: int, body: FormatBody, user: CurrentUser, db: DbSession):
|
||||
fmt = await db.get(OrderFormat, format_id)
|
||||
if fmt is None or fmt.company_id != user.company_id:
|
||||
return fail("模板不存在")
|
||||
err = _validate_blocks(body.blocks)
|
||||
if err:
|
||||
return fail(err)
|
||||
fmt.name = body.name
|
||||
fmt.order_type = body.order_type
|
||||
fmt.blocks = json.dumps(body.blocks, ensure_ascii=False)
|
||||
if body.is_default:
|
||||
for other in (await db.execute(select(OrderFormat).where(
|
||||
OrderFormat.company_id == user.company_id))).scalars():
|
||||
other.is_default = 0
|
||||
fmt.is_default = 1
|
||||
else:
|
||||
fmt.is_default = 0
|
||||
return ok(data=_to_out(fmt), message="模板已更新")
|
||||
|
||||
|
||||
@router.delete("/{format_id}")
|
||||
async def delete_format(format_id: int, user: CurrentUser, db: DbSession):
|
||||
fmt = await db.get(OrderFormat, format_id)
|
||||
if fmt is None or fmt.company_id != user.company_id:
|
||||
return fail("模板不存在")
|
||||
await db.delete(fmt)
|
||||
return ok(message="模板已删除")
|
||||
@@ -7,6 +7,7 @@ from huamenchuang.api import (
|
||||
contracts,
|
||||
customers,
|
||||
optimize,
|
||||
order_format,
|
||||
orders,
|
||||
platform,
|
||||
quote,
|
||||
@@ -25,6 +26,7 @@ api_router.include_router(windoor.router)
|
||||
api_router.include_router(optimize.router)
|
||||
api_router.include_router(quote.router)
|
||||
api_router.include_router(script.router)
|
||||
api_router.include_router(order_format.router)
|
||||
api_router.include_router(warehouse.router)
|
||||
api_router.include_router(business.router)
|
||||
api_router.include_router(platform.router)
|
||||
|
||||
@@ -19,6 +19,7 @@ from huamenchuang.models.extend import (
|
||||
Article,
|
||||
CompanyParam,
|
||||
Device,
|
||||
OrderFormat,
|
||||
PayOrder,
|
||||
SmsCode,
|
||||
)
|
||||
@@ -37,6 +38,7 @@ __all__ = [
|
||||
"Device",
|
||||
"MeasureOrder",
|
||||
"Order",
|
||||
"OrderFormat",
|
||||
"OrderLog",
|
||||
"OrderProcess",
|
||||
"PayOrder",
|
||||
|
||||
@@ -68,6 +68,22 @@ class Article(Base):
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class OrderFormat(Base):
|
||||
"""订单打印模板(对齐目标站 order_format:可自定义模板区块与默认模板)。"""
|
||||
|
||||
__tablename__ = "order_formats"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
company_id: Mapped[int] = mapped_column(default=1, index=True)
|
||||
name: Mapped[str] = mapped_column(String(64))
|
||||
order_type: Mapped[int] = mapped_column(default=0) # 0门窗订单 1阳光房订单
|
||||
# 模板区块 JSON: [{type: title|info|image|table|sign, text?, fields?, kind?}]
|
||||
blocks: Mapped[str] = mapped_column(Text, default="[]")
|
||||
is_default: Mapped[int] = mapped_column(default=0)
|
||||
sort: Mapped[int] = mapped_column(default=0)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class SmsCode(Base):
|
||||
"""短信验证码(provider 适配层:未配置短信签名时走 mock 通道)。"""
|
||||
|
||||
|
||||
@@ -117,6 +117,35 @@ async def seed() -> None:
|
||||
await db.commit()
|
||||
print("价格脚本就绪")
|
||||
|
||||
# 7) 打印模板(幂等:按名称去重)
|
||||
async with SessionLocal() as db:
|
||||
from huamenchuang.models.extend import OrderFormat
|
||||
FORMATS = [
|
||||
("标准生产订单", [
|
||||
{"type": "title", "text": "生产订单"},
|
||||
{"type": "info", "fields": ["order_no", "customer_name", "phone", "address", "count", "money"]},
|
||||
{"type": "image"},
|
||||
{"type": "table", "kind": "process"},
|
||||
{"type": "sign"},
|
||||
], 1),
|
||||
("生产订单+BOM下料单", [
|
||||
{"type": "title", "text": "生产订单"},
|
||||
{"type": "info", "fields": ["order_no", "customer_name", "phone", "address",
|
||||
"count", "area", "money", "created_at"]},
|
||||
{"type": "image"},
|
||||
{"type": "table", "kind": "bom"},
|
||||
{"type": "table", "kind": "process"},
|
||||
{"type": "sign"},
|
||||
], 0),
|
||||
]
|
||||
existing = {n for (n,) in (await db.execute(select(OrderFormat.name))).all()}
|
||||
for i, (name, blocks, is_default) in enumerate(FORMATS):
|
||||
if name not in existing:
|
||||
db.add(OrderFormat(name=name, order_type=0, sort=i, is_default=is_default,
|
||||
blocks=__import__("json").dumps(blocks, ensure_ascii=False)))
|
||||
await db.commit()
|
||||
print("打印模板就绪")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
|
||||
Reference in New Issue
Block a user